Skip to content

Commit 5be5e8c

Browse files
authored
Merge pull request #42 from vitry/fix/rescue-child-recovery
fix: discover empty-preview Rescue children
2 parents 073eaeb + 5941e05 commit 5be5e8c

17 files changed

Lines changed: 598 additions & 101 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ All notable changes follow Semantic Versioning.
44

55
## Unreleased
66

7+
- Fixed restored empty-preview Codex children disappearing from Rescue collision planning: preparation now uses the exact parent relationship query, treats host-only rows as occupancy without granting follow-up authority, and fails closed when that API is unsupported instead of retrying or guessing a colliding spawn.
78
- Added persisted stopped Rescue child recovery before replacement spawn: the plugin restores the original Codex thread and history by joining sanitized app-server identity with private executor provenance, rejoins already active children, and never treats age or a name/path collision as authority.
89
- Restored compatibility with the legacy PR #39 Rescue route: after origin-to-worktree binding, a child launched or resumed from the conversation root can resolve the immutable execution target. This qualification does not broaden authority beyond that bound target.
910
- Added compatible Rescue worktree late binding: lifecycle proof retains the origin workspace while the first trusted prepare automatically and immutably binds one execution workspace from the same canonical Git common-dir, without manual handoff. Role preview and children cannot claim; unrelated repositories fail closed; Root Stop, a new prompt, and SessionEnd revoke or replace authority before target cleanup.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ Rescue has two equivalent entry forms. An explicit `$zcode:rescue` request is li
5656

5757
Root starts `prepare rescue` on a raw-capable TTY. The companion enables raw mode before it emits the exact task-free readiness line; readiness is nonterminal. Only after that line does Root send one JSON line terminated by LF over private stdin, with no EOF or U+0004. The companion consumes that one frame, restores raw mode, and commits exact session, turn, workspace, and executor-bound prepared state. Non-TTY or raw-mode failure stops before task delivery and no child is spawned. Tool output never contains or echoes the payload; only the task-free readiness and final prepared acknowledgement cross back. The named Role and generic child then run the same constant `invoke-prepared rescue` forwarder, without receiving the task, options, capability, or authorization material. If an active `rescueChildId` already exists, Root rejoins and waits for that exact Rescue child instead of repeating preflight, preparation, spawn, or invocation.
5858

59-
The plugin's Companion, during preparation and before any replacement spawn, discovers the parent's persisted Codex children. A persisted stopped Rescue child is recovered before any spawn: the original Codex thread and its history are restored, rather than creating a lookalike child. The plugin joins sanitized Codex app-server identity with private executor provenance and requires one exact match; ambiguous or contradictory evidence fails closed. Root receives and executes only the resulting task-free directive; it does not discover or choose a child identity. An active Rescue child is rejoined through the existing active-child path. Neither 30-minute age nor a name or path collision is authority and neither authorizes recovery or replacement.
59+
The plugin's Companion, during preparation and before any replacement spawn, discovers the parent's persisted Codex children through the app-server's exact parent relationship query. The global thread listing omits restored children whose preview is empty, so it is never used to prove a spawn name is free. If the installed Codex line cannot prove support for the exact-parent API, preparation fails closed instead of retrying or guessing a spawn. A persisted stopped Rescue child is recovered before any spawn: the original Codex thread and its history are restored, rather than creating a lookalike child. The plugin joins sanitized Codex app-server identity with private executor provenance and requires one exact match; a host-only child occupies its path but cannot authorize follow-up. Ambiguous or contradictory evidence fails closed. Root receives and executes only the resulting task-free directive; it does not discover or choose a child identity. An active Rescue child is rejoined through the existing active-child path. Neither 30-minute age nor a name or path collision is authority and neither authorizes recovery or replacement.
6060

6161
A durable Rescue binding now keeps one exact stopped Rescue child attached to one exact ZCode session. Its private `anchorJobId` identifies the adopted operation, and `currentJobId` advances when each continuation job is durably reserved and published, even if that job later queues, fails, or is cancelled; neither identifier is sent in a child message. A clear proactive continuation prepares resume and follows up the same stopped child, which runs the same `invoke-prepared rescue` assignment with no second `SubagentStart`. An explicit bound request without `--resume` or `--fresh` also follows up that same child and lets its bound `needs-choice` result drive the one user choice. `--fresh` always prepares a new independent ZCode operation, but it need not allocate a fresh Codex child: the planner may reactivate and follow up a qualified stopped Rescue child, preferring the managed base and then the deterministic newest compatible executor, and prescribes a spawn only when none exists. Reusing a Codex child does not resume its prior ZCode binding or session; the new operation creates a peer session with the current permission snapshot. A name or path collision is never authority for that choice.
6262

README.zh-CN.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ Rescue 有两种等价入口:显式 `$zcode:rescue` 是请求中字面且适
5656

5757
Root 在 raw-capable TTY 上启动 `prepare rescue`。companion 先启用 raw mode,再输出精确且不含 task 的 readiness;readiness 是非终态。只有看到该行之后,Root 才通过私有 stdin 发送一行 JSON,并以 LF 结尾;不发送 EOF 或 U+0004。companion 消费这一帧、恢复 raw mode,并提交绑定精确 session、turn、workspace 和 executor 的 prepared state。非 TTY 或 raw mode 失败会在 task 交付前停止,且不会 spawn child。tool output 绝不包含或回显 payload;返回边界只有不含 task 的 readiness 和最终 prepared 确认。随后具名 Role 或 generic child 都只运行常量 `invoke-prepared rescue` forwarder,不接收 task、options、capability 或授权材料。若已经有活动的 `rescueChildId`,Root 会重新加入并等待这个精确的 Rescue child,不会重复 preflight、prepare、spawn 或 invoke。
5858

59-
插件的 Companion 在 preparation 期间、任何替代 spawn 之前,发现该 parent 的持久化 Codex children。持久化且已停止的 Rescue child 会被恢复,这发生在任何 spawn 之前:原 Codex thread 及其历史会被恢复,而不是创建一个外观相似的新 child。插件把已净化的 Codex app-server identity 与私有 executor provenance 结合,并要求只有一个精确匹配;证据有歧义或相互矛盾时会 fail closed。Root 只接收并执行由此产生且不含 task 的 directive,不负责发现或选择 child identity。活动的 Rescue child 仍通过现有路径重新加入。30 分钟 age 与名称或路径碰撞都不构成授权或权威,也不能授权恢复或替代创建。
59+
插件的 Companion 在 preparation 期间、任何替代 spawn 之前,通过 app-server 的精确 parent relationship query 发现该 parent 的持久化 Codex children。全局 thread listing 会漏掉 preview 为空的已恢复 child,因此绝不用它证明某个 spawn 名称空闲。如果已安装的 Codex 版本不能证明支持精确 parent API,preparation 会 fail closed,不会重试或猜测 spawn。持久化且已停止的 Rescue child 会被恢复,这发生在任何 spawn 之前:原 Codex thread 及其历史会被恢复,而不是创建一个外观相似的新 child。插件把已净化的 Codex app-server identity 与私有 executor provenance 结合,并要求只有一个精确匹配;host-only child 会占用其路径,但不能授权 follow-up。证据有歧义或相互矛盾时会 fail closed。Root 只接收并执行由此产生且不含 task 的 directive,不负责发现或选择 child identity。活动的 Rescue child 仍通过现有路径重新加入。30 分钟 age 与名称或路径碰撞都不构成授权或权威,也不能授权恢复或替代创建。
6060

6161
durable Rescue binding 现在把同一个已停止的 Rescue child 绑定到一个精确 ZCode session。私有 `anchorJobId` 标识被采用的操作,`currentJobId` 在每个续做 job 被持久预留并发布时前移,即使该 job 随后排队、失败或取消;两个标识都不会进入 child message。明确的主动续做会 prepare resume 并 follow up 同一个已停止的 Rescue child;它复用相同的 `invoke-prepared rescue` assignment,不会产生第二次 `SubagentStart`。显式 bound 请求若没有 `--resume` 或 `--fresh`,也 follow up 同一 child,并由其 bound `needs-choice` 结果触发一次用户选择。`--fresh` 始终准备新的独立 ZCode 操作,但不要求分配新的 Codex child:planner 可以恢复并 follow up 合格的已停止 Rescue child,优先选择受管 base,其次选择确定性的最新兼容 executor;两者都不存在时才 prescribe spawn。复用 Codex child 不会 resume 其先前的 ZCode binding 或 session;新操作会以当前 permission 快照创建 peer session。名称或路径碰撞绝不是该选择的权威。
6262

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
# Rescue Exact-Parent Discovery Implementation Plan
2+
3+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4+
5+
**Goal:** Make Rescue route planning discover empty-preview persisted children from Codex's exact parent spawn graph so it never prescribes a colliding spawn name.
6+
7+
**Architecture:** Keep `listCodexThreadSpawnChildren` as the sole discovery interface, but initialize app-server with experimental API capability and scope every page to the exact `parentThreadId`. Retain all current validation and planner authorization; exact-parent rows are a complete occupied-path set, while only rows joined to private stopped-executor provenance are eligible for follow-up.
8+
9+
**Tech Stack:** Node.js 22.13 ESM, built-in `node:test`, Codex 0.147 app-server JSONL protocol, existing Companion/planner seams, verified marketplace builder.
10+
11+
---
12+
13+
### Task 1: Exact-parent app-server discovery
14+
15+
**Files:**
16+
- Modify: `tests/helpers/fake-codex-app-server.mjs`
17+
- Modify: `tests/codex-app-server.test.mjs`
18+
- Modify: `scripts/lib/codex-app-server.mjs`
19+
20+
- [ ] **Step 1: Write the failing empty-preview relationship test**
21+
22+
Extend the fake server so `thread/list` can distinguish an unscoped global
23+
query from an exact `parentThreadId` query. Add a test whose direct child has
24+
`preview: ""`; assert the client returns it and records both:
25+
26+
```js
27+
assert.deepEqual(calls[0].params.capabilities, { experimentalApi: true });
28+
assert.equal(listCall.params.parentThreadId, parentId);
29+
assert.equal(children[0].agentPath, '/root/zcode_rescue_task');
30+
```
31+
32+
The fake global behavior must omit this row so removing the exact parent filter
33+
makes the test fail with an empty child list.
34+
35+
- [ ] **Step 2: Run the focused test and verify RED**
36+
37+
Run:
38+
39+
```bash
40+
node --test --test-name-pattern='empty-preview exact-parent' tests/codex-app-server.test.mjs
41+
```
42+
43+
Expected: FAIL because initialize still sends `capabilities: null` and
44+
`thread/list` omits `parentThreadId`, reproducing the production empty list.
45+
46+
- [ ] **Step 3: Implement the minimal protocol change**
47+
48+
Change only the list operation's app-server initialization/request behavior:
49+
50+
```js
51+
const LIST_INITIALIZE_PARAMS = {
52+
clientInfo: INITIALIZE_PARAMS.clientInfo,
53+
capabilities: { experimentalApi: true },
54+
};
55+
56+
request('thread/list', {
57+
parentThreadId,
58+
sourceKinds: ['subAgentThreadSpawn'],
59+
limit: pageSize,
60+
sortKey: 'created_at',
61+
sortDirection: 'desc',
62+
...(cursor === null ? {} : { cursor }),
63+
});
64+
```
65+
66+
Validate the initialize response as a known supporting Codex line (semver
67+
0.141.0 or newer) before listing, independently of the originator prefix.
68+
Codex 0.117 silently ignores the unknown parent filter,
69+
so its empty global response must fail closed rather than authorize a base-name
70+
spawn. Unknown or unparseable versions also fail closed.
71+
72+
Do not change `thread/read` initialization. Remove the global foreign-row skip
73+
from the list path: every row returned by an exact-parent query must pass the
74+
existing full raw child validation for that parent.
75+
76+
- [ ] **Step 4: Add fail-closed exact-parent cases**
77+
78+
Cover missing/foreign/contradictory parent rows, unsupported capability or
79+
request errors, a Codex 0.117 server that silently ignores the parent field,
80+
an unparseable initialize version, duplicate IDs/paths across pages, cursor
81+
bounds, cancellation, and reaping. Update old global-compatibility assertions
82+
so they no longer claim that unrelated global rows are part of this interface.
83+
84+
- [ ] **Step 5: Run Task 1 verification**
85+
86+
Run:
87+
88+
```bash
89+
node --test tests/codex-app-server.test.mjs
90+
npm run lint
91+
npm run typecheck
92+
git diff --check
93+
```
94+
95+
Expected: all commands exit zero.
96+
97+
- [ ] **Step 6: Commit Task 1**
98+
99+
```bash
100+
git add scripts/lib/codex-app-server.mjs tests/codex-app-server.test.mjs tests/helpers/fake-codex-app-server.mjs
101+
git commit -m "fix: discover exact persisted child graph"
102+
```
103+
104+
### Task 2: Lock the collision incident and distribution contract
105+
106+
**Files:**
107+
- Modify: `tests/integration/companion.test.mjs`
108+
- Modify if required by copied critical bytes: `tests/plugin-contracts.test.mjs`
109+
- Modify generated snapshot: `marketplace/`
110+
- Modify: `README.md`
111+
- Modify: `README.zh-CN.md`
112+
- Modify: `CHANGELOG.md`
113+
114+
- [ ] **Step 1: Write the failing Companion incident regression**
115+
116+
Add an installed-style prepare scenario where the fake exact-parent app-server
117+
returns an empty-preview host child at `/root/zcode_rescue_task`, but the plugin
118+
has no stopped-executor provenance for it. Assert:
119+
120+
```js
121+
assert.deepEqual(prepared.route, {
122+
version: 1,
123+
action: 'spawn',
124+
taskName: 'zcode_rescue_task_2',
125+
});
126+
```
127+
128+
Also assert one exact-parent list request, zero follow-up/invoke actions, no
129+
second prepare, and no private task text in public output.
130+
131+
- [ ] **Step 2: Run the incident regression and verify RED if Task 1 is reverted**
132+
133+
Run the new test green on Task 1, then temporarily execute it against the Task 1
134+
parent commit (or revert only the Task 1 production hunk without committing).
135+
Expected: the old client produces `zcode_rescue_task` or an empty discovery,
136+
proving the test catches the reported collision. Restore Task 1 immediately.
137+
138+
- [ ] **Step 3: Update release guidance**
139+
140+
Document bilingually that direct-child discovery uses Codex's exact relationship
141+
query because global listing omits empty-preview restored agents. State that an
142+
unsupported exact-parent API fails preparation closed and never retries a spawn.
143+
Record the fix under Unreleased in `CHANGELOG.md`.
144+
145+
- [ ] **Step 4: Regenerate the marketplace from a clean committed source**
146+
147+
Commit source/test/docs changes, temporarily remove only the three untracked
148+
planning files from the worktree, run the repository's verified marketplace
149+
builder, restore the planning files, and commit the generated snapshot. Never
150+
hand-edit generated provenance.
151+
152+
- [ ] **Step 5: Run focused and full verification**
153+
154+
Run:
155+
156+
```bash
157+
node --test tests/codex-app-server.test.mjs tests/integration/companion.test.mjs
158+
npm run check
159+
git diff --check origin/main...HEAD
160+
```
161+
162+
The clean-source full gate must show zero failures. Opt-in authenticated Codex
163+
and real ZCode skips remain documented rather than counted as qualification.
164+
165+
- [ ] **Step 6: Commit Task 2**
166+
167+
```bash
168+
git add tests/integration/companion.test.mjs tests/plugin-contracts.test.mjs README.md README.zh-CN.md CHANGELOG.md marketplace
169+
git commit -m "test: cover empty-preview Rescue collision"
170+
```
171+
172+
### Task 3: Review, PR update, and CI
173+
174+
**Files:**
175+
- Review: `origin/main...HEAD`
176+
177+
- [ ] **Step 1: Run independent spec review**
178+
179+
Require explicit confirmation that exact-parent discovery covers the incident,
180+
host-only rows remain occupancy-only, and unsupported APIs fail closed.
181+
182+
- [ ] **Step 2: Run independent code-quality review**
183+
184+
Review validation bounds, experimental capability scope, pagination,
185+
cancellation/reaping, fake-server fidelity, and whether tests would fail on the
186+
old global implementation. Resolve every important finding and re-review.
187+
188+
- [ ] **Step 3: Push the existing PR branch**
189+
190+
```bash
191+
git push origin fix/rescue-child-recovery
192+
```
193+
194+
- [ ] **Step 4: Monitor PR #41 until all checks pass**
195+
196+
Use `gh pr checks --watch 41`. For each failure, read the exact job log, add a
197+
red regression at the correct seam, fix only the confirmed root cause, rerun
198+
local verification, and push. Finish only when the PR head matches local HEAD,
199+
the merge state is clean, and every required matrix job succeeds.

0 commit comments

Comments
 (0)