@sema-agent/core 7.11.0 → 7.11.2

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 (48) hide show
  1. package/CHANGELOG.md +39 -1
  2. package/dist/core/auto-mode-defaults.d.ts +13 -9
  3. package/dist/core/auto-mode-defaults.js +1 -1
  4. package/dist/core/governance-codes.d.ts +1 -0
  5. package/dist/core/governance-codes.js +1 -0
  6. package/dist/core/runner/compaction-knobs.d.ts +45 -0
  7. package/dist/core/runner/compaction-knobs.js +3 -0
  8. package/dist/core/runner/contracts.d.ts +78 -1
  9. package/dist/core/runner/prepare-caps-and-workflow.js +23 -8
  10. package/dist/core/runner/run-attachment-seats.d.ts +20 -0
  11. package/dist/core/runner/run-attachment-seats.js +187 -0
  12. package/dist/core/runner/run-brain-sinks.d.ts +29 -0
  13. package/dist/core/runner/run-brain-sinks.js +61 -0
  14. package/dist/core/runner/run-clock-and-content.d.ts +52 -0
  15. package/dist/core/runner/run-clock-and-content.js +26 -0
  16. package/dist/core/runner/run-compaction-machinery.d.ts +35 -0
  17. package/dist/core/runner/run-compaction-machinery.js +98 -0
  18. package/dist/core/runner/run-git-lane.d.ts +64 -0
  19. package/dist/core/runner/run-git-lane.js +102 -0
  20. package/dist/core/runner/run-identity-wiring.d.ts +96 -0
  21. package/dist/core/runner/run-identity-wiring.js +84 -0
  22. package/dist/core/runner/run-reasoning-seat.d.ts +27 -0
  23. package/dist/core/runner/run-reasoning-seat.js +48 -0
  24. package/dist/core/runner/run-recovery-lanes.d.ts +54 -0
  25. package/dist/core/runner/run-recovery-lanes.js +180 -0
  26. package/dist/core/runner/run-stop-and-final-verify.d.ts +32 -0
  27. package/dist/core/runner/run-stop-and-final-verify.js +159 -0
  28. package/dist/core/runner/run-telemetry-and-budget-seats.d.ts +38 -0
  29. package/dist/core/runner/run-telemetry-and-budget-seats.js +159 -0
  30. package/dist/core/runner/run-tool-mount-facts.d.ts +26 -0
  31. package/dist/core/runner/run-tool-mount-facts.js +58 -0
  32. package/dist/core/runner/run-turn-boundary.d.ts +0 -35
  33. package/dist/core/runner/run-turn-boundary.js +1 -3
  34. package/dist/core/runner/runtask.js +126 -1624
  35. package/dist/core/runner/stream-halt-verbs.d.ts +38 -0
  36. package/dist/core/runner/stream-halt-verbs.js +82 -0
  37. package/dist/core/runner/stream-lifecycle-verbs.d.ts +34 -0
  38. package/dist/core/runner/stream-lifecycle-verbs.js +126 -0
  39. package/dist/core/runner/stream-reap.d.ts +30 -0
  40. package/dist/core/runner/stream-reap.js +40 -0
  41. package/dist/core/runner/stream-settle-backstop.d.ts +38 -0
  42. package/dist/core/runner/stream-settle-backstop.js +113 -0
  43. package/dist/core/runner/stream-steer-verb.d.ts +30 -0
  44. package/dist/core/runner/stream-steer-verb.js +185 -0
  45. package/dist/tools/fs/bash-readonly-classifier.d.ts +5 -0
  46. package/dist/tools/fs/bash-readonly-classifier.js +1 -0
  47. package/dist/tools/fs/fs-bash.js +6 -4
  48. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # Changelog
2
2
 
3
+ ## 7.11.2 — 2026-09-10
4
+
5
+ ### Fixes — B-057 second half: the `run_in_background:true` arm of the shell probe now mints the read-boundary mandate too (test [6825]; security axis; @server @cli @test)
6
+ - 7.11.1 closed B-057 on the foreground path only: `bashReversibilityProbe` returned early for a backgrounded call with the out-of-root half of the mandate alone, so a backgrounded `cat`/`grep` of a deny-listed or recursively-walked operand carried no `mandated` and a STORED allow rule cleared it — zero asks, the file's content read and spooled. The flag now decides only `reversible:false`; where the command reads is judged by the same `boundaryGate` as the foreground path (one verdict source, no new arm). No wire change.
7
+ - Observable: `run_in_background:true` + `readDenyPatterns:["**/secrets/**"]` + a stored EXACT allow rule for the command (`Bash(grep needle secrets/app.txt)` — a `Bash(grep:*)` prefix rule does not admit the backgrounded shape by the rule lane's own reading) ⇒ `grep needle secrets/app.txt` / `grep -r needle sub` ask ONCE (`origin:"shell_gate_tighten"`), no `permission.read_only_allowed` trace; the same rule with no deny pattern on the foreground shape ⇒ zero asks. Pins: `test/backlog-b057-read-boundary-mandate.test.ts`.
8
+
9
+ ### Internal — design/393 S5: `runLocked`'s seat lanes R1–R12 leave `runtask.ts` as eleven `run-*.ts` lanes, and its fourteen parameters become one Input (byte-invariant; zero behavior surface)
10
+ - **What moved.** `Runner.runLocked`'s R1–R12 — the identity wiring (post-prepare bindings, `ident`, the manifest frame, the harness sinks, the bridges, the idle redelivery, the loop latch, `onReady`), the telemetry and budget seats (pricing / degrade / limits / budget writers, the run record), the attachment seats (the counters / attach groups and the resume re-derivations — the one async lane), the tool-mount facts (mount gates, the write-family resolver, the three task-start trace frames), the reasoning seat, the clock and content seats (the walltime window, the hard timer, `pushContent`, `emitCommitted`), the brain sinks, the compaction machinery (the accounted brain, the breaker, the window-safety builder), the stop gate + final-verification seat, the recovery lanes (the shared forced pass, the loop recovery chain, the guard chain's arm-B seat) and the git lane — moved whole to `run-identity-wiring.ts`, `run-telemetry-and-budget-seats.ts`, `run-attachment-seats.ts`, `run-tool-mount-facts.ts`, `run-reasoning-seat.ts`, `run-clock-and-content.ts`, `run-brain-sinks.ts`, `run-compaction-machinery.ts`, `run-stop-and-final-verify.ts`, `run-recovery-lanes.ts`, `run-git-lane.ts` (layer 3, run lanes), each a factory behind ONE Input (`Run…Input` / `Run…Result`, 88 seats in all) that hands back the values the leg reads under the same names. The driver keeps R0 (the notification lane + prepare), the two assemblies (the harness handlers — R8 — and the turn-boundary call, each the S2 lane's call site; the design's 18-seat R11 is cut there, leaving the recovery lane at 10 seats) and the legs; it mints the run state (`createRunState`, a pure zero-value, ~50 lines earlier than before) and three views over its own `let`s (the notification lane's bindings, the held agent_end account, the reasoning resolution) so a lane writes the driver's variable, never a copy. `runLocked(spec, queue, …, entryTracer)` — fourteen positional parameters — is now `runLocked(input: RunLockedInput)` (driver-private; the seat types are the contracts.ts seats the stream lanes already spell). The three compaction knobs (`MAX_CONSECUTIVE_COMPACTION_FAILURES`, `COMPACTION_REGROWTH_FACTOR`, `COMPACTION_FREED_EPSILON`) sank from the turn-boundary lane to `compaction-knobs.ts` (layer 1): the recovery lane reads them too, and a lane may not name a sibling. `STOP_HOOK_BLOCK_CAP` moved with the stop gate.
11
+ - **Byte-invariant.** dist `runtask.js` differs from the previous release by import lines, the call-site object literal, the `RunLockedInput` destructure, the three views and the eleven lane calls (−1 117 / +≈85 lines, of which the run IIFE's `setResult` callback is re-indented under its key, not changed); `run-turn-boundary.js` by three constant declarations out and one import in; the eleven lane files and `compaction-knobs.js` are the only additions; no other runtime file changed; the export surface is unchanged. Every lane whose top-level await count is 0 (all but one) is a synchronous factory; the attachment-seats lane is `async` and is awaited where its session reads always sat — inside it every read sits on the tick it did, and the driver's continuation after it is ONE microtask later than the one-function body's (the 390 v2.0 ruling ① class the design named for R3; nothing between the lane's return and the driver's next read can write what that read reads). `runtask.ts` 6 594 → 4 587.
12
+ - **Doors that moved with it.** module-size ratchet (runtask and run-turn-boundary banked; eleven lane entries), `gate:phase-api` floor 771 → 859 and the run- row's residents pin, `gate:layering` `seatLanesNote` + `compaction-knobs` on layer 1, the run-lane residents pin, the rb466 / fail-open / retired-key per-file lists (+11 each), the reminder-literal rows (`fvMarked` / the bare final-verification probe → `run-attachment-seats.ts`), the nuia baseline regenerated, INTEGRATION-CORE's two driver-assembly anchors re-read (the rest stay #667's), the RunState writer note in contracts.ts. `@server @cli @test`: nothing to pick up — no wire key, no closed set, no export moved.
13
+
14
+ ### Internal — design/393 S4: the TaskStream façade leaves `runtask.ts` as five `stream-*.ts` lanes (byte-invariant; zero behavior surface)
15
+ - **What moved.** `Runner.runTaskStream`'s T3–T7 — the settle backstop (the `run.catch` handler: checkpoint reopen compensation, terminal resume-failure unpin/destroy, the `failed` terminal mint, the backstop `task.end`, the owed delegation terminal, the drained `done` push), the suspended-run reap (`reapSuspended`), and the eight verb closures (`steer`; `notify` / `optOutMemoryCapture` / `compact` / `detach` / `interrupt`; `halt` / `destroy`) — moved whole to `stream-settle-backstop.ts`, `stream-reap.ts`, `stream-steer-verb.ts`, `stream-lifecycle-verbs.ts`, `stream-halt-verbs.ts` (layer 3, run lanes), each a factory behind ONE Input (`Stream…Input` / `Stream…Result`, 43 seats in all) that hands back the closure(s) the stream object carries under the same names. The driver keeps the seats (T1), the run IIFE (T2) and the eleven-name stream object; it mints the lanes' live view (`TaskStreamLiveSeat` — getters over `resultValue` / `handle` / `reapHandle`, one setter for the backstop's mint) and threads the Runner's registries and live deps seat as borrowed seats. Six seat types (`LiveHandle`, `TaskIdRef`, `ManualCompactRef`, `NotifyRef`, `CaptureOptOutRef`, `TaskStreamLiveSeat`) went down to `contracts.ts` from the driver's inline annotations. `steerChain` and `destroyOnce` moved with the only verb that read each.
16
+ - **Byte-invariant.** dist `runtask.js` differs from the previous release by import lines, the live-seat mint, five factory calls and eight property names only (−510 / +34); the five new files are the only additions; no other runtime file changed; the export surface is unchanged. Every await inside a moved closure sits on the tick it did: the lanes return the same functions the driver still installs in the same positions (`run.catch(onRunRejected)`, the stream object's properties), and every pre-await host read (`live.resultValue`, `live.handle`, the options bags) is the same read on the same tick. `runtask.ts` 7 565 → 6 594.
17
+ - **Doors that moved with it.** module-size ratchet (runtask banked; five lane entries; contracts.ts +70 with reason), `gate:phase-api` floor 728 → 771 and the stream- row's pins, `gate:layering` `streamLanesNote`, the run-lane residents pin, the rb466 / fail-open / retired-key per-file lists (+5 each), INTEGRATION-CORE's prepare-throw-mint coordinates. `@server @cli @test`: nothing to pick up — no wire key, no closed set, no export moved.
18
+
19
+ ### Fixes — the auto-mode classifier's reasoning seat: a model that cannot switch thinking off is asked to think `low`, not `off` (#661 ⑤ re-form; B-056; ruling: "关不掉思考的模型,席位思考改 low"; @cli 1.0.106 @test @server)
20
+ - **The ruling, as one predicate.** The classifier request's seat is `thinkingOffExpressible(model) ? { reasoning:"off", maxTokens: 256 } : { reasoning:"low", maxTokens: 256 + 2048 }` — a wire that can say off pays for the verdict alone (unchanged since #618); a wire that cannot (`reasoning:true` on an effort-only wire with no declared off spelling, the anthropic adaptive family) is asked for `low` (the request word; what lands on the wire follows the catalog's level mapping — see the wire note and the residual below) under a FIXED 2304-token output cap. Before, such a model was sent `off` with NO cap and kept its own budget (7.11.0 and earlier) — a request the wire could not honor, paid for open-endedly. A model that spends the whole cap on thinking still answers nothing: that is one `parse_error` per ask, and three of them trip the breaker as before.
21
+ - **The re-ask is the same seat.** 7.11.0's one re-ask of an empty-at-cap reply (text `""` at `stopReason:"length"` / `errorKind:"length_empty"`) stays, but the second request is now the `low` seat (`reasoning:"low"`, 256 + 2048) instead of `off` at the widened cap: a catalog entry that says it does not reason and then answers nothing at the `off` cap has just said it thinks anyway, and is re-asked as a model known to think would have been asked. A seat already on `low` has no second request. Everything else holds: one decision per ask, no new breaker-cause word, a second empty reply / a verdict-free reply / an empty clean stop is the `parse_error` it was.
22
+ - **The constant.** `AUTO_MODE_CLASSIFIER_THINKING_ALLOWANCE_TOKENS` (7.11.0, module-internal) is renamed `AUTO_MODE_CLASSIFIER_LOW_REASONING_BUDGET_TOKENS` = 2048: CC 2.1.250 `E4`'s allowance for its `rejects_disabled_thinking` family, and 2 × `MIN_THINKING_TOKENS` — so on the anthropic BUDGET path a HARD cap of 256 + 2048 clears the cap-wins skip and hosts a legal `low` budget. Still not on the index face.
23
+ - **Wire note (`@test`).** `reasoning:"low"` reaches the wire only where the catalog declares `reasoning:true` (the appliers' entry gate `reasoningRequestCarried`); on a catalog entry with no `reasoning` flag the re-ask's `low` puts no thinking key on the wire — the model thinks at its own default and the widened cap is what makes the re-ask succeed. On a declared-reasoning wire: anthropic budget form `thinking:{type:"enabled", budget_tokens ∈ [1024, 2303]}`, adaptive `output_config.effort:"low"`, openai effort wires `reasoning_effort:"low"` (clamped to the endpoint's declared levels), binary enable wires `enable_thinking:true`.
24
+ - **Known residual (review r1).** "Cannot say off" does not imply `low` reaches the wire: an anthropic adaptive entry with NO `compat.effortLevels` declared sends no `output_config.effort` (the shipped `fable5Model` preset declares them; a hand-written catalog entry may not), and an openai entry with `supportsReasoningEffort:false` or `thinkingLevelMap.low:null` sends no effort value — those models think at their DEFAULT tier inside the 2304 cap, where they used to be uncapped, and the `low` seat is never re-asked. Declare the effort levels / the `low` spelling so the seat's word lands; judging "is `low` spellable" inside the seat would be a second copy of the appliers' per-wire knowledge (`reasoning.ts` is the one source), so the seat reads the one capability predicate the engine has (`thinkingOffExpressible`) and this cell is disclosed rather than special-cased.
25
+ - **Other engine-side seats.** `rg 'reasoning: "off"' src/` finds ONE seat — the classifier's; the titler / summarize / compaction legs do not pin a level (they inherit), so nothing else moves.
26
+ - `@cli` 1.0.106: pick up this version with 7.11.0 — a thinking-locked classifier model is now asked for `low` inside a 2304-token cap instead of being sent an off it ignores (where the catalog maps `low` onto the wire, it thinks at that tier; where it does not, at its default — see the residual); a model that exhausts the cap still fails once per ask; nothing to render. `@test`: known cannot-off (a catalog entry `reasoning:true` on the openai wire with no `thinkingLevelMap.off`, or anthropic adaptive) ⇒ the FIRST classifier request carries `reasoning:"low"` and `max_tokens` 2304, one request; an unknown entry (no `reasoning`) answering empty `finish_reason:"length"` then `<block>no</block>` ⇒ two requests, the second `low` at 2304; a can-off entry (deepseek format / declared off spelling) ⇒ `off` at 256, one request. `@server`: zero surface.
27
+ - Pins: `test/backlog661-classifier-cap-retry.test.ts` (the seat predicate — cannot-off adaptive and stock-openai first request `low` + 2304; can-off `off` + 256 ×3; the re-ask's second request `low`; cannot-off empty ⇒ one request); `test/backlog618-classifier-request-shape.test.ts` re-pinned (the no-cap seat is gone: cannot-off ⇒ `low` + 2304).
28
+
29
+ ## 7.11.1 — 2026-09-09
30
+
31
+ ### Fixes — a read the DEPLOYMENT'S READ BOUNDARY demoted is a mandated ask; the read-only shell arm cannot clear it (B-057; security axis; @server 7.69.0 six cells @test @cli)
32
+ - **The regression (7.11.0 #619).** `bashReversibilityProbe` minted `mandated` only for an operand OUTSIDE the roots; a deny-listed operand (`grep needle secrets/app.txt` under `readDenyPatterns:["**/secrets/**"]`) and a recursive walk under a wired deny judge (`grep -r needle sub`, `du`) came back as a bare `reversible:false` — an "ordinary" classify-tier ask — and the new read-only arm (a reader of command text, blind to paths) retired it and the command ran, with `decisionReason:"read_only"`. Fail-open; measured red first.
33
+ - **The rule, ONE arm (`boundaryGate`):** a demotion the read boundary raised is structural — outside the roots, on the deny judge (new structured `CompoundReadonlyVerdict.readDenied`, stamped where the deny arm demotes), or a recursive walk under the deny judge — and neither a stored allow rule nor the read-only arm may retire it (`probe_mandate`, the same word #502 minted). Rules do not grow: the mandate's trigger widens from "outside the roots" to "the boundary spoke". No wire change; `ReversibilityVerdict.mandated` unchanged in shape.
34
+ - The same rule covers a recursion the classifier cannot bound even with NO deny pattern: `find . -name x && pwd` under `shellGate:"classify"` asks once again (the 7.11.0 reading that let the read-only arm clear it was the same fail-open, pinned in `test/backlog482-compound-readonly-e2e.test.ts`).
35
+ - Observable: with `readFace:"roots"`, `shellGate:"classify"` and a deny pattern, those three commands ask ONCE (`origin:"shell_gate_tighten"`), no `permission.read_only_allowed` trace, and run only after the person answers; with no pattern (or one that matches nothing) the same readers are reversible at the fold and ask nothing. Pins: `test/backlog-b057-read-boundary-mandate.test.ts`.
36
+
37
+ ### Fixes — a `spec.tools` entry the ToolSpec arm cannot rebuild is refused by name with a stable code (#666 ③; test [6794] G4.b; @server @test)
38
+ - The caller mount rebuilds a raw ToolSpec by spreading it, so an entry whose `name` / `execute` live on a prototype (a class instance, `Object.create(spec)`, `Object.create(product)`), a function carrying the brand, or a Proxy lying about its own keys mounted with `name: undefined` and failed on an unrelated roster read (`undefined.startsWith`). ONE predicate — can the spec arm construct this entry (own enumerable string `name` + own function `execute`)? — now refuses the whole family with terminal code **`config.tool_mount_denied`** (registered, terminal) and a remedy sentence shared with the rebind seal (plain-object ToolSpec / the product itself / the supported wrapper form via `stampDefineToolBrand`). The 7.11.0-era brand-inheritance arm (`inheritsDefineToolBrand`) is retired — it closed only the branded subset. Pins: the four shapes through a real Runner + the own-property control.
39
+
40
+
3
41
  ## 7.11.0 — 2026-09-09
4
42
 
5
43
  ### Fixes — the auto-mode classifier reads its verdict the way CC 2.1.250 does, and re-asks once when a capped reply came back empty (#661 ① ⑤; B-056; @cli 1.0.106 @test @server)
@@ -52,7 +90,7 @@
52
90
  - **Widening half — EMPTY** (no allow gets wider: the allow arm cannot be constructed). Pins: `test/backlog657-tailed-bare-path-rule-depth.test.ts`; the #654 file's anchored example moved from `dist/**` to `dist/**/x`.
53
91
 
54
92
  ### Narrowing — a cwd-relative BARE path rule reads at ANY depth, the upstream file-rule (gitignore) anchoring (#654; @cli @server @test)
55
- - **The rule** (`resolvePathPattern`, contract `path_rule.bare_name`): a cwd-relative pattern body with NO `/` — `Edit(.env)`, `Read(secret.txt)`, `Read(*.env)` — names that segment at any depth under the working directory (`<cwd>/**/<body>`); a body carrying a `/` (`dist/**`, `a/b`) stays anchored at cwd; `./x` is the same pattern as `x` (the `./` marker only spells the cwd word); the `~/x` / `/x` / `//x` forms are unchanged (anchored at home / root / absolute). Anchored on CC 2.1.250's matcher: the base-relative pattern is handed to a gitignore engine, so a slash-less pattern matches at every depth. `ruleBasesNeeded` is unchanged — a bare name still names `cwd` (the root stands in when cwd is absent).
93
+ - **The rule** (`resolvePathPattern`, contract `path_rule.bare_name`): a cwd-relative pattern body with NO `/` — `Edit(.env)`, `Read(secret.txt)`, `Read(*.env)` — names that segment at any depth under the working directory (`<cwd>/**/<body>`); a body carrying a `/` BEFORE any `/**` tail (`dist/sub/**`, `a/b`) stays anchored at cwd (erratum, test [6794] G1: a ONE-segment `dist/**` is the peeled form of #657 above and reads at any depth for deny/ask); `./x` is the same pattern as `x` (the `./` marker only spells the cwd word); the `~/x` / `/x` / `//x` forms are unchanged (anchored at home / root / absolute). Anchored on CC 2.1.250's matcher: the base-relative pattern is handed to a gitignore engine, so a slash-less pattern matches at every depth. `ruleBasesNeeded` is unchanged — a bare name still names `cwd` (the root stands in when cwd is absent).
56
94
  - **Narrowing half (deny/ask get WIDER reach = stricter):** a migrated `deny: ["Edit(.env)"]` now refuses `nested/.env` too; a persisted `Read(secret.txt)` deny/ask row reaches every `secret.txt` under the working directory. ONE reader (`pathRuleReachOf`) serves the persisted store lane, the content-rule policy lane and its compile step, so all move together. `@cli`: the `/permissions` editor's breadth wording for slash-less path rules can drop the root-only caveat; `@server`: task-settings content rules ride the same lane, zero code change; `@test`: black-box `deny:["Edit(.env)"]` ⇒ `nested/.env` refused, `Read(*.env)` ask reaches `a/b/prod.env`, `Write(dist/**/x)` still only `<cwd>/dist/…/x` (the `dist/**` tail form is #657's), `./x` ≡ `x`.
57
95
  - **Widening half — EMPTY:** an allow in the pattern form has no rule form on any lane (persisted parse `invalid.grammar` / `unsupported.tool`, policy compile `unsupported.file_glob`; only the `Read(//abs/**)` directory form is a standing approval), so no allow gets wider — pinned.
58
96
  - The `/**`-tailed form (`secrets/**`) is the #657 entry below — the residual this entry once carried is closed there.
@@ -24,16 +24,20 @@ export declare const AUTO_MODE_DEFAULT_WINDOW_MAX_CHARS = 2000;
24
24
  */
25
25
  export declare const AUTO_MODE_CLASSIFIER_MAX_TOKENS = 256;
26
26
  /**
27
- * CC 2.1.250 `E4`: for a model in the `rejects_disabled_thinking` family (`BHe`) the classifier request
28
- * adds 2048 output tokens on top of the verdict cap the thinking ALLOWANCE for a model that reasons
29
- * whether or not it is told to. The engine has no per-model declaration of that fact (a gateway model
30
- * with no `reasoning`/`compat` in the catalog reads as "off is expressible"), so it learns it from the
31
- * reply instead: a capped classify that comes back EMPTY at the cap (the output went to reasoning) is
32
- * re-asked ONCE with the cap raised by this allowance, thinking still off (CC `cEt` re-asks such a
33
- * reply up to `maxRetries` = 4 times at the same cap; one re-ask at the allowance-widened cap is the
34
- * subset the engine spends). A second empty reply is the contract failure it always was.
27
+ * The output-budget ALLOWANCE for a classifier request that must think: added to the verdict cap when the
28
+ * seat asks for `reasoning:"low"` instead of `"off"`a model whose wire cannot say off (`reasoning:true`
29
+ * with no off spelling on an effort-only wire, or the anthropic adaptive family), or an unknown model whose
30
+ * capped off-request came back EMPTY at the cap (it reasoned anyway). The seat then pays for the lowest
31
+ * reasoning tier plus the verdict, never for an open-ended budget.
32
+ *
33
+ * 2048 is anchored twice: CC 2.1.250 `E4` adds exactly this many tokens to the verdict cap for a model in
34
+ * its `rejects_disabled_thinking` family (`BHe`); and it is 2 × `MIN_THINKING_TOKENS` (1024), so on the
35
+ * anthropic BUDGET path a HARD cap of 256 + 2048 clears the cap-wins skip (`budgetCapSkipsThinking`) and
36
+ * hosts a legal `low` budget (`1024 ≤ budget_tokens < max_tokens`) without the brain having to raise a cap
37
+ * it refuses to raise. A model that spends more than this on thinking still answers nothing — that reply
38
+ * is the contract failure it always was.
35
39
  */
36
- export declare const AUTO_MODE_CLASSIFIER_THINKING_ALLOWANCE_TOKENS = 2048;
40
+ export declare const AUTO_MODE_CLASSIFIER_LOW_REASONING_BUDGET_TOKENS = 2048;
37
41
  /** The sentinel a deployment puts INSIDE a paired rule list to splice the CC default rules back in at
38
42
  * that position (CC `XYt = "$defaults"`). It lives here, beside the other defaults, because the #503
39
43
  * recipe canonicalizer needs its VALUE and must not load the assembly face (and its SHA-locked assets)
@@ -3,7 +3,7 @@ export const AUTO_MODE_DEFAULT_FAILURE_THRESHOLD = 3;
3
3
  export const AUTO_MODE_DEFAULT_WINDOW_MAX_ENTRIES = 40;
4
4
  export const AUTO_MODE_DEFAULT_WINDOW_MAX_CHARS = 2_000;
5
5
  export const AUTO_MODE_CLASSIFIER_MAX_TOKENS = 256;
6
- export const AUTO_MODE_CLASSIFIER_THINKING_ALLOWANCE_TOKENS = 2048;
6
+ export const AUTO_MODE_CLASSIFIER_LOW_REASONING_BUDGET_TOKENS = 2048;
7
7
  export const AUTO_MODE_DEFAULTS_SENTINEL = "$defaults";
8
8
  export const AUTO_MODE_DENIAL_LIMIT_DEFAULTS = Object.freeze({ maxConsecutive: 3, maxTotal: 20 });
9
9
  export const AUTO_MODE_DENIAL_AUTO_DENY_DEFAULT_MS = 120_000;
@@ -12,6 +12,7 @@ export declare const GOVERNANCE_CODES: {
12
12
  readonly "config.compliance_required": "transient";
13
13
  readonly "config.compliance_denied": "terminal";
14
14
  readonly "config.locked_key": "terminal";
15
+ readonly "config.tool_mount_denied": "terminal";
15
16
  readonly "memory.capture_optout_denied": "terminal";
16
17
  };
17
18
  export type GovernanceCode = keyof typeof GOVERNANCE_CODES;
@@ -5,6 +5,7 @@ export const GOVERNANCE_CODES = {
5
5
  "config.compliance_required": "transient",
6
6
  "config.compliance_denied": "terminal",
7
7
  "config.locked_key": "terminal",
8
+ "config.tool_mount_denied": "terminal",
8
9
  "memory.capture_optout_denied": "terminal",
9
10
  };
10
11
  export const NON_GOVERNANCE_MEMORY_CODES = new Set([
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The run loop's three compaction knobs (design/393 S5): the consecutive-failure breaker trip, the anti-thrash
3
+ * regrowth factor and the effective-compaction epsilon. They were declared at the turn-boundary lane and the driver
4
+ * reached DOWN for them; the recovery lane (the forced-compaction pass the prompt-too-long and guard-chain arms
5
+ * share) reads the same three, and a lane may not name a sibling lane — so the three values sank to the machinery
6
+ * stratum, where the two lanes and the driver all reach down for them. Values verbatim, comments included.
7
+ *
8
+ * No `run-` family prefix, for the reason initial-run-state.ts gives: a module the lanes stand on is not a driven
9
+ * lane, and the prefix would put it under the phase-api gate and the run-lanes glob.
10
+ */
11
+ /**
12
+ * §17.4 (design/64 GAP-25): stop attempting within-task compaction after this many CONSECUTIVE
13
+ * failures in one task — a never-recovering summary gateway must not burn an API call at every turn
14
+ * boundary (CC: MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3). Reset on any success; an open breaker also
15
+ * skips the end-of-task attempt in `finish()` (teardown unaffected). Per-task scope.
16
+ *
17
+ * RB-190 — this used to say "a flaky/never-recovering gateway", and the word `flaky` was not true. Because
18
+ * any success zeroes the counter, a gateway that alternates never reaches three CONSECUTIVE failures and
19
+ * the breaker never opens; it keeps paying for a call at every failed boundary, indefinitely.
20
+ *
21
+ * The behaviour is right and stays: a gateway succeeding half the time is compacting half the time, and
22
+ * opening the breaker on it would disable compaction outright and let the context overflow — trading a
23
+ * wasted call for a failed task. What was wrong is the comment claiming a guarantee the code does not
24
+ * make. An alternating gateway is deliberately NOT caught here; if that ever needs catching it wants a
25
+ * different signal (a failure RATE over a window), not a consecutive counter.
26
+ */
27
+ export declare const MAX_CONSECUTIVE_COMPACTION_FAILURES = 3;
28
+ /**
29
+ * §25.2 anti-thrash (LONGRUN-1b, search [84]): after a compaction, the context must regrow past
30
+ * (tokensBefore − freed) × this factor — the post-compaction size in trigger units — before another
31
+ * summary call fires. An EFFECTIVE compaction (freed large) leaves the floor far below the threshold,
32
+ * so behavior is unchanged; an INEFFECTIVE one (small window, chunky turns, large summary ⇒ freed
33
+ * small, still at/over the threshold) raises the floor so attempts space out instead of firing at
34
+ * every boundary (measured: 26/39 boundaries, +135% wall). The floor binds only when post-compaction
35
+ * size > threshold/1.5 ≈ compaction freed <33% headroom. Degrades gracefully: the retained summary
36
+ * chain keeps early facts; only the un-summarized tail falls back to trim.
37
+ */
38
+ export declare const COMPACTION_REGROWTH_FACTOR = 1.5;
39
+ /**
40
+ * design/84 Seam C reuse guard (b): a compaction that frees fewer than this many (structural) tokens is
41
+ * treated as NO-EFFECTIVE-COMPACTION and does NOT raise the anti-thrash floor. Guards against a reused
42
+ * provider summary (or a pathological LLM summary) that reclaims no headroom yet suppresses the next real
43
+ * summary by inflating the floor to trigger×factor.
44
+ */
45
+ export declare const COMPACTION_FREED_EPSILON = 256;
@@ -0,0 +1,3 @@
1
+ export const MAX_CONSECUTIVE_COMPACTION_FAILURES = 3;
2
+ export const COMPACTION_REGROWTH_FACTOR = 1.5;
3
+ export const COMPACTION_FREED_EPSILON = 256;
@@ -2041,7 +2041,14 @@ export interface ParkAsk {
2041
2041
  * Grouped by lifecycle semantics per the B15 pre-recon partition. Function members start as inert
2042
2042
  * placeholders and are assigned at their ORIGINAL declaration sites (the original code could not
2043
2043
  * call them earlier either — TDZ proved it). Not on the npm face; this extraction is what enabled
2044
- * createTurnBoundary / createHarnessHandlers (三期, both in runtask.ts).
2044
+ * createTurnBoundary / createHarnessHandlers (三期; both are run lanes of their own since design/393 S2).
2045
+ *
2046
+ * WRITERS, by group (design/393 S5 — the driver mints the zero value, initial-run-state.ts, and hands it to every
2047
+ * lane as a borrowed-mutable seat): `telemetry` / `degrade` / `limits` / `budget` initial values and function
2048
+ * members — the telemetry-and-budget-seats lane; `counters` / `attach` initial values — the attachment-seats lane
2049
+ * (the clock lane zeroes `counters.walltimeSyncBackstopFired`; the tool-mount facts only read); the per-turn writes — the harness-handlers lane
2050
+ * (`turn`, `counters`, `limits`, `budget`), the turn-boundary lane (`counters`, `attach`, `budget`) and the
2051
+ * recovery lane (`counters.compactionFloor`, `attach.attachState`); the settle-time reads stay the driver's.
2045
2052
  */
2046
2053
  export interface RunState {
2047
2054
  telemetry: {
@@ -2424,3 +2431,73 @@ export interface RunnerSelfSeat {
2424
2431
  /** Streaming form of {@link RunnerSelfSeat.resume}: the pre-CAS guards and the CAS run first, then the live stream is returned. */
2425
2432
  resumeStream(token: CheckpointToken, outcome: ResumeOutcome, taskConfig: ResumeTaskConfig, internals?: RunInternals): Promise<TaskStream>;
2426
2433
  }
2434
+ /** The live-task handle `runLocked` publishes once the harness exists (design/47): the harness + abort
2435
+ * controller, the loop-liveness latch (`ended` flips when the single `harness.prompt` settles; `userInterrupted`
2436
+ * / `userHalted` are the interrupt and halt verbs' attribution seats), the run's reminder mark, its session and
2437
+ * engine-minted run id, and the hook bound + identity envelope the steer entrance screen runs under. */
2438
+ export interface LiveHandle {
2439
+ harness: AgentHarness;
2440
+ abortController: AbortController;
2441
+ loop: {
2442
+ ended: boolean;
2443
+ userInterrupted: boolean;
2444
+ userHalted: boolean;
2445
+ };
2446
+ reminderMark: string;
2447
+ sessionId: string;
2448
+ /** #499 — the run body's engine-minted run id, so the stream-layer verbs can name the run their disclosures are about. */
2449
+ runId: string;
2450
+ hookTimeoutMs: number;
2451
+ hookIdentity: HookInvocationIdentity;
2452
+ }
2453
+ /** The run body's backstop CARRIER (F-05/W8/件①/#327/#499/#281 r2-D2): the effective ids and the post-prepare
2454
+ * observations the run body publishes as they are minted, so the stream layer's failure backstop names the
2455
+ * same run, session and observations the frames the run body already emitted did. */
2456
+ export interface TaskIdRef {
2457
+ current?: string;
2458
+ sessionId?: string;
2459
+ runId?: string;
2460
+ effectiveMemoryScopes?: TaskResult["effectiveMemoryScopes"];
2461
+ effectiveReasoning?: TaskResult["effectiveReasoning"];
2462
+ delegationTerminalOwed?: HookInvocationIdentity;
2463
+ editedFiles?: () => TaskResult["editedFiles"];
2464
+ }
2465
+ /** design/99 MF-18 — the manual `/compact` request seat: the `requested` flag, the parked waiters (each with its
2466
+ * caller's cancel signal and per-call instructions), the run body's mooted-frame channel and the registration
2467
+ * gate the run-end backstop closes before its final drain. */
2468
+ export interface ManualCompactRef {
2469
+ requested: boolean;
2470
+ waiters: Array<{
2471
+ resolve: (outcome: CompactOutcome) => void;
2472
+ signal?: AbortSignal;
2473
+ instructions?: string;
2474
+ }>;
2475
+ emitMooted?: (reason: string) => void;
2476
+ closed?: boolean;
2477
+ }
2478
+ /** design/144 §2 — the `notify()` bridge: `runLocked` binds `inject` the moment the task-notification lane exists. */
2479
+ export interface NotifyRef {
2480
+ inject?: (n: TaskNotificationPayload, opts?: {
2481
+ priority?: SystemInjectionPriority;
2482
+ }) => void;
2483
+ }
2484
+ /** design/383 §2.1 — the capture opt-out flip verb's binding; bound only when the run mounted a memory session. */
2485
+ export interface CaptureOptOutRef {
2486
+ flip?: (reason?: string) => Promise<{
2487
+ outcome: "created" | "existed";
2488
+ }>;
2489
+ }
2490
+ /**
2491
+ * The stream's LIVE state as the verb lanes read it — a view over the driver's own locals (getters, and one
2492
+ * setter), never a copy: `resultValue` is written by the run body's `setResult` callback and by the failure
2493
+ * backstop, `handle` by the run body's `onReady`, `reapHandle` by its `onSuspend`; every lane reads each of them
2494
+ * at the instant of its own read, exactly as the one-function façade read the closure variables.
2495
+ */
2496
+ export interface TaskStreamLiveSeat {
2497
+ /** The assembled result once the run settled (the run body's `setResult`, or the failure backstop's mint). */
2498
+ resultValue: TaskResult | undefined;
2499
+ /** The live-task handle once `runLocked` published it; `undefined` before, and forever when prepare threw. */
2500
+ readonly handle: LiveHandle | undefined;
2501
+ /** design/51 — what `destroy()` reaps when the run SUSPENDED; unset for every other terminal state. */
2502
+ readonly reapHandle: SuspendReap | undefined;
2503
+ }
@@ -7,7 +7,7 @@ import { isSelfOrchestrationActive } from "../../orchestration/workflow-script-r
7
7
  import { resolveWorkflowSizeGuideline } from "../../orchestration/workflow-size-guideline.js";
8
8
  import { thinkingOffExpressible } from "../../brain/reasoning.js";
9
9
  import { autoModeArmingRecipeOf } from "../auto-mode-arming.js";
10
- import { AUTO_MODE_CLASSIFIER_MAX_TOKENS, AUTO_MODE_CLASSIFIER_THINKING_ALLOWANCE_TOKENS } from "../auto-mode-defaults.js";
10
+ import { AUTO_MODE_CLASSIFIER_LOW_REASONING_BUDGET_TOKENS, AUTO_MODE_CLASSIFIER_MAX_TOKENS } from "../auto-mode-defaults.js";
11
11
  import { buildAutoModePrompt, renderAutoModeAction, renderAutoModeWindow } from "../auto-mode-prompt.js";
12
12
  import { createAutoModeDecider, createAutoModeDenialTracker } from "../auto-mode.js";
13
13
  import { resolveCheckpointStore } from "../checkpoint-store.js";
@@ -18,6 +18,7 @@ import { brainToRuntime } from "../runtime.js";
18
18
  import { defaultTaskRegistry } from "../task-registry.js";
19
19
  import { emitTrace } from "../trace.js";
20
20
  import { defineTool, isDefineToolProduct, rebindDefineToolCtx } from "../tools.js";
21
+ const MOUNT_REMEDY = "author it as a plain-object ToolSpec (own `name` and `execute`), hand the defineTool product itself, or wrap a product through the supported wrapper form (a fresh object re-stamped with `stampDefineToolBrand` whose rebind re-wraps the rebound product)";
21
22
  import { derivedRouteFallsBack } from "./derived-route-fallback.js";
22
23
  import { REPORT_FINDINGS_TOOL_NAME, createReportBlockedTool, createReportFindingsTool } from "./synthetic-tools.js";
23
24
  function classifierReplyText(reply) {
@@ -82,9 +83,22 @@ export async function prepareCapsAndWorkflow(input) {
82
83
  const mounted = mountCallerTool(t);
83
84
  roster.mount(mounted, { source: callerMountSource(mounted), mountedBy: "scenario" });
84
85
  }
86
+ function specArmCanConstruct(t) {
87
+ if (typeof t !== "object" || t === null)
88
+ return false;
89
+ const name = Object.getOwnPropertyDescriptor(t, "name");
90
+ const execute = Object.getOwnPropertyDescriptor(t, "execute");
91
+ return name !== undefined && "value" in name && typeof name.value === "string" && name.enumerable === true && execute !== undefined && "value" in execute && typeof execute.value === "function" && execute.enumerable === true;
92
+ }
85
93
  function mountCallerTool(t) {
86
94
  if (isDefineToolProduct(t))
87
95
  return maybeOffload(rebindDefineToolCtx(t, enrichSpecToolCtx), t);
96
+ if (!specArmCanConstruct(t)) {
97
+ const shown = Object.getOwnPropertyDescriptor(t, "name")?.value;
98
+ const e = new Error(`spec.tools entry ${typeof shown === "string" ? JSON.stringify(shown) : "(no own name)"}: the ToolSpec arm rebuilds a caller tool from its OWN enumerable members, and this entry does not carry a string \`name\` and a function \`execute\` as own properties (a class instance, a prototype child of a spec or of a defineTool product, a function, or a Proxy over one) — ${MOUNT_REMEDY}`);
99
+ e.code = "config.tool_mount_denied";
100
+ throw e;
101
+ }
88
102
  return maybeOffload(defineTool({
89
103
  ...t,
90
104
  execute: (args, ctx) => t.execute(args, enrichSpecToolCtx(ctx)),
@@ -175,7 +189,9 @@ export async function prepareCapsAndWorkflow(input) {
175
189
  const classifierLaneRule = peerLaneActive && peerSendMessageBuiltIn;
176
190
  const classifierSystemPrompt = buildAutoModePrompt(classifierLaneRule ? { ...am, crossSessionMessagesRule: CROSS_SESSION_CLASSIFIER_RULE } : am);
177
191
  const classifierRuntime = brainToRuntime(deps.brain);
178
- const classifierCap = thinkingOffExpressible(classifierModel) ? { maxTokens: AUTO_MODE_CLASSIFIER_MAX_TOKENS } : {};
192
+ const classifierOffSeat = { reasoning: "off", maxTokens: AUTO_MODE_CLASSIFIER_MAX_TOKENS };
193
+ const classifierLowSeat = { reasoning: "low", maxTokens: AUTO_MODE_CLASSIFIER_MAX_TOKENS + AUTO_MODE_CLASSIFIER_LOW_REASONING_BUDGET_TOKENS };
194
+ const classifierSeat = thinkingOffExpressible(classifierModel) ? classifierOffSeat : classifierLowSeat;
179
195
  autoModeDenialTracking = createAutoModeDenialTracker(am.denialLimit);
180
196
  autoModeDecider = createAutoModeDecider({
181
197
  ...(am.timeoutMs !== undefined ? { timeoutMs: am.timeoutMs } : {}),
@@ -201,16 +217,15 @@ export async function prepareCapsAndWorkflow(input) {
201
217
  (m.role === "user" || m.role === "assistant" || m.role === "toolResult"));
202
218
  const userPrompt = renderAutoModeWindow(known, am.window) + renderAutoModeAction(input);
203
219
  const classifierAuth = await spec.getApiKeyAndHeaders?.(classifierModel);
204
- const request = (cap) => classifierRuntime.completeSimple(classifierModel, { systemPrompt: classifierSystemPrompt, messages: [{ role: "user", content: userPrompt, timestamp: Date.now() }] }, {
220
+ const request = (seat) => classifierRuntime.completeSimple(classifierModel, { systemPrompt: classifierSystemPrompt, messages: [{ role: "user", content: userPrompt, timestamp: Date.now() }] }, {
205
221
  signal,
206
- reasoning: "off",
207
- ...cap,
222
+ ...seat,
208
223
  ...(classifierAuth?.apiKey !== undefined ? { apiKey: classifierAuth.apiKey } : {}),
209
224
  ...(classifierAuth?.headers !== undefined ? { headers: classifierAuth.headers } : {}),
210
225
  });
211
- let response = await request(classifierCap);
212
- if (classifierCap.maxTokens !== undefined && classifierReplyEmptyAtCap(response)) {
213
- response = await request({ maxTokens: classifierCap.maxTokens + AUTO_MODE_CLASSIFIER_THINKING_ALLOWANCE_TOKENS });
226
+ let response = await request(classifierSeat);
227
+ if (classifierSeat === classifierOffSeat && classifierReplyEmptyAtCap(response)) {
228
+ response = await request(classifierLowSeat);
214
229
  }
215
230
  return classifierReplyText(response);
216
231
  },
@@ -0,0 +1,20 @@
1
+ import type { TaskSpec } from "../types.js";
2
+ import type { Prepared, ResumeRun, RunnerDepsSeat, RunState } from "./contracts.js";
3
+ export interface RunAttachmentSeatsInput {
4
+ /** borrowed-readonly — the task spec: the attachments config and the `finalVerification` opt-in. */
5
+ spec: TaskSpec;
6
+ /** borrowed-mutable — the leg's prepared seat: `announcedListingsRef` is seeded; the session, the listing faces,
7
+ * the date-change / instruction-source / size-guideline faces, the tool effects and the reminder mark are read. */
8
+ prepared: Prepared;
9
+ /** borrowed-readonly — the resume plan when this leg resumes a checkpoint (its `announcedListings` mirror is the first seed rung). */
10
+ resume: ResumeRun | undefined;
11
+ /** borrowed-mutable — the run's mutable state: this lane is the WRITER of the `counters` and `attach` groups' initial
12
+ * values (later writers: the harness handlers, the boundary, the recovery pass). */
13
+ rs: RunState;
14
+ /** borrowed-readonly — the Runner's deployment deps, read once for `probeInstructionSources`. */
15
+ runner: RunnerDepsSeat;
16
+ }
17
+ /** Nothing comes back: the lane's products are the `counters` / `attach` groups it wrote on the borrowed run state. */
18
+ export interface RunAttachmentSeatsResult {
19
+ }
20
+ export declare function runAttachmentSeats(input: RunAttachmentSeatsInput): Promise<RunAttachmentSeatsResult>;
@@ -0,0 +1,187 @@
1
+ import { eventDefaultOn } from "../../prompt-assembly/event-registry.js";
2
+ import { stripGitStatusUnits } from "./git-status-frame.js";
3
+ import { SKILLS_LISTING_PROBE_HEADER } from "./synthetic-tools.js";
4
+ import { AGENT_LISTING_REMOVED_HEADER, SKILLS_LISTING_DELTA_HEADER, SKILLS_LISTING_REMOVED_HEADER, agentListingDeltaHeader, agentListingInitialHeader, createAttachmentState, replayAnnouncedListing, replayAnnouncedModels } from "./turn-attachments.js";
5
+ export async function runAttachmentSeats(input) {
6
+ const { spec, prepared, resume, rs, runner } = input;
7
+ rs.counters.wroteThisRun = false;
8
+ rs.counters.finalVerifyInjections = 0;
9
+ rs.counters.groundingSignalPreR9 = false;
10
+ rs.counters.groundingSignalPostR9 = false;
11
+ rs.attach.attachmentsCfg = spec.attachments;
12
+ rs.attach.agentListingOn = rs.attach.attachmentsCfg?.agentListing !== false && eventDefaultOn("agent_listing");
13
+ rs.attach.skillsListingOn = rs.attach.attachmentsCfg?.skillsListing !== false && eventDefaultOn("skills_listing");
14
+ const listingsLive = (rs.attach.agentListingOn && prepared.agentListing !== undefined) || (rs.attach.skillsListingOn && prepared.skillsListing !== undefined);
15
+ const backgroundTasksLive = (rs.attach.attachmentsCfg?.backgroundTasks ?? eventDefaultOn("background_tasks")) === true;
16
+ rs.attach.attachState = rs.attach.attachmentsCfg !== undefined || listingsLive || backgroundTasksLive ? createAttachmentState() : undefined;
17
+ rs.attach.dateState = prepared.dateChange !== undefined ? { announcedDate: prepared.dateChange.legDate } : undefined;
18
+ rs.attach.instrProbe = runner.deps.probeInstructionSources;
19
+ rs.attach.instrState =
20
+ rs.attach.instrProbe !== undefined && prepared.instructionSources !== undefined && prepared.instructionSources.length > 0
21
+ ? { lastAnnouncedHash: new Map(prepared.instructionSources.map((s) => [s.path, s.contentHash])) }
22
+ : undefined;
23
+ rs.attach.sizeGuidelineState =
24
+ prepared.workflowSizeGuideline !== undefined
25
+ ? { announcedGuideline: prepared.workflowSizeGuideline.legGuideline, current: prepared.workflowSizeGuideline.current }
26
+ : undefined;
27
+ rs.counters.cadenceTurns = 0;
28
+ rs.turn.lastTurnHadToolCalls = false;
29
+ if (rs.attach.attachState !== undefined && (rs.attach.attachmentsCfg?.backgroundTasks ?? eventDefaultOn("background_tasks")) === true) {
30
+ try {
31
+ const branch = await prepared.session.getBranch();
32
+ for (let i = branch.length - 1; i >= 0; i--) {
33
+ const e = branch[i];
34
+ if (e.type === "compaction") {
35
+ rs.attach.attachState.postCompactPending = true;
36
+ break;
37
+ }
38
+ if (e.type === "message" && e.message.role === "assistant")
39
+ break;
40
+ }
41
+ }
42
+ catch {
43
+ }
44
+ }
45
+ if (rs.attach.attachState !== undefined &&
46
+ ((rs.attach.agentListingOn && prepared.agentListing?.seedAnnounced === true) ||
47
+ (rs.attach.skillsListingOn && prepared.skillsListing?.seedAnnounced === true))) {
48
+ const cpListings = resume?.cp.state.announcedListings;
49
+ let entryListings;
50
+ try {
51
+ entryListings = await prepared.session.getAnnouncedListing();
52
+ }
53
+ catch {
54
+ entryListings = undefined;
55
+ }
56
+ const textOf = (m) => {
57
+ const c = m.content;
58
+ if (typeof c === "string")
59
+ return c;
60
+ if (Array.isArray(c)) {
61
+ return c
62
+ .map((b) => (b !== null && typeof b === "object" && b.type === "text" ? String(b.text ?? "") : ""))
63
+ .join("\n");
64
+ }
65
+ return "";
66
+ };
67
+ let branchTexts = null;
68
+ const branchLoad = async () => {
69
+ if (branchTexts === null) {
70
+ const branch = await prepared.session.getBranch();
71
+ branchTexts = branch.flatMap((e) => {
72
+ if (e.type !== "message")
73
+ return [];
74
+ const m = e.message;
75
+ if (m.role !== "user")
76
+ return [];
77
+ const full = textOf(e.message);
78
+ if (m.engineMinted === true)
79
+ return [stripGitStatusUnits(full, prepared.reminderMark)];
80
+ if (Array.isArray(m.engineSegments) && m.engineSegments.length > 0) {
81
+ return m.engineSegments.map((s) => stripGitStatusUnits(full.slice(Math.max(0, s.start), Math.max(0, s.end)), prepared.reminderMark));
82
+ }
83
+ if (typeof m.enginePrefixChars === "number" && m.enginePrefixChars > 0)
84
+ return [stripGitStatusUnits(full.slice(0, m.enginePrefixChars), prepared.reminderMark)];
85
+ return [];
86
+ });
87
+ }
88
+ return branchTexts;
89
+ };
90
+ const branchReplay = async (headers) => {
91
+ try {
92
+ return replayAnnouncedListing(await branchLoad(), headers);
93
+ }
94
+ catch {
95
+ return undefined;
96
+ }
97
+ };
98
+ if (rs.attach.agentListingOn && prepared.agentListing?.seedAnnounced === true) {
99
+ const listing = prepared.agentListing;
100
+ if (cpListings?.agents === undefined && entryListings?.agents !== undefined) {
101
+ const descOf = new Map(listing.entries.map((e) => [e.name, e.description]));
102
+ rs.attach.attachState.announcedAgentTypes = new Map(entryListings.agents.map((n) => [n, descOf.get(n) ?? ""]));
103
+ prepared.announcedListingsRef.agents = [...entryListings.agents];
104
+ if (entryListings.models !== undefined) {
105
+ rs.attach.attachState.announcedModels = [...entryListings.models];
106
+ prepared.announcedListingsRef.models = [...entryListings.models];
107
+ }
108
+ }
109
+ else if (cpListings?.agents !== undefined) {
110
+ const descOf = new Map(listing.entries.map((e) => [e.name, e.description]));
111
+ rs.attach.attachState.announcedAgentTypes = new Map(cpListings.agents.map((n) => [n, descOf.get(n) ?? ""]));
112
+ prepared.announcedListingsRef.agents = [...cpListings.agents];
113
+ if (cpListings.models !== undefined) {
114
+ rs.attach.attachState.announcedModels = [...cpListings.models];
115
+ prepared.announcedListingsRef.models = [...cpListings.models];
116
+ }
117
+ }
118
+ else {
119
+ const replayed = await branchReplay({
120
+ initial: agentListingInitialHeader(listing.toolName),
121
+ delta: agentListingDeltaHeader(listing.toolName),
122
+ removed: AGENT_LISTING_REMOVED_HEADER,
123
+ });
124
+ if (replayed !== undefined) {
125
+ const descOf = new Map(listing.entries.map((e) => [e.name, e.description]));
126
+ rs.attach.attachState.announcedAgentTypes = new Map([...replayed].map((n) => [n, descOf.get(n) ?? ""]));
127
+ prepared.announcedListingsRef.agents = [...replayed];
128
+ const replayedModels = replayAnnouncedModels(await branchLoad());
129
+ if (replayedModels !== undefined) {
130
+ rs.attach.attachState.announcedModels = [...replayedModels];
131
+ prepared.announcedListingsRef.models = [...replayedModels];
132
+ }
133
+ }
134
+ }
135
+ }
136
+ if (rs.attach.skillsListingOn && prepared.skillsListing?.seedAnnounced === true) {
137
+ const listing = prepared.skillsListing;
138
+ if (cpListings?.skills === undefined && entryListings?.skills !== undefined) {
139
+ const descOf = new Map(listing.entries.map((e) => [e.name, e.description]));
140
+ rs.attach.attachState.announcedSkills = new Map(entryListings.skills.map((n) => [n, descOf.get(n) ?? ""]));
141
+ prepared.announcedListingsRef.skills = [...entryListings.skills];
142
+ }
143
+ else if (cpListings?.skills !== undefined) {
144
+ const descOf = new Map(listing.entries.map((e) => [e.name, e.description]));
145
+ rs.attach.attachState.announcedSkills = new Map(cpListings.skills.map((n) => [n, descOf.get(n) ?? ""]));
146
+ prepared.announcedListingsRef.skills = [...cpListings.skills];
147
+ }
148
+ else {
149
+ const replayed = await branchReplay({
150
+ initial: SKILLS_LISTING_PROBE_HEADER,
151
+ delta: SKILLS_LISTING_DELTA_HEADER,
152
+ removed: SKILLS_LISTING_REMOVED_HEADER,
153
+ });
154
+ if (replayed !== undefined) {
155
+ const descOf = new Map(listing.entries.map((e) => [e.name, e.description]));
156
+ rs.attach.attachState.announcedSkills = new Map([...replayed].map((n) => [n, descOf.get(n) ?? ""]));
157
+ prepared.announcedListingsRef.skills = [...replayed];
158
+ }
159
+ }
160
+ }
161
+ }
162
+ if (spec.finalVerification === true && resume !== undefined) {
163
+ try {
164
+ const branch = await prepared.session.getBranch();
165
+ for (const e of branch) {
166
+ if (e.type !== "message")
167
+ continue;
168
+ const m = e.message;
169
+ if (m.role === "user") {
170
+ const text = typeof m.content === "string"
171
+ ? m.content
172
+ : m.content.map((b) => (b !== null && typeof b === "object" && b.type === "text" ? String(b.text ?? "") : "")).join("\n");
173
+ const fvMarked = m.engineMinted === true && text.includes(`<system-reminder mark="${prepared.reminderMark}">[final verification]`);
174
+ if ((fvMarked || text.includes("<system-reminder>[final verification]")) && rs.counters.finalVerifyInjections < 2)
175
+ rs.counters.finalVerifyInjections += 1;
176
+ }
177
+ else if (m.role === "toolResult" && !rs.counters.wroteThisRun && (prepared.toolEffects.get(m.toolName) ?? "write") !== "read") {
178
+ rs.counters.wroteThisRun = true;
179
+ }
180
+ }
181
+ }
182
+ catch {
183
+ }
184
+ }
185
+ rs.attach.attachmentsInjected = 0;
186
+ return {};
187
+ }
@@ -0,0 +1,29 @@
1
+ import type { ReasoningWireFacts } from "../../brain/reasoning.js";
2
+ import type { PushQueue } from "../push-queue.js";
3
+ import type { TaskEvent } from "../types.js";
4
+ import type { RunInternals, RunState } from "./contracts.js";
5
+ export interface RunBrainSinksInput {
6
+ /** borrowed-mutable — the run's event queue: the `status` frames. */
7
+ queue: PushQueue<TaskEvent>;
8
+ /** borrowed-readonly — the trusted run-scoped channel: `agentName` (the display name), `onStatusEvent` (the second sink). */
9
+ internals: RunInternals | undefined;
10
+ /** borrowed-readonly — the run's mutable state, read for the telemetry frames' tracer and task id. */
11
+ rs: RunState;
12
+ /** borrowed-readonly — the run's event-identity mint (stamped on every status frame). */
13
+ ident: () => {
14
+ eventId: string;
15
+ parentToolCallId?: string;
16
+ sourceTaskId?: string;
17
+ };
18
+ /** borrowed-readonly — the spawning tool call's id when this task runs AS A SUB-AGENT; absent on a top-level run. */
19
+ parentToolCallId: string | undefined;
20
+ /** borrowed-readonly — the reasoning lane's wire-facts consumer, the third sink the composition installs. */
21
+ observeReasoningWireFacts: (facts: ReasoningWireFacts) => void;
22
+ }
23
+ export interface RunBrainSinksResult {
24
+ /** design/99 BC-2: the subagent's human display name for its `task_progress` ticks, or undefined. */
25
+ subagentName: string | undefined;
26
+ /** Compose the three brain ALS sinks around a brain-driving call. */
27
+ withBrainSinks: <T>(fn: () => Promise<T>) => Promise<T>;
28
+ }
29
+ export declare function runBrainSinks(input: RunBrainSinksInput): RunBrainSinksResult;