@sema-agent/core 5.65.0 → 7.0.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 (165) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/dist/agents/subagent.d.ts +2 -2
  3. package/dist/agents/subagent.js +11 -0
  4. package/dist/agents/verify.d.ts +1 -1
  5. package/dist/brain/anthropic.js +1 -1
  6. package/dist/brain/errors.d.ts +29 -0
  7. package/dist/brain/errors.js +20 -0
  8. package/dist/brain/open-responses.js +2 -2
  9. package/dist/brain/route-adjudicator.d.ts +8 -1
  10. package/dist/brain/route-adjudicator.js +1 -0
  11. package/dist/brain/status-sink.js +12 -1
  12. package/dist/brain/stream-engine.js +17 -6
  13. package/dist/core/auto-compaction.d.ts +26 -0
  14. package/dist/core/auto-compaction.js +7 -2
  15. package/dist/core/auto-mode-arming.d.ts +138 -0
  16. package/dist/core/auto-mode-arming.js +181 -0
  17. package/dist/core/auto-mode-defaults.d.ts +13 -0
  18. package/dist/core/auto-mode-defaults.js +5 -0
  19. package/dist/core/auto-mode-prompt.d.ts +14 -3
  20. package/dist/core/auto-mode-prompt.js +10 -7
  21. package/dist/core/auto-mode-rebuild.d.ts +75 -0
  22. package/dist/core/auto-mode-rebuild.js +41 -0
  23. package/dist/core/auto-mode.d.ts +15 -0
  24. package/dist/core/auto-mode.js +4 -2
  25. package/dist/core/checkpoint-store.d.ts +18 -0
  26. package/dist/core/context-edit.d.ts +47 -5
  27. package/dist/core/context-guard.d.ts +1 -1
  28. package/dist/core/file-history-retention.d.ts +106 -0
  29. package/dist/core/file-history-retention.js +36 -0
  30. package/dist/core/file-history-store.d.ts +768 -0
  31. package/dist/core/file-history-store.js +880 -0
  32. package/dist/core/governance-codes.d.ts +13 -3
  33. package/dist/core/governance-codes.js +15 -0
  34. package/dist/core/hooks.d.ts +39 -7
  35. package/dist/core/hooks.js +38 -21
  36. package/dist/core/lsp.d.ts +2 -2
  37. package/dist/core/mcp.d.ts +29 -7
  38. package/dist/core/memory-engine/consolidation-driver.d.ts +11 -0
  39. package/dist/core/memory-engine/consolidation-driver.js +71 -4
  40. package/dist/core/memory-engine/consolidation.d.ts +25 -2
  41. package/dist/core/memory-engine/consolidation.js +4 -1
  42. package/dist/core/memory-engine/distiller.d.ts +84 -1
  43. package/dist/core/memory-engine/distiller.js +68 -0
  44. package/dist/core/memory-engine/dual-root.js +3 -0
  45. package/dist/core/memory-engine/engine.d.ts +328 -15
  46. package/dist/core/memory-engine/engine.js +355 -29
  47. package/dist/core/memory-engine/file-backend.d.ts +30 -0
  48. package/dist/core/memory-engine/file-backend.js +14 -13
  49. package/dist/core/memory-engine/frontmatter.d.ts +22 -1
  50. package/dist/core/memory-engine/frontmatter.js +3 -0
  51. package/dist/core/memory-engine/header-hints.d.ts +5 -0
  52. package/dist/core/memory-engine/index.d.ts +5 -4
  53. package/dist/core/memory-engine/index.js +5 -4
  54. package/dist/core/memory-engine/layout.d.ts +88 -2
  55. package/dist/core/memory-engine/layout.js +112 -3
  56. package/dist/core/memory-engine/provenance-wording.d.ts +7 -0
  57. package/dist/core/memory-engine/provenance-wording.js +3 -0
  58. package/dist/core/memory-engine/tools.d.ts +89 -8
  59. package/dist/core/memory-engine/tools.js +263 -22
  60. package/dist/core/memory-engine/types.d.ts +64 -1
  61. package/dist/core/memory-recall.d.ts +6 -0
  62. package/dist/core/memory.d.ts +27 -1
  63. package/dist/core/memory.js +16 -2
  64. package/dist/core/permission-rule-consent.d.ts +62 -2
  65. package/dist/core/permission-rule-consent.js +105 -14
  66. package/dist/core/permission-rule-model.d.ts +118 -16
  67. package/dist/core/permission-rule-model.js +57 -9
  68. package/dist/core/permission-rule-session.d.ts +124 -0
  69. package/dist/core/permission-rule-session.js +121 -0
  70. package/dist/core/permission-rule-store.d.ts +65 -2
  71. package/dist/core/permission-rule-store.js +75 -16
  72. package/dist/core/permission-rule-sync.d.ts +9 -0
  73. package/dist/core/permission-rule-sync.js +52 -19
  74. package/dist/core/retention-policy.d.ts +9 -0
  75. package/dist/core/retention-policy.js +5 -2
  76. package/dist/core/retention.d.ts +13 -2
  77. package/dist/core/runner/assemble-result.d.ts +19 -1
  78. package/dist/core/runner/assemble-result.js +17 -2
  79. package/dist/core/runner/compaction-call-options.d.ts +93 -0
  80. package/dist/core/runner/compaction-call-options.js +3 -0
  81. package/dist/core/runner/memory-capture-optout.d.ts +80 -0
  82. package/dist/core/runner/memory-capture-optout.js +53 -0
  83. package/dist/core/runner/prepare-config-doors.d.ts +5 -0
  84. package/dist/core/runner/prepare-config-doors.js +16 -0
  85. package/dist/core/runner/prepare-hands-readface.d.ts +110 -5
  86. package/dist/core/runner/prepare-hands-readface.js +99 -7
  87. package/dist/core/runner/prepare-memory.d.ts +88 -0
  88. package/dist/core/runner/prepare-memory.js +305 -24
  89. package/dist/core/runner/prepare-task.d.ts +141 -1
  90. package/dist/core/runner/prepare-task.js +478 -81
  91. package/dist/core/runner/runtask.d.ts +9 -20
  92. package/dist/core/runner/runtask.js +133 -96
  93. package/dist/core/runner/session-file-state-replay.d.ts +18 -10
  94. package/dist/core/runner/session-file-state-replay.js +52 -1
  95. package/dist/core/runner/tool-disclosure.js +2 -1
  96. package/dist/core/runner/turn-attachments.d.ts +22 -12
  97. package/dist/core/session-store.d.ts +1 -1
  98. package/dist/core/session-store.js +6 -1
  99. package/dist/core/session.d.ts +34 -1
  100. package/dist/core/store-contracts/file-history-store-contract.d.ts +3 -0
  101. package/dist/core/store-contracts/file-history-store-contract.js +720 -0
  102. package/dist/core/store-contracts/permission-rule-sync-contract.js +15 -1
  103. package/dist/core/task-registry-shared.js +11 -1
  104. package/dist/core/tool-errors.js +1 -0
  105. package/dist/core/tool-policy.d.ts +172 -1
  106. package/dist/core/tool-policy.js +32 -1
  107. package/dist/core/tool-result-store.js +2 -1
  108. package/dist/core/trace.d.ts +31 -2
  109. package/dist/core/types.d.ts +795 -89
  110. package/dist/core/types.js +4 -3
  111. package/dist/core/untrusted-text.d.ts +1 -1
  112. package/dist/core/untrusted-text.js +8 -0
  113. package/dist/core/workflow-run-store-contract.js +6 -2
  114. package/dist/core/workflow-run-store.d.ts +4 -1
  115. package/dist/engine/compaction/compaction.d.ts +88 -10
  116. package/dist/engine/compaction/compaction.js +109 -30
  117. package/dist/engine/execution-env/node-execution-env.d.ts +9 -1
  118. package/dist/engine/execution-env/node-execution-env.js +28 -0
  119. package/dist/engine/harness/agent-harness.d.ts +52 -1
  120. package/dist/engine/harness/agent-harness.js +36 -1
  121. package/dist/engine/harness/types.d.ts +26 -1
  122. package/dist/engine/llm/types.d.ts +50 -4
  123. package/dist/engine/loop/agent-loop.d.ts +5 -1
  124. package/dist/engine/loop/agent-loop.js +25 -0
  125. package/dist/engine/loop/types.d.ts +19 -0
  126. package/dist/engine/lsp/node-lsp-manager.d.ts +1 -1
  127. package/dist/engine/session/session.js +1 -1
  128. package/dist/index.d.ts +20 -9
  129. package/dist/index.js +16 -7
  130. package/dist/orchestration/run-workflow-tool.d.ts +20 -2
  131. package/dist/orchestration/run-workflow-tool.js +22 -3
  132. package/dist/orchestration/workflow-governance.d.ts +59 -1
  133. package/dist/orchestration/workflow-governance.js +61 -8
  134. package/dist/orchestration/workflow-meta.d.ts +4 -2
  135. package/dist/orchestration/workflow-primitives.js +56 -13
  136. package/dist/orchestration/workflow-types.d.ts +78 -2
  137. package/dist/orchestration/workflow.d.ts +20 -0
  138. package/dist/orchestration/workflow.js +163 -14
  139. package/dist/prompt-assembly/event-registry.js +1 -1
  140. package/dist/prompts/default.d.ts +7 -7
  141. package/dist/stores/file/file-history-store.d.ts +368 -0
  142. package/dist/stores/file/file-history-store.js +1248 -0
  143. package/dist/stores/file/index.d.ts +22 -13
  144. package/dist/stores/file/index.js +4 -4
  145. package/dist/stores/file/permission-rule-store.d.ts +11 -0
  146. package/dist/stores/file/permission-rule-store.js +23 -9
  147. package/dist/stores/file/strategy-store.d.ts +3 -3
  148. package/dist/tools/fs/bash-readonly-classifier.d.ts +87 -3
  149. package/dist/tools/fs/bash-readonly-classifier.js +106 -4
  150. package/dist/tools/fs/fs-bash.js +9 -5
  151. package/dist/tools/fs/fs-shared.d.ts +52 -1
  152. package/dist/tools/fs/fs-shared.js +14 -0
  153. package/dist/tools/fs/fs-write.d.ts +5 -5
  154. package/dist/tools/fs/fs-write.js +71 -14
  155. package/dist/tools/fs/index.d.ts +6 -1
  156. package/dist/tools/fs/index.js +1 -1
  157. package/dist/tools/web.js +2 -1
  158. package/package.json +5 -1
  159. package/test/export-surface.snapshot.json +169 -23
  160. package/dist/core/file-snapshot-store.d.ts +0 -165
  161. package/dist/core/file-snapshot-store.js +0 -259
  162. package/dist/core/store-contracts/file-snapshot-store-contract.d.ts +0 -13
  163. package/dist/core/store-contracts/file-snapshot-store-contract.js +0 -134
  164. package/dist/stores/file/file-snapshot-store.d.ts +0 -58
  165. package/dist/stores/file/file-snapshot-store.js +0 -353
package/CHANGELOG.md CHANGED
@@ -1,5 +1,63 @@
1
1
  # Changelog
2
2
 
3
+ ## 7.0.0 — 2026-08-30
4
+
5
+ > Ships as its own major immediately after 6.0.0 (ruled: the three-dimensional rule scope travels alone so downstream adapts to it in isolation). The 6.0.0 artifact is this same tree with the design/382 P1 surface excised whole (type-driven surgery in an isolated worktree, tag v6.0.0); `npm run handoff:diff v6.0.0 v7.0.0` reproduces exactly this surface.
6
+
7
+ ### BREAKING (design/382 P1 — the three-dimensional rule scope)
8
+ - **`RuleScope` grows a third member** `{ kind: "session"; sessionId }` (design/382 §4): exhaustive switches over the two-member set no longer compile; `sameScope`/`scopeKey` answer three members. Org is deliberately NOT a member — an approval card is structurally unable to spell an org-wide grant. Two NAMED validation faces replace any one-table reading: `isValidConsentScope` (three members; the consent protocol and its server projection) vs `isValidDurableScope` (two members; the persisted store and rule-sync — a session row is refused at every durable entrance).
9
+ - **The consent scope default flips** (#490②): `prepareCardApproval` no longer defaults an absent `scope` to global. Explicit scope wins (validated; garbage refuses `config.invalid_argument`); absent scope with `cwd` present derives `{ kind: "project", root: cwd }`; BOTH absent refuses loudly with the new code `config.missing_scope`. Hosts relying on absent-scope-⇒-global must pass `{kind:"global"}` explicitly or thread the adjudicated cwd — the failure direction is toward asking, never a silent widening. Starter batch stays literal global; import scope mapping unchanged.
10
+ - **A session row is refused at every durable entrance** (§4.3, the open-set duty): `redemption-add`/`tighten-delete` deltas throw typed `unsupported.session_scope_store` (new shared backend assert `assertWriteDeltaScopeDurable`); sync-join drops session rules AND tombstones per-row with the new closed-set reason `session_scope_not_durable` (`RULE_SYNC_DROP_CODES` +1; `RuleQuarantineReason` excludes it — the quarantine area is itself durable) without withholding the round; a session-scope quarantine instruction throws; the file backend screens at-rest bytes off every read face including the sync outbound `readRaw`; `removePersistedRule` refuses a session scope — a session rule is not individually deletable, it dies with its session. The `permission.rule_sync_resurrected/dropped` trace events' `scopeKind` widens to include `"session"`.
11
+
12
+ ### Added
13
+ - **The session-rule overlay** (`permission-rule-session.ts`): `SessionRuleOverlay` contract + `InMemorySessionRuleOverlay` reference implementation (apply idempotent by dot; termination seals the lifetime epoch FIRST then clears — a late in-flight write refuses `session_ended`; `snapshotSession`/`restoreSession` carry rows across park/resume, restore fail-closed on scope and on the canonical re-projection). `RuleConsentDeps.sessionRules` lands session-scope redemptions there (the store write leg is never taken; dot-first crash order and re-apply-on-replay preserved); `RunnerDeps.sessionPermissionRules` splices the current session's rows in front of the persisted `list()` at the gate lane (narrowest-first report order; `sessionId` becomes the eligibility context's third axis, fail-closed when unthreaded). Overlay-served rows pass the same `normalizePersistedRule` re-projection screen the sync face runs on store rows, at all three re-entry doors (gate lane, prepare coverage, restore). New exports: `isValidConsentScope`, `isValidDurableScope`, `assertWriteDeltaScopeDurable`, `SessionRuleOverlay`, `SessionRuleOverlayAdd`, `SessionRuleOverlayApplyResult`, `InMemorySessionRuleOverlay`. The rule-sync contract kit grows the durable-two-member-face conformance vector (server twin duty, mechanized).
14
+
15
+ ## 6.0.0 — 2026-08-30
16
+
17
+ ### BREAKING
18
+ - **FileSnapshotStore family retired** (design/381): removed exports `FileSnapshotStore`/`InMemoryFileSnapshotStore`/`FileFileSnapshotStore`/`captureManifest`/`applyManifest`/`fileSnapshotStoreContract`/`DEFAULT_SNAPSHOT_BOUNDS` + types `FileFileSnapshotStoreOptions`/`FileSnapshotBounds`/`FileSnapshotError`/`FileSnapshotResult` (11 symbols). Replacement: the `FileHistoryStore` family — per-edited-file rewind topology with track/restore/export/reap verbs, two bundled backends, a cross-backend contract kit, restore preflight (symlink/kind/parent-chain), mode preservation, `diffStats`, and a fork-carry seat (`adoptScope` wired at prepare).
19
+ - **File-history self-trims by default** (#501): bundled backends trim each scope to the newest **100** boundaries after every committed snapshot (CC-anchored default). `retention: { keep: "unbounded" }` is the ONE spelling of "do not trim" — omitting the knob selects the default and trims. Malformed retention knobs REFUSE at construction (`config.retention_policy_invalid`), never fall back silently.
20
+ - **Out-of-root read asks join the mandated family** (#502, CC-anchored): a persisted allow rule can no longer silence the demote-to-ask raised by an out-of-root read (`cat <outside>` piped or bare, launcher-wrapped, or via an unmodelled `cd`/`pushd` base move). Matching rules are disclosed but do not clear the ask; explicit per-call confirmation is the only exit in this version (a future directory-grant offer is the designed clearing channel). Downstream probes pinned on "allow rule clears the out-of-root ask" will red. **Widened callback surface** (named, because it is deployment-facing): a `ToolSpec.reversibilityProbe` is now ALSO called when the surviving decision is already `ask` — that arm is the mandate's only source, so an ask-first policy (`defaultAction:"ask"`, any ask-first deployment policy) pays one probe call and a bounded wait (`approvalTimeoutMs`, 30s default) on every `"maybe"`-tier call, and a throwing/hanging probe reports through `onError(phase:"hook")` on those calls too. The verdict itself is DISCARDED there (a probe never un-asks another layer's question) and the tighten is still `allow`→`ask` only.
21
+ - **`TaskStream` gains a required member** `optOutMemoryCapture(options?)` (design/383): structural implementors/mocks must add it.
22
+ - **`TaskStream` gains a required member** `halt()` (#504): structural implementors/mocks must add it. The bare user interrupt in the CC Esc shape — cuts the running turn at a manufactured boundary (finished tool calls keep their real results; never-started ones settle as interrupted) and the run collects to a clean, RESUMABLE stop with no further model turn. Returns `{ turnCut: boolean }`.
23
+ - **Session memory-capture opt-out, whole machine** (design/383 slices 2+3): `TaskSpec.memory.capture: "off"` (single-member closed set; unknown spellings refuse `config.memory_capture_spelling`), one-way per-session record (create-only, fail-closed reads, irreversible by design — the exit is a new session), three forward legs + retroactive consolidation arm + mid-run flip verb with boundary sweep, delegation floor over the trusted internals channel, and `RuntimeCaps.allowMemoryOptOut` under `RunnerDeps.memoryCapturePolicy` (open/governed/capture-required; `config.memory_capture_policy` on garbage). Default OFF: no declaration + no record + no floor ⇒ byte-identical behavior.
24
+
25
+ ### Narrowings
26
+ - Trust-marked memory entries route to the marked consolidation arm; their products carry external/derived origin. New refusal paths: `purity_violation` stop reason, `config.memory_provenance` divergence arm, `config.memory_distiller_purity_contract` (design/383 slice 1).
27
+ - `task.user_steer_undrained`/`task.user_followup_undrained` `detail.taskId` is now always present; harvest-refused onError lines carry the report uuid; the unbound disclosure keeps exactly one current row; mode sweep reports once per path (#495).
28
+ - Governance/notice closed sets grew: `memory.capture_optout_denied` (terminal; classified `config`), notices `memory.capture_opted_out`/`memory.capture_optout_unpersisted`/`memory.consolidation_withheld` (audience user), config codes `memory_capture_spelling`/`memory_capture_policy`/`memory_capture_unsupported`.
29
+
30
+ ### Added
31
+ - Engine-minted per-run identity (#499): `TaskResult.runId` (uuidv7, minted as prepare's first statement), run-pairable `task.start`/`task.end`, `WorkflowAgentRun.taskRunId`, `SessionStoreSummary.lastTaskRunId`, `SessionStore.noteTaskRun` third arg; five per-run notice codes carry `detail.runId`. `config.models_swapped`/`route.base_url_changed_key_unchanged` carry a fresh `occurrenceId` per delivery.
32
+ - Workflow approval-wait visibility (#481/#485): `WorkflowAgentRun.pendingApproval`/`approvalWaitedMs`, `WorkflowRun.timeoutInterruption.approvalWaitedMs` (largest single leg); the stall watchdog stops charging retries while approvals are outstanding, bounded by a live total-timeout backstop.
33
+ - Summarization instruction-file channel (#500): `MaybeCompactOptions.contextInstructionFiles` — an optional deployment seat rendering instruction-file content as the request's first section (outside the transcript fence, both split-turn legs, bounded); fork requests carry an unconditional authority clarification clause. Absent seat ⇒ independent/update-pass requests byte-identical to before; the production runner wiring landed in this same version (see Changed below) — the deferral this line once recorded closed before shipping.
34
+ - Distillation purity surface (design/383 slice 1): `MEMORY_DISTILLER_PURITY_CONTRACT_V1`, `detectCleanArmVerbatimLeak`, `consolidationExposedFrontmatter`, egress disclosure, provenance divergence assertion.
35
+ - File-history retention knobs: `DEFAULT_FILE_HISTORY_BOUNDARY_KEEP`, `resolveFileHistoryRetention`, `fileHistoryBoundariesToKeep`, `FileHistoryRetentionPolicy`, `InMemoryFileHistoryStoreOptions`.
36
+ - `memory_index` tool (design/493) + memory tools resident by default; `SYSTEM_INJECTION_PRIORITIES` family; capture opt-out store surface (`fileSessionCaptureRecordStore`, mark/read/list, `SessionCaptureRecordStore`).
37
+ - Full export delta vs 5.65.0: `npm run handoff:diff v5.65.0 v6.0.0` reproduces it mechanically (11 removed; the added set grew across the freeze-window batches — the ship post carries the final count).
38
+ - Bare user halt, the whole verb (#504): `TaskResult.haltedByUser?: true` — the "a user stopped this" discriminator on an otherwise-completed result (absent when an abort/limit owned the ending first: first-writer-wins, do not read absence as the negative); `task.turn_interrupted` grows the `detail.cause: "user_halt"` arm (the steer-now mint carries no cause key — that IS the discriminator; no closed set gained a member); queued-but-undelivered frames are KEPT, never cleared (CC `still_queued` anchor), settling through the existing terminal accounts; the resume carriage is the user lane itself — the next runTask on the same session reads the halt marker then the new objective. Zero new exports beyond the interface member; no new stop reason (CC 2.1.250 has no typed interrupted terminal — a completed-with-marker ending is the anchored form).
39
+ - **A parked ancestor's auto-mode classifier can be rebuilt across a process boundary** (#503, opt-in, default OFF). An inherited chain layer's classifier is a live closure, so a durable park could freeze only its PRESENCE — a redemption in another process had nothing to build from, answered `unavailable`, and re-parked the same approval for a human on every attempt. With `RunnerDeps.autoMode.persistArming: true` the park now also records the arming's SERIALIZABLE criteria (rule sections, settings-deny rules, session context, window bounds, `timeoutMs`/`failureThreshold`, plus an optional deployment-declared `settingsEpoch`) on the frozen constraint-chain entry as `ConstraintChainEntry.autoModeArming`. The redeeming host reads that recipe off the row, calls the new `rebuildAutoModeDecider` with a FRESH model leg of its own (core assembles both prompt halves from the recipe — the trust gate is unchanged, rules still enter only through a deployment-authored object), and hands the returned `arming` back on the re-supplied chain entry so the constraint digest still matches. New exports: `rebuildAutoModeDecider`, `autoModeArmingRecipeOf`, `sanitizeAutoModeArmingRecipe`, `foldAutoModeArming`, `AUTO_MODE_ARMING_RECIPE_VERSION` + 7 types.
40
+ - **Two bounds, both mechanical.** A recipe is recorded only while the layer's breaker is untripped AND its consecutive-failure streak is zero — an ancestor that fell back to non-auto, or is one failure from doing so, hands nothing forward (the rebuilt decider starts a fresh streak, so carrying it would give the redemption more tolerance than the ancestor had left). And the rebuild folds the recorded arming against the REDEEMING deployment's current settings tighten-only: knobs take the minimum of their EFFECTIVE values (an omitted knob is the runtime default, never "unbounded"), and a prompt body that moved refuses the rebuild outright rather than guessing which free-text rule set is stricter — the redemption then keeps the pre-#503 behavior.
41
+ - **What a rebuild must declare** (both REQUIRED, both for permissiveness reasons rather than ergonomics): the redeeming deployment's own current settings — the bound is "the settings in force", and a caller that declares none has put nothing in force — and a transcript provider, because the 207 process judges an action against the run that produced it, so a classifier handed no history can allow what it would otherwise block, and an empty window states as fact that nothing has happened yet. A provider that returns `[]` is declaring the transcript empty, which is a different statement. The recipe also binds a digest of the exact assembled classifier prompt: the recipe carries the deployment's OVERRIDES while the base shell and permissions document come from the build's own SHA-locked assets, so a fleet mid-rollout could otherwise pass every recipe and digest check while enforcing different default rules. A build whose assembly differs — or an arming that recorded no digest — refuses (`prompt_assets_moved`).
42
+ - **Not cumulative across deployments, by design**: a row keeps binding what the ANCESTOR recorded (that is what its digest authenticates), so a tightening applied by one redeemer is not carried into the row it re-parks. Every redemption is bounded by its own declared settings instead; a deployment that wants a tightening to be durable tightens its own settings, where it is auditable.
43
+ - **Re-supply contract** (deployment-facing): hand back exactly what the row carries. The digest binds the bytes the ROW recorded, so a re-supply that DROPS or ALTERS a recorded arming refuses pre-CAS (`resume.parent_constraint_mismatch`, row stays pending). A re-supply that adds an arming to a row that recorded NONE is accepted — it is physically indistinguishable from the legal redemption where the same live layer still declares one, and the bound's real property is untouched: a withdrawn ancestor's CRITERIA never travel, so nothing on the row can rebuild its classifier. (The earlier both-directions refusal keyed the digest on live decider health and BRICKED armed parks whose ancestor's breaker moved after the mint — withdrawn deliberately, pre-release.) A `persistArming` over a face that does not canonicalize records nothing and announces once (`phase:"config"`).
44
+ - **`AutoModeDecider` gains an optional `consecutiveFailures()`** — implemented by `createAutoModeDecider`; a hand-rolled decider that omits it cannot carry a persisted arming (unknown state takes the same arm as a tripped latch).
45
+ - **Recorded boundary** (not a promise this makes): a withdrawn arming leaves the row at `autoModeArmed` with no recipe, which is digest-identical to an armed layer that never opted in — so the digest does not stop a re-supplier from handing back a fresh classifier of its own. That is the pre-existing re-supply trust posture (a caller able to invoke `resumeStream` holds full Runner authority) and is unchanged here; what the bound governs is that the ancestor's CRITERIA never travel from an unhealthy latch. A fleet running `persistArming` everywhere can read `autoModeArmed && !autoModeArming` as "withdrawn or unrecordable" and re-supply a refusing decider.
46
+ - Deployments that do not set `persistArming` are unaffected: no recipe exists, chain entries and their digests are byte-identical to 5.65.0, and a redemption keeps today's conservative refusal.
47
+
48
+ - **API failures disclose themselves on the assistant frame** (#506 ①, additive; consumer = the serving layer's transcript projection). `AssistantMessage` gains three optional seats, lifted at the streaming engine's single terminal-error mint point: `isApiErrorMessage` (this turn ended because the provider or the transport failed — CC-anchored spelling), `apiErrorStatus` (the failing HTTP status), and `requestId` (the provider's own request identifier, read from the failing attempt's `request-id` / `x-request-id` header). `model` — the model SENT — and `errorKind` were already there; **no closed set gained a member and no new event type exists.** The mark is DECLARED BY THE MINTER (`BrainError` gains `apiError`), not inferred from the error class, because the same class also carries pre-flight refusals that never touch the wire — a credential-pairing refusal is an `invalid_request` minted before any `fetch`, and calling that a provider fault points an operator at the wrong system. **Presence rules, since absence is a fact and never a default**: the mark is absent on success, on an aborted turn, on the circuit breaker's local fast-fail, on pre-flight refusals, and on the in-band terminal-output causes (`stream_torn` / `refusal` / `length_empty` — already classified as model-output problems rather than provider outages); `apiErrorStatus` carries only a status that NAMES the failure — a real HTTP status that is not a success — so it is absent (never zeroed, never a 2xx) for a connect failure, a stall, a mid-stream tear, an in-band error frame delivered inside a 200, and an `ok`-but-unstreamable response (an empty 200 from a proxy, a 204), which still marks the message and still carries its request id; `requestId` is absent when the failing attempt got no response or the provider stated no such header, is admitted only in a conservative identifier shape (`[A-Za-z0-9._:-]{1,128}`) because it is upstream bytes that end up persisted and rendered, and is cleared per attempt so a retry never inherits an earlier attempt's id. A consumer must not read absence as the negative on messages minted by a custom Brain that does not stamp these.
49
+ - **The RUN-LEVEL face** is `TaskResult.apiFailure?: { status?, requestId? }` (rides the `done` event), lifted from the terminal message at the one assembly point rather than re-derived. Its PRESENCE is the assertion that the run's terminal failure came from the provider boundary; an EMPTY object is meaningful (a provider failure nobody could label further) and is a different statement from the field being absent. Filled only when the provider failure is the terminal that NAMED the result — a run that then crossed a higher-ranked terminal (a budget ceiling, a walltime cut) reports that other cause and leaves this absent, the same rule `usageWindowRetryAfterMs` states for itself. **This is the face to consume**: an errored assistant message carrying no content is deliberately not persisted to the session transcript (a standing engine ruling), so the message-level seats above are observable on the live brain result and through this run-level projection, not by reloading a session. Making terminal API failures durable transcript entries is recorded as an open item — it is a transcript-composition change, not an additive one.
50
+ - **The retry-wait status frame states when the wait ends** (#506 ②, additive). `BrainStatus` and the wire `status` event gain `retryAtMs` — the WALL-CLOCK instant (epoch ms, read at emission in the emitting core process) the announced wait is expected to end. Present exactly when `retryInMs` is present, absent on frames that announce no wait (`recovered` / `gave_up`, and the immediate output-cap re-send); stamped centrally, so all four wait producers carry it (the connect ladder, both mid-stream retry tiers, and the circuit breaker's cooldown hint). It exists because `Date.now() + retryInMs` is only correct for a reader that received the frame instantly: a consumer several process hops away recomputes a deadline already shifted by the transit, so its countdown restarts on every re-announced slice instead of converging. `retryInMs` remains the authoritative RELATIVE quantity; this is a wall-clock convenience derived from it and must not be compared against a consumer's monotonic timers. The frame's provider-neutrality is unchanged — no HTTP status or provider taxonomy crosses this channel. **Note for consumers surveying this channel**: `attempt`, `retryInMs`, `retryInSec`, `maxRetries` and `errClass` were already on the wire `status` event before this release.
51
+
52
+ ### Changed
53
+ - **Summarization instruction files are wired in production** (#500): all three runner compaction lanes (turn boundary / prompt-too-long recovery / end-of-task) now fill `MaybeCompactOptions.contextInstructionFiles` from the deployment's own `loadProjectMemory` content — the raw loader text, not the composed memory block — so the summarizer reads the project's standing instructions at the one boundary where the transcript carrying them is replaced. The library seat shipped first with the runner wiring deferred; the wiring landed before this version shipped, so 6.0.0 carries both halves. **Cost, measured**: a deployment that wires `loadProjectMemory` now sends its instruction-file content plus exactly **716 chars** of framing (preamble + fence) on **every** summarization request, on both summary forms — payloads in this family run to tens of KB, so the payload is the bill, not the framing. **Bound, stated precisely**: the section is fitted to the compaction model's window on the INDEPENDENT form only (cut disclosed in-prompt, framing preserved) — and that form is the fallback, not the default. When a prefix-forking summary is eligible and the model answers in its envelope, the fork is the pass and its section rides WHOLE at any size, bounded only by a prompt-too-long that degrades once to the independent form (real, but not total: an oversize rejection the classifier cannot read as prompt-too-long returns terminally). A payload that overflows the section itself cannot be recovered by the retry loop, which may drop only conversation — that boundary fails rather than sending a shorter request. Deployments with a large instruction file and a small or unknown `contextWindow` should size accordingly. Deployments that never wired the loader are unaffected: the key is absent, so independent/update-pass requests stay byte-identical.
54
+ - **Provenance note for deployments** (#500): the content fed to the summarizer is the loader's own text, which on this engine is repository-controlled — the same bytes the main lane frames as data are given instruction standing (scoped by the section's preamble to "how to summarize") in the request whose output replaces the transcript. Deployments that do not want repository-authored text in that position should not wire `loadProjectMemory`, or should return only content they vouch for. A separate deployment-vouched summarization-instructions seat is recorded as an open design candidate.
55
+ - **`<instruction-files>` joins the DISCLOSED envelope set** (#500): now that the engine mints this envelope on ordinary production paths, a copy of it appearing inside verbatim external data (file content, command output, server responses) gets the design/319 trailer instead of containment alone. `DISCLOSED_ENVELOPE_TAGS` grows by one member; `scope` remains fenced-only.
56
+ - **A governed workflow spec accepts `model` as an alias for `modelName`** (#505 ②): one axis — which model a spawned child runs on — is spelled `model` by the Agent tool and by agents frontmatter, and `modelName` here, so the spelling two of the three faces teach used to fall to the strip survey and the child ran on the deployment default while the call reported success. `model` is now normalized to `modelName` at a single door (the untrusted-spec whitelist read); nothing downstream sees the alias spelling, and `WHITELIST_KEYS` is unchanged — the accepted VOCABULARY widened, not what a script can put inside a child. **Loudness, both directions**: a `Model` OBJECT written as `model` is now type-refused by name (it was silently ignored — same safety property, one notch louder, and a probe pinning the old silence needs re-seating), and writing BOTH spellings with DIFFERENT values refuses whole rather than picking one by precedence (two names for one field naming two models has no reading that runs the agent the author meant). Both spellings agreeing folds; an own key carrying `undefined` counts as absent on either side. `model`/`modelName` written on the OPTIONS object (second argument) keeps the #505 ① typed refusal pointing at the spec, and both lanes now derive that vocabulary from one map. **Which spelling counts is decided by OWN keys**, captured in the same name phase as the strip survey: an inherited `model` cannot refuse or override an own `modelName` (or the reverse), an accessor cannot add or delete a spelling mid-read to change the answer, and an inherited `modelName` is still consulted — and still refused when malformed — in the one case that predates the alias, where the spec wrote neither spelling.
57
+ - **The Workflow tool card says what a legal `modelName` value is** (#505 ②, requested by a downstream shell): the bullet previously stated only that the field needs a configured allowlist. It now also says the legal values are the deployment's own model **catalog keys** — not generic tier words, so a bare `opus`/`sonnet`/`haiku` is refused rather than approximated — and that the field belongs on the spec (first argument), where `model` is accepted as its alias. The allowlist precondition and the omit-it-if-unsure advice are unchanged.
58
+ - **The workflow launch receipt carries the two facts its Agent-tool sibling had and it did not** ([5674] F1, #505 ③, additive): the `details` card gains `task_id` — every other face of the unified task lane spells "which task is this" that way, and this card was the one place a structured consumer had to know the lane-private `workflowRunId` spelling; `type`/`status`/`workflowRunId` are byte-untouched, so the shell's background-line marker is unaffected. The JSON body gains `handling`, the internal-metadata / do-not-echo contract the Agent tool's async-launch card has carried since RB-291 — and this is the receipt that hands back a host filesystem path (`scriptPath`) beside the ids. It is a separate key from `note` because `note` is written per notifier tier and this is true of every tier. **Deliberately NOT aligned**: the envelope GRAMMAR stays split (JSON body here, text card there) — both have shipped consumers and the Agent card is the CC-anchored form, so converting either direction would be breaking; the ruling is pinned so a later change to it is deliberate.
59
+
60
+
3
61
  ## 5.65.0 — 2026-08-28
4
62
 
5
63
  ### Added
@@ -51,7 +51,7 @@ export declare const DEFAULT_SUBAGENT_TOOL_NAME = "Agent";
51
51
  */
52
52
  export declare const EXTRA_TOOLS_MAX_FACTORY_CALLS_PER_TREE = 64;
53
53
  /** RB-292 — default delegation nesting depth. CC 2.1.220 `bee()` @272064 falls back to `aHu = 3`
54
- * @272076; the previous value 5 was CC 2.1.201's `Y4t` and is a stale anchor. Semantics (off-by-one)
54
+ * @272076 (cli250.js:37597); the previous value 5 was CC 2.1.201's `Y4t` and is a stale anchor. Semantics (off-by-one)
55
55
  * are pinned on {@link SubagentToolOptions.maxDepth}. */
56
56
  export declare const DEFAULT_SUBAGENT_MAX_DEPTH = 3;
57
57
  /**
@@ -628,7 +628,7 @@ export interface SubagentToolOptions {
628
628
  * `d + 1 < maxDepth`, so it cannot recurse past the limit.
629
629
  *
630
630
  * Default 3 (RB-292) — CC 2.1.220 parity: `bee()` @272064 returns `CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH`
631
- * when set, else a remote-config value, else the constant `aHu = 3` @272076. The old default of 5 was
631
+ * when set, else a remote-config value, else the constant `aHu = 3` @272076 (cli250.js:37597). The old default of 5 was
632
632
  * anchored on CC 2.1.201's `Y4t` and upstream has since moved; 5 is a stale anchor, not a sema choice.
633
633
  * sema exposes the override as THIS option rather than an env var (deployments configure the tool, not
634
634
  * the process), so the env/remote-config half of `bee()` has no sema counterpart to keep in sync.
@@ -1952,6 +1952,17 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1952
1952
  ...(childDefaultPersona !== undefined ? { defaultSystemPrompt: childDefaultPersona } : {}),
1953
1953
  isDelegatedChild: true,
1954
1954
  ...(ctx.interactionPosture !== undefined ? { parentInteractionPosture: ctx.interactionPosture } : {}),
1955
+ ...(reviveClaim === undefined
1956
+ ? {
1957
+ ...(ctx.memoryCaptureOptedOut === true ? { memoryCaptureFloor: true } : {}),
1958
+ ...(ctx.memoryCaptureOptedOut !== true && ctx.memoryCaptureIndeterminate === true ? { memoryCaptureFloorIndeterminate: true } : {}),
1959
+ ...(ctx.memoryCaptureControlDir !== undefined ? { memoryCaptureQueryDir: ctx.memoryCaptureControlDir } : {}),
1960
+ ...(ctx.memoryCaptureAncestors !== undefined ? { memoryCaptureAncestors: ctx.memoryCaptureAncestors } : {}),
1961
+ }
1962
+ : (() => {
1963
+ const rows = [...new Set([reviveClaim.row.parentSessionId, reviveClaim.row.rootSessionId].filter((s) => s !== undefined))].map((sessionId) => ({ sessionId }));
1964
+ return rows.length > 0 ? { memoryCaptureAncestors: rows } : {};
1965
+ })()),
1955
1966
  parentToolCallId: ctx.toolCallId,
1956
1967
  ...(childAgentName ? { agentName: childAgentName } : {}),
1957
1968
  ...(agentName !== undefined ? { explicitAgentName: agentName } : {}),
@@ -14,7 +14,7 @@ import type { ModelRef, TaskResult, TaskSpec, ToolSpec } from "../core/types.js"
14
14
  * 2.1.207 through 2.1.220 ship NO verification agent. The current upstream completion-gate shape is
15
15
  * (a) the Stop hook (`decision:"block"` + reason fed back — sema's Stop hook is at full parity) and
16
16
  * (b) — corrected — NOT a bundled skill. There is no built-in `verify` skill in the 220 corpus. What
17
- * 220 ships is an INSTRUCTION TO CREATE ONE PER PROJECT: its memory prompt (@190146) forbids creating
17
+ * 220 ships is an INSTRUCTION TO CREATE ONE PER PROJECT: its memory prompt (@190146 (cli250.js:38927)) forbids creating
18
18
  * project skills ("never create one — a new project skill silently shadows a same-named built-in
19
19
  * skill") and makes exactly one exception — "The single exception is verify, because how a project
20
20
  * verifies changes is project-specific: put a verify correction in the `.claude/skills/verify/
@@ -510,7 +510,7 @@ export function createAnthropicBrain(config = {}) {
510
510
  : undefined;
511
511
  const inputTooLong = e?.type === "request_too_large" ||
512
512
  (e?.type === "invalid_request_error" && typeof e.message === "string" && startsWithInputTooLongAnchor(e.message));
513
- throw new BrainError(anthropicStreamErrorCode(e?.type), `anthropic stream error (${e?.type ?? "unknown"}): ${e?.message ?? ""}`, undefined, e === undefined ? undefined : inputTooLong ? { inputTooLong: true } : { inputTooLongRuledOut: true });
513
+ throw new BrainError(anthropicStreamErrorCode(e?.type), `anthropic stream error (${e?.type ?? "unknown"}): ${e?.message ?? ""}`, undefined, { apiError: true, ...(e === undefined ? {} : inputTooLong ? { inputTooLong: true } : { inputTooLongRuledOut: true }) });
514
514
  }
515
515
  default:
516
516
  break;
@@ -48,11 +48,40 @@ export declare class BrainError extends Error {
48
48
  * `inputTooLong`; BOTH absent = classification could not establish provenance ("unknown"),
49
49
  * and the prose fallback keeps its historic reach. */
50
50
  readonly inputTooLongRuledOut?: true;
51
+ /**
52
+ * This failure was produced AT THE PROVIDER BOUNDARY — a transport attempt was made against the
53
+ * serving endpoint, or the provider itself answered with a failure (an HTTP error response, an
54
+ * in-band SSE error frame, a connect/first-token/idle stall on a live request). It is what lifts
55
+ * `AssistantMessage.isApiErrorMessage` at the terminal-shell mint, so a consumer can tell "the
56
+ * provider/network failed this turn" from "this deployment refused to send the request at all".
57
+ *
58
+ * DECLARED BY THE MINTER, never inferred from the class: `BrainError` is also the carrier for
59
+ * PRE-FLIGHT refusals that never touch the wire (the route adjudicator's credential-pairing
60
+ * refusal is an `invalid_request` minted before any `fetch`), and treating those as provider
61
+ * faults would point an operator at the wrong system. Absent = not claimed, which is the
62
+ * conservative reading for every pre-existing and third-party construction site.
63
+ */
64
+ readonly apiError?: true;
51
65
  constructor(code: BrainErrorCode, detail: string, status?: number, opts?: {
52
66
  inputTooLong?: boolean;
53
67
  inputTooLongRuledOut?: boolean;
68
+ apiError?: boolean;
54
69
  });
55
70
  }
71
+ /**
72
+ * Lift the provider's own request identifier off a response, when it stated one in a shape worth
73
+ * carrying (see {@link REQUEST_ID_RE}). This is the handle a provider's support channel asks for, and
74
+ * without it a failed turn's only evidence is prose the operator cannot correlate upstream.
75
+ *
76
+ * Duck-typed on `headers.get` rather than requiring a real `Response`: the engine's transport is a
77
+ * caller-injected `fetch`, so a deployment (or a test) may hand back any response-shaped object, and
78
+ * a missing/throwing header bag must degrade to "no id" instead of failing the call it is describing.
79
+ */
80
+ export declare function readProviderRequestId(res: {
81
+ headers?: {
82
+ get?: (name: string) => string | null;
83
+ };
84
+ } | undefined): string | undefined;
56
85
  /** Map an HTTP status to an error class. 401/403 = auth (don't retry); 429 = rate limit; 5xx = server. */
57
86
  export declare function classifyHttp(status: number): BrainErrorCode;
58
87
  /** Lift the machine-readable code back out of a `[code] …` prefixed `errorMessage` (the single shared
@@ -14,6 +14,7 @@ export class BrainError extends Error {
14
14
  status;
15
15
  inputTooLong;
16
16
  inputTooLongRuledOut;
17
+ apiError;
17
18
  constructor(code, detail, status, opts) {
18
19
  super(`[${code}] ${detail}`);
19
20
  this.name = "BrainError";
@@ -23,8 +24,27 @@ export class BrainError extends Error {
23
24
  this.inputTooLong = true;
24
25
  else if (opts?.inputTooLongRuledOut === true)
25
26
  this.inputTooLongRuledOut = true;
27
+ if (opts?.apiError === true)
28
+ this.apiError = true;
26
29
  }
27
30
  }
31
+ const REQUEST_ID_HEADERS = ["request-id", "x-request-id"];
32
+ const REQUEST_ID_RE = /^[A-Za-z0-9._:-]{1,128}$/;
33
+ export function readProviderRequestId(res) {
34
+ try {
35
+ const get = res?.headers?.get;
36
+ if (typeof get !== "function")
37
+ return undefined;
38
+ for (const name of REQUEST_ID_HEADERS) {
39
+ const raw = get.call(res.headers, name);
40
+ if (typeof raw === "string" && REQUEST_ID_RE.test(raw))
41
+ return raw;
42
+ }
43
+ }
44
+ catch {
45
+ }
46
+ return undefined;
47
+ }
28
48
  export function classifyHttp(status) {
29
49
  if (status === 401 || status === 403)
30
50
  return "auth";
@@ -551,10 +551,10 @@ export function createOpenResponsesBrain(config = {}) {
551
551
  }
552
552
  case "response.failed": {
553
553
  const err = frame.response?.error;
554
- throw new BrainError(responsesErrorCode(err?.code), `responses stream failed (${err?.code ?? "unknown"}): ${err?.message ?? ""}`);
554
+ throw new BrainError(responsesErrorCode(err?.code), `responses stream failed (${err?.code ?? "unknown"}): ${err?.message ?? ""}`, undefined, { apiError: true });
555
555
  }
556
556
  case "error": {
557
- throw new BrainError(responsesErrorCode(frame.code), `responses stream error (${frame.code ?? "unknown"}): ${frame.message ?? ""}`);
557
+ throw new BrainError(responsesErrorCode(frame.code), `responses stream error (${frame.code ?? "unknown"}): ${frame.message ?? ""}`, undefined, { apiError: true });
558
558
  }
559
559
  default:
560
560
  return;
@@ -180,7 +180,13 @@ export declare function sameRouteIdentity(a: Pick<Model, "id" | "baseUrl">, b: P
180
180
  * what an operator correlates the line against ("which task ran on the fallback model"). The
181
181
  * AUDIENCE stays `"operator"` — the fix is a credential/catalog fix, and no end user can act on it;
182
182
  * attribution is a routing key, not an entitlement. Omitted when a caller has no session in hand,
183
- * never fabricated. */
183
+ * never fabricated.
184
+ *
185
+ * `runId` (#499, additive): the SESSION is not the unit this line is about — the fallback happened
186
+ * while resolving ONE INVOCATION's seats, and two runs of one session each falling back on the same
187
+ * seat used to mint byte-identical notices ("did the same task fall back twice, or two tasks once
188
+ * each?" was unanswerable). Same posture as `sessionId`: a correlation key, omitted when the caller
189
+ * has none, never fabricated. */
184
190
  export declare function fallbackToPrimaryNotice(args: {
185
191
  seat: string;
186
192
  from: string;
@@ -189,6 +195,7 @@ export declare function fallbackToPrimaryNotice(args: {
189
195
  ok: false;
190
196
  }>;
191
197
  sessionId?: string;
198
+ runId?: string;
192
199
  }): EngineNotice;
193
200
  /** The auth carriers of a header bag, canonicalized (case-fold name → value, sorted) — the
194
201
  * Model-visible credential fingerprint used by catalog-change disclosure (a same-name entry whose
@@ -180,6 +180,7 @@ export function fallbackToPrimaryNotice(args) {
180
180
  cause: args.verdict.code,
181
181
  fixHint: args.verdict.detail.fixHint,
182
182
  ...(args.sessionId !== undefined ? { sessionId: args.sessionId } : {}),
183
+ ...(args.runId !== undefined ? { runId: args.runId } : {}),
183
184
  },
184
185
  };
185
186
  }
@@ -5,11 +5,22 @@ export function runWithStatusSink(emit, fn) {
5
5
  }
6
6
  export function emitBrainStatus(status) {
7
7
  try {
8
- statusSinkStore.getStore()?.emit(status);
8
+ const sink = statusSinkStore.getStore();
9
+ if (sink === undefined)
10
+ return;
11
+ sink.emit(withRetryDeadline(status));
9
12
  }
10
13
  catch {
11
14
  }
12
15
  }
16
+ function withRetryDeadline(status) {
17
+ if (status.retryAtMs !== undefined)
18
+ return status;
19
+ const { retryInMs } = status;
20
+ if (retryInMs === undefined || !Number.isFinite(retryInMs))
21
+ return status;
22
+ return { ...status, retryAtMs: Date.now() + retryInMs };
23
+ }
13
24
  const telemetrySinkStore = new AsyncLocalStorage();
14
25
  export function runWithBrainTelemetry(emit, fn) {
15
26
  return telemetrySinkStore.run({ emit }, fn);
@@ -1,6 +1,6 @@
1
1
  import { createAssistantMessageEventStream, } from "../internal/llm.js";
2
2
  import { FLOOR_OUTPUT_TOKENS, parseContextOverflow, planOutputCapAdjustment } from "./context-overflow.js";
3
- import { BrainError, classifyConnectFailure, classifyHttp, describeNetworkError } from "./errors.js";
3
+ import { BrainError, classifyConnectFailure, classifyHttp, describeNetworkError, readProviderRequestId } from "./errors.js";
4
4
  import { classifyInputTooLong } from "./input-too-long.js";
5
5
  import { FAST_MAX_BACKOFF_MS, providerWaitHint, retryBackoffMs } from "./retry.js";
6
6
  import { emitBrainStatus, emitBrainTelemetry } from "./status-sink.js";
@@ -251,6 +251,7 @@ export function runStreamingBrain(args) {
251
251
  let announcedRetry = false;
252
252
  let terminalRetryPhase = "recovered";
253
253
  let terminalRetryDetail = "recovered after retrying";
254
+ let requestIdSeen;
254
255
  void run()
255
256
  .catch((err) => {
256
257
  const aborted = signal?.aborted === true || isAbortError(err);
@@ -264,6 +265,14 @@ export function runStreamingBrain(args) {
264
265
  errorMsg.errorKind = "input_too_long";
265
266
  else if (err.inputTooLongRuledOut === true)
266
267
  errorMsg.inputTooLongRuledOut = true;
268
+ if (err.apiError === true) {
269
+ errorMsg.isApiErrorMessage = true;
270
+ const st = err.status;
271
+ if (st !== undefined && st >= 100 && !(st >= 200 && st < 300))
272
+ errorMsg.apiErrorStatus = st;
273
+ if (requestIdSeen !== undefined)
274
+ errorMsg.requestId = requestIdSeen;
275
+ }
267
276
  }
268
277
  errorMsg.usageMissing = true;
269
278
  out.push({ type: "error", reason: aborted ? "aborted" : "error", error: errorMsg });
@@ -305,8 +314,10 @@ export function runStreamingBrain(args) {
305
314
  const cc = createConnectController(connectTimeoutMs, signal);
306
315
  let r;
307
316
  let netErr;
317
+ requestIdSeen = undefined;
308
318
  try {
309
319
  r = await doFetch(req.url, { method: "POST", headers: req.headers, body: req.body, signal: cc.signal });
320
+ requestIdSeen = readProviderRequestId(r);
310
321
  }
311
322
  catch (e) {
312
323
  if (cc.timedOut()) {
@@ -433,11 +444,11 @@ export function runStreamingBrain(args) {
433
444
  continue;
434
445
  }
435
446
  if (netErr)
436
- throw new BrainError("network", describeNetworkError(netErr));
447
+ throw new BrainError("network", describeNetworkError(netErr), undefined, { apiError: true });
437
448
  const detail = overflowBody ?? (r ? (await readErrorBody(r, signal)).text : "");
438
449
  const status = r?.status ?? 0;
439
450
  const itl = classifyInputTooLong(status, detail);
440
- throw new BrainError(classifyHttp(status), `${httpLabel} HTTP ${status || "ERR"}: ${detail.slice(0, 500)}${overflowDeclineNote}`, status, itl === "yes" ? { inputTooLong: true } : itl === "no" ? { inputTooLongRuledOut: true } : undefined);
451
+ throw new BrainError(classifyHttp(status), `${httpLabel} HTTP ${status || "ERR"}: ${detail.slice(0, 500)}${overflowDeclineNote}`, status, { apiError: true, ...(itl === "yes" ? { inputTooLong: true } : itl === "no" ? { inputTooLongRuledOut: true } : {}) });
441
452
  }
442
453
  finally {
443
454
  cc.abort();
@@ -536,7 +547,7 @@ export function runStreamingBrain(args) {
536
547
  throw e;
537
548
  failure = {
538
549
  kind: "connection",
539
- err: new BrainError("network", `mid-stream read failed: ${describeNetworkError(e)}`),
550
+ err: new BrainError("network", `mid-stream read failed: ${describeNetworkError(e)}`, undefined, { apiError: true }),
540
551
  };
541
552
  break readLoop;
542
553
  }
@@ -567,10 +578,10 @@ export function runStreamingBrain(args) {
567
578
  idleTimer = undefined;
568
579
  }
569
580
  if (!failure && firstTokenTimedOut) {
570
- failure = { kind: "stalled", err: new BrainError("network", `first-token timeout after ${firstTokenTimeoutMs}ms`) };
581
+ failure = { kind: "stalled", err: new BrainError("network", `first-token timeout after ${firstTokenTimeoutMs}ms`, undefined, { apiError: true }) };
571
582
  }
572
583
  if (!failure && idleTimedOut) {
573
- failure = { kind: "stalled", err: new BrainError("network", `stream idle timeout after ${idleTimeoutMs}ms`) };
584
+ failure = { kind: "stalled", err: new BrainError("network", `stream idle timeout after ${idleTimeoutMs}ms`, undefined, { apiError: true }) };
574
585
  }
575
586
  if (!failure) {
576
587
  buffer += decoder.decode();
@@ -159,6 +159,32 @@ export interface MaybeCompactOptions {
159
159
  * maybeCompact via {@link sanitizeCompactionInstructions} (design/145 §4 — every channel,
160
160
  * including the preCompact hook's additions, gets the same treatment). */
161
161
  customInstructions?: string;
162
+ /**
163
+ * #500 — the user's / project's own INSTRUCTION-FILE content (the CLAUDE.md family this
164
+ * deployment loads into an ordinary session), for the INDEPENDENT summary form.
165
+ *
166
+ * Why it needs a seat at all: the summarization prompt carries CC's closing invitation to follow
167
+ * "additional summarization instructions provided in the included context", and the independent
168
+ * form swaps in its own system prompt — so the loader-mounted files never reached that
169
+ * summarizer and the invitation named nothing this form delivers. Supplying this makes it true:
170
+ * the content is rendered as the request's first section, ahead of the conversation, with
171
+ * instruction standing for the summarization task only. The FORK form carries it too — in the ONE
172
+ * message that form appends, so the cached main prefix is untouched: it preserves whatever system
173
+ * prompt the main request had, and nothing establishes that this prompt contains the files, so a
174
+ * conforming fork response would otherwise succeed having never seen them. Budget for the seat's
175
+ * bytes on BOTH forms.
176
+ *
177
+ * TRUST TIER — host supply, same as `systemPrompt`: this string is handed to the model as
178
+ * instructions. Never fill it from tool output, model text, or any other in-band channel. It is
179
+ * NOT sanitized or length-capped here (unlike `customInstructions`, which is per-pass steering
180
+ * text of unknown provenance) precisely because it is the deployment's own prompt material.
181
+ *
182
+ * ABSENT (default) ⇒ every summarization request byte is what it was before this option existed.
183
+ * That invariant is over the summarization REQUEST; the one deliberate effect outside it — the
184
+ * new `<instruction-files>` authority envelope being defused on the fenced/shelled lanes for every
185
+ * deployment — is stated at `CompactionPreparation.contextInstructionFiles`.
186
+ */
187
+ contextInstructionFiles?: string;
162
188
  signal?: AbortSignal;
163
189
  /**
164
190
  * Manual compact() withdrawal signal — the composed cancel signals of the manual callers THIS
@@ -111,7 +111,7 @@ export async function maybeCompact(opts) {
111
111
  };
112
112
  }
113
113
  const branch = await opts.session.getBranch();
114
- const prep = prepareCompaction(branch, settings, cpt, window);
114
+ const prep = prepareCompaction(branch, settings, cpt, window, opts.contextInstructionFiles);
115
115
  if (!prep.ok) {
116
116
  throw prep.error;
117
117
  }
@@ -208,7 +208,12 @@ export async function maybeCompact(opts) {
208
208
  if (wantsWindowSafety) {
209
209
  dry = dryRunSummarizationClamp(prep.value, summaryModel, effectiveInstructions);
210
210
  }
211
- const contentTokens = dry !== undefined ? Math.ceil((dry.conversationChars + (prep.value.previousSummary?.length ?? 0)) / cpt) : 0;
211
+ const mainSection = dry !== undefined && prep.value.contextInstructionFiles !== undefined
212
+ ? dryRunSummarizationClamp(prep.value, opts.model, effectiveInstructions).instructionFilesChars
213
+ : 0;
214
+ const contentTokens = dry !== undefined
215
+ ? Math.ceil((dry.conversationChars + mainSection + (prep.value.previousSummary?.length ?? 0)) / cpt)
216
+ : 0;
212
217
  const observeWindowSafety = (info) => {
213
218
  notifier.notify(() => opts.onWindowSafety?.(info), "compaction.onWindowSafety");
214
219
  };
@@ -0,0 +1,138 @@
1
+ import type { AutoModeRules, AutoModeWindowOptions } from "./auto-mode-prompt.js";
2
+ /** The recipe format's version. A reader that does not know a version REFUSES it (never guesses): the
3
+ * recipe names the criteria a classifier enforces, and a partially-understood criteria set is the one
4
+ * thing a permission gate may not improvise. */
5
+ export declare const AUTO_MODE_ARMING_RECIPE_VERSION = 1;
6
+ /**
7
+ * The SERIALIZABLE half of an auto-mode arming — everything `createAutoModeDecider` +
8
+ * `buildAutoModePrompt` need except the model leg itself. Plain data by construction (JSON /
9
+ * `structuredClone` round-trips), because it rides a durable checkpoint row and is bound by the
10
+ * constraint-chain digest.
11
+ *
12
+ * Deliberately NOT carried: `onBreakerOpen` (a closure — the redeeming deployment wires its own alarm
13
+ * seat, which is where the operator watching THAT process can see it) and the model/roster selection
14
+ * (the redeeming deployment routes its own classifier leg; a recorded model id would name a catalog
15
+ * entry that need not exist in the redeeming fleet).
16
+ */
17
+ export interface AutoModeArmingRecipe {
18
+ /** {@link AUTO_MODE_ARMING_RECIPE_VERSION}. An unknown version is refused, never partially read. */
19
+ v: number;
20
+ /** `buildAutoModePrompt` rule overrides for the four paired sections (omitted = CC defaults verbatim). */
21
+ rules?: AutoModeRules;
22
+ /** Settings-plane deny rules rendered into `<settings_deny_rules>`. */
23
+ settingsDenyRules?: readonly string[];
24
+ /** `## Session Context` facts appended to the assembled document. */
25
+ sessionContext?: readonly string[];
26
+ /** Transcript-window bounds for the classify prompt. */
27
+ window?: AutoModeWindowOptions;
28
+ /** Classify round-trip cap, ms. */
29
+ timeoutMs?: number;
30
+ /** Consecutive-failure threshold opening the one-way breaker (floored, as the decider itself floors it). */
31
+ failureThreshold?: number;
32
+ /**
33
+ * A digest of the EXACT classifier system prompt this arming assembled (see
34
+ * `rebuildAutoModeDecider`). The recipe records the deployment's OVERRIDES; the bulk of the criteria —
35
+ * the base shell and the permissions document — lives in the build's own SHA-locked assets, which the
36
+ * recipe cannot carry and a redemption assembles from its OWN binary. Under a rolling upgrade two
37
+ * builds can accept the same recipe and enforce different default rules, and every recipe and digest
38
+ * check would pass. Binding the assembled prompt closes that: a redeeming build whose assembly differs
39
+ * by one byte refuses to rebuild instead of enforcing criteria the ancestor never ran.
40
+ */
41
+ promptDigest?: string;
42
+ /**
43
+ * The deployment's auto-mode SETTINGS GENERATION at arming time — an opaque string the deployment
44
+ * owns (a config revision, a settings hash, a publish id). It is a DECLARATION axis and can only make
45
+ * the rebuild stricter: an epoch that MOVED while the recorded body still equals the redeeming
46
+ * deployment's own is harmless (the fold proceeds), and an epoch that is EQUAL while the bodies
47
+ * differ is a contradiction the fold refuses loudly rather than resolving by picking a side.
48
+ */
49
+ settingsEpoch?: string;
50
+ }
51
+ /** The deployment assembly face this recipe is minted from — structurally `RunnerDeps.autoMode`. */
52
+ export interface AutoModeArmingFace {
53
+ rules?: AutoModeRules;
54
+ settingsDenyRules?: readonly string[];
55
+ sessionContext?: readonly string[];
56
+ window?: AutoModeWindowOptions;
57
+ timeoutMs?: number;
58
+ failureThreshold?: number;
59
+ settingsEpoch?: string;
60
+ }
61
+ /**
62
+ * Canonicalize + VALIDATE an arming recipe: the plain-data form that persists, or `undefined` when the
63
+ * input is not one (unknown version, a non-string in a rule list, a NaN knob, a non-object).
64
+ *
65
+ * IDEMPOTENT by construction (the output is a fresh object built from known keys only, with the
66
+ * absent/empty collapse and the knob normalization already applied), which is what lets the mint side
67
+ * and the resume RE-SUPPLY side run it independently and still digest identically — the two sites never
68
+ * see the same object, only the same values.
69
+ *
70
+ * A malformed recipe yields `undefined` rather than a partially-pruned one: silently dropping half a
71
+ * rule list would change the criteria a classifier enforces while every downstream face still called it
72
+ * "the ancestor's arming". The LOUD half of this rule lives at the mint (prepare-task announces a
73
+ * requested-but-unpersistable arming); here the answer is simply "no recipe", which degrades to the
74
+ * pre-#503 behavior (an honest refusal at the redemption).
75
+ */
76
+ export declare function sanitizeAutoModeArmingRecipe(value: unknown): AutoModeArmingRecipe | undefined;
77
+ /**
78
+ * Mint a recipe from a deployment's auto-mode assembly face (`RunnerDeps.autoMode`): the same values the
79
+ * Runner arms its own decider with, canonicalized. `undefined` when the face is not serializable — the
80
+ * caller is expected to say so out loud rather than park a silently thinner arming.
81
+ *
82
+ * Used on BOTH ends of the boundary: the Runner mints it at arming time (so a park can record it), and a
83
+ * redeeming host mints its OWN face into the `current` half of {@link foldAutoModeArming}.
84
+ */
85
+ export declare function autoModeArmingRecipeOf(face: AutoModeArmingFace, bind?: {
86
+ promptDigest?: string;
87
+ }): AutoModeArmingRecipe | undefined;
88
+ /** Why a rebuild refused. Every arm leaves the redemption on the PRE-#503 path (honest `unavailable` →
89
+ * the frozen approver chain → a human), so a refusal costs a second park, never a widened gate. */
90
+ export type AutoModeRebuildRefusal =
91
+ /** The recorded recipe is not a recipe this build understands (unknown version / not plain data). */
92
+ "malformed_recipe"
93
+ /** No current settings were declared, so nothing bounds the rebuild — see {@link foldAutoModeArming}. */
94
+ | "current_settings_missing"
95
+ /** The redeeming deployment's OWN settings face is malformed — it cannot bound the rebuild, so nothing is built. */
96
+ | "malformed_current_settings"
97
+ /** The redeeming build assembles a DIFFERENT classifier prompt than the arming recorded (asset skew). */
98
+ | "prompt_assets_moved"
99
+ /** The prompt body moved between the recorded arming and the redeeming deployment's current settings. */
100
+ | "settings_moved"
101
+ /** The two sides declare the SAME settings epoch while their bodies differ — a contradiction, not a fold. */
102
+ | "epoch_inconsistent";
103
+ export type AutoModeArmingFold = {
104
+ ok: true;
105
+ /** The RECORDED recipe, canonical — the digest-bearing half (what the ancestor's row says). */
106
+ recorded: AutoModeArmingRecipe;
107
+ /** The criteria the rebuilt decider runs under: the agreed prompt body + the MINIMUM of each knob. */
108
+ effective: AutoModeArmingRecipe;
109
+ /** True iff a knob was actually pulled in by the current settings (informational, for a host log). */
110
+ tightened: boolean;
111
+ } | {
112
+ ok: false;
113
+ reason: AutoModeRebuildRefusal;
114
+ message: string;
115
+ };
116
+ /**
117
+ * Safety bound ② — fold a RECORDED arming against the redeeming deployment's CURRENT settings,
118
+ * tighten-only.
119
+ *
120
+ * · `current` ABSENT ⇒ REFUSED. The bound reads "the rebuilt arming is bounded by the settings in
121
+ * force", and a caller that declares none has not put anything in force — an unbounded rebuild is the
122
+ * single outcome this fold exists to prevent. A redeeming deployment that genuinely runs no auto-mode
123
+ * settings of its own says so by passing its own (possibly bare) face, which is a declaration and
124
+ * leaves a record; silence is not a declaration. This is also what keeps a CHAIN of redemptions
125
+ * honest: an intermediate leg's tightening is not carried in the row (the row keeps binding what the
126
+ * ANCESTOR recorded, which is what its digest authenticates), so every later redemption is bounded by
127
+ * ITS OWN declared settings rather than by whatever the previous redeemer happened to run under. A
128
+ * deployment that wants a tightening to be durable tightens its own settings, where it is auditable.
129
+ * · bodies EQUAL ⇒ rebuild, with `timeoutMs`/`failureThreshold` taking the MINIMUM of the two. Both
130
+ * directions of that minimum are fail-closed: a shorter timeout turns a slow classifier into
131
+ * `unavailable` (the original chain, i.e. a human), and a lower threshold opens the one-way breaker
132
+ * sooner (the session falls back to non-auto). Neither can widen a verdict.
133
+ * · bodies DIFFER ⇒ refuse. `settings_moved` normally; `epoch_inconsistent` when the two sides
134
+ * nevertheless declare the same epoch — that is a deployment telling us its settings did not move
135
+ * while its own data says otherwise, and picking either side would be a guess about which one is the
136
+ * policy in force.
137
+ */
138
+ export declare function foldAutoModeArming(recorded: unknown, current: unknown): AutoModeArmingFold;