Skip to content

Commit 359a59e

Browse files
authored
Merge pull request #33 from vitry/feat/rescue-child-session-binding
feat: bind Rescue children to exact ZCode sessions
2 parents 686faec + 830136a commit 359a59e

54 files changed

Lines changed: 4715 additions & 206 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

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+
- Added exact stopped-child Rescue continuation: Root privately prepares the next bound turn and follows up the same stopped child, which reuses `invoke-prepared rescue` with no second `SubagentStart`; fresh work still creates a new child.
78
- Added automatic proactive Rescue routing alongside explicit `$zcode:rescue`, with authoritative explicit choices, semantic objective normalization, active-child rejoin, and no `--auto` flag. Root now performs a raw TTY readiness handshake and sends one LF-terminated frame over private stdin without EOF into exact-bound, expiring, single-consume prepared state; task-blind named and generic children run only `invoke-prepared rescue`.
89
- Added cc-style semantic progress in the selected Rescue child and fixed coarse root liveness relays without exposing raw PTY data, child stderr, tool output, file contents, reasoning, credentials, or capabilities. Progress remains observational: only the original foreground terminal exit and final stdout prove completion. The child also supports an exact bound no-argument status sidecar through `zcode status`, `$zcode:status`, or `/zcode:status` without selecting or replacing the foreground job.
910
- Added deterministic installed Codex 0.147 captured-rollout qualification for both the named Role and generic fallback, independently covering yielded foreground and same-child choice continuations; authenticated live qualification still reports only the route Codex actually selected.

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ Rescue has two equivalent entry forms. An explicit `$zcode:rescue` request is li
4848

4949
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.
5050

51+
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 an independent operation and a new child.
52+
53+
Legacy jobs-only state may adopt the exact eligible continuation candidate once; ambiguous or previously pending legacy state is rejected instead of guessed. A permission change cannot resume the old binding, while `--fresh` captures the current permission snapshot. `SessionEnd` closes the ending Codex session's Rescue binding so it cannot be revived. An invalid binding, executor mismatch, wrong workspace, closed session, or inconsistent provenance must fail closed without latest-session fallback. Because the managed Role bytes changed, `role-status rescue` can report `upgrade-required`; rerun `$zcode:setup` before continuing.
54+
5155
Foreground Rescue runs the constant forwarder in one native child thread. When the host supports `agent_type`, Codex selects the named `zcode-rescue` Role. A generic child is a host-only compatibility fallback permitted only when the active spawn schema omits `agent_type` or proves that field unsupported before any child starts; missing, shadowed, drifted, or foreign Role state is never fallback-eligible. The parent runs only the read-only Role preflight and private preparation rollout, shows native lifecycle activity, and returns the child's final public stdout; it does not execute Rescue inline or copy child stderr, tool output, raw conversation frames, or intermediate progress into the parent thread.
5256

5357
Rescue children use the task-independent native display base `zcode_rescue_task`, with a bounded ordinal on sibling collision. No objective or task text is encoded in this metadata. Names and paths are for navigation only: matching `zcode_rescue_*` neither proves Rescue nor grants authority; a different display name does not remove authority from an otherwise trusted Rescue child.

README.zh-CN.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ Rescue 有两种等价入口:显式 `$zcode:rescue` 是请求中字面且适
4848

4949
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。
5050

51+
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` 始终准备独立操作和新的 child。
52+
53+
legacy jobs-only 状态只会采用唯一且精确合格的续做候选;有歧义或旧 pending 状态会被拒绝而不是猜测。权限变化不能 resume 旧 binding,而 `--fresh` 会捕获当前 permission 快照。`SessionEnd` 会关闭结束 Codex session 的 Rescue binding,使其不能再次恢复。无效 binding、executor 不匹配、错误 workspace、已关闭 session 或 provenance 不一致都会 fail closed,不会 fallback 到 latest session。受管 Role 字节已经变化,因此 `role-status rescue` 可能返回 `upgrade-required`;继续前请重新运行 `$zcode:setup` 完成需要升级的 Role。
54+
5155
前台 Rescue 只在一个原生子线程中运行常量 forwarder。host 支持 `agent_type` 时,Codex 选择具名 `zcode-rescue` Role。generic child 只是 host-only 兼容回退:仅当当前 spawn schema 缺少 `agent_type`,或能证明该字段在任何 child 启动前已被拒绝时才允许;Role 缺失、被 shadow、漂移或属于外部配置时绝不回退。父线程只运行只读 Role preflight 和私有 prepare rollout、显示原生生命周期并返回 child 的最终公开 stdout;它不会 inline 执行 Rescue,也不会把 child stderr、工具输出、原始 conversation frame 或中间进度复制到父线程。
5256

5357
Rescue child 使用与任务无关的原生显示基名 `zcode_rescue_task`;同级名称冲突时会添加有界序号。该 metadata 不编码业务目标或 task 文本。名称和路径只用于导航:符合 `zcode_rescue_*` 规范既不能证明 child 是 Rescue,也不会授予 Rescue 权限;显示名称不同也不会移除一个已由可信链路确认的 Rescue child 的权限。

SECURITY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Report suspected vulnerabilities privately through GitHub's private vulnerabilit
1515
- Semantic progress is an allowlist over untrusted conversation frames. A command or query preview is one control-free line with a 96-character display bound, but truncation is not secret redaction. Never place credentials or authorization material in commands or searches; raw output, file contents, reasoning, and environment values are not allowed progress fields.
1616
- Child stderr and detailed progress stay in the child thread. Parent-visible output is limited to host lifecycle events and the final public result. Subscription or optional progress-sink failure is observational and cannot weaken the authoritative completion guard.
1717
- Rescue task material exists in the routing rollout only as the Root parent's single LF-terminated JSON line sent through `write_stdin` to `prepare rescue`. The companion requires a raw-capable TTY and enables raw mode before emitting task-free readiness and before accepting any task bytes; readiness is nonterminal. Root sends no EOF or U+0004. Non-TTY/readiness/raw-mode failure stops before delivery. Tool output must never contain or echo the payload. Prepared state is bound to the exact Codex session, initiating turn, canonical workspace, and executor identity; it is single consume, has a bounded expiry, and is subject to private-state cleanup. The task, source, and options must never appear in argv, environment variables, output, logs, artifacts, relays, status, task names, or any child assignment/transcript. Named and generic children are task-blind and capability-free and receive only the constant `invoke-prepared rescue` assignment.
18+
- A durable Rescue binding authorizes the same stopped child to continue only its exact ZCode session. The private `anchorJobId` and `currentJobId` remain inside protected plugin state and never cross the parent-to-child message boundary. Missing, closed, corrupt, permission-incompatible, workspace-mismatched, executor-mismatched, or provenance-inconsistent bindings fail closed.
1819
- Uninstall does not automatically erase stable plugin data, managed Role artifacts, durable jobs, or user-config leaves. Verify receipt-based ownership before removing residue. ZCode does not own the host's `hide_spawn_agent_metadata`; only complete numeric-v1 evidence authorizes removal of the exact legacy target-layer `false`, never a foreign, project-layer, true, or unproven value.
1920

2021
Only the latest release receives security fixes. Rotate exposed credentials and disable the plugin until a compromised capability has expired or its Codex turn has ended.

agents/zcode-rescue.toml.template

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
developer_instructions = """
22
You are the installed ZCode Rescue forwarder. You are task-blind and capability-free. Accept only the exact initial assignment, one of the two exact continuation assignments, or an exact status intent documented below. Run only the command mapped to that assignment in the current workspace, preserve stderr, and return public stdout verbatim and byte-for-byte. Never inspect or modify code independently, interpret results, retry an assignment, cancel, choose a pending branch, or request/print/persist authorization material. Do not accept task text, command arguments, source, options, job or session identity, workspace identity, permissions, credentials, or authorization values in the assignment. Reject every other assignment without running a command.
33

4+
The same exact prepared assignment is valid for either the initial turn or a stopped same-child prepared continuation selected by the parent. The one-command-per-turn rule applies to both. The assignment alone does not prove the sender or binding: run only its mapped companion command, which validates the exact executor and private binding before work starts.
5+
Reject every non-exact assignment, arbitrary message, nested Rescue request, and independent repository work without running a command.
6+
47
Each exact assignment and child turn may start at most one mapped foreground `exec_command` companion process. Never start concurrent or retry foreground executions for the same assignment. Same-turn continuation calls only observe that turn's original running handle. The one expressly allowed status sidecar below is observational and does not replace that foreground process. A companion result containing an exit code is terminal. A result containing a running execution or session handle is nonterminal: poll only that same handle with the host continuation tool until it reports an exit code. Partial stdout, stderr, heartbeat text, or an outer code-cell completion is not terminal and must not be returned as final output. A needs-choice response with exit code 3 is terminal for the current child turn. After that initial needs-choice terminal, the next exact parent continuation assignment may start one new exact `invoke-choice` foreground handle in the same child.
58

69
For every result yielded by the original foreground handle, parse only complete dedicated `[zcode-relay]` lines. Before relay, require JSON with exact keys `version`, `sequence`, `phase`, `code`, and `observedAt`; require version 1, a positive bounded strictly increasing sequence, an allowlisted phase/code pair, and a valid bounded RFC3339 timestamp. Map only through this fixed allowlisted code-to-message map: `started` -> `ZCode Rescue started.`; `model-active` -> `ZCode is generating a response.`; `tool-active` -> `ZCode is working with a tool.`; `editing` -> `ZCode is applying workspace changes.`; `verifying` -> `ZCode is verifying the work.`; `waiting` -> `ZCode Rescue is still running.`; `finalizing` -> `ZCode Rescue is finalizing.`. Coalesce a repeated identical phase. If the native `send_message` tool is available, use `send_message` only to `/root` with the fixed mapped message. If it is unavailable or relay fails, continue polling the original handle. Relay is liveness only and never completion.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
status: accepted
3+
supersedes: stopped-rescue-choice-continuation-in-adr-0010
4+
---
5+
6+
# Bind a stopped Rescue child to its exact ZCode session
7+
8+
## Decision
9+
10+
Root may send the constant `invoke-prepared rescue` assignment to the same stopped child after privately preparing a new turn. The durable binding keeps the original `anchorJobId` and advances `currentJobId` when the continuation job is durably reserved and published, even if that job later queues, fails, or is cancelled. Those identifiers, the task, permissions, workspace identity, and executor provenance remain private plugin state and never enter a child assignment.
11+
12+
An active exact child is rejoined without preparation or invocation. A stopped exact same-operation child is re-authorized by the new prepared turn and followed up without another spawn or `SubagentStart`. A fresh or independent operation prepares `fresh` and creates a new child. Root alone decides these semantics; the child only executes its fixed assignment once per child turn.
13+
14+
## Compatibility and lifecycle
15+
16+
Legacy jobs-only state may be adopted only when it supplies one exact eligible candidate and no conflicting pending state. A permission change prevents resume and requires an explicit fresh operation to capture the current permission snapshot. `SessionEnd` closes the ending Codex session's whole binding partition. Missing, invalid, closed, corrupt, ambiguous, wrong-workspace, wrong-executor, or provenance-mismatched state must fail closed without selecting a latest session or another child.
17+
18+
This replaces ADR 0010 only where that decision treated a stopped child as reusable solely for the immediate `needs-choice` exchange. Choice continuation remains same-child; this decision also permits a later prepared turn for the exact durably bound operation.

0 commit comments

Comments
 (0)