@sema-agent/core 7.23.1 → 7.23.3
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.
- package/CHANGELOG.md +40 -0
- package/dist/agents/agent-transcript-tool.js +2 -1
- package/dist/agents/cascade.js +2 -1
- package/dist/agents/list-agents-tool.js +2 -1
- package/dist/agents/observer.js +3 -2
- package/dist/agents/peer-session-drain.js +2 -1
- package/dist/agents/retain-ledger.js +2 -1
- package/dist/agents/send-message-tool.js +11 -10
- package/dist/agents/subagent.js +18 -17
- package/dist/agents/teacher.js +5 -4
- package/dist/brain/circuit-breaker.js +2 -1
- package/dist/brain/degrading.js +2 -1
- package/dist/brain/errors.js +24 -15
- package/dist/brain/failover.js +2 -1
- package/dist/brain/stream-engine.js +2 -1
- package/dist/core/a2a.js +4 -3
- package/dist/core/artifact-host.js +2 -1
- package/dist/core/ask-question.js +3 -2
- package/dist/core/auto-promote.js +3 -2
- package/dist/core/gate-bounded-await.js +2 -1
- package/dist/core/gate-fold.js +4 -1
- package/dist/core/gate-lanes.js +4 -2
- package/dist/core/git-worktree-env.js +6 -5
- package/dist/core/hooks.js +4 -4
- package/dist/core/lsp-session.js +3 -2
- package/dist/core/mcp.js +4 -3
- package/dist/core/memory-admission.js +2 -1
- package/dist/core/memory-engine/consolidation-driver.js +5 -4
- package/dist/core/memory-engine/consolidation.js +2 -1
- package/dist/core/memory-engine/delegation-settlement.js +4 -3
- package/dist/core/memory-engine/distiller.js +8 -4
- package/dist/core/memory-engine/engine.js +63 -55
- package/dist/core/memory-engine/file-backend.js +11 -10
- package/dist/core/memory-engine/layout.js +5 -4
- package/dist/core/memory-engine/session-incarnation.js +2 -1
- package/dist/core/memory.js +2 -1
- package/dist/core/oracle-isolation.js +3 -2
- package/dist/core/permission-rule-store.js +2 -1
- package/dist/core/runner/announce-once-ledger.js +2 -1
- package/dist/core/runner/compaction-seams.js +3 -2
- package/dist/core/runner/gate-exit.d.ts +5 -0
- package/dist/core/runner/git-leg-delivery.js +4 -3
- package/dist/core/runner/git-status-frame.js +2 -1
- package/dist/core/runner/mcp-redial.js +2 -1
- package/dist/core/runner/memory-consolidation-driver.js +2 -1
- package/dist/core/runner/permission-rule-lanes.js +3 -2
- package/dist/core/runner/prepare-artifact.js +2 -1
- package/dist/core/runner/prepare-ask-lane.js +2 -1
- package/dist/core/runner/prepare-caps-and-workflow.js +4 -3
- package/dist/core/runner/prepare-inherited-gate.js +2 -1
- package/dist/core/runner/prepare-memory-engine-session.js +2 -1
- package/dist/core/runner/prepare-memory.js +5 -4
- package/dist/core/runner/prepare-policy-chain.js +3 -2
- package/dist/core/runner/prepare-project-context.js +3 -2
- package/dist/core/runner/prepare-protocol-tools.js +3 -2
- package/dist/core/runner/prepare-suspend-saga.js +2 -1
- package/dist/core/runner/prepare-task.js +6 -5
- package/dist/core/runner/prepare-turn-wiring.js +2 -1
- package/dist/core/runner/resume-apply.js +5 -4
- package/dist/core/runner/resume-preflight.js +3 -2
- package/dist/core/runner/run-git-lane.js +4 -3
- package/dist/core/runner/run-leg.js +4 -3
- package/dist/core/runner/run-recovery-lanes.js +3 -2
- package/dist/core/runner/run-settle-and-teardown.js +2 -1
- package/dist/core/runner/run-stop-and-final-verify.js +2 -1
- package/dist/core/runner/run-telemetry-and-budget-seats.js +2 -1
- package/dist/core/runner/run-terminal-adoption.js +6 -5
- package/dist/core/runner/run-turn-boundary.js +5 -4
- package/dist/core/runner/runtask.js +2 -1
- package/dist/core/runner/stream-settle-backstop.js +5 -4
- package/dist/core/runner/stream-steer-verb.js +2 -1
- package/dist/core/runner/strict-output-schema.js +2 -1
- package/dist/core/runner/teardown-bounded.js +2 -1
- package/dist/core/safe-notify.js +2 -1
- package/dist/core/skills-directory.js +2 -1
- package/dist/core/strategy-store.js +2 -1
- package/dist/core/task-registry-agent.js +2 -1
- package/dist/core/task-registry-monitor.js +2 -1
- package/dist/core/task-registry-workflow.js +2 -1
- package/dist/core/task-registry.js +3 -2
- package/dist/core/thrown-value.d.ts +79 -0
- package/dist/core/thrown-value.js +56 -0
- package/dist/core/tool-errors.d.ts +0 -6
- package/dist/core/tool-errors.js +36 -14
- package/dist/core/tool-model-gate.js +2 -1
- package/dist/core/tool-policy.js +6 -13
- package/dist/core/tool-result-store.js +2 -1
- package/dist/core/tool-spec.d.ts +12 -0
- package/dist/core/write-protect.js +3 -2
- package/dist/engine/compaction/compaction.js +3 -4
- package/dist/engine/harness/agent-harness.js +2 -1
- package/dist/engine/harness/types.js +8 -7
- package/dist/engine/loop/agent-loop.js +7 -6
- package/dist/engine/lsp/node-lsp-manager.js +5 -4
- package/dist/engine/lsp/stdio-lsp-transport.js +12 -5
- package/dist/orchestration/dev-vm-script-runner.js +2 -1
- package/dist/orchestration/run-workflow-tool.js +7 -6
- package/dist/orchestration/workflow-governance.js +2 -1
- package/dist/orchestration/workflow-sandbox-conformance.js +3 -2
- package/dist/orchestration/workflow-script-store.js +2 -1
- package/dist/orchestration/workflow.js +15 -14
- package/dist/server/http.js +3 -2
- package/dist/stores/file/adoption/adopt.js +3 -2
- package/dist/stores/file/fs-atomic.js +2 -1
- package/dist/stores/file/permission-rule-adopt.js +2 -1
- package/dist/stores/file/permission-rule-store.js +2 -1
- package/dist/stores/file/session-store.js +4 -3
- package/dist/stores/file/strategy-store.js +7 -6
- package/dist/tools/artifact/artifact-tool.js +3 -2
- package/dist/tools/artifact/conformance.js +4 -3
- package/dist/tools/fs/bash-readonly-classifier.d.ts +20 -9
- package/dist/tools/fs/bash-readonly-classifier.js +7 -3
- package/dist/tools/fs/fs-bash.d.ts +16 -10
- package/dist/tools/fs/fs-bash.js +84 -35
- package/dist/tools/fs/fs-write.js +3 -2
- package/dist/tools/fs/safety.js +2 -1
- package/dist/tools/fs/search.js +3 -2
- package/dist/tools/monitor.js +3 -2
- package/dist/tools/sql.js +2 -1
- package/dist/tools/web.js +5 -15
- package/dist/tools/worktree.js +12 -9
- package/package.json +2 -1
- package/wire-changes/core@7.23.2.json +42 -0
- package/wire-changes/core@7.23.3.json +25 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 7.23.3 — 2026-09-19
|
|
4
|
+
|
|
5
|
+
### Read boundary — an operand the shell EXPANDS is judged by the deployment's deny judge, at the one moment the expansion exists (#934; P1; execute-time recheck; @cli @server @client-core)
|
|
6
|
+
- **Before.** Under `shellGate:"classify"` + `readFace:"roots"` with a deny table wired, `cat .en?` cost ZERO cards and returned `.env`'s bytes; so did `cat secrets*/app.txt` and `grep . secret?/app.txt`, in the headless and the wired-approver deployment alike. Three passes each declined, and each was right to: the classify walk judges an unexpanded glob on its UNEXPANDED spelling (`<root>/.en?` is on no deny table) and parks it as `undecidedPaths`; the read-only command tables then cleared that ordinary card (`cat` is a listed reader; the table knows nothing about paths); and the execute-time canonical recheck (#775) — the one pass that HOLDS a filesystem and does expand the pattern — asked only "does this land outside the roots?". **A deny-listed expansion is in-root by definition**, so the containment question can never be the one that catches it. Not a 7.23.1 regression: the same reading since 7.22.x.
|
|
7
|
+
- **After — one rule: an EXPANSION IS AN OPERAND.** The recheck leg hands every path it establishes to `BashReadonlyRootBoundary.denyMatch` — the SAME function object the approval-time walk holds, forwarded, never a second matcher — and asks it BEFORE containment, the order the walk itself applies (deny outranks; no root grant clears a deny row). Rules: ±0 judges, −1 drop. The expander's property-6 shortcut used to DELETE an in-root non-symlink match outright; what that shortcut buys is the `realpath` call, so the match is now handed over WITH the canonical location the enumeration already established, and the judge answers both questions on it at the same cost. The deny question is asked of the FOLDED view, because that is the form the walk decides on — `cat secrets/../sub/a.txt` reads `sub/a.txt` and is not refused.
|
|
8
|
+
- **Two views per path, and which is NEW is the whole of the change.** The operand's own spelling — already judged for a literal candidate, NEVER judged for an expansion the shell produces after the verdict — and the canonical TARGET, which no lexical pass can reach on either list. The second closes the deny seat's stated residual ② (an in-root symlink whose target is guarded: `cat notes.txt` → `<root>/.env` also disclosed at zero asks), which that seat's own contract already named this leg as the closer of. Judging the spelling of expansions ALONE would have been a second rule.
|
|
9
|
+
- **The same leak had a second site, and the face was the reason.** With `readFace:"open"` the leg exited before doing anything — a CONTAINMENT-shaped exit applied to the whole pass — so `cat .en?` and `cat notes.txt` disclosed the file at zero asks there too, on a face whose deny seat's own contract reads "the deny half above keeps judging in both faces" (the LITERAL `cat .env` was mandated there all along, which is what proves the floor is real). Which half still has a question is now one value the face decides member by member: containment OFF under open (an out-of-roots operand rides `checkedPaths` there as an ACCEPTED candidate; re-judging it would make the open face stricter than the closed one), deny ON in both. With neither question the leg does not run, so an open face with no deny table is answered exactly as before. Rules: −1 face special case. Both directions pinned — the deny cells AND the containment controls (an out-of-root read, plain and globbed, still runs under open with a deny table wired).
|
|
10
|
+
- **A third site, found by the adversarial round and confirmed by construction.** The expander had one more containment shortcut: when a glob's LAST directory canonicalized OUTSIDE the roots, it pushed that DIRECTORY and discarded the matches. Containment reached the right verdict through the directory, which is why the shortcut survived — and the deny half never can, because a deny row is about the FILE. Measured on the parent: under the open face, `cat vendor/.en?` (an in-root directory symlink pointing out) and `cat <outside>/.en?` both disclosed the file at zero asks. The expander now hands over the matches themselves with whatever canonical facts it already holds; containment may use those as shortcuts, it may not replace the operands. Consequence, disclosed: the closed face's `readonly_out_of_root` refusal for such a glob now names the FILES (`<outside>/secret`, `<outside>/second`) instead of the directory — strictly more precise, and `test/backlog775-glob-canonical-recheck.test.ts` A1-a is re-pinned to it. A consumer counting `details.paths` entries for this refusal sees one row per escaping match rather than one per escaping directory.
|
|
11
|
+
- **Precedence, made one rule.** An expansion the leg cannot establish used to RETURN immediately, which ranked it above a deny row a later operand would have produced — the one order this leg is not free to choose. It is now recorded like every other objection and the three are ranked once, at the end: deny > unestablished > containment. Every such command was refused before and is refused now; only which sentence it carries can change.
|
|
12
|
+
- **Not the other repair.** Widening the classify seat's weak rung to `boundaryUnread` would turn every in-root glob into an ordinary card — a narrowing for every deployment, deny table or not. RB-474b's ordinary card, the classify seat's card shape and the off/bypass seats are untouched.
|
|
13
|
+
- **Reach.** Only a deployment with a deny judge wired (`readDenyPatterns`, or a selected built-in tier — default OFF) sees anything: without one the boundary carries no `denyMatch` and this leg's answer is byte-identical, on both read faces. `bash_readonly` is unaffected — that face wires no deny judge at all (v1 ruling). No second adjudication: a deny row the walk RAISED returns a verdict carrying no candidates, so an approver's yes still runs the command. An execute-time deny refusal is NOT clearable by a stored allow rule or by the auto classifier — it is past the approval step, which is what makes it stronger than the accepted reach #891 states for a retired ordinary card.
|
|
14
|
+
- **Stated reach on the open face.** The leg's fail-closed arm travels with the deny half: under `readFace:"open"` WITH a deny table, a candidate this leg cannot ESTABLISH — a glob nobody can expand (an uncompilable range, an unenumerable directory, an expansion past the enumeration caps), or a literal whose chain the resolver could neither answer nor rule absent — is now refused rather than run, because a location that cannot be established cannot be deny-judged either. The closed face always answered that way; an open face with no deny table does not run the leg at all and is unchanged. Measured in both directions.
|
|
15
|
+
- **Still open, stated rather than assumed gone.** A DECLARED isolated sandbox keeps the lexical-only leg (property 3): its `canonicalPath` is an RPC into a namespace this process cannot resolve, so no expansion can be established there and the deny half stays at the approval walk's unexpanded-spelling reading. The sandbox is that deployment's declared floor. A RECURSIVE verb's directory operand (`grep -r . <dir>`) is judged on the operand, never on its subtree — measured at one mandated ask with no disclosure, so it is asked rather than leaked, but what a yes then opens is the execution environment's floor.
|
|
16
|
+
- **headless.** Restrictive direction: a deny-listed read that reached a headless run through a glob, or through an in-root symlink, is now refused instead of executed. The refusal is the fs boundary's own card (`read_path_denied`, with `target` and the matched `pattern`), the same one `Read`/`Grep`/`Glob` mint — one rule, learned once.
|
|
17
|
+
- Pins: `test/backlog934-glob-expansion-deny-judge.test.ts` (3 shapes × 2 approver arms, judge-identity via a case-folded pattern, the symlink view, the wire card, and five controls: RB-474b unchanged, no deny table, #775 unflipped, two `..`-over-fire shapes, and the approved deny row).
|
|
18
|
+
|
|
19
|
+
## 7.23.2 — 2026-09-19
|
|
20
|
+
|
|
21
|
+
### Rendering a thrown value is one total rule — a hook could kill the engine with its choice of rejection value (#926; P1, failure face; @cli @server @client-core: text only, no wire key)
|
|
22
|
+
- **Before.** `err instanceof Error ? err.message : String(err)` and its normalizing twin `err instanceof Error ? err : new Error(String(err))` appeared 270 times across 100 files. Both are PARTIAL on the non-Error arm: `String` calls the value's own `toString` / `Symbol.toPrimitive`, so a null-prototype object throws `TypeError: Cannot convert object to primitive value` and a value whose own hook throws propagates that instead. The thrower is deployment code — a `Hooks` callback, a plugin, an adapter rejection — so the rendering step was the one step that could destroy the report it exists to produce.
|
|
23
|
+
- **On the hook seat it was fatal, twice, on two different failure modes.** A hook that REJECTS reached the seat's reject arm, where the render sat inside a promise rejection handler whose derived promise nobody awaited: the `TypeError` became an unhandled rejection and Node terminated the process. A hook that throws SYNCHRONOUSLY escaped the seat before it wrapped anything and reached the crash-text site instead, which carried the same coercion. Measured on the parent tree, each mode exits 1 from its own line. A `preToolUse` hook returning `Promise.reject(Object.create(null))` was enough to end the run — and the engine with it.
|
|
24
|
+
- **After.** One rule, in one leaf module. `thrownText` is total: a thrown STRING is its own message, an `Error` hands over `message` VERBATIM, and anything else is described structurally by the describer the engine already shares for rejected values. `asThrownError` is the same rule for callers that need an `Error` object, and passes an `Error` through by IDENTITY. Every read inside both is guarded, because the reads are themselves attacker-reachable (`instanceof` walks a prototype chain a revoked proxy can trap; `message` can be an accessor that throws). `describeThrown` stops carrying its own duplicate label and becomes what it always claimed to be — the containment face of that one rule. Rules: −1 describer, and 285 copies of one idiom become one call.
|
|
25
|
+
- **Byte identity, which is the promise that matters for a sweep this wide.** The two arms that carry nearly all production traffic are unchanged: an `Error` yields exactly `err.message` (no cap, no one-lining, no quoting, empty stays empty so `… || "unknown"` folds as before), a thrown string yields exactly itself, and every primitive keeps the spelling `String` gave it. Pinned as literals, in both failure modes.
|
|
26
|
+
- **Reach.** What moves is only the text for values that used to render uselessly or fatally: `[object Object]` becomes `an object with keys [a, b]`, `1,2` becomes `an array of 2 member(s)`, a thrown FUNCTION stops printing its entire source (unbounded, and forgeable through `toString`) and becomes `a function`, `1n` is no longer spelled `1`, and the three shapes that used to throw now describe. No gate decision, no control flow and no wire key changes anywhere; a hook crash still fails CLOSED and still reports through the same seats. Deployments that match on the text of a non-Error failure — a log scraper, a test that pinned `[object Object]` — are the only readers affected, and headless is unchanged: the same refusal, with a sentence a reader can act on.
|
|
27
|
+
- **Cross-realm Errors, and the face that names the class.** `instanceof Error` compares against THIS realm's prototype, so a genuine `Error` thrown by a workflow script running inside `vm.createContext` used to be described as an anonymous object with its message gone; the rule brands by `[object Error]` instead, which is realm-independent. And a bare `String(err)` never said the message — it said `TypeError: boom`, the CLASS and the message. Sites that spelled the bare form keep that text through a second face of the same rule, because at least one of them is load-bearing: the teacher's stuck detector keys its signature on it, and collapsing two error classes with equal messages into one string would have let an alternating student accumulate a streak and be abandoned mid-run.
|
|
28
|
+
- **Local normalizers that hand-rolled totality are now total.** Measured before the change: a revoked-proxy stdin error, and one whose `message` is a throwing getter, both ESCAPE the LSP transport's event handler — so the transport never closes. Those duck-typed `.message` reads sat outside any guard. Same shape in the worktree reason helper and the harness normalizer. All three read inside the guard now, with the shared rule as the fallback.
|
|
29
|
+
- **Two sites were reverted after the full suite, and say so in the gate rather than going quiet.** A rejected closed-set WORD is not a thrown value, and the structural describer quotes strings — a word already inside quotes came out doubled; and the notifier door's coercion already sat inside its own guard, where a constant stand-in is the stated contract rather than an accident. Both keep their own partial coercion and are ticketed. A third, the erasure announcement, deliberately RELAYS the value's own text scarred by the neutralizer — a structural description would delete the evidence the announcement exists to carry — so its coercion is kept and merely made total.
|
|
30
|
+
- **Known and unchanged.** The deployment error lane still receives the RAW value for a synchronous hook throw and a normalized `Error` for a rejection; the lane is typed `unknown` and that asymmetry predates this change, so it is pinned rather than quietly fixed. `asThrownError` deliberately does NOT attach the original value as `cause`: cause chains are walked elsewhere to attribute error codes, and letting an arbitrary thrown object start supplying codes belongs with those readers, not with a rendering fix. Two functions named `toError` (engine/lsp, engine/harness) still exist with different behaviour; both are total now, and the harness one still asks the value to narrate itself through `JSON.stringify` — the older self-narration class, untouched here.
|
|
31
|
+
|
|
32
|
+
# Changelog
|
|
33
|
+
|
|
34
|
+
### Read boundary — a read the boundary DECLINED to judge is no longer cleared by the engine's read-only tables (#930 / sema-internal #65; classify seat; @cli @server @test)
|
|
35
|
+
- **Before.** Under `shellGate:"classify"` + `readFace:"roots"`, a command whose operand list arrives through a STREAM (`echo <path> | xargs grep .`) is one the read walk cannot judge: it says so with a decline, and 7.23.1's classify seat reads no decline as mandate evidence (ruling C-R67 (B), #891) — deliberately, so that a person, the auto-mode classifier or a person's stored allow rule may answer the resulting card. The seat's stated premise for that trade was that the shape classifier "refuses all these shapes anyway", which was true of the PROBE and false of the OUTCOME: the gate's allow layer has a second member that is not the shape classifier — the read-only command tables (#619) — which recognises `xargs <a listed target>` from a table of program names, knows nothing about paths, and retired the card at ZERO asks. Fourteen shapes measured on a real shell, in BOTH the headless and the wired-approver deployment, with the file's contents coming back in the tool output: `echo <path> | xargs grep|head|tail|wc -l|grep -l`, `find … | xargs wc -l`, `printf '%s\0' … | xargs -0 wc -l`. **No deny table is needed** — roots alone are the boundary, which is what a `permissionMode` of "default" or "acceptEdits" mounts; the deny-listed spellings are the second half. The SANDBOX-ADMISSION arm cleared the same card on the same reasoning, measured at the unit face. `default`/`off`/`bypass` were never affected (their seat reads both declines as evidence, so the ask was mandated there), and `| xargs cat` was never affected (`cat` is not in the read-only tables' xargs target list).
|
|
36
|
+
- **After.** The boundary's answer over a command is a LADDER with three rungs, one question: DECLARED an objection (`mandated` — no lane, no rule) > DECLINED to judge (`ReversibilityVerdict.boundaryDeclined` → the gate's `GatePass.probeBoundaryDeclined`) > silent. The middle rung leaves the card ORDINARY for every ANSWERER — a person, the classifier and a stored allow rule all still clear it, which is exactly what C-R67 (B) bought — and withholds **both** allow-layer members that answer nothing: the read-only command tables (which recognise a command SHAPE) and sandbox admission (whose own comment already says containment holds EFFECTS and does not speak to what reaches the model). Rules: ±0 predicates (`readBoundaryMandate` is still the single seat-shared answer and gains a return rung; `refusedCommandMayRead` now reports the decline it already computed instead of returning a bare "exempt" that reads as "the boundary judged and objects to nothing"). No new mandate word: `PERSISTED_RULE_MANDATES` is unchanged, so the persisted-rule lane and the classifier read this ask exactly as they did.
|
|
37
|
+
- **Difference from 7.23.0.** 7.23.0 refused these shapes with a MANDATE (no classifier, no stored rule, confirm-or-refuse). This version is strictly looser than that and strictly tighter than 7.23.1: an ordinary card an answerer may settle.
|
|
38
|
+
- **Reach.** Any deployment that wires a read boundary (`readFace` + roots, with or without `readDenyPatterns`) under `shellGate:"classify"`: a shell command whose operands the walk could not read — a stream-fed operand list, an unreadable program position — now reaches the person/classifier instead of being auto-allowed by the read-only tables. In-root spellings of the same shapes (`echo sub/x.txt | xargs grep .`) are in that set too: the boundary declined for them as well, and "in root" is a fact nobody established. Commands with no decline are untouched — `git status`, `ls -la` and every other read-only shape the tables clear keep costing zero questions.
|
|
39
|
+
- **headless.** Restrictive direction, and the reason this is a P1: a headless classify-tier run that executed these stream-fed reads with no approver now refuses them (an ask with no approver is a refusal). A deployment relying on that auto-allow should widen its roots, store an allow rule for the command, or wire an approver/classifier.
|
|
40
|
+
- **The other half, measured here and FIXED in this same version by #934 below.** "The boundary could not read where this command reads" has a second family — an UNEXPANDED GLOB (`undecidedPaths`), a plain card by ruling (RB-474b) and still cleared by the read-only tables. Its containment half was already caught at execute time by the canonical recheck (#775); its DENY half was not, because that recheck judged roots only, so `cat .en?` under `readDenyPatterns:["**/.env"]` disclosed `.env` at zero asks on 7.23.1 and on this tree alike. This version does not change the CARD (RB-474b stands); it gives the execute-time recheck the boundary's own deny judge — see the #934 section.
|
|
41
|
+
- Pins: `test/backlog930-decline-not-read-only-clearable.test.ts` (14 shapes × 2 arms, + the C-R67 (B) surface, the sandbox arm with its own three-way control, the off seat's mandate, the read-only arm still alive, and the `xargs cat` / in-root controls).
|
|
42
|
+
|
|
3
43
|
## 7.23.1 — 2026-09-18
|
|
4
44
|
|
|
5
45
|
### Read boundary — a `$` inside SINGLE QUOTES no longer refuses the whole command (#921, ruling C-R67 ③; behaviour face, both seats; @cli @server @client-core re-pin)
|
|
@@ -7,6 +7,7 @@ import { stepsFromMessages } from "./subagent-steps.js";
|
|
|
7
7
|
import { REPORT_FIELD_MAX } from "./subagent.js";
|
|
8
8
|
import { getSessionRetainLedger } from "./retain-ledger.js";
|
|
9
9
|
import { toolFace } from "../core/tool-catalog-entries.js";
|
|
10
|
+
import { thrownText } from "../core/thrown-value.js";
|
|
10
11
|
export const AGENT_TRANSCRIPT_TOOL_NAME = "AgentTranscript";
|
|
11
12
|
const AGENT_TRANSCRIPT_DEFAULT_N = 20;
|
|
12
13
|
const AGENT_TRANSCRIPT_MAX_N = 50;
|
|
@@ -110,7 +111,7 @@ export function createAgentTranscriptTool(opts) {
|
|
|
110
111
|
catch (e) {
|
|
111
112
|
const code = e?.code;
|
|
112
113
|
return {
|
|
113
|
-
content: `No transcript: agent ${id}'s session could not be read (${code ?? (
|
|
114
|
+
content: `No transcript: agent ${id}'s session could not be read (${code ?? (thrownText(e))}).`,
|
|
114
115
|
details: { error: code ?? "read_failed", id },
|
|
115
116
|
isError: true,
|
|
116
117
|
};
|
package/dist/agents/cascade.js
CHANGED
|
@@ -3,6 +3,7 @@ import { releaseSession } from "./session-util.js";
|
|
|
3
3
|
import { mapNestedSuspend, isDurablePause } from "./suspend-guard.js";
|
|
4
4
|
import { buildCumulativeStats } from "./cumulative-stats.js";
|
|
5
5
|
import { createSafeNotifier } from "../core/safe-notify.js";
|
|
6
|
+
import { thrownText } from "../core/thrown-value.js";
|
|
6
7
|
const CASCADE_ON_RUNG_SITE = "cascade.onRung";
|
|
7
8
|
const CASCADE_CONFIG_ERROR_CODE = "config.cascade_invalid";
|
|
8
9
|
const CASCADE_NO_DISPATCH_ERROR_CODE = "cascade.budget_exhausted";
|
|
@@ -172,7 +173,7 @@ export async function runCascade(runner, spec, config, internals) {
|
|
|
172
173
|
}
|
|
173
174
|
catch (e) {
|
|
174
175
|
passed = false;
|
|
175
|
-
gateError =
|
|
176
|
+
gateError = thrownText(e);
|
|
176
177
|
}
|
|
177
178
|
const statusRejected = passed && config.acceptPartial !== true && result.terminal.kind !== "completed";
|
|
178
179
|
if (statusRejected)
|
|
@@ -5,6 +5,7 @@ import { formatPeerNameRef } from "./cross-session-ref.js";
|
|
|
5
5
|
import { entryAccessible } from "./roster-store.js";
|
|
6
6
|
import { mintPeerSessionCandidates, peerSessionBoxHandle } from "./peer-directory.js";
|
|
7
7
|
import { toolFace } from "../core/tool-catalog-entries.js";
|
|
8
|
+
import { thrownText } from "../core/thrown-value.js";
|
|
8
9
|
export const LIST_AGENTS_TOOL_NAME = "ListAgents";
|
|
9
10
|
export const LIST_AGENTS_TOOL_ALIAS = "ListPeers";
|
|
10
11
|
export const LIST_AGENTS_MAX_RESULT_CHARS = 10_000;
|
|
@@ -27,7 +28,7 @@ export function createListAgentsTool(opts) {
|
|
|
27
28
|
peerRows = await opts.peerDirectory.listPeerSessions({ scope: opts.self.scope });
|
|
28
29
|
}
|
|
29
30
|
catch (e) {
|
|
30
|
-
directoryFault =
|
|
31
|
+
directoryFault = thrownText(e);
|
|
31
32
|
}
|
|
32
33
|
for (const c of mintPeerSessionCandidates(peerRows, { sessionId: opts.self.sessionId })) {
|
|
33
34
|
const r = c.record;
|
package/dist/agents/observer.js
CHANGED
|
@@ -3,6 +3,7 @@ import { defineTool } from "../core/tools.js";
|
|
|
3
3
|
import { createSafeNotifier } from "../core/safe-notify.js";
|
|
4
4
|
import { escapeEnvelopeTag } from "../core/untrusted-text.js";
|
|
5
5
|
import { toolFace } from "../core/tool-catalog-entries.js";
|
|
6
|
+
import { thrownText } from "../core/thrown-value.js";
|
|
6
7
|
export const OBSERVER_EVENT_TAGS = ["tool-call", "user-message", "tool-result", "turn-ended"];
|
|
7
8
|
const TAG_ESCAPE_RE = new RegExp(`<(?=/?(?:${OBSERVER_EVENT_TAGS.join("|")})(?:[>\\s/]|$))`, "gi");
|
|
8
9
|
export function escapeObserverTags(text) {
|
|
@@ -309,7 +310,7 @@ export class ObserverPairing {
|
|
|
309
310
|
if (this.buffer.length > 0) {
|
|
310
311
|
const strandedCount = this.buffer.length;
|
|
311
312
|
this.buffer = [];
|
|
312
|
-
this.safeOnError(new Error(`observer delivery: ${strandedCount} segment(s) enqueued mid-fault were stranded and dropped (never attempted) after: ${
|
|
313
|
+
this.safeOnError(new Error(`observer delivery: ${strandedCount} segment(s) enqueued mid-fault were stranded and dropped (never attempted) after: ${thrownText(err)}`));
|
|
313
314
|
}
|
|
314
315
|
return;
|
|
315
316
|
}
|
|
@@ -341,7 +342,7 @@ export class ObserverPairing {
|
|
|
341
342
|
catch (restartErr) {
|
|
342
343
|
this.firstRunDone = false;
|
|
343
344
|
throw restartErr instanceof Error
|
|
344
|
-
? new Error(`${restartErr.message} (after resume-state loss: ${
|
|
345
|
+
? new Error(`${restartErr.message} (after resume-state loss: ${thrownText(err)})`, { cause: restartErr })
|
|
345
346
|
: restartErr;
|
|
346
347
|
}
|
|
347
348
|
}
|
|
@@ -11,6 +11,7 @@ import { peerHeldQueueFor } from "./peer-held-queue.js";
|
|
|
11
11
|
import { peerIdleMachineFor } from "./peer-idle.js";
|
|
12
12
|
import { describePeerIdleSubscription, isPeerDeliveryReceiptState, isPeerIdleNoticeKind, peerIdleNoticeLabel, renderCrossSessionDeliveryNotice, renderCrossSessionDroppedNotice, renderCrossSessionIdleNotice, resolveCrossSessionDialogExpiry } from "./peer-notices.js";
|
|
13
13
|
import { routePeerDeliveryReceipt, routePeerDroppedReceipt, routePeerIdleNotice } from "./peer-notice-route.js";
|
|
14
|
+
import { thrownText } from "../core/thrown-value.js";
|
|
14
15
|
export const PEER_DRAIN_LEASE_TTL_MS = 5 * 60_000;
|
|
15
16
|
export const PEER_SESSION_ID_UNGRAMMATICAL_CODE = "peer.session_id_ungrammatical";
|
|
16
17
|
export function judgePeerLaneMount(deps, sessionId) {
|
|
@@ -304,7 +305,7 @@ export function createPeerSessionDrain(opts) {
|
|
|
304
305
|
payload = buildPeerMessagePayload(m);
|
|
305
306
|
}
|
|
306
307
|
catch (e) {
|
|
307
|
-
disclose("refused", `record fields cannot be rendered canonically (${e instanceof CrossSessionCodecError ? e.code :
|
|
308
|
+
disclose("refused", `record fields cannot be rendered canonically (${e instanceof CrossSessionCodecError ? e.code : thrownText(e)})`, m);
|
|
308
309
|
settledUpTo = m.seq;
|
|
309
310
|
continue;
|
|
310
311
|
}
|
|
@@ -2,6 +2,7 @@ import { delimitUntrusted } from "../core/untrusted-text.js";
|
|
|
2
2
|
import { PEER_MESSAGE_NOTICE } from "./peer-admission.js";
|
|
3
3
|
import { createSafeNotifier } from "../core/safe-notify.js";
|
|
4
4
|
import { RETAIN_DEFAULT_TTL_MS, RETAIN_DEFAULT_MAX } from "../config/defaults.js";
|
|
5
|
+
import { asThrownError } from "../core/thrown-value.js";
|
|
5
6
|
export const SUBAGENT_RESUME_CAP = 8;
|
|
6
7
|
export class SubagentRetainLedger {
|
|
7
8
|
ttlMs;
|
|
@@ -30,7 +31,7 @@ export class SubagentRetainLedger {
|
|
|
30
31
|
return this.notifier.failuresAt("retainLedger.onEvictionError");
|
|
31
32
|
}
|
|
32
33
|
noteDetachedFailure(trigger, error, parentToolCallId) {
|
|
33
|
-
const err =
|
|
34
|
+
const err = asThrownError(error);
|
|
34
35
|
this.failedEvictions += 1;
|
|
35
36
|
this.lastFailure = err;
|
|
36
37
|
this.notifier.notify(() => this.hooks?.onEvictionError?.({ ...(parentToolCallId !== undefined ? { parentToolCallId } : {}), trigger, error: err }), "retainLedger.onEvictionError");
|
|
@@ -15,6 +15,7 @@ import { SUBAGENT_RESUME_CAP, SubagentRetainLedger, getSessionRetainLedger } fro
|
|
|
15
15
|
import { createSubagentResume } from "./subagent.js";
|
|
16
16
|
import { appendHopToken, judgePeerAdmission, peerAxisToken, resolvePeerAdmissionConfig, PEER_MESSAGE_NOTICE, } from "./peer-admission.js";
|
|
17
17
|
import { toolFace } from "../core/tool-catalog-entries.js";
|
|
18
|
+
import { thrownText } from "../core/thrown-value.js";
|
|
18
19
|
export const SEND_MESSAGE_TOOL_NAME = "SendMessage";
|
|
19
20
|
let uplinkSeqGlobal = Date.now();
|
|
20
21
|
const UPLINK_RESULT_MAX = 8000;
|
|
@@ -245,7 +246,7 @@ export function createSendMessageTool(opts) {
|
|
|
245
246
|
return resolvePeerSessions(rows, to, peerSelfSessionId !== undefined ? { sessionId: peerSelfSessionId } : undefined);
|
|
246
247
|
}
|
|
247
248
|
catch (e) {
|
|
248
|
-
return { status: "directory_fault", message:
|
|
249
|
+
return { status: "directory_fault", message: thrownText(e) };
|
|
249
250
|
}
|
|
250
251
|
};
|
|
251
252
|
const deliverToPeerSession = async (c) => {
|
|
@@ -288,7 +289,7 @@ export function createSendMessageTool(opts) {
|
|
|
288
289
|
if (e?.code === MAILBOX_TOMBSTONED_RECIPIENT_CODE) {
|
|
289
290
|
return peerRefusal("peer_send.invalid_target", `${peerLabel} is being deleted — its box no longer accepts messages. Nothing was parked; do not retry.`);
|
|
290
291
|
}
|
|
291
|
-
return peerRefusal("peer_send.other", `the session box refused the message (${
|
|
292
|
+
return peerRefusal("peer_send.other", `the session box refused the message (${thrownText(e)}) — nothing was parked. ${DEDUP_RETRY_NOTE}`);
|
|
292
293
|
}
|
|
293
294
|
}
|
|
294
295
|
let subscription;
|
|
@@ -314,7 +315,7 @@ export function createSendMessageTool(opts) {
|
|
|
314
315
|
}
|
|
315
316
|
catch (e) {
|
|
316
317
|
peerIdleMachineFor(peerScope, peerSelfSessionId).requester.settle(record.sessionId, "unavailable");
|
|
317
|
-
subscription = { ok: false, code: e?.code === MAILBOX_TOMBSTONED_RECIPIENT_CODE ? "peer_send.invalid_target" : "peer_send.other", severity: "error", message: `the session box refused the subscription record (${(
|
|
318
|
+
subscription = { ok: false, code: e?.code === MAILBOX_TOMBSTONED_RECIPIENT_CODE ? "peer_send.invalid_target" : "peer_send.other", severity: "error", message: `the session box refused the subscription record (${(thrownText(e)).slice(0, 200)}) — no subscription was made.` };
|
|
318
319
|
}
|
|
319
320
|
}
|
|
320
321
|
}
|
|
@@ -427,7 +428,7 @@ export function createSendMessageTool(opts) {
|
|
|
427
428
|
}, { priority: "next" });
|
|
428
429
|
}
|
|
429
430
|
catch (e) {
|
|
430
|
-
return { content: `Message not sent: the parent's notification lane rejected it (${
|
|
431
|
+
return { content: `Message not sent: the parent's notification lane rejected it (${thrownText(e)}). ${DEDUP_RETRY_NOTE}`, details: { error: "uplink_failed", to }, isError: true };
|
|
431
432
|
}
|
|
432
433
|
return {
|
|
433
434
|
content: `Message sent to main — queued for the spawning conversation at its next turn boundary. If that conversation finishes before reading it, the message may not survive. Continue with your task; do not wait for a reply.`,
|
|
@@ -568,7 +569,7 @@ export function createSendMessageTool(opts) {
|
|
|
568
569
|
announceTranscriptIntegrityGapOnce(handle, scope, opts.onTranscriptIntegrityGap);
|
|
569
570
|
return queueFullReceipt(`No transcript found for agent ${handle}, so a revival cannot drain the backlog.`);
|
|
570
571
|
}
|
|
571
|
-
return queueFullReceipt(`Its transcript store did not answer (${
|
|
572
|
+
return queueFullReceipt(`Its transcript store did not answer (${thrownText(e)}) — this send did not start a backlog drain; a later resend will try again.`);
|
|
572
573
|
}
|
|
573
574
|
const drainLeaseOwner = `${opts.registry.writerId}:${ctx.toolCallId}`;
|
|
574
575
|
let drainLease = null;
|
|
@@ -589,7 +590,7 @@ export function createSendMessageTool(opts) {
|
|
|
589
590
|
drainSpawned = await opts.reviveSpawn({ row: claimed, rev: claimedRev, prompt: drainPrompt, peerSeed: { hopChain: [...drainSeed] } });
|
|
590
591
|
}
|
|
591
592
|
catch (e) {
|
|
592
|
-
drainSpawned = { isError: true, content:
|
|
593
|
+
drainSpawned = { isError: true, content: thrownText(e) };
|
|
593
594
|
}
|
|
594
595
|
if (drainSpawned.isError === true) {
|
|
595
596
|
try {
|
|
@@ -672,7 +673,7 @@ export function createSendMessageTool(opts) {
|
|
|
672
673
|
};
|
|
673
674
|
}
|
|
674
675
|
return {
|
|
675
|
-
content: `Message not sent: ${whoT3}'s transcript store did not answer (${
|
|
676
|
+
content: `Message not sent: ${whoT3}'s transcript store did not answer (${thrownText(e)}) — nothing was parked; send again. ${DEDUP_RETRY_NOTE}`,
|
|
676
677
|
details: { error: "session_store_unavailable", to },
|
|
677
678
|
isError: true,
|
|
678
679
|
};
|
|
@@ -691,7 +692,7 @@ export function createSendMessageTool(opts) {
|
|
|
691
692
|
};
|
|
692
693
|
}
|
|
693
694
|
return {
|
|
694
|
-
content: `Message not sent: the durable mailbox refused the message (${
|
|
695
|
+
content: `Message not sent: the durable mailbox refused the message (${thrownText(e)}) — nothing was parked and the agent was not revived. ${DEDUP_RETRY_NOTE}`,
|
|
695
696
|
details: { error: "mailbox_failed", to },
|
|
696
697
|
isError: true,
|
|
697
698
|
};
|
|
@@ -720,7 +721,7 @@ export function createSendMessageTool(opts) {
|
|
|
720
721
|
spawned = await opts.reviveSpawn({ row: claimed, rev: claimedRev, prompt: revivePrompt, peerSeed: { hopChain: [...reviveSeed] } });
|
|
721
722
|
}
|
|
722
723
|
catch (e) {
|
|
723
|
-
spawned = { isError: true, content:
|
|
724
|
+
spawned = { isError: true, content: thrownText(e), details: { error: "revive_failed" } };
|
|
724
725
|
}
|
|
725
726
|
if (spawned.isError === true) {
|
|
726
727
|
try {
|
|
@@ -1040,7 +1041,7 @@ export function createSendMessageTool(opts) {
|
|
|
1040
1041
|
`${who}'s registry row is being adjudicated right now (a revival claim or a reap sweep holds it) — send again in a moment. ${DEDUP_RETRY_NOTE}`
|
|
1041
1042
|
: code === "resume.row_gone"
|
|
1042
1043
|
? `${who}'s registry row no longer exists (terminal GC) — relaunch a new agent instead.`
|
|
1043
|
-
: `${
|
|
1044
|
+
: `${thrownText(e)} ${DEDUP_RETRY_NOTE}`;
|
|
1044
1045
|
return { content: `Message not sent: ${text}`, details: { error: code ?? "resume_failed", to }, isError: true };
|
|
1045
1046
|
}
|
|
1046
1047
|
});
|
package/dist/agents/subagent.js
CHANGED
|
@@ -67,6 +67,7 @@ import { LAUNCH_RECEIPT_OWN_WORDS_CLAUSE, launchReceiptNoQuoteClause } from "./l
|
|
|
67
67
|
import { ObserverDigestTap, ObserverPairing, createObserverReportToolSpec, markObserverTaskId, unmarkObserverTaskId, isObserverTaskId, ObserverResumeStateError, ObserverStoppedByUserError, observerFramingPrompt, observerSlug, resolveObserverDeclaration, } from "./observer.js";
|
|
68
68
|
import { SubagentStepRecorder } from "./subagent-steps.js";
|
|
69
69
|
import { toolFace } from "../core/tool-catalog-entries.js";
|
|
70
|
+
import { thrownText } from "../core/thrown-value.js";
|
|
70
71
|
const BG_AGENT_RESULT_MAX = 4_000;
|
|
71
72
|
const BG_AGENT_RESULT_FULL_MAX = 200_000;
|
|
72
73
|
function resultSettleFields(result) {
|
|
@@ -291,7 +292,7 @@ export function createSubagentWorktreeHelper(baseEnv, repoRoot) {
|
|
|
291
292
|
async mint(childId) {
|
|
292
293
|
const probe = await baseEnv.exec("git rev-parse --is-inside-work-tree", { cwd: repoRoot });
|
|
293
294
|
if (!probe.ok || probe.value.exitCode !== 0) {
|
|
294
|
-
const detail = probe.ok ? (probe.value.stderr || probe.value.stdout).trim().split("\n")[0] :
|
|
295
|
+
const detail = probe.ok ? (probe.value.stderr || probe.value.stdout).trim().split("\n")[0] : thrownText(probe.error);
|
|
295
296
|
throw new Error(`the working directory ${repoRoot} is not a git repository${detail ? ` (${detail})` : ""}`);
|
|
296
297
|
}
|
|
297
298
|
const { env, worktreeDir } = await addWorktree(baseEnv, {
|
|
@@ -817,7 +818,7 @@ export function createSubagentResume(deps) {
|
|
|
817
818
|
}
|
|
818
819
|
}
|
|
819
820
|
catch (e) {
|
|
820
|
-
const msgRevive =
|
|
821
|
+
const msgRevive = thrownText(e);
|
|
821
822
|
const errCodeReviveReject = abort.signal.aborted ? undefined : thrownErrorCode(e);
|
|
822
823
|
const errClassReviveReject = abort.signal.aborted ? undefined : classifySubagentError({ kind: "failed", ...(errCodeReviveReject !== undefined ? { code: errCodeReviveReject } : {}) });
|
|
823
824
|
if (reviveCycle !== undefined && deps.registry !== undefined && deps.taskId !== undefined) {
|
|
@@ -1692,7 +1693,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1692
1693
|
catch (e) {
|
|
1693
1694
|
return {
|
|
1694
1695
|
isError: true,
|
|
1695
|
-
content: `Sub-agent not started: worktree isolation failed — ${
|
|
1696
|
+
content: `Sub-agent not started: worktree isolation failed — ${thrownText(e)}. Retry without the isolation parameter, or fix the repository state.`,
|
|
1696
1697
|
details: { error: "isolation.failed" },
|
|
1697
1698
|
};
|
|
1698
1699
|
}
|
|
@@ -1711,7 +1712,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1711
1712
|
}
|
|
1712
1713
|
catch (e) {
|
|
1713
1714
|
worktreeKeptPath = worktreeDir;
|
|
1714
|
-
return `worktree: ${worktreeDir} (cleanup check failed: ${
|
|
1715
|
+
return `worktree: ${worktreeDir} (cleanup check failed: ${thrownText(e)})`;
|
|
1715
1716
|
}
|
|
1716
1717
|
};
|
|
1717
1718
|
let injectedTools = [];
|
|
@@ -1845,7 +1846,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1845
1846
|
let observerDegradedCount = 0;
|
|
1846
1847
|
let observerDegradedFirst;
|
|
1847
1848
|
const noteObserverFailure = (err) => {
|
|
1848
|
-
const msg = (
|
|
1849
|
+
const msg = (thrownText(err)).slice(0, REPORT_FIELD_MAX);
|
|
1849
1850
|
if (observerPairing !== undefined && observerPairing.state === "armed") {
|
|
1850
1851
|
observerDegradedCount++;
|
|
1851
1852
|
if (observerDegradedFirst === undefined)
|
|
@@ -2362,7 +2363,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2362
2363
|
}
|
|
2363
2364
|
catch (e) {
|
|
2364
2365
|
const wt = await finishWorktree();
|
|
2365
|
-
return { isError: true, content: `Sub-agent not started: session fork failed (${
|
|
2366
|
+
return { isError: true, content: `Sub-agent not started: session fork failed (${thrownText(e)}).${wt ? `\n${wt}` : ""}`, details: { error: "fork failed" } };
|
|
2366
2367
|
}
|
|
2367
2368
|
if (!forkedId) {
|
|
2368
2369
|
const wt = await finishWorktree();
|
|
@@ -2414,7 +2415,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2414
2415
|
const wt = await finishWorktree();
|
|
2415
2416
|
return {
|
|
2416
2417
|
isError: true,
|
|
2417
|
-
content: `Sub-agent not started in background: the delegation settlement account could not record the launch (${(
|
|
2418
|
+
content: `Sub-agent not started in background: the delegation settlement account could not record the launch (${(thrownText(e)).slice(0, 300)}) — fail-closed; repair the memory control plane or retry.${wt ? `\n${wt}` : ""}`,
|
|
2418
2419
|
details: { error: "settlement_write_ahead_failed" },
|
|
2419
2420
|
};
|
|
2420
2421
|
}
|
|
@@ -2443,7 +2444,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2443
2444
|
}
|
|
2444
2445
|
}
|
|
2445
2446
|
if (!settled) {
|
|
2446
|
-
const detail =
|
|
2447
|
+
const detail = thrownText(lastErr);
|
|
2447
2448
|
try {
|
|
2448
2449
|
enqueueMemoryAnnouncement(forkSettlementSeat.controlDir, {
|
|
2449
2450
|
kind: "gate",
|
|
@@ -2490,7 +2491,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2490
2491
|
catch {
|
|
2491
2492
|
}
|
|
2492
2493
|
}
|
|
2493
|
-
return { isError: true, content: `Sub-agent not started in background: ${
|
|
2494
|
+
return { isError: true, content: `Sub-agent not started in background: ${thrownText(e)}${wt ? `\n${wt}` : ""}`, details: { error: "register_failed" } };
|
|
2494
2495
|
}
|
|
2495
2496
|
if (forkEntryGate.key !== undefined)
|
|
2496
2497
|
chargeDelegationEntryHandle(bg.registry, forkEntryGate.key, bg.agentStore, taskId);
|
|
@@ -2741,7 +2742,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2741
2742
|
dropHostAbortListener();
|
|
2742
2743
|
void finishWorktree();
|
|
2743
2744
|
const killed = abort.signal.aborted;
|
|
2744
|
-
const msgFork =
|
|
2745
|
+
const msgFork = thrownText(e);
|
|
2745
2746
|
const errCodeForkReject = killed ? undefined : thrownErrorCode(e);
|
|
2746
2747
|
const errClassForkReject = killed ? undefined : classifySubagentError({ kind: "failed", ...(errCodeForkReject !== undefined ? { code: errCodeForkReject } : {}) });
|
|
2747
2748
|
const settledBg = bg.registry.settleBackgroundAgent(taskId, {
|
|
@@ -2850,7 +2851,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2850
2851
|
catch {
|
|
2851
2852
|
}
|
|
2852
2853
|
const wt = await finishWorktree();
|
|
2853
|
-
return { isError: true, content: `Fork failed: the forked run threw (${
|
|
2854
|
+
return { isError: true, content: `Fork failed: the forked run threw (${thrownText(e)}).${wt ? `\n${wt}` : ""}`, details: { error: "fork run failed" } };
|
|
2854
2855
|
}
|
|
2855
2856
|
finally {
|
|
2856
2857
|
syncForkAdmission.release();
|
|
@@ -2956,7 +2957,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2956
2957
|
const wt = await finishWorktree();
|
|
2957
2958
|
return {
|
|
2958
2959
|
isError: true,
|
|
2959
|
-
content: `Sub-agent not started in background: the delegation settlement account could not record the launch (${(
|
|
2960
|
+
content: `Sub-agent not started in background: the delegation settlement account could not record the launch (${(thrownText(e)).slice(0, 300)}) — fail-closed; repair the memory control plane or retry.${wt ? `\n${wt}` : ""}`,
|
|
2960
2961
|
details: { error: "settlement_write_ahead_failed" },
|
|
2961
2962
|
};
|
|
2962
2963
|
}
|
|
@@ -2985,7 +2986,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2985
2986
|
}
|
|
2986
2987
|
}
|
|
2987
2988
|
if (!settled) {
|
|
2988
|
-
const detail =
|
|
2989
|
+
const detail = thrownText(lastErr);
|
|
2989
2990
|
try {
|
|
2990
2991
|
enqueueMemoryAnnouncement(settlementSeat.controlDir, {
|
|
2991
2992
|
kind: "gate",
|
|
@@ -3064,7 +3065,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
3064
3065
|
catch {
|
|
3065
3066
|
}
|
|
3066
3067
|
}
|
|
3067
|
-
return { isError: true, content: `Sub-agent not started in background: ${
|
|
3068
|
+
return { isError: true, content: `Sub-agent not started in background: ${thrownText(e)}${wt ? `\n${wt}` : ""}`, details: { error: "register_failed" } };
|
|
3068
3069
|
}
|
|
3069
3070
|
if (capLedgerKey !== undefined)
|
|
3070
3071
|
chargeDelegationEntryHandle(bg.registry, capLedgerKey, bg.agentStore, reviveRow === undefined ? taskId : undefined);
|
|
@@ -3281,7 +3282,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
3281
3282
|
}
|
|
3282
3283
|
catch {
|
|
3283
3284
|
}
|
|
3284
|
-
const msg =
|
|
3285
|
+
const msg = thrownText(e);
|
|
3285
3286
|
throw new Error(disp === "rolled_back"
|
|
3286
3287
|
? `parked resume rolled back — the approval is still pending (retryable): ${msg}`
|
|
3287
3288
|
: disp === "failed"
|
|
@@ -3609,7 +3610,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
3609
3610
|
else
|
|
3610
3611
|
bgRetainLedger?.markSettled(ctx.toolCallId);
|
|
3611
3612
|
}
|
|
3612
|
-
const msg =
|
|
3613
|
+
const msg = thrownText(e);
|
|
3613
3614
|
const errCodeBgReject = killed ? undefined : thrownErrorCode(e);
|
|
3614
3615
|
const errClassBgReject = killed ? undefined : classifySubagentError({ kind: "failed", ...(errCodeBgReject !== undefined ? { code: errCodeBgReject } : {}) });
|
|
3615
3616
|
const settled = bg.registry.settleBackgroundAgent(taskId, {
|
|
@@ -3672,7 +3673,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
3672
3673
|
]);
|
|
3673
3674
|
if (startup === "settled") {
|
|
3674
3675
|
if (parkedResume !== undefined) {
|
|
3675
|
-
const err = await bgChildPromise.then(() => undefined, (e) => (
|
|
3676
|
+
const err = await bgChildPromise.then(() => undefined, (e) => (thrownText(e)));
|
|
3676
3677
|
return {
|
|
3677
3678
|
isError: true,
|
|
3678
3679
|
content: `Parked resume did not complete: ${(err ?? "the resumed run settled before its delivery lane bound").slice(0, 600)}`,
|
package/dist/agents/teacher.js
CHANGED
|
@@ -6,6 +6,7 @@ import { mapNestedSuspend, isDurablePause } from "./suspend-guard.js";
|
|
|
6
6
|
import { isDefineToolProduct, rebindDefineToolCtx, stampDefineToolBrand } from "../core/tools.js";
|
|
7
7
|
import { delimitUntrusted, inlineUntrusted } from "../core/untrusted-text.js";
|
|
8
8
|
import { buildCumulativeStats } from "./cumulative-stats.js";
|
|
9
|
+
import { thrownLabel, thrownText } from "../core/thrown-value.js";
|
|
9
10
|
export const TEACHER_PROMPT = `You are an expert advisor to a less-capable "student" agent that got stuck.
|
|
10
11
|
You receive the task and the student's recent failed attempts. Your job is to help the student RECOVER,
|
|
11
12
|
not to do the work for it.
|
|
@@ -97,7 +98,7 @@ export async function runWithTeacher(runner, studentSpec, teacher = {}) {
|
|
|
97
98
|
catch (e) {
|
|
98
99
|
if (isConfigCoded(e))
|
|
99
100
|
throw e;
|
|
100
|
-
discloseStoreIncident({ op: "find", error:
|
|
101
|
+
discloseStoreIncident({ op: "find", error: thrownText(e) });
|
|
101
102
|
found = [];
|
|
102
103
|
}
|
|
103
104
|
let budget = 0;
|
|
@@ -143,7 +144,7 @@ export async function runWithTeacher(runner, studentSpec, teacher = {}) {
|
|
|
143
144
|
catch (e) {
|
|
144
145
|
if (isConfigCoded(e))
|
|
145
146
|
throw e;
|
|
146
|
-
discloseStoreIncident({ op: "save", error:
|
|
147
|
+
discloseStoreIncident({ op: "save", error: thrownText(e) });
|
|
147
148
|
}
|
|
148
149
|
}
|
|
149
150
|
}
|
|
@@ -218,7 +219,7 @@ async function runTeacherCore(runner, studentSpec, teacher) {
|
|
|
218
219
|
return out;
|
|
219
220
|
}
|
|
220
221
|
catch (e) {
|
|
221
|
-
const errText =
|
|
222
|
+
const errText = thrownLabel(e);
|
|
222
223
|
logTool(t.name, rawParams, `ERROR: ${errText}`);
|
|
223
224
|
onToolFail(t.name, errText);
|
|
224
225
|
throw e;
|
|
@@ -242,7 +243,7 @@ async function runTeacherCore(runner, studentSpec, teacher) {
|
|
|
242
243
|
return out;
|
|
243
244
|
}
|
|
244
245
|
catch (e) {
|
|
245
|
-
const errText =
|
|
246
|
+
const errText = thrownLabel(e);
|
|
246
247
|
logTool(t.name, args, `ERROR: ${errText}`);
|
|
247
248
|
onToolFail(t.name, errText);
|
|
248
249
|
throw e;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createAssistantMessageEventStream } from "../internal/llm.js";
|
|
2
2
|
import { BrainError, IN_BAND_OUTPUT_CODES, ZERO_USAGE, extractErrorCode } from "./errors.js";
|
|
3
3
|
import { emitBrainStatus, emitBrainTelemetry } from "./status-sink.js";
|
|
4
|
+
import { thrownText } from "../core/thrown-value.js";
|
|
4
5
|
export const CIRCUIT_OPEN_MARKER = "circuit open for ";
|
|
5
6
|
class InMemoryBreakerState {
|
|
6
7
|
m = new Map();
|
|
@@ -147,7 +148,7 @@ export function createCircuitBreakerBrain(inner, opts = {}) {
|
|
|
147
148
|
out.push({
|
|
148
149
|
type: "error",
|
|
149
150
|
reason: aborted ? "aborted" : "error",
|
|
150
|
-
error: errorAssistantMessage(model, code,
|
|
151
|
+
error: errorAssistantMessage(model, code, thrownText(e), aborted ? "aborted" : "error"),
|
|
151
152
|
});
|
|
152
153
|
}
|
|
153
154
|
})();
|
package/dist/brain/degrading.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createAssistantMessageEventStream, } from "../internal/llm.js";
|
|
2
2
|
import { ZERO_USAGE, extractErrorCode } from "./errors.js";
|
|
3
3
|
import { CIRCUIT_OPEN_MARKER } from "./circuit-breaker.js";
|
|
4
|
+
import { thrownText } from "../core/thrown-value.js";
|
|
4
5
|
export const DEGRADED_DIAGNOSTIC_TYPE = "degraded";
|
|
5
6
|
const VALID_REASONS = new Set([
|
|
6
7
|
"breaker_open",
|
|
@@ -53,7 +54,7 @@ function errorMessageOf(model, e, aborted) {
|
|
|
53
54
|
provider: model.provider,
|
|
54
55
|
model: model.id,
|
|
55
56
|
stopReason: aborted ? "aborted" : "error",
|
|
56
|
-
errorMessage:
|
|
57
|
+
errorMessage: thrownText(e),
|
|
57
58
|
usage: ZERO_USAGE,
|
|
58
59
|
timestamp: Date.now(),
|
|
59
60
|
};
|
package/dist/brain/errors.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { thrownText } from "../core/thrown-value.js";
|
|
1
2
|
export const ZERO_USAGE = {
|
|
2
3
|
input: 0,
|
|
3
4
|
output: 0,
|
|
@@ -81,12 +82,16 @@ const STALE_CONNECTION_CODES = new Set([
|
|
|
81
82
|
"StreamSuspended",
|
|
82
83
|
]);
|
|
83
84
|
function hasStaleConnectionCode(e) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
85
|
+
try {
|
|
86
|
+
let cur = e;
|
|
87
|
+
for (let depth = 0; depth < 5 && cur !== undefined && cur !== null; depth++) {
|
|
88
|
+
const code = typeof cur.code === "string" ? cur.code : undefined;
|
|
89
|
+
if (code !== undefined && STALE_CONNECTION_CODES.has(code))
|
|
90
|
+
return true;
|
|
91
|
+
cur = cur instanceof Error ? cur.cause : undefined;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
90
95
|
}
|
|
91
96
|
return false;
|
|
92
97
|
}
|
|
@@ -131,16 +136,20 @@ export function classifyConnectFailure(e) {
|
|
|
131
136
|
return walkConnectFailure(e, { left: CONNECT_GRAPH_NODE_BUDGET }) === "deterministic" ? "deterministic" : "indeterminate";
|
|
132
137
|
}
|
|
133
138
|
export function describeNetworkError(e) {
|
|
134
|
-
const top =
|
|
139
|
+
const top = thrownText(e);
|
|
135
140
|
const parts = [];
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
parts.
|
|
143
|
-
|
|
141
|
+
try {
|
|
142
|
+
let cur = e instanceof Error ? e.cause : undefined;
|
|
143
|
+
for (let depth = 0; depth < 4 && cur !== undefined && cur !== null; depth++) {
|
|
144
|
+
const code = typeof cur.code === "string" ? cur.code : undefined;
|
|
145
|
+
const msg = thrownText(cur);
|
|
146
|
+
const piece = code !== undefined && !msg.includes(code) ? `${code}: ${msg}` : msg;
|
|
147
|
+
if (piece.length > 0 && piece !== top && !parts.includes(piece))
|
|
148
|
+
parts.push(piece);
|
|
149
|
+
cur = cur instanceof Error ? cur.cause : undefined;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
144
153
|
}
|
|
145
154
|
const body = parts.length > 0 ? `${top} (${parts.join(" ← ")})` : top;
|
|
146
155
|
return hasStaleConnectionCode(e) ? `${body} — possibly a stale connection; a fresh one may succeed` : body;
|
package/dist/brain/failover.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createAssistantMessageEventStream, } from "../internal/llm.js";
|
|
|
2
2
|
import { extractErrorCode } from "./errors.js";
|
|
3
3
|
import { emitBrainTelemetry } from "./status-sink.js";
|
|
4
4
|
import { passthroughStream } from "./degrading.js";
|
|
5
|
+
import { thrownText } from "../core/thrown-value.js";
|
|
5
6
|
export function createFailoverBrain(brains) {
|
|
6
7
|
if (brains.length === 0) {
|
|
7
8
|
throw new Error("createFailoverBrain: need at least one brain");
|
|
@@ -73,7 +74,7 @@ export function createFailoverBrain(brains) {
|
|
|
73
74
|
provider: model.provider,
|
|
74
75
|
model: model.id,
|
|
75
76
|
stopReason: aborted ? "aborted" : "error",
|
|
76
|
-
errorMessage:
|
|
77
|
+
errorMessage: thrownText(e),
|
|
77
78
|
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
|
|
78
79
|
timestamp: Date.now(),
|
|
79
80
|
};
|