@sema-agent/core 5.13.0 → 5.14.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 (86) hide show
  1. package/CHANGELOG.md +296 -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/open-responses.js +24 -10
  9. package/dist/brain/openai.js +29 -11
  10. package/dist/brain/request-params.d.ts +2 -0
  11. package/dist/brain/request-params.js +16 -0
  12. package/dist/brain/stream-engine.d.ts +9 -1
  13. package/dist/brain/stream-engine.js +256 -27
  14. package/dist/brain/timeout.d.ts +1 -0
  15. package/dist/brain/timeout.js +1 -0
  16. package/dist/core/a2a.d.ts +2 -2
  17. package/dist/core/a2a.js +3 -3
  18. package/dist/core/ask-question.d.ts +47 -2
  19. package/dist/core/ask-question.js +209 -28
  20. package/dist/core/background-agent-store.d.ts +2 -0
  21. package/dist/core/checkpoint-store.d.ts +41 -17
  22. package/dist/core/checkpoint-store.js +114 -3
  23. package/dist/core/hooks.d.ts +24 -2
  24. package/dist/core/hooks.js +97 -10
  25. package/dist/core/human-input-projection.d.ts +12 -0
  26. package/dist/core/human-input-projection.js +27 -0
  27. package/dist/core/mcp.d.ts +7 -2
  28. package/dist/core/mcp.js +7 -7
  29. package/dist/core/memory-admission.d.ts +4 -0
  30. package/dist/core/memory-admission.js +3 -0
  31. package/dist/core/runner/assemble-result.d.ts +1 -0
  32. package/dist/core/runner/assemble-result.js +1 -1
  33. package/dist/core/runner/prepare-task.d.ts +16 -6
  34. package/dist/core/runner/prepare-task.js +301 -24
  35. package/dist/core/runner/runtask.d.ts +3 -6
  36. package/dist/core/runner/runtask.js +186 -36
  37. package/dist/core/runner/tool-output-projection.js +1 -0
  38. package/dist/core/session-store.d.ts +3 -0
  39. package/dist/core/session-store.js +4 -0
  40. package/dist/core/session.d.ts +1 -0
  41. package/dist/core/store-contracts/background-agent-store-contract.js +19 -0
  42. package/dist/core/store-contracts/checkpoint-store-contract.js +62 -3
  43. package/dist/core/task-notification.d.ts +2 -0
  44. package/dist/core/task-notification.js +5 -3
  45. package/dist/core/task-registry-agent.d.ts +1 -0
  46. package/dist/core/task-registry-agent.js +6 -0
  47. package/dist/core/task-registry.d.ts +1 -0
  48. package/dist/core/task-registry.js +4 -1
  49. package/dist/core/tool-policy.d.ts +5 -0
  50. package/dist/core/tool-policy.js +2 -1
  51. package/dist/core/types.d.ts +32 -1
  52. package/dist/core/wiring-manifest.d.ts +97 -0
  53. package/dist/core/wiring-manifest.js +186 -0
  54. package/dist/engine/compaction/compaction.js +2 -2
  55. package/dist/engine/harness/agent-harness.d.ts +2 -1
  56. package/dist/engine/harness/agent-harness.js +8 -1
  57. package/dist/engine/harness/types.d.ts +3 -1
  58. package/dist/engine/llm/types.d.ts +7 -0
  59. package/dist/engine/llm/types.js +8 -1
  60. package/dist/engine/session/import-validate.d.ts +6 -1
  61. package/dist/engine/session/import-validate.js +29 -6
  62. package/dist/engine/session/memory-repo.d.ts +3 -1
  63. package/dist/engine/session/memory-repo.js +2 -2
  64. package/dist/index.d.ts +7 -4
  65. package/dist/index.js +7 -4
  66. package/dist/internal/harness-types.d.ts +1 -1
  67. package/dist/internal/llm.d.ts +2 -2
  68. package/dist/internal/llm.js +1 -1
  69. package/dist/orchestration/run-workflow-tool.d.ts +4 -0
  70. package/dist/orchestration/run-workflow-tool.js +3 -0
  71. package/dist/orchestration/workflow-types.d.ts +8 -0
  72. package/dist/orchestration/workflow-types.js +14 -0
  73. package/dist/orchestration/workflow.d.ts +4 -0
  74. package/dist/orchestration/workflow.js +134 -5
  75. package/dist/prompts/default.js +1 -1
  76. package/dist/stores/file/checkpoint-store.d.ts +3 -5
  77. package/dist/stores/file/checkpoint-store.js +31 -2
  78. package/dist/stores/file/index.js +1 -1
  79. package/dist/stores/file/session-store.d.ts +3 -1
  80. package/dist/stores/file/session-store.js +2 -2
  81. package/dist/stores/file/shared-ledger.js +8 -1
  82. package/dist/tools/fs/bash-readonly-classifier.d.ts +3 -0
  83. package/dist/tools/fs/bash-readonly-classifier.js +94 -0
  84. package/dist/tools/fs/fs-bash.js +31 -12
  85. package/dist/tools/fs/safety.js +34 -10
  86. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,301 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.14.0 — 2026-08-06
4
+
5
+ ### BREAKING
6
+
7
+ - **`CheckpointStore.setPendingSteer` APPENDS to a bounded ordered queue instead of overwriting a single seat**
8
+ (design/171 §5.3/§6.1/§6.3). Two people steering one durably-suspended run used to mean the second steer
9
+ silently destroyed the first — an undelivered operator instruction, gone with no record that anything was
10
+ lost. `CheckpointState` gains `pendingSteerQueue: PendingSteerEntry[]`
11
+ (`{text, trusted, actor?, seq, inputId, priority?}`); resume drains the WHOLE queue in `seq` order, each
12
+ entry framed under its own trust semantics. New exports: `readPendingSteerQueue` (the single read point —
13
+ it folds a pre-queue row's legacy `pendingSteer` seat in as member 0, so old checkpoints deliver unchanged
14
+ with no migration step), `appendPendingSteer` (the shared append every backend must build its next queue
15
+ with: bounds + `seq` mint live there, not once per store), `PENDING_STEER_QUEUE_BYTE_BUDGET_BYTES` (48,000 —
16
+ under a MySQL `TEXT` column with UTF-8 headroom), `MAX_PENDING_STEER_ENTRIES` (derived from it and
17
+ `MAX_PENDING_STEER_CHARS`), `PENDING_STEER_FROZEN_FIELDS` / `ACTOR_ASSERTION_FROZEN_FIELDS`, and the
18
+ `ActorAssertion` type (`{id (namespaced), hostAsserted, issuer?}` — attribution ONLY, never authority;
19
+ `hostAsserted` must be derived by the host from ingress credentials and never taken from a request body).
20
+ Bounds are fail-loud: crossing either throws the **new error code `steering.queue_full`** rather than
21
+ evicting an accepted instruction. `validatePendingSteer` now REFUSES an unknown field
22
+ (`steering.invalid_content`) instead of dropping it, so a producer one release ahead learns its field would
23
+ not survive the parked leg. Idempotency moved from last-writer-wins to the `inputId` key: re-appending the
24
+ same `inputId` with an IDENTICAL payload is a no-op returning `true`, while the same `inputId` carrying
25
+ DIFFERENT content is refused with the **new error code `steering.duplicate_input_id`** (swallowing it would
26
+ reintroduce exactly the silent instruction loss the queue replaces); an absent `inputId` is minted (uuidv7),
27
+ and the synthetic id a pre-queue seat reads back as is reserved against caller supply. The file backend's
28
+ ledger replay also gained a fail-loud arm: an event kind the reader does not recognise now refuses the whole
29
+ replay (`checkpoint.unsupported_version`) instead of folding in as a silent no-op — **note that a binary
30
+ released BEFORE this one still ignores unknown kinds, so rolling back across the queue event loses steers
31
+ parked in the un-compacted ledger tail.**
32
+ **Consumer flips**: ① any code reading `state.pendingSteer` directly sees `undefined` for every new write —
33
+ read through `readPendingSteerQueue`; ② probes pinning "a second setPendingSteer overwrites the first" red;
34
+ ③ a custom `CheckpointStore` must round-trip BOTH state fields and build its next queue with
35
+ `appendPendingSteer` (the cross-backend contract kit gained the queue arms and will fail it otherwise);
36
+ ④ `steering.queue_full` and `steering.duplicate_input_id` are new members of the `CheckpointError` code
37
+ union — exhaustive consumers add both arms.
38
+
39
+ ### Added
40
+
41
+ - **Shared-session attribution (design/171, core half).** Four pieces, one contract: ① `UserMessage.actor`
42
+ carries an `ActorAssertion` (attribution ONLY, never authority; snapshot-minted at the API boundary, wire
43
+ metadata stripped). ② ONE projection point (`human-input-projection.ts`, exported `projectHumanInput` /
44
+ `buildHumanInputEvent`) renders the speaker envelope for every human-input carrier — objective, live steer,
45
+ nextTurn, parked-steer resume frames, wake — as `[from "<id>"]` (+ ` (unverified)` when the host did not
46
+ derive the identity from ingress credentials), through the same neutralizers as the external-notification
47
+ header, always INSIDE the trust frame it attributes. No actor, or `source:"system"` ⇒ byte-identical
48
+ passthrough, so every pre-171 caller and single-user host is pinned unchanged. ③ **New TaskEvent arm
49
+ `human_input`** — the lifecycle ledger of who fed the run what: `{inputId, sessionSeq (leg-scoped),
50
+ carrier, source, delivery, issuer?, actor?, principal?, entryId?}`; live emissions precede the commit and
51
+ omit `entryId` (join via `message_committed` + stream order). Exhaustive TaskEvent switches need an arm.
52
+ ④ The compaction attribution invariant: speaker labels survive summarization across all three summary-path
53
+ variants (the summarizer is instructed to preserve `[from …]` attributions).
54
+
55
+ - **Assembly self-evidence (design/173, all eight items).** The engine can now PROVE what it wired instead of
56
+ the operator inferring it from behavior:
57
+ - `describeStaticWiring(spec, deps)` (package-root export) — the pure static half of the wiring manifest;
58
+ per-leg EFFECTIVE manifests are built at prepare and emitted as the **new TaskEvent arm `wiring_manifest`**
59
+ (root/child/resume legs each emit their own). Together with `human_input` above this release takes
60
+ `TaskEvent` **16→18** — an exhaustive switch adds TWO arms, not one. A child leg's manifest rides the
61
+ CHILD's own stream and is deliberately not in the parent-forwarding whitelist: "every leg self-evidences"
62
+ 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),
63
+ question (three-valued channel state incl. the engine-stripped bg lane, plus the composition-lie flag
64
+ `interactiveToolsWithoutDeliveryFace`), elicit, the park lane (capability vs effective policy, reason
65
+ codes, checkpoint durability), session-store durability, fleet seats, and a governance presence section
66
+ stamped `audience:"operator"` — **a serving layer forwarding this event to a multi-tenant stream MUST
67
+ project the governance section for operators only; no projection ⇒ do not disclose**. `configFingerprint`
68
+ (sha256 prefix over the manifest's own resolved facts) correlates legs that ran under the same assembly.
69
+ - **Store durability declarations**: `SessionStore`/`CheckpointStore` gain `readonly durability?`
70
+ ("durable" | "process-local"); absent reads fail-closed as process-local, unparseable is refused loud
71
+ (`config.store_durability_invalid`). Built-ins declare themselves; an armed park lane over a
72
+ process-local store is now a REPORTABLE degrade shape instead of a silent one.
73
+ - **`TaskSpec.interactionPosture` / `RunnerDeps.interactionPosture`** ("interactive" | "headless") with a
74
+ fail-loud prepare door: posture `"interactive"` refuses a leg whose resolved wiring cannot reach a human
75
+ (`config.interaction_posture` / `config.interaction_wiring`) instead of silently auto-answering. Child
76
+ legs resolve `spec ?? parent posture (trusted internals) ?? deps`. A deployment that declares interactive
77
+ at deps level must supply a persistent question face for session-scoped bg legs — the door refusing that
78
+ leg is the honest reading of an unfulfillable declaration.
79
+ - **AskUserQuestion's three exits mint an in-band card** (`details.type: "ask-question"`, a new
80
+ `CC_DETAIL_TYPES` member): `continuationSource: "human_response" | "synthetic_self_answer_instruction"`
81
+ plus reason codes (a THREE-member closed set: `seam_absent` / `callback_failed` /
82
+ `declined_unavailable`), so a consumer can tell a human answer from the engine's self-answer fallback
83
+ without prose-matching. The card is a two-arm union — the coded-failure arm (`question.human_channel_failed` /
84
+ `question.human_unavailable`) carries neither `continuationSource` nor `runContinues`, so consumers
85
+ must branch on `code` FIRST and only then read `continuationSource`. Under posture `"interactive"`, the callback-failed
86
+ arm becomes a coded failure (`question.human_channel_failed`) unless the explicit
87
+ `interactiveQuestionFallback` knob opts back into degrade; the degrade path emits onError
88
+ (phase `"degraded"`, classification `"no-human-autoanswered"`).
89
+ - **The four question-face strip sites pair-produce a disclosure flag** through one helper: when the engine
90
+ deliberately strips a spawn turn's per-request question face from a bg/fork child, the manifest reports
91
+ the channel as `"stripped_bg_lane"` (flag minted only when a face was actually stripped).
92
+ - **`effectiveShellGate === "off"` with a real writable shell now leaves a per-prepare forensic note**
93
+ (onError phase `"config"`, classification `"shell-gate-off"`) — the assembly state that previously had
94
+ zero observable trace. onError `classification` is filled on the config phase for the first time.
95
+ - `isDelegatedAgentTerminal` is re-exported at the package root so a serving layer's escalation code shares
96
+ core's own terminal classification.
97
+
98
+ - **The brain honors a provider's explicit retry verdict, and recovers from the context-overflow 400.**
99
+ ① The `x-should-retry` response header is now read double-sided: `true` retries a normally-terminal status,
100
+ `false` stops retrying a normally-retryable one; header absent ⇒ prior behavior byte-identical.
101
+ ② The `input length and max_tokens exceed context limit` 400 (previously terminal `invalid_request`) now
102
+ adaptively lowers `max_tokens` and retries, with two loud guards: a floor (3000 output tokens — at the
103
+ floor and still overflowing ⇒ give up) and a no-progress check (same error after a cut ⇒ give up); every
104
+ cut is disclosed as a status frame. The narrow error-shape parser refuses anything it does not recognize
105
+ (prior terminal behavior preserved).
106
+
107
+ - **Question sync-first (design/174).** With a live question face, an adjudicated `AskUserQuestion` is
108
+ answered in-stream — same turn, no checkpoint; the durable park is what happens when nobody is there
109
+ (no live face / the face affirms nobody is reachable / `forceDurableGate` — and the third park-lane arming,
110
+ the safety vocabulary, behaves the same: **any park-lane arming keeps the park**, sync-first only applies
111
+ to the durable-approval posture with a live face). Assemblies without a live face are pinned byte-identical
112
+ to the previous release. `OnQuestion` may now return `{kind: "unavailable"}` — implementers additive,
113
+ **callers BREAKING** (`.answers` direct access goes red; `isQuestionUnavailable` is exported for relaying
114
+ wrappers). `AskQuestionRequest` gains `boundInputHash` and an engine-minted per-delivery `deliveryId`
115
+ (the identity; `(taskId, toolCallId, boundInputHash)` repeats for two identical sequential questions and is
116
+ reconciliation data only). The question tool executes in its own batch (`executionMode: "sequential"`).
117
+ New closed-set members: `SyntheticContinuationReason` += `declined_unavailable`; card `code` +=
118
+ `question.human_unavailable`; onError classification += `"unconsumed-human-answer"` and
119
+ `"interaction-posture-refused"`.
120
+
121
+ - **Pre-release verification hardening (test AI's A/B groups + the convergence review).**
122
+ - A question outcome claiming NEITHER arm (no `unavailable` discriminant, no `answers` array — a failed
123
+ decode) is now refused as a delivery failure instead of being read as "the user selected nothing";
124
+ the explicit `{answers: []}` stays a real answer. The resume lane applies the same rule pre-CAS and
125
+ captures the redeemed answer ONCE (`structuredClone`) — the persisted outcome and the delivered answer
126
+ are the same snapshot, so a stateful getter can no longer make audit and delivery diverge.
127
+ - **`TaskResult.strandedHumanAnswers`** (new field): `{deliveryId, toolCallId}` records of questions a
128
+ person answered that no call executed to collect — keyed by the engine-minted delivery identity, since
129
+ call ids can repeat (two lost answers on one call id are two records). This is the MANDATORY disclosure
130
+ face; the `onError` (classification `"unconsumed-human-answer"`) alert remains as an additional lane.
131
+ An answer arriving after the result is already terminal can only reach `onError` — the structural limit
132
+ of a result face; a value that does not READ as an answer is never recorded as one.
133
+ - **`AskRequest.riskAxes`** (additive): `{irreversible?, egress?}`, filled by the gate from the SAME
134
+ resolved axes the safety tightens read (declared tiers + `toolAxes` folds + shellGate marks; `"maybe"`
135
+ counts as irreversible). Either axis ABSENT = the engine did not judge it — consumers must render
136
+ "unjudged", never fold absence to `false`. An EXPLICIT caller negative (`egress:false` /
137
+ `irreversibility:"never"` via MCP/A2A `toolAxes`) now survives the override algebra and reports as a
138
+ judged `false` — enforcement stays tighten-only and ignores negatives.
139
+ - **`WiringManifest.interaction.posture`** (new section): the posture declaration ("interactive" /
140
+ "headless" / "absent") joins the manifest — previously the one interactive-face declaration it did not
141
+ report. A posture refusal now also emits an onError frame (phase `"config"`,
142
+ classification `"interaction-posture-refused"`): the coded `TaskResult.errorCode` remains the primary
143
+ carrier (neither entry point throws), the frame serves hosts wired only to `onError`.
144
+ - **`configFingerprint` now hashes the ASSEMBLY, excluding leg identity AND seat provenance** (the
145
+ delegation lane re-homes a parent's deps face onto the child's spec seat — same callback, different
146
+ seat) — root/child/resume legs of one assembly really do share a fingerprint (its advertised use).
147
+ Fingerprint VALUES change with this release; pin the equality relation, not literals.
148
+ Presence condition for the resume leg: a resume's assembly comes from the `ResumeTaskConfig` you
149
+ pass, not from the root's spec — omit a seam face there (e.g. `onQuestion`) and the resume leg
150
+ HONESTLY reports a different assembly (and a different fingerprint), because that leg really has no
151
+ in-stream answerer. Re-supply the same faces on resume to keep the fingerprint equal.
152
+ - The synthetic-continuation disclosure dedup is **per ask-call** (key = tool-call id; one call may carry
153
+ 1–4 questions and discloses once) and scoped to ONE leg — a continuation leg re-discloses, and a host
154
+ aggregating across legs dedups on `(sessionId, toolCallId)` itself.
155
+
156
+ - **Capture-domain narrowing (design/174 convergence review, rounds 9–14).** Deployment-supplied resume
157
+ values are read ONCE and captured as canonical plain data; the canonicalizer refuses what the JSON
158
+ checkpoint store cannot faithfully round-trip. Concretely:
159
+ - resume `updatedInput` and a content-ask `answer` now REFUSE (pre-CAS, `checkpoint.invalid_outcome`,
160
+ checkpoint stays pending) values carrying Map/Set/Date/buffers/class instances, shared references or
161
+ cycles, sparse or expando arrays, non-finite numbers, bigint/symbol/function anywhere in the tree;
162
+ object keys with `undefined` values drop and `-0` normalizes to `0` (JSON semantics, applied at capture
163
+ so audit, replay comparison and execution all read the same value);
164
+ - `decision` is domain-checked at capture: anything other than `"allow"`/`"deny"` refuses pre-CAS
165
+ (previously an unknown word could fall past the deny check into the execute arm);
166
+ - `updatedInput: null` flows into the tool's own validation and fails THERE, instead of silently
167
+ executing the ORIGINAL arguments while the record said the input was rewritten;
168
+ - the gate discriminant and every `policy_ask` field are read once into a plain twin consumed by all
169
+ guards, the env-failed replay equality, persistence and the answer face; array equality visits every
170
+ index and compares named non-index keys;
171
+ - upgrade note: a winner persisted RAW by an older release still replays (equality compares canonical
172
+ forms — no checkpoint migration), but an OLD row whose winner is outside the new domain fails closed
173
+ on replay; the row stays recoverable by re-deciding.
174
+ - The direct tool lane applies the same one reader: an answer payload with a throwing accessor refuses
175
+ the WHOLE outcome (no partial salvage of well-formed siblings), and `{}` is a failed decode, not
176
+ "selected nothing" — `{answers: []}` remains the legitimate empty selection.
177
+
178
+ - **`AskRequest.boundInputHash`** (additive): the canonical digest of the presented args, minted once at
179
+ the `resolveAsk` chokepoint over the retained snapshot — the SAME `boundInputHashOf` digest a durable
180
+ park binds its checkpoint to, so an aggregating approver can reconcile a synchronous ask row against a
181
+ parked checkpoint row for the same call on `(toolCallId, hash)` equality. Reconciliation metadata only
182
+ (repeats for identical args; re-minted per `updatedInput` edit round); a caller-supplied value is not
183
+ trusted over the chokepoint's own.
184
+
185
+ - **Cross-process revival inherits the row's org-admission verdict (issue #22, tier-3 half).**
186
+ `BackgroundAgentRecord` gains `admittedOrgScopes?: string[]` and `admittedOrgWriteScope?: string | null`
187
+ (both additive): each leg writes its own adjudicated org verdict at the injector-ready barrier, and a
188
+ tier-3 claim seeds the revived child's admission fold from the row (seed ∩ live — narrow-only, the same
189
+ fold the in-process half uses). A row with the fields ABSENT reads as ZERO admission (`{scopes: [],
190
+ writeScope: null}`) — "no record" and "adjudicated to nothing" deliberately converge fail-closed.
191
+ Store contract additions (the cross-backend kit enforces both): the two fields round-trip verbatim
192
+ (`null` write-scope stays `null`), and ABSENT must read back absent, never materialize as `[]`.
193
+ Behavioral note: a revival leg that carries a seeded verdict now counts as governed, so an `org:`-shaped
194
+ scope in a non-v2 spelling is refused on that leg even with no resolver configured. Fork rows are not
195
+ written (they refuse tier-3 claims).
196
+
197
+ - **`task_progress` gains `taskType`** (additive; new exported type `DelegationTaskType` =
198
+ `"background_agent" | "workflow"`): the discriminator a consumer needs to route a progress frame
199
+ without inferring the task's kind from its id shape. Stamped at both mint sites for background/fork
200
+ legs and both workflow spawn legs (retained-resume included); absent on a synchronous child's frames —
201
+ absence means "no fleet row", tolerate it.
202
+
203
+ - **The scratchpad prompt section states volatility**: one added sentence ("Treat it as ephemeral — it may
204
+ not survive a long suspension or a resume on a different worker; keep durable outputs in the working
205
+ directory"); the CC-verbatim remainder is byte-unchanged. sema runs under durable suspend / multi-replica
206
+ serving where the scratchpad is a local-disk copy; without the sentence a model parks critical
207
+ intermediates in a directory that can vanish mid-task.
208
+
209
+ - **`shellGate:"classify"` now auto-allows a bounded read-only polling loop** (`classifyBoundedReadonlyPollLoop`,
210
+ consulted by `bashReversibilityProbe` only after the plain compound face rejects). A monitoring consumer's
211
+ core idiom is `for i in $(seq 1 8); do tail -n 5 x.log; sleep 2; done`; the compound classifier rejects any
212
+ control structure (`for` is not an allowlisted name), so `classify` behaved like `always` for the Monitor
213
+ tool — an approval prompt on every poll. The new arm accepts EXACTLY that grammar and nothing else: a loop
214
+ head with a LITERAL finite bound (`$(seq <int> <int>)`, ascending `{<int>..<int>}`, or a literal word list,
215
+ capped at 120 iterations), a body of `;`-separated allowlisted readers plus a literal `sleep` (≤600s), the
216
+ SAME read-boundary scan the plain face runs (RB-412/413, unchanged; an unexpanded glob rejects — stricter
217
+ than the plain face), and everything else banned wholesale (redirection, pipes, backgrounding, substitution
218
+ other than the one `$(seq)` exemption, subshells, braces, escapes, nested control structures, and any
219
+ non-ASCII/control whitespace). The loop variable must be a single lowercase letter, so it cannot shadow
220
+ `PATH`/`IFS`/`LD_PRELOAD` and alter how the body's commands resolve. Additive and fail-closed: it only ever
221
+ converts a rejection into an allow for this exact shape, never the reverse, and any unparseable input falls
222
+ through to a rejection — so a deployment on `off`/`always`, or one not polling, is byte-identical. New
223
+ exports from the fs tools barrel: `classifyBoundedReadonlyPollLoop`, `POLL_LOOP_MAX_BEATS` (120),
224
+ `POLL_LOOP_MAX_SLEEP_SECONDS` (600).
225
+
226
+ - **`HookToolContext.env`** (additive): a read-only path-resolution capability face over the env the task's
227
+ hands run against, on the PreToolUse / PostToolUse / PostToolUseFailure contexts alike. A hook that judges
228
+ paths (containment, symlink destinations) must resolve them in the filesystem the tools actually write to —
229
+ for a sandboxed or remote deployment that env is minted by `executionEnvFactory` DURING prepare, after the
230
+ caller built its hooks, so a hook closed over a spec-time env answers about the wrong machine; reading it
231
+ off the CALL context removes the ordering problem and picks up a resumed leg's rebuilt env with no
232
+ re-instantiation protocol. The face (new exported type `HookEnvCapabilities`, builder
233
+ `createHookEnvCapabilities` — public because `runToolGate` is) carries FOUR wrapped read primitives under
234
+ the env's own names: `canonicalPath`, `exists`, `readLink`, `cwd()` (the seam sketch's `canonicalize`/`root`
235
+ — `ExecutionEnv` declares no "root", so the honest anchor is the env's own working directory, read live).
236
+ **`cwd()` is the ENVIRONMENT's directory, not the task's tracked cwd** — the engine tracks a separate
237
+ per-task cwd (starting at the resolved task root, moving with `cd`/worktree entry) that the TOOLS' relative
238
+ paths resolve against and the env is never told about, so a hook judging a RELATIVE target must judge
239
+ absolute paths or get the tracked cwd through the deployment's own channel.
240
+ Deliberately capabilities and not the env object: no back-reference, null prototype, frozen, each primitive
241
+ captured once at build time. **Every member is optional and ABSENCE is the capability signal** — a member is
242
+ present iff the env exposes that primitive, never a stub that throws or an emulation that guesses, so a hook
243
+ branches on the shape; `ctx.env` itself is absent when the deployment wired no execution env at all
244
+ (`null` included). Existing hooks are unaffected.
245
+
246
+ ### Fixed
247
+
248
+ - **The auto-mode handback review now runs on EVERY sub-agent completion path, not only the synchronous
249
+ spawn.** A child finishing on the sync-fork path, on either background resolve leg (plain and fork),
250
+ through the parked-resume drive, or on a woken retained-resume cycle handed its output to the parent
251
+ without the completed-work review the sync path has performed since it landed — so exactly the
252
+ delegation shapes that outlive the spawning call were the ones that skipped it. The review, its
253
+ evidence set (the child's final text, the step recorder's observed tool activity, and — on the one path
254
+ that delivers it — the streamed partial-findings tail of a child that stopped without completing), its
255
+ framing and its fail-open posture are now one shared mint point that every call site uses. Carriers per path: the
256
+ fork report card leads with the warning line exactly as the sync card does; a background or resumed
257
+ child's warning is prefixed to the one value the durable row and the completion notification BOTH
258
+ derive from, so those two faces can never disagree about whether a child was flagged. Two deliberate
259
+ skips: a lane whose abort already fired (a stopped child settles on its stop story instead of waiting
260
+ on a classifier — the residual is stated at `reviewHandback`), and a durably-paused child (it has
261
+ handed nothing back yet — its resumed cycle gets the review). Deployments without auto-mode armed are
262
+ byte-identical on every path.
263
+ **Consumer notes**: ① the completed-agent structured card (`details`) gained an optional
264
+ **`handbackWarning`** key — the content face has always led with the warning line, but a host reading
265
+ `details` (afterToolCall hooks, persisted tool results) saw nothing; it rides as its own key because
266
+ `result` is contracted to be the child's text verbatim. Present only when a child was flagged, so
267
+ presence must not be read as a signal of anything else. ② the classifier sees one new `toolName` value,
268
+ `Agent(fork handback)`, beside the existing `Agent(handback)`. ③ `createSubagentResume` gained a
269
+ `currentAutoModeReview` seat in the same `current*` family as `currentOnQuestion`/`currentClamps` (the
270
+ waking run's decider, not the frozen spawn snapshot's); a host wiring SendMessage itself supplies it
271
+ from its own tool ctx, and absent it a woken cycle carries through unreviewed.
272
+
273
+ - **A retained background child's revival leg now inherits the session's own org-admission verdict.** The
274
+ in-process resume leg (a settled retained child woken onto its existing session) is a same-session
275
+ continuation that never goes through a checkpoint, so it re-adjudicated org memory admission from scratch
276
+ with no session freeze: a resolver whose answer WIDENED between the two legs remounted a tenant layer the
277
+ session had already lost, and a read-only first leg could gain an org WRITE grant on the revival — the
278
+ exact "a continuation can never widen" invariant the checkpoint plane enforces with seed ∩ live. The
279
+ verdict now rides `RunInternals.ownOrgAdmissionRef` (trusted internal, one ref per spawned child, threaded
280
+ through the frozen internals snapshot the revival replays); prepare INTERSECTS it with any checkpoint seed
281
+ rather than picking one, so the fold narrows along both the scope and the write axis. Deployments with no
282
+ governance surface are byte-identical (the ref is never read or written). The CROSS-PROCESS (tier-3) revival
283
+ half stays open and is now recorded in `memory-admission.ts`'s header instead of the old blanket residual
284
+ note: that leg rebuilds the spec from the reviving caller's mount, and the durable agent row records lookup
285
+ keys only, so it has no carrier for the original verdict.
286
+
287
+ - **A background delegation no longer needs a NAME to park at an approval gate.** The §7.3 park predicate
288
+ carried an `agentName !== undefined` term, so an ANONYMOUS background child that hit an approval gate was
289
+ sentenced to `no_park_lane`: the row settled `failed`, and the approval never surfaced anywhere an operator
290
+ could decide it (the checkpoint stayed committed but undiscoverable through the agent faces). A name was
291
+ never a structural precondition — the park is keyed on the taskId (`parkBackgroundAgent(taskId, …)`), the
292
+ claim/resume chain addresses the row by handle, and every revival face already treats `row.name` as optional.
293
+ The term is dropped from BOTH sites of the family in one commit: the settle-time park predicate and the
294
+ `durableApproval` forwarding predicate (a divergence would auto-deny an anonymous child's plain ask while its
295
+ safety-tier ask parks). The fork lane is unaffected (r4 F-07 — it has no park block at all), and genuinely
296
+ un-wired deployments still take `no_park_lane`, with the reason text no longer naming the retired
297
+ precondition. Consumer note: probes pinning "an anonymous background child never parks" red.
298
+
3
299
  ## 5.13.0 — 2026-08-05
4
300
 
5
301
  ### 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;