@sema-agent/core 5.13.0 → 5.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/CHANGELOG.md +418 -0
  2. package/dist/agents/send-message-tool.js +1 -0
  3. package/dist/agents/subagent.d.ts +4 -0
  4. package/dist/agents/subagent.js +133 -41
  5. package/dist/brain/anthropic.js +33 -10
  6. package/dist/brain/context-overflow.d.ts +20 -0
  7. package/dist/brain/context-overflow.js +58 -0
  8. package/dist/brain/errors.js +21 -1
  9. package/dist/brain/open-responses.js +24 -10
  10. package/dist/brain/openai.js +29 -11
  11. package/dist/brain/request-params.d.ts +2 -0
  12. package/dist/brain/request-params.js +16 -0
  13. package/dist/brain/retry.d.ts +5 -0
  14. package/dist/brain/retry.js +16 -4
  15. package/dist/brain/stream-engine.d.ts +9 -1
  16. package/dist/brain/stream-engine.js +261 -28
  17. package/dist/brain/timeout.d.ts +1 -0
  18. package/dist/brain/timeout.js +1 -0
  19. package/dist/core/a2a.d.ts +2 -2
  20. package/dist/core/a2a.js +3 -3
  21. package/dist/core/ask-question.d.ts +47 -2
  22. package/dist/core/ask-question.js +209 -28
  23. package/dist/core/background-agent-store.d.ts +2 -0
  24. package/dist/core/checkpoint-store.d.ts +41 -17
  25. package/dist/core/checkpoint-store.js +137 -6
  26. package/dist/core/hooks.d.ts +24 -2
  27. package/dist/core/hooks.js +97 -10
  28. package/dist/core/human-input-projection.d.ts +12 -0
  29. package/dist/core/human-input-projection.js +27 -0
  30. package/dist/core/mcp.d.ts +7 -2
  31. package/dist/core/mcp.js +7 -7
  32. package/dist/core/memory-admission.d.ts +4 -0
  33. package/dist/core/memory-admission.js +3 -0
  34. package/dist/core/memory-engine/engine.d.ts +3 -1
  35. package/dist/core/memory-engine/engine.js +4 -3
  36. package/dist/core/memory-recall.d.ts +1 -1
  37. package/dist/core/memory-recall.js +3 -2
  38. package/dist/core/runner/assemble-result.d.ts +1 -0
  39. package/dist/core/runner/assemble-result.js +1 -1
  40. package/dist/core/runner/prepare-memory.d.ts +1 -0
  41. package/dist/core/runner/prepare-memory.js +3 -3
  42. package/dist/core/runner/prepare-task.d.ts +19 -6
  43. package/dist/core/runner/prepare-task.js +343 -35
  44. package/dist/core/runner/runtask.d.ts +3 -6
  45. package/dist/core/runner/runtask.js +290 -64
  46. package/dist/core/runner/tool-disclosure.d.ts +5 -0
  47. package/dist/core/runner/tool-disclosure.js +65 -16
  48. package/dist/core/runner/tool-output-projection.js +1 -0
  49. package/dist/core/runner/turn-attachments.d.ts +4 -0
  50. package/dist/core/runner/turn-attachments.js +15 -2
  51. package/dist/core/session-store.d.ts +3 -0
  52. package/dist/core/session-store.js +4 -0
  53. package/dist/core/session.d.ts +1 -0
  54. package/dist/core/store-contracts/background-agent-store-contract.js +19 -0
  55. package/dist/core/store-contracts/checkpoint-store-contract.js +62 -3
  56. package/dist/core/task-notification.d.ts +2 -0
  57. package/dist/core/task-notification.js +5 -3
  58. package/dist/core/task-registry-agent.d.ts +1 -0
  59. package/dist/core/task-registry-agent.js +6 -0
  60. package/dist/core/task-registry.d.ts +1 -0
  61. package/dist/core/task-registry.js +4 -1
  62. package/dist/core/task-tool-shape.js +4 -3
  63. package/dist/core/tool-policy.d.ts +5 -0
  64. package/dist/core/tool-policy.js +2 -1
  65. package/dist/core/trace.d.ts +6 -0
  66. package/dist/core/types.d.ts +32 -1
  67. package/dist/core/wiring-manifest.d.ts +97 -0
  68. package/dist/core/wiring-manifest.js +186 -0
  69. package/dist/engine/compaction/compaction.js +2 -2
  70. package/dist/engine/harness/agent-harness.d.ts +2 -1
  71. package/dist/engine/harness/agent-harness.js +8 -1
  72. package/dist/engine/harness/types.d.ts +3 -1
  73. package/dist/engine/llm/types.d.ts +7 -0
  74. package/dist/engine/llm/types.js +8 -1
  75. package/dist/engine/session/import-validate.d.ts +6 -1
  76. package/dist/engine/session/import-validate.js +29 -6
  77. package/dist/engine/session/memory-repo.d.ts +3 -1
  78. package/dist/engine/session/memory-repo.js +2 -2
  79. package/dist/index.d.ts +10 -7
  80. package/dist/index.js +8 -5
  81. package/dist/internal/harness-types.d.ts +1 -1
  82. package/dist/internal/llm.d.ts +2 -2
  83. package/dist/internal/llm.js +1 -1
  84. package/dist/orchestration/run-workflow-tool.d.ts +4 -0
  85. package/dist/orchestration/run-workflow-tool.js +3 -0
  86. package/dist/orchestration/workflow-types.d.ts +8 -0
  87. package/dist/orchestration/workflow-types.js +14 -0
  88. package/dist/orchestration/workflow.d.ts +4 -0
  89. package/dist/orchestration/workflow.js +134 -5
  90. package/dist/prompts/default.js +1 -1
  91. package/dist/prompts/supervisor.d.ts +2 -2
  92. package/dist/prompts/supervisor.js +5 -4
  93. package/dist/stores/file/checkpoint-store.d.ts +3 -5
  94. package/dist/stores/file/checkpoint-store.js +31 -2
  95. package/dist/stores/file/index.js +1 -1
  96. package/dist/stores/file/session-store.d.ts +3 -1
  97. package/dist/stores/file/session-store.js +2 -2
  98. package/dist/stores/file/shared-ledger.js +8 -1
  99. package/dist/tools/fs/bash-readonly-classifier.d.ts +3 -0
  100. package/dist/tools/fs/bash-readonly-classifier.js +94 -0
  101. package/dist/tools/fs/fs-bash.d.ts +1 -0
  102. package/dist/tools/fs/fs-bash.js +32 -13
  103. package/dist/tools/fs/gh-rate-limit.d.ts +1 -1
  104. package/dist/tools/fs/gh-rate-limit.js +4 -3
  105. package/dist/tools/fs/index.d.ts +1 -0
  106. package/dist/tools/fs/index.js +1 -0
  107. package/dist/tools/fs/safety.js +34 -10
  108. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,423 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.15.0 — 2026-08-06
4
+
5
+ ### BREAKING
6
+
7
+ - **`TaskSpec.toolMaterializeStrategy` defaults to `"swap"` again** (it defaulted to `"static"` for one
8
+ release window). Under `"static"` a deferred tool's placeholder never swaps: the tools block keeps
9
+ advertising `{"type":"object","properties":{}}` and the real schema reaches the model only as text in
10
+ the `ToolSearch` result. That is fine for a model whose tool arguments are free-form JSON, and
11
+ structurally unusable for one served with **constrained decoding**, where the advertised wire schema is
12
+ enforced at the sampler. On the mainstream implementations — grammar compiled from the declared
13
+ `properties` — `{}` becomes the only argument object the model can emit, so the engine's corrective
14
+ invalid-arguments rejection cannot be acted on and the run burns turns without converging. (A strictly
15
+ draft-faithful decoder would read the absent `additionalProperties` as permissive; the model still has
16
+ no schema to form other keys from, so that path degrades to guessing rather than converging.)
17
+ The condition for `"static"` is also a property of the WHOLE serving route rather than of one model —
18
+ the degrading brain can fall back across providers mid-run and `limits.degrade` can hand the run to a
19
+ different model outright, and neither rebuilds the disclosure strategy — so it is now a strict explicit
20
+ opt-in: set it only if every model the run can reach, fallback targets included, has been verified to
21
+ read schemas from result text. **Who is affected:** a deployment that relied on the default to keep its
22
+ provider prompt cache intact across activations now sees the pre-`"static"` behavior (activation calls
23
+ `setTools`, invalidating the cache suffix) unless it sets `toolMaterializeStrategy: "static"` itself.
24
+ **Pinned faces that move with the default:** the `tools_delta` attachment says "now available … Their
25
+ full schemas are loaded" (not "now active"), the `ToolSearch` description says activation "loads its
26
+ full parameter schema" (not "returns … in the result"), the `ToolSearch` result no longer inlines
27
+ `parameters:` per matched tool, and the post-activation tools block carries the real schema again — a
28
+ consumer pinning any of these under the default must re-pin. `SEMA_TOOL_MATERIALIZE_STRATEGY` still
29
+ fills an absent spec value, and an env-sourced `"static"` on a `deferSelfResolve: false` task still
30
+ degrades to `"swap"` (the explicit spec pairing stays refused with `config.tool_materialize_unreachable`).
31
+
32
+ ### Added
33
+
34
+ - **`prompt.assembled` reports the resolved deferred-tool disclosure strategy** —
35
+ `toolDisclosure: {deferredTools, strategy, source}` (new exported type `ToolDisclosureManifest`;
36
+ absent when the leg deferred nothing). `strategy` is the EFFECTIVE value after the direct-call-lane
37
+ interlock, and `source` names the seat that chose it: `"spec"` / `"env"` / `"default"` /
38
+ `"degraded_no_direct_lane"` (the interlock narrowed a `"static"` request to `"swap"`; that value
39
+ implies an env-sourced request, since the spec pairing is refused at prepare and the default is not
40
+ `"static"`). "Which strategy did this leg run under, and who chose it" was previously answerable only
41
+ by re-deriving spec > env > release default by hand.
42
+
43
+ ### Fixed
44
+
45
+ - **Model-visible text stops teaching retired tool names** (#181 census F batch). Three faces taught
46
+ names a model cannot call (RB-476-A retired alias resolution — a retired name is a loud roster
47
+ miss): the selective-recall manifest affordance said "call Recall" (a name core registers as
48
+ removed; the affordance's tool name is now caller-supplied via `composeSelectiveBody`'s new
49
+ optional `recallToolName` parameter, name-free when omitted); both `ORCHESTRATION_GUIDANCE`
50
+ blocks said `run_workflow` — on the deferred short form a complete dead link, since `ToolSearch`'s
51
+ `select:` arm matches exact wire names — and now interpolate `RUN_WORKFLOW_TOOL_NAME`
52
+ (`"Workflow"`); TaskOutput's lanes description and `task_id` parameter said `RunWorkflow` and now
53
+ say `Workflow`. A consumer pinning any of these strings must re-pin. A new gate
54
+ (`test/tool-name-literal-gate.test.ts`) scans every src string literal against the whole
55
+ `RETIRED_TOOL_NAMES` table with a frozen per-bucket ratchet, so this class cannot regrow silently.
56
+ - **The `# Memory` write instruction requires the Write tool on the roster** (behavior face). The
57
+ memory engine's injection gated the CC-verbatim write instruction on a writable scope alone;
58
+ a `handsReadOnly` (or hands-less) run with a memory `writeScope` was instructed to write with a
59
+ tool its roster does not carry. `MemoryEngine.inject` takes an optional `writeToolMounted`
60
+ (omitted ⇒ historic behavior for engine-direct hosts; the Runner passes the real hands-write-half
61
+ predicate), and the RB-276 index seed follows the same gate. **Who is affected:** a deployment
62
+ running memory-enabled tasks with `handsReadOnly: true` (or no execution env) no longer receives
63
+ the `# Memory` instruction or the index read-seed on those runs — the fenced index still injects.
64
+ - **The gh rate-limit hint's Monitor clause follows the real Monitor mount.** The SR-2 reminder
65
+ unconditionally closed with "use the Monitor tool"; Monitor mounts only when background task tools
66
+ do. `ghRateLimitHint` (and `HandsToolkitOptions`/`createBashTool` `monitorToolActive`) drop exactly
67
+ that clause when the caller declares Monitor unmounted; omitted keeps the byte-identical historic
68
+ wording.
69
+ - **`ToolSearch` no longer activates a name the roster has withdrawn.** The deferred registry is frozen
70
+ at prepare; the roster is not — an MCP refresh can take a deferred name off the mount. An exact
71
+ `select:` still resolved through the stale registry, added the name to the active set, "succeeded" at
72
+ a rematerialize that mounted nothing, and announced a tool that could not be called. Withdrawn names
73
+ are now reported as `No longer available: <names> — withdrawn by its provider and cannot be activated
74
+ or called`, ride the result's `details.missing`, and never enter the active set.
75
+ - **The deferred placeholder's corrective round is now bounded.** A shape-invalid direct call is answered
76
+ with the real declared schema so the corrected call is one turn away — which assumes the caller can act
77
+ on a schema once it has one. A caller whose arguments are constrained to the ADVERTISED schema cannot,
78
+ and re-sent the identical rejected arguments for as many turns as the budget allowed. After
79
+ `DEFERRED_NO_PROGRESS_LIMIT` (3) consecutive rejections of the IDENTICAL failure shape on one
80
+ placeholder, the lane stops re-teaching: it returns a terminal rejection that names the condition with
81
+ the stable token `tool.deferred_schema_incompatible` (in the model-facing text and on the tool result's
82
+ `details.noProgress`) and votes the tool batch terminated. Any other outcome — a different validation
83
+ error, or a successful call — resets the count, so the bound tracks being STUCK, not being wrong.
84
+ `TaskResult.errorCode` is unchanged: this terminal is reported on the tool result, not as a task
85
+ failure code.
86
+ - **The invalid-arguments rejection no longer overstates where its schema lives.** It used to say "use it
87
+ for this and later calls" under both strategies; under `"static"` the schema is carried by that one
88
+ result and is gone with the next compaction. The text now says either "the next request's tools list
89
+ will advertise it too" (swap) or "carried by THIS result only" plus the `ToolSearch` re-select spelling
90
+ (static). A declaration too large to inline is called an "ABRIDGED copy" instead of "full" — past the
91
+ model-facing bound the serialization comes back with its middle spliced out and is not valid JSON.
92
+ - **A declaration the static face cannot carry in text is exempted from the static face per tool.** Under
93
+ `"static"` the only in-context schema carrier is result text; a declaration that does not serialize, or
94
+ that the model-facing bound truncates, has no carrier there, and the tool was announced as activated
95
+ with a schema the model never received in usable form. Such a tool now materializes into the tools block
96
+ on activation (the rest of the run stays static), the `ToolSearch` line says where its declaration is,
97
+ and the result head stops promising schemas it did not print. The `tools_delta` boundary frame carries
98
+ the same exception (one `Exception: <names> — too large to inline…` line) instead of asserting compact
99
+ placeholders for a name whose full schema is visibly on the wire; with no exemptions in play the frame
100
+ is byte-identical to before. The exemption is read from the LIVE roster on every call, so an MCP
101
+ refresh that changes a declaration's size cannot leave the lane describing the old carrier — and the
102
+ exemption is MONOTONE within a leg: a declaration that shrinks back under the bound does not put the
103
+ compact placeholder back on a tool whose schema was only ever delivered through the tools block.
104
+
105
+ ### Fixed (receive-time additions)
106
+
107
+ - Ruled at receive: the deferred no-progress terminal stays a TOOL-RESULT face (`tool.deferred_schema_incompatible`
108
+ marker + `details.noProgress`) and does NOT mint a `TaskResult.errorCode` — the run can continue on other
109
+ tools, so a run-level code would misclassify a per-tool condition.
110
+ - Ruled at receive: a malformed `SEMA_TOOL_MATERIALIZE_STRATEGY` still refuses the task loudly even when a
111
+ valid spec value is present — a deployment running with a broken env var must hear about it before the
112
+ first task that omits the spec value inherits the typo.
113
+
114
+ ## 5.14.0 — 2026-08-06
115
+
116
+ > **Post-release addendum (2026-08-06, disclosure gap reported by a consumer):** the fail-open survey
117
+ > (shipped in 5.13.0's window, but its consequence surfaced against 5.14.0 pickups) hardened
118
+ > `canonicalizeTarget`'s existence probe — an ERRORED `exists` no longer reads as "does not exist yet"
119
+ > and instead reports `{ok:false, unresolvedSymlink:true}`. Combined with `createSensitivePathPolicy`'s
120
+ > deny on `unresolvedSymlink`, an execution env whose `exists` always throws (a stub/lexical env with
121
+ > no real filesystem) now sees every write target denied — the gate degrades from "guarded" to
122
+ > "closed". That is the intended fail-closed direction for a PROBE FAILURE, but such an env should
123
+ > answer honestly instead of throwing: return `ok(false)` ("this env cannot see a filesystem") and the
124
+ > gate resolves normally. Also: `DelegationTaskType` was named in the ship note but missed the package
125
+ > root; the export lands in the next release — type the `taskType` key as an open `string` until then.
126
+
127
+ ### BREAKING
128
+
129
+ - **`CheckpointStore.setPendingSteer` APPENDS to a bounded ordered queue instead of overwriting a single seat**
130
+ (design/171 §5.3/§6.1/§6.3). Two people steering one durably-suspended run used to mean the second steer
131
+ silently destroyed the first — an undelivered operator instruction, gone with no record that anything was
132
+ lost. `CheckpointState` gains `pendingSteerQueue: PendingSteerEntry[]`
133
+ (`{text, trusted, actor?, seq, inputId, priority?}`); resume drains the WHOLE queue in `seq` order, each
134
+ entry framed under its own trust semantics. New exports: `readPendingSteerQueue` (the single read point —
135
+ it folds a pre-queue row's legacy `pendingSteer` seat in as member 0, so old checkpoints deliver unchanged
136
+ with no migration step), `appendPendingSteer` (the shared append every backend must build its next queue
137
+ with: bounds + `seq` mint live there, not once per store), `PENDING_STEER_QUEUE_BYTE_BUDGET_BYTES` (48,000 —
138
+ under a MySQL `TEXT` column with UTF-8 headroom), `MAX_PENDING_STEER_ENTRIES` (derived from it and
139
+ `MAX_PENDING_STEER_CHARS`), `PENDING_STEER_FROZEN_FIELDS` / `ACTOR_ASSERTION_FROZEN_FIELDS`, and the
140
+ `ActorAssertion` type (`{id (namespaced), hostAsserted, issuer?}` — attribution ONLY, never authority;
141
+ `hostAsserted` must be derived by the host from ingress credentials and never taken from a request body).
142
+ Bounds are fail-loud: crossing either throws the **new error code `steering.queue_full`** rather than
143
+ evicting an accepted instruction. `validatePendingSteer` now REFUSES an unknown field
144
+ (`steering.invalid_content`) instead of dropping it, so a producer one release ahead learns its field would
145
+ not survive the parked leg. Idempotency moved from last-writer-wins to the `inputId` key: re-appending the
146
+ same `inputId` with an IDENTICAL payload is a no-op returning `true`, while the same `inputId` carrying
147
+ DIFFERENT content is refused with the **new error code `steering.duplicate_input_id`** (swallowing it would
148
+ reintroduce exactly the silent instruction loss the queue replaces); an absent `inputId` is minted (uuidv7),
149
+ and the synthetic id a pre-queue seat reads back as is reserved against caller supply. The file backend's
150
+ ledger replay also gained a fail-loud arm: an event kind the reader does not recognise now refuses the whole
151
+ replay (`checkpoint.unsupported_version`) instead of folding in as a silent no-op — **note that a binary
152
+ released BEFORE this one still ignores unknown kinds, so rolling back across the queue event loses steers
153
+ parked in the un-compacted ledger tail.**
154
+ **Consumer flips**: ① any code reading `state.pendingSteer` directly sees `undefined` for every new write —
155
+ read through `readPendingSteerQueue`; ② probes pinning "a second setPendingSteer overwrites the first" red;
156
+ ③ a custom `CheckpointStore` must round-trip BOTH state fields and build its next queue with
157
+ `appendPendingSteer` (the cross-backend contract kit gained the queue arms and will fail it otherwise);
158
+ ④ `steering.queue_full` and `steering.duplicate_input_id` are new members of the `CheckpointError` code
159
+ union — exhaustive consumers add both arms.
160
+
161
+ ### Added
162
+
163
+ - **Shared-session attribution (design/171, core half).** Four pieces, one contract: ① `UserMessage.actor`
164
+ carries an `ActorAssertion` (attribution ONLY, never authority; snapshot-minted at the API boundary, wire
165
+ metadata stripped). ② ONE projection point (`human-input-projection.ts`, exported `projectHumanInput` /
166
+ `buildHumanInputEvent`) renders the speaker envelope for every human-input carrier — objective, live steer,
167
+ nextTurn, parked-steer resume frames, wake — as `[from "<id>"]` (+ ` (unverified)` when the host did not
168
+ derive the identity from ingress credentials), through the same neutralizers as the external-notification
169
+ header, always INSIDE the trust frame it attributes. No actor, or `source:"system"` ⇒ byte-identical
170
+ passthrough, so every pre-171 caller and single-user host is pinned unchanged. ③ **New TaskEvent arm
171
+ `human_input`** — the lifecycle ledger of who fed the run what: `{inputId, sessionSeq (leg-scoped),
172
+ carrier, source, delivery, issuer?, actor?, principal?, entryId?}`; live emissions precede the commit and
173
+ omit `entryId` (join via `message_committed` + stream order). Exhaustive TaskEvent switches need an arm.
174
+ ④ The compaction attribution invariant: speaker labels survive summarization across all three summary-path
175
+ variants (the summarizer is instructed to preserve `[from …]` attributions).
176
+
177
+ - **Assembly self-evidence (design/173, all eight items).** The engine can now PROVE what it wired instead of
178
+ the operator inferring it from behavior:
179
+ - `describeStaticWiring(spec, deps)` (package-root export) — the pure static half of the wiring manifest;
180
+ per-leg EFFECTIVE manifests are built at prepare and emitted as the **new TaskEvent arm `wiring_manifest`**
181
+ (root/child/resume legs each emit their own). Together with `human_input` above this release takes
182
+ `TaskEvent` **16→18** — an exhaustive switch adds TWO arms, not one. A child leg's manifest rides the
183
+ CHILD's own stream and is deliberately not in the parent-forwarding whitelist: "every leg self-evidences"
184
+ means on its own stream; a host that wants a delegated child's manifest subscribes to the child. The manifest covers ask (form/provenance/effective),
185
+ question (three-valued channel state incl. the engine-stripped bg lane, plus the composition-lie flag
186
+ `interactiveToolsWithoutDeliveryFace`), elicit, the park lane (capability vs effective policy, reason
187
+ codes, checkpoint durability), session-store durability, fleet seats, and a governance presence section
188
+ stamped `audience:"operator"` — **a serving layer forwarding this event to a multi-tenant stream MUST
189
+ project the governance section for operators only; no projection ⇒ do not disclose**. `configFingerprint`
190
+ (sha256 prefix over the manifest's own resolved facts) correlates legs that ran under the same assembly.
191
+ - **Store durability declarations**: `SessionStore`/`CheckpointStore` gain `readonly durability?`
192
+ ("durable" | "process-local"); absent reads fail-closed as process-local, unparseable is refused loud
193
+ (`config.store_durability_invalid`). Built-ins declare themselves; an armed park lane over a
194
+ process-local store is now a REPORTABLE degrade shape instead of a silent one.
195
+ - **`TaskSpec.interactionPosture` / `RunnerDeps.interactionPosture`** ("interactive" | "headless") with a
196
+ fail-loud prepare door: posture `"interactive"` refuses a leg whose resolved wiring cannot reach a human
197
+ (`config.interaction_posture` / `config.interaction_wiring`) instead of silently auto-answering. Child
198
+ legs resolve `spec ?? parent posture (trusted internals) ?? deps`. A deployment that declares interactive
199
+ at deps level must supply a persistent question face for session-scoped bg legs — the door refusing that
200
+ leg is the honest reading of an unfulfillable declaration.
201
+ - **AskUserQuestion's three exits mint an in-band card** (`details.type: "ask-question"`, a new
202
+ `CC_DETAIL_TYPES` member): `continuationSource: "human_response" | "synthetic_self_answer_instruction"`
203
+ plus reason codes (a THREE-member closed set: `seam_absent` / `callback_failed` /
204
+ `declined_unavailable`), so a consumer can tell a human answer from the engine's self-answer fallback
205
+ without prose-matching. The card is a two-arm union — the coded-failure arm (`question.human_channel_failed` /
206
+ `question.human_unavailable`) carries neither `continuationSource` nor `runContinues`, so consumers
207
+ must branch on `code` FIRST and only then read `continuationSource`. Under posture `"interactive"`, the callback-failed
208
+ arm becomes a coded failure (`question.human_channel_failed`) unless the explicit
209
+ `interactiveQuestionFallback` knob opts back into degrade; the degrade path emits onError
210
+ (phase `"degraded"`, classification `"no-human-autoanswered"`).
211
+ - **The four question-face strip sites pair-produce a disclosure flag** through one helper: when the engine
212
+ deliberately strips a spawn turn's per-request question face from a bg/fork child, the manifest reports
213
+ the channel as `"stripped_bg_lane"` (flag minted only when a face was actually stripped).
214
+ - **`effectiveShellGate === "off"` with a real writable shell now leaves a per-prepare forensic note**
215
+ (onError phase `"config"`, classification `"shell-gate-off"`) — the assembly state that previously had
216
+ zero observable trace. onError `classification` is filled on the config phase for the first time.
217
+ - `isDelegatedAgentTerminal` is re-exported at the package root so a serving layer's escalation code shares
218
+ core's own terminal classification.
219
+
220
+ - **The brain honors a provider's explicit retry verdict, and recovers from the context-overflow 400.**
221
+ ① The `x-should-retry` response header is now read double-sided: `true` retries a normally-terminal status,
222
+ `false` stops retrying a normally-retryable one; header absent ⇒ prior behavior byte-identical.
223
+ ② The `input length and max_tokens exceed context limit` 400 (previously terminal `invalid_request`) now
224
+ adaptively lowers `max_tokens` and retries, with two loud guards: a floor (3000 output tokens — at the
225
+ floor and still overflowing ⇒ give up) and a no-progress check (same error after a cut ⇒ give up); every
226
+ cut is disclosed as a status frame. The narrow error-shape parser refuses anything it does not recognize
227
+ (prior terminal behavior preserved).
228
+
229
+ - **Question sync-first (design/174).** With a live question face, an adjudicated `AskUserQuestion` is
230
+ answered in-stream — same turn, no checkpoint; the durable park is what happens when nobody is there
231
+ (no live face / the face affirms nobody is reachable / `forceDurableGate` — and the third park-lane arming,
232
+ the safety vocabulary, behaves the same: **any park-lane arming keeps the park**, sync-first only applies
233
+ to the durable-approval posture with a live face). Assemblies without a live face are pinned byte-identical
234
+ to the previous release. `OnQuestion` may now return `{kind: "unavailable"}` — implementers additive,
235
+ **callers BREAKING** (`.answers` direct access goes red; `isQuestionUnavailable` is exported for relaying
236
+ wrappers). `AskQuestionRequest` gains `boundInputHash` and an engine-minted per-delivery `deliveryId`
237
+ (the identity; `(taskId, toolCallId, boundInputHash)` repeats for two identical sequential questions and is
238
+ reconciliation data only). The question tool executes in its own batch (`executionMode: "sequential"`).
239
+ New closed-set members: `SyntheticContinuationReason` += `declined_unavailable`; card `code` +=
240
+ `question.human_unavailable`; onError classification += `"unconsumed-human-answer"` and
241
+ `"interaction-posture-refused"`.
242
+
243
+ - **Pre-release verification hardening (test AI's A/B groups + the convergence review).**
244
+ - A question outcome claiming NEITHER arm (no `unavailable` discriminant, no `answers` array — a failed
245
+ decode) is now refused as a delivery failure instead of being read as "the user selected nothing";
246
+ the explicit `{answers: []}` stays a real answer. The resume lane applies the same rule pre-CAS and
247
+ captures the redeemed answer ONCE (`structuredClone`) — the persisted outcome and the delivered answer
248
+ are the same snapshot, so a stateful getter can no longer make audit and delivery diverge.
249
+ - **`TaskResult.strandedHumanAnswers`** (new field): `{deliveryId, toolCallId}` records of questions a
250
+ person answered that no call executed to collect — keyed by the engine-minted delivery identity, since
251
+ call ids can repeat (two lost answers on one call id are two records). This is the MANDATORY disclosure
252
+ face; the `onError` (classification `"unconsumed-human-answer"`) alert remains as an additional lane.
253
+ An answer arriving after the result is already terminal can only reach `onError` — the structural limit
254
+ of a result face; a value that does not READ as an answer is never recorded as one.
255
+ - **`AskRequest.riskAxes`** (additive): `{irreversible?, egress?}`, filled by the gate from the SAME
256
+ resolved axes the safety tightens read (declared tiers + `toolAxes` folds + shellGate marks; `"maybe"`
257
+ counts as irreversible). Either axis ABSENT = the engine did not judge it — consumers must render
258
+ "unjudged", never fold absence to `false`. An EXPLICIT caller negative (`egress:false` /
259
+ `irreversibility:"never"` via MCP/A2A `toolAxes`) now survives the override algebra and reports as a
260
+ judged `false` — enforcement stays tighten-only and ignores negatives.
261
+ - **`WiringManifest.interaction.posture`** (new section): the posture declaration ("interactive" /
262
+ "headless" / "absent") joins the manifest — previously the one interactive-face declaration it did not
263
+ report. A posture refusal now also emits an onError frame (phase `"config"`,
264
+ classification `"interaction-posture-refused"`): the coded `TaskResult.errorCode` remains the primary
265
+ carrier (neither entry point throws), the frame serves hosts wired only to `onError`.
266
+ - **`configFingerprint` now hashes the ASSEMBLY, excluding leg identity AND seat provenance** (the
267
+ delegation lane re-homes a parent's deps face onto the child's spec seat — same callback, different
268
+ seat) — root/child/resume legs of one assembly really do share a fingerprint (its advertised use).
269
+ Fingerprint VALUES change with this release; pin the equality relation, not literals.
270
+ Presence condition for the resume leg: a resume's assembly comes from the `ResumeTaskConfig` you
271
+ pass, not from the root's spec — omit a seam face there (e.g. `onQuestion`) and the resume leg
272
+ HONESTLY reports a different assembly (and a different fingerprint), because that leg really has no
273
+ in-stream answerer. Re-supply the same faces on resume to keep the fingerprint equal.
274
+ - The synthetic-continuation disclosure dedup is **per ask-call** (key = tool-call id; one call may carry
275
+ 1–4 questions and discloses once) and scoped to ONE leg — a continuation leg re-discloses, and a host
276
+ aggregating across legs dedups on `(sessionId, toolCallId)` itself.
277
+
278
+ - **Capture-domain narrowing (design/174 convergence review, rounds 9–14).** Deployment-supplied resume
279
+ values are read ONCE and captured as canonical plain data; the canonicalizer refuses what the JSON
280
+ checkpoint store cannot faithfully round-trip. Concretely:
281
+ - resume `updatedInput` and a content-ask `answer` now REFUSE (pre-CAS, `checkpoint.invalid_outcome`,
282
+ checkpoint stays pending) values carrying Map/Set/Date/buffers/class instances, shared references or
283
+ cycles, sparse or expando arrays, non-finite numbers, bigint/symbol/function anywhere in the tree;
284
+ object keys with `undefined` values drop and `-0` normalizes to `0` (JSON semantics, applied at capture
285
+ so audit, replay comparison and execution all read the same value);
286
+ - `decision` is domain-checked at capture: anything other than `"allow"`/`"deny"` refuses pre-CAS
287
+ (previously an unknown word could fall past the deny check into the execute arm);
288
+ - `updatedInput: null` flows into the tool's own validation and fails THERE, instead of silently
289
+ executing the ORIGINAL arguments while the record said the input was rewritten;
290
+ - the gate discriminant and every `policy_ask` field are read once into a plain twin consumed by all
291
+ guards, the env-failed replay equality, persistence and the answer face; array equality visits every
292
+ index and compares named non-index keys;
293
+ - upgrade note: a winner persisted RAW by an older release still replays (equality compares canonical
294
+ forms — no checkpoint migration), but an OLD row whose winner is outside the new domain fails closed
295
+ on replay; the row stays recoverable by re-deciding.
296
+ - The direct tool lane applies the same one reader: an answer payload with a throwing accessor refuses
297
+ the WHOLE outcome (no partial salvage of well-formed siblings), and `{}` is a failed decode, not
298
+ "selected nothing" — `{answers: []}` remains the legitimate empty selection.
299
+
300
+ - **`AskRequest.boundInputHash`** (additive): the canonical digest of the presented args, minted once at
301
+ the `resolveAsk` chokepoint over the retained snapshot — the SAME `boundInputHashOf` digest a durable
302
+ park binds its checkpoint to, so an aggregating approver can reconcile a synchronous ask row against a
303
+ parked checkpoint row for the same call on `(toolCallId, hash)` equality. Reconciliation metadata only
304
+ (repeats for identical args; re-minted per `updatedInput` edit round); a caller-supplied value is not
305
+ trusted over the chokepoint's own.
306
+
307
+ - **Cross-process revival inherits the row's org-admission verdict (issue #22, tier-3 half).**
308
+ `BackgroundAgentRecord` gains `admittedOrgScopes?: string[]` and `admittedOrgWriteScope?: string | null`
309
+ (both additive): each leg writes its own adjudicated org verdict at the injector-ready barrier, and a
310
+ tier-3 claim seeds the revived child's admission fold from the row (seed ∩ live — narrow-only, the same
311
+ fold the in-process half uses). A row with the fields ABSENT reads as ZERO admission (`{scopes: [],
312
+ writeScope: null}`) — "no record" and "adjudicated to nothing" deliberately converge fail-closed.
313
+ Store contract additions (the cross-backend kit enforces both): the two fields round-trip verbatim
314
+ (`null` write-scope stays `null`), and ABSENT must read back absent, never materialize as `[]`.
315
+ Behavioral note: a revival leg that carries a seeded verdict now counts as governed, so an `org:`-shaped
316
+ scope in a non-v2 spelling is refused on that leg even with no resolver configured. Fork rows are not
317
+ written (they refuse tier-3 claims).
318
+
319
+ - **`task_progress` gains `taskType`** (additive; new exported type `DelegationTaskType` =
320
+ `"background_agent" | "workflow"`): the discriminator a consumer needs to route a progress frame
321
+ without inferring the task's kind from its id shape. Stamped at both mint sites for background/fork
322
+ legs and both workflow spawn legs (retained-resume included); absent on a synchronous child's frames —
323
+ absence means "no fleet row", tolerate it.
324
+
325
+ - **The scratchpad prompt section states volatility**: one added sentence ("Treat it as ephemeral — it may
326
+ not survive a long suspension or a resume on a different worker; keep durable outputs in the working
327
+ directory"); the CC-verbatim remainder is byte-unchanged. sema runs under durable suspend / multi-replica
328
+ serving where the scratchpad is a local-disk copy; without the sentence a model parks critical
329
+ intermediates in a directory that can vanish mid-task.
330
+
331
+ - **`shellGate:"classify"` now auto-allows a bounded read-only polling loop** (`classifyBoundedReadonlyPollLoop`,
332
+ consulted by `bashReversibilityProbe` only after the plain compound face rejects). A monitoring consumer's
333
+ core idiom is `for i in $(seq 1 8); do tail -n 5 x.log; sleep 2; done`; the compound classifier rejects any
334
+ control structure (`for` is not an allowlisted name), so `classify` behaved like `always` for the Monitor
335
+ tool — an approval prompt on every poll. The new arm accepts EXACTLY that grammar and nothing else: a loop
336
+ head with a LITERAL finite bound (`$(seq <int> <int>)`, ascending `{<int>..<int>}`, or a literal word list,
337
+ capped at 120 iterations), a body of `;`-separated allowlisted readers plus a literal `sleep` (≤600s), the
338
+ SAME read-boundary scan the plain face runs (RB-412/413, unchanged; an unexpanded glob rejects — stricter
339
+ than the plain face), and everything else banned wholesale (redirection, pipes, backgrounding, substitution
340
+ other than the one `$(seq)` exemption, subshells, braces, escapes, nested control structures, and any
341
+ non-ASCII/control whitespace). The loop variable must be a single lowercase letter, so it cannot shadow
342
+ `PATH`/`IFS`/`LD_PRELOAD` and alter how the body's commands resolve. Additive and fail-closed: it only ever
343
+ converts a rejection into an allow for this exact shape, never the reverse, and any unparseable input falls
344
+ through to a rejection — so a deployment on `off`/`always`, or one not polling, is byte-identical. New
345
+ exports from the fs tools barrel: `classifyBoundedReadonlyPollLoop`, `POLL_LOOP_MAX_BEATS` (120),
346
+ `POLL_LOOP_MAX_SLEEP_SECONDS` (600).
347
+
348
+ - **`HookToolContext.env`** (additive): a read-only path-resolution capability face over the env the task's
349
+ hands run against, on the PreToolUse / PostToolUse / PostToolUseFailure contexts alike. A hook that judges
350
+ paths (containment, symlink destinations) must resolve them in the filesystem the tools actually write to —
351
+ for a sandboxed or remote deployment that env is minted by `executionEnvFactory` DURING prepare, after the
352
+ caller built its hooks, so a hook closed over a spec-time env answers about the wrong machine; reading it
353
+ off the CALL context removes the ordering problem and picks up a resumed leg's rebuilt env with no
354
+ re-instantiation protocol. The face (new exported type `HookEnvCapabilities`, builder
355
+ `createHookEnvCapabilities` — public because `runToolGate` is) carries FOUR wrapped read primitives under
356
+ the env's own names: `canonicalPath`, `exists`, `readLink`, `cwd()` (the seam sketch's `canonicalize`/`root`
357
+ — `ExecutionEnv` declares no "root", so the honest anchor is the env's own working directory, read live).
358
+ **`cwd()` is the ENVIRONMENT's directory, not the task's tracked cwd** — the engine tracks a separate
359
+ per-task cwd (starting at the resolved task root, moving with `cd`/worktree entry) that the TOOLS' relative
360
+ paths resolve against and the env is never told about, so a hook judging a RELATIVE target must judge
361
+ absolute paths or get the tracked cwd through the deployment's own channel.
362
+ Deliberately capabilities and not the env object: no back-reference, null prototype, frozen, each primitive
363
+ captured once at build time. **Every member is optional and ABSENCE is the capability signal** — a member is
364
+ present iff the env exposes that primitive, never a stub that throws or an emulation that guesses, so a hook
365
+ branches on the shape; `ctx.env` itself is absent when the deployment wired no execution env at all
366
+ (`null` included). Existing hooks are unaffected.
367
+
368
+ ### Fixed
369
+
370
+ - **The auto-mode handback review now runs on EVERY sub-agent completion path, not only the synchronous
371
+ spawn.** A child finishing on the sync-fork path, on either background resolve leg (plain and fork),
372
+ through the parked-resume drive, or on a woken retained-resume cycle handed its output to the parent
373
+ without the completed-work review the sync path has performed since it landed — so exactly the
374
+ delegation shapes that outlive the spawning call were the ones that skipped it. The review, its
375
+ evidence set (the child's final text, the step recorder's observed tool activity, and — on the one path
376
+ that delivers it — the streamed partial-findings tail of a child that stopped without completing), its
377
+ framing and its fail-open posture are now one shared mint point that every call site uses. Carriers per path: the
378
+ fork report card leads with the warning line exactly as the sync card does; a background or resumed
379
+ child's warning is prefixed to the one value the durable row and the completion notification BOTH
380
+ derive from, so those two faces can never disagree about whether a child was flagged. Two deliberate
381
+ skips: a lane whose abort already fired (a stopped child settles on its stop story instead of waiting
382
+ on a classifier — the residual is stated at `reviewHandback`), and a durably-paused child (it has
383
+ handed nothing back yet — its resumed cycle gets the review). Deployments without auto-mode armed are
384
+ byte-identical on every path.
385
+ **Consumer notes**: ① the completed-agent structured card (`details`) gained an optional
386
+ **`handbackWarning`** key — the content face has always led with the warning line, but a host reading
387
+ `details` (afterToolCall hooks, persisted tool results) saw nothing; it rides as its own key because
388
+ `result` is contracted to be the child's text verbatim. Present only when a child was flagged, so
389
+ presence must not be read as a signal of anything else. ② the classifier sees one new `toolName` value,
390
+ `Agent(fork handback)`, beside the existing `Agent(handback)`. ③ `createSubagentResume` gained a
391
+ `currentAutoModeReview` seat in the same `current*` family as `currentOnQuestion`/`currentClamps` (the
392
+ waking run's decider, not the frozen spawn snapshot's); a host wiring SendMessage itself supplies it
393
+ from its own tool ctx, and absent it a woken cycle carries through unreviewed.
394
+
395
+ - **A retained background child's revival leg now inherits the session's own org-admission verdict.** The
396
+ in-process resume leg (a settled retained child woken onto its existing session) is a same-session
397
+ continuation that never goes through a checkpoint, so it re-adjudicated org memory admission from scratch
398
+ with no session freeze: a resolver whose answer WIDENED between the two legs remounted a tenant layer the
399
+ session had already lost, and a read-only first leg could gain an org WRITE grant on the revival — the
400
+ exact "a continuation can never widen" invariant the checkpoint plane enforces with seed ∩ live. The
401
+ verdict now rides `RunInternals.ownOrgAdmissionRef` (trusted internal, one ref per spawned child, threaded
402
+ through the frozen internals snapshot the revival replays); prepare INTERSECTS it with any checkpoint seed
403
+ rather than picking one, so the fold narrows along both the scope and the write axis. Deployments with no
404
+ governance surface are byte-identical (the ref is never read or written). The CROSS-PROCESS (tier-3) revival
405
+ half stays open and is now recorded in `memory-admission.ts`'s header instead of the old blanket residual
406
+ note: that leg rebuilds the spec from the reviving caller's mount, and the durable agent row records lookup
407
+ keys only, so it has no carrier for the original verdict.
408
+
409
+ - **A background delegation no longer needs a NAME to park at an approval gate.** The §7.3 park predicate
410
+ carried an `agentName !== undefined` term, so an ANONYMOUS background child that hit an approval gate was
411
+ sentenced to `no_park_lane`: the row settled `failed`, and the approval never surfaced anywhere an operator
412
+ could decide it (the checkpoint stayed committed but undiscoverable through the agent faces). A name was
413
+ never a structural precondition — the park is keyed on the taskId (`parkBackgroundAgent(taskId, …)`), the
414
+ claim/resume chain addresses the row by handle, and every revival face already treats `row.name` as optional.
415
+ The term is dropped from BOTH sites of the family in one commit: the settle-time park predicate and the
416
+ `durableApproval` forwarding predicate (a divergence would auto-deny an anonymous child's plain ask while its
417
+ safety-tier ask parks). The fork lane is unaffected (r4 F-07 — it has no park block at all), and genuinely
418
+ un-wired deployments still take `no_park_lane`, with the reason text no longer naming the retired
419
+ precondition. Consumer note: probes pinning "an anonymous background child never parks" red.
420
+
3
421
  ## 5.13.0 — 2026-08-05
4
422
 
5
423
  ### BREAKING
@@ -534,6 +534,7 @@ export function createSendMessageTool(opts) {
534
534
  ...(ctx.interactiveTools === false ? { interactiveTools: false } : {}),
535
535
  ...(ctx.oneShot === true ? { oneShot: true } : {}),
536
536
  },
537
+ ...(ctx.autoModeReview !== undefined ? { currentAutoModeReview: ctx.autoModeReview } : {}),
537
538
  });
538
539
  const fromPrefix = senderIsChild ? `(message from teammate "${senderLabel}")\n` : "";
539
540
  try {
@@ -66,6 +66,7 @@ export declare function completedAgentCard(child: {
66
66
  worktreePath?: string;
67
67
  toolStats?: SubagentToolStats;
68
68
  modelFallback?: "inherit_no_tier_binding";
69
+ handbackWarning?: string;
69
70
  }): Record<string, unknown>;
70
71
  export declare const SUBAGENT_SUSPENDED_AWAITING_APPROVAL = "suspended.awaiting_approval";
71
72
  export declare const SUBAGENT_SUSPENDED_NEEDS_REVIEW = "suspended.needs_review";
@@ -96,6 +97,9 @@ export declare function createSubagentResume(deps: {
96
97
  interactiveTools?: false;
97
98
  oneShot?: true;
98
99
  };
100
+ currentAutoModeReview?: {
101
+ decider: import("../core/auto-mode.js").AutoModeDecider;
102
+ };
99
103
  taskId?: string;
100
104
  taskAccess?: import("../core/task-registry.js").TaskAccess;
101
105
  bgSink?: (event: import("../core/types.js").BackgroundChildEvent) => void;