@sema-agent/core 5.21.1 → 5.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +103 -0
- package/dist/agents/send-message-tool.js +6 -3
- package/dist/agents/subagent.d.ts +6 -0
- package/dist/agents/subagent.js +48 -6
- package/dist/brain/errors.d.ts +20 -0
- package/dist/brain/errors.js +40 -0
- package/dist/brain/retry.d.ts +16 -2
- package/dist/brain/retry.js +3 -2
- package/dist/brain/status-sink.d.ts +9 -2
- package/dist/brain/stream-engine.d.ts +22 -0
- package/dist/brain/stream-engine.js +41 -10
- package/dist/core/ask-class.d.ts +48 -0
- package/dist/core/ask-class.js +33 -0
- package/dist/core/checkpoint-store.d.ts +103 -10
- package/dist/core/checkpoint-store.js +3 -1
- package/dist/core/governance-codes.d.ts +38 -0
- package/dist/core/governance-codes.js +14 -0
- package/dist/core/hooks.d.ts +101 -1
- package/dist/core/hooks.js +116 -14
- package/dist/core/locked-config.d.ts +7 -1
- package/dist/core/locked-config.js +2 -1
- package/dist/core/memory-engine/delegation-provenance.d.ts +62 -0
- package/dist/core/memory-engine/delegation-provenance.js +26 -0
- package/dist/core/memory-engine/engine.d.ts +94 -1
- package/dist/core/memory-engine/engine.js +329 -12
- package/dist/core/memory-engine/header-hints.d.ts +30 -0
- package/dist/core/memory-engine/header-hints.js +41 -0
- package/dist/core/memory-engine/index.d.ts +3 -2
- package/dist/core/memory-engine/index.js +3 -2
- package/dist/core/memory-engine/layout.d.ts +232 -0
- package/dist/core/memory-engine/layout.js +469 -1
- package/dist/core/memory-engine/tools.d.ts +30 -0
- package/dist/core/memory-engine/tools.js +108 -17
- package/dist/core/permission-rule-consent.d.ts +25 -9
- package/dist/core/permission-rule-consent.js +93 -21
- package/dist/core/permission-rule-model.d.ts +9 -1
- package/dist/core/permission-rule-model.js +2 -2
- package/dist/core/permission-rule-org.d.ts +195 -0
- package/dist/core/permission-rule-org.js +234 -0
- package/dist/core/permission-rule-store.d.ts +249 -6
- package/dist/core/permission-rule-store.js +314 -3
- package/dist/core/permission-rule-sync.d.ts +139 -0
- package/dist/core/permission-rule-sync.js +343 -0
- package/dist/core/runner/prepare-memory.js +35 -8
- package/dist/core/runner/prepare-task.d.ts +64 -3
- package/dist/core/runner/prepare-task.js +357 -32
- package/dist/core/runner/runtask.js +166 -6
- package/dist/core/shared-memory/contract.js +19 -4
- package/dist/core/shared-memory/normalize.d.ts +3 -1
- package/dist/core/shared-memory/tools.js +73 -17
- package/dist/core/shared-memory/types.d.ts +27 -1
- package/dist/core/store-contracts/permission-rule-sync-contract.d.ts +33 -0
- package/dist/core/store-contracts/permission-rule-sync-contract.js +186 -0
- package/dist/core/task-notification.d.ts +5 -2
- package/dist/core/task-registry-agent.d.ts +1 -1
- package/dist/core/task-registry-agent.js +6 -2
- package/dist/core/task-registry-shared.d.ts +9 -2
- package/dist/core/task-registry.d.ts +9 -3
- package/dist/core/task-registry.js +2 -0
- package/dist/core/tool-policy.d.ts +155 -4
- package/dist/core/tool-policy.js +148 -10
- package/dist/core/tool-result-store.d.ts +9 -1
- package/dist/core/tool-result-store.js +2 -1
- package/dist/core/trace.d.ts +79 -1
- package/dist/core/types.d.ts +94 -3
- package/dist/core/wiring-manifest.d.ts +16 -1
- package/dist/core/wiring-manifest.js +7 -1
- package/dist/index.d.ts +15 -8
- package/dist/index.js +12 -5
- package/dist/orchestration/goal.d.ts +10 -0
- package/dist/orchestration/goal.js +6 -5
- package/dist/stores/file/adoption/adopt.d.ts +146 -0
- package/dist/stores/file/adoption/adopt.js +616 -0
- package/dist/stores/file/adoption/marker.d.ts +194 -0
- package/dist/stores/file/adoption/marker.js +198 -0
- package/dist/stores/file/background-agent-store.js +2 -0
- package/dist/stores/file/checkpoint-store.d.ts +4 -0
- package/dist/stores/file/checkpoint-store.js +3 -0
- package/dist/stores/file/file-snapshot-store.js +2 -0
- package/dist/stores/file/index.d.ts +2 -0
- package/dist/stores/file/index.js +4 -0
- package/dist/stores/file/mailbox-store.js +2 -0
- package/dist/stores/file/memory-store.js +2 -0
- package/dist/stores/file/permission-rule-adopt.d.ts +62 -0
- package/dist/stores/file/permission-rule-adopt.js +95 -0
- package/dist/stores/file/permission-rule-store.d.ts +80 -2
- package/dist/stores/file/permission-rule-store.js +189 -46
- package/dist/stores/file/session-policy-store.js +2 -0
- package/dist/stores/file/session-store.js +2 -0
- package/dist/stores/file/task-list-store.js +2 -0
- package/dist/stores/file/tool-result-store.js +2 -0
- package/dist/stores/file/usage-window-store.js +2 -0
- package/dist/stores/file/workflow-journal-store.js +2 -0
- package/dist/stores/file/workflow-run-store.js +2 -0
- package/dist/tools/fs/bash-readonly-classifier.js +59 -10
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,108 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.23.0 — 2026-08-10
|
|
4
|
+
|
|
5
|
+
No BREAKING changes.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **design/183 adoption protocol, core file half.** Root adoption marker + six-phase
|
|
10
|
+
`adoptLocalDataRoot` state machine + `readAdoptionStatus` standing introspection + config account
|
|
11
|
+
(witness/ack) + quarantine listing; 23 new exports. The I6 boot gate is a new refusal surface: every
|
|
12
|
+
file-store constructor under a root whose `adoption.json` marker is IN FLIGHT throws
|
|
13
|
+
`AdoptionError("adoption_in_flight")` (corrupt marker: `adoption_marker_corrupt`, fail-closed).
|
|
14
|
+
Roots without a marker are untouched. The terminal report carries `notMigratedByDesign` — the
|
|
15
|
+
machine-readable closed set of assets ruled to stay local, sharded per migration form.
|
|
16
|
+
- **design/182 org layer wired into the decision chain** (org-governed deployments only): org deny
|
|
17
|
+
blocks on both the main gate and the durable-resume leg; an unavailable org snapshot past
|
|
18
|
+
`ORG_ADJUDICATION_TIMEOUT_MS` (15s, mandatory at both call sites) downgrades every terminal allow
|
|
19
|
+
to a human ask, fail-closed. Closed-set additions consumers should re-pin: `DecisionReason`
|
|
20
|
+
+`org_rule`/`org_unavailable`; `PermissionDeniedSource` +`org`; `TraceEvent`
|
|
21
|
+
+`rule_sync_resurrected`/`rule_sync_dropped`/`org_snapshot_unavailable`.
|
|
22
|
+
- `AskOutcome` terminal arms accept an optional `settledBy` (host self-report; `allow` × `"timeout"`
|
|
23
|
+
is loudly refused); `GoalResult.budgetCause` names which ceiling ended a budget run;
|
|
24
|
+
`NON_GOVERNANCE_MEMORY_CODES` +3 members (`memory.challenge_ledger_oversize`,
|
|
25
|
+
`memory.control_plane_rebuilt`, `memory.control_plane_not_corrupt`);
|
|
26
|
+
`MemoryEngine.rebuildCorruptControlPlaneLedger` (host-only, refuses a healthy ledger, quarantines
|
|
27
|
+
corrupt bytes) and `CHALLENGE_LEDGER_MAX_EVENTS` disclosure threshold.
|
|
28
|
+
|
|
29
|
+
### Fixed / narrowed
|
|
30
|
+
|
|
31
|
+
- **#109**: separated option values (`head -c 5`, `cut -f 1`, `grep -A 3 pat`, …) are no longer
|
|
32
|
+
counted as file operands by the read-only shell classifier — fileless forms of these commands now
|
|
33
|
+
ask instead of auto-passing.
|
|
34
|
+
- **#110**: a checkpoint `put` that errors but whose row is durably present (read-back claims it)
|
|
35
|
+
suspends instead of running on; `put`+`get` both failing aborts the run fail-closed.
|
|
36
|
+
- **#114**: `settledBy:"timeout"` documented promise narrowed to the two core-managed wait windows.
|
|
37
|
+
- **#115**: the offload truncation notice states where the remainder IS instead of implying any
|
|
38
|
+
caller can fetch it.
|
|
39
|
+
- Consent-record ids and the in-memory CRDT actor default now take real entropy (`randomBytes`);
|
|
40
|
+
format is opaque, do not parse.
|
|
41
|
+
|
|
42
|
+
### Tooling
|
|
43
|
+
|
|
44
|
+
- New `gate:error-surface` (four invariants over model-visible error minting: no `.stack`,
|
|
45
|
+
>400-char templates ledgered, uncapped-arm ratchet, verbatim-guidance dedup) wired into CI.
|
|
46
|
+
- The environment-knob surface (25 names) is under an exhaustive AST-driven liveness registry;
|
|
47
|
+
`docs/CONFIG-CATALOG-GUIDE.md` gained the environment-knobs chapter (two comment-only pseudo-knobs
|
|
48
|
+
called out: `MAX_TOOL_CONCURRENCY`, `APPROVAL_TTL_MS` — setting them has no effect).
|
|
49
|
+
|
|
50
|
+
## 5.22.0 — 2026-08-09
|
|
51
|
+
|
|
52
|
+
### BREAKING
|
|
53
|
+
|
|
54
|
+
- **Checkpoint version 6 → 7.** A checkpoint row that carries the new inheritance-chain constraint
|
|
55
|
+
snapshot or delegation-provenance state is stamped v7; a pre-5.22 worker refuses it up front
|
|
56
|
+
(`checkpoint.unsupported_version`) instead of resuming with the ancestors' gate silently dropped.
|
|
57
|
+
Rows without the new faces keep their old stamp and resume everywhere.
|
|
58
|
+
- **Durable re-supply contract: content digest replaces the count check.** Resuming a suspended task
|
|
59
|
+
under inherited constraints now verifies a `cpv1:` sha256 digest of the persisted constraint
|
|
60
|
+
projection (topology + decision-chain metadata: autoMode armed, durable/content mandates) — a
|
|
61
|
+
same-length substitute chain, or one that dropped the ancestor's classifier, is refused
|
|
62
|
+
(`resume.parent_constraint_mismatch`). Old checkpoints without a digest fall back to the count check.
|
|
63
|
+
- **Approver-edited args re-adjudicate BEFORE the resolve CAS, and a refusal keeps the park pending.**
|
|
64
|
+
Previously the edit recheck ran after the checkpoint was consumed; a refusal now surfaces as
|
|
65
|
+
`resume.constraint_rejected` / `resume.constraint_unprojectable` with the checkpoint still
|
|
66
|
+
`pending` and re-decidable (new reopen reason `constraint_rejected`). Deployments that pinned the
|
|
67
|
+
old "resume completes, denial injected" shape must re-pin.
|
|
68
|
+
- **`basePolicyForResumeEdit` absent no longer skips the edit recheck.** The knob becomes an
|
|
69
|
+
override; absence falls back to the deployment's locked preflight policy, so an approver edit is
|
|
70
|
+
never executed with zero re-adjudication. Configured deployments are unchanged.
|
|
71
|
+
|
|
72
|
+
### Added
|
|
73
|
+
|
|
74
|
+
- **Inheritance-chain decision-chain integrity (F-012).** Ancestor deny verdicts freeze into the
|
|
75
|
+
chain as a canonical projection; the ancestor's auto-mode classifier now travels with the chain
|
|
76
|
+
(both wrapper arms and the approved-edit recheck) — a delegated child no longer executes a call the
|
|
77
|
+
ancestor's own gate would have classified into a block. The budget ledger is documented as
|
|
78
|
+
task-scoped (it does not follow the chain), and the edit-recheck path now sees a real budget
|
|
79
|
+
snapshot instead of an absent one.
|
|
80
|
+
- **Sandbox admission (opt-in, narrow form).** Under an execution environment that declares
|
|
81
|
+
`capabilities.isolation`, surviving engine-minted local asks on engine hand tools admit
|
|
82
|
+
automatically with a durable `permission.sandbox_admitted` disclosure (`decisionReason:"sandbox"`)
|
|
83
|
+
instead of parking. Caller-registered in-process tools never auto-admit — the isolation capability
|
|
84
|
+
only vouches for what runs inside that environment. Enabling the mode requires durable parking and
|
|
85
|
+
a checkpoint store that declares re-decision capability; otherwise preparation refuses loudly.
|
|
86
|
+
- **Deterministic connect refusals take a short retry lane (#107).** ECONNREFUSED/ENOTFOUND-class
|
|
87
|
+
failures retry 2× with a 1s backoff cap instead of consuming the full transient budget
|
|
88
|
+
(measured 186s → 1.8s); `BrainStatus` and `brain.retry` telemetry carry a closed `errClass` and
|
|
89
|
+
the in-force lane budget. Consumers pinning the `reconnecting` detail text or reading
|
|
90
|
+
`maxRetries` as the engine-wide budget are affected.
|
|
91
|
+
- **Delegation attestation persists across processes (design/180 R-4 half).** The three-value
|
|
92
|
+
delegation-provenance aggregate rides the durable checkpoint; a resumed leg missing the state
|
|
93
|
+
reads as `unknown` (static floor), never as `clean`.
|
|
94
|
+
- **`buildToolResultRef` exported from the package root** — a host wiring an HTTP tool-result read
|
|
95
|
+
face binds an incoming ref to its owning task by prefix recomputation.
|
|
96
|
+
- **Content mandate split from the approval mandate (#94).** A delegated child's question routes on
|
|
97
|
+
the QUESTION seat, not the approver seat. Behavior widening, called out explicitly: under
|
|
98
|
+
`durableApproval` with a live `onQuestion`, an absent or string approver seat used to leave child
|
|
99
|
+
questions undeliverable — they now reach the live question face. The permission leg is unchanged.
|
|
100
|
+
- **Background-agent revival honesty (#84).** `resumable` answers the real revival ladder;
|
|
101
|
+
the revive lane distinguishes a transient `recycling` window (`resume.row_recycling`, retry) from
|
|
102
|
+
`row_gone` (terminal), checks access before disclosing row lifecycle, and
|
|
103
|
+
`TaskRegistry.reviveBackgroundAgent`'s refusal union gains the `recycling` member — exhaustive
|
|
104
|
+
consumers add one arm.
|
|
105
|
+
|
|
3
106
|
## 5.21.1 — 2026-08-09
|
|
4
107
|
|
|
5
108
|
- **Fix: every `hands: none` deployment failed at the door on 5.21.0** (P0). `HAND_TOOL_EFFECTS`
|
|
@@ -709,9 +709,12 @@ export function createSendMessageTool(opts) {
|
|
|
709
709
|
: code === "steering.still_running"
|
|
710
710
|
?
|
|
711
711
|
`${who} (or a prior follow-up to it) is still running — wait for its completion notification. ${DEDUP_RETRY_NOTE}`
|
|
712
|
-
: code === "resume.
|
|
713
|
-
?
|
|
714
|
-
|
|
712
|
+
: code === "resume.row_recycling"
|
|
713
|
+
?
|
|
714
|
+
`${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}`
|
|
715
|
+
: code === "resume.row_gone"
|
|
716
|
+
? `${who}'s registry row no longer exists (terminal GC) — relaunch a new agent instead.`
|
|
717
|
+
: `${e instanceof Error ? e.message : String(e)} ${DEDUP_RETRY_NOTE}`;
|
|
715
718
|
return { content: `Message not sent: ${text}`, details: { error: code ?? "resume_failed", to }, isError: true };
|
|
716
719
|
}
|
|
717
720
|
});
|
|
@@ -293,6 +293,12 @@ export declare function completedAgentCard(child: {
|
|
|
293
293
|
* flagged (or no review was armed), so a consumer must not read presence as a verdict source for
|
|
294
294
|
* anything else. */
|
|
295
295
|
handbackWarning?: string;
|
|
296
|
+
/** design/180 half A — the child's delegation runtime-provenance attestation, reduced from its
|
|
297
|
+
* recorder aggregate at delivery. Present only when the parent chain armed the recorder; the
|
|
298
|
+
* parent-side judgment (the content-origin wrap) reads it off this card — `clean` retires the
|
|
299
|
+
* static face verdict for this call, `external` marks with the observed-event reason, `unknown`
|
|
300
|
+
* (or absence) keeps the static floor. Deliberate whitelist admission (see the boundary note). */
|
|
301
|
+
attestation?: import("../core/memory-engine/delegation-provenance.js").DelegationAttestation;
|
|
296
302
|
}): Record<string, unknown>;
|
|
297
303
|
/**
|
|
298
304
|
* ruled 2026-08-04 — the DELEGATION-REPORT vocabulary for a child that durably paused at an approval
|
package/dist/agents/subagent.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
import { isAbsolute } from "node:path";
|
|
3
3
|
import { withDelegationProvenance } from "../core/tool-policy.js";
|
|
4
|
+
import { isHighSurrogate, isLowSurrogate } from "../core/surrogate-safe-slice.js";
|
|
5
|
+
import { newDelegationProvenanceAggregate, reduceDelegationAttestation } from "../core/memory-engine/delegation-provenance.js";
|
|
4
6
|
import { resolveModel, resolveModelDisplayLabel } from "../core/roles.js";
|
|
5
7
|
import { OUTPUT_TOOL_NAME, REPORT_BLOCKED_TOOL_NAME } from "../core/runner/synthetic-tools.js";
|
|
6
8
|
import { TOOL_SEARCH_NAME } from "../core/runner/tool-disclosure.js";
|
|
@@ -64,8 +66,8 @@ function defuseGapMarkers(slice) {
|
|
|
64
66
|
return slice.replace(/\[…/g, `${REVIEW_MARKER_SCAR}…`);
|
|
65
67
|
}
|
|
66
68
|
function surrogateSafeSlice(text, from, width, anchorEnd = false) {
|
|
67
|
-
const isLow = (i) =>
|
|
68
|
-
const isHigh = (i) =>
|
|
69
|
+
const isLow = (i) => isLowSurrogate(text.charCodeAt(i));
|
|
70
|
+
const isHigh = (i) => isHighSurrogate(text.charCodeAt(i));
|
|
69
71
|
let start = from;
|
|
70
72
|
if (start > 0 && start < text.length && isLow(start) && isHigh(start - 1))
|
|
71
73
|
start--;
|
|
@@ -311,6 +313,13 @@ const ccElapsedTag = (ms) => {
|
|
|
311
313
|
return sec >= 60 ? ` \u00b7 ${Math.floor(sec / 60)}m${sec % 60}s` : ` \u00b7 ${sec}s`;
|
|
312
314
|
};
|
|
313
315
|
const ccCompletionText = (desc, settled, rawStatus, elapsedMs) => `Agent "${desc}" ${settled === "killed" ? "stopped" : settled === "completed" ? "finished" : rawStatus}${ccElapsedTag(elapsedMs)}`;
|
|
316
|
+
function resumabilityClaim(f) {
|
|
317
|
+
if (f.status === "killed" && f.stoppedBy === "user")
|
|
318
|
+
return false;
|
|
319
|
+
if (f.liveRetainPresent)
|
|
320
|
+
return f.liveRetainHasBudget;
|
|
321
|
+
return f.durableRow && f.named;
|
|
322
|
+
}
|
|
314
323
|
const BG_AGENT_COLLATERAL_REAP_REASON = "its parent run ended";
|
|
315
324
|
const RESERVED_AGENT_NAMES = new Set([OUTPUT_TOOL_NAME, TOOL_SEARCH_NAME, OFFLOAD_TOOL_NAME, REPORT_BLOCKED_TOOL_NAME, DEFAULT_SUBAGENT_TOOL_NAME]);
|
|
316
325
|
export const REPORT_FIELD_MAX = 300;
|
|
@@ -341,6 +350,7 @@ export function classifySubagentError(child) {
|
|
|
341
350
|
export function completedAgentCard(child, extras) {
|
|
342
351
|
return {
|
|
343
352
|
type: "agent",
|
|
353
|
+
...(extras.attestation !== undefined ? { attestation: extras.attestation } : {}),
|
|
344
354
|
...(extras.subagentType !== undefined ? { subagent_type: extras.subagentType } : {}),
|
|
345
355
|
status: child.status,
|
|
346
356
|
taskId: child.taskId,
|
|
@@ -522,7 +532,9 @@ export function createSubagentResume(deps) {
|
|
|
522
532
|
throw configError(revived.reason === "still_running"
|
|
523
533
|
?
|
|
524
534
|
"resume unavailable: the agent's registry row is not available for a resume right now (it is running, or another revival claimed it) — wait for its completion notification and send again."
|
|
525
|
-
:
|
|
535
|
+
: revived.reason === "recycling"
|
|
536
|
+
? "resume unavailable: the agent's registry row is being adjudicated right now (a revival claim or a reap sweep holds it) — this clears on its own; send again in a moment."
|
|
537
|
+
: "resume unavailable: the agent's registry row no longer exists (terminal GC) — relaunch a new agent instead.", revived.reason === "still_running" ? "steering.still_running" : revived.reason === "recycling" ? "resume.row_recycling" : "resume.row_gone");
|
|
526
538
|
}
|
|
527
539
|
reviveCycle = revived.cycle;
|
|
528
540
|
if (deps.bgSink !== undefined) {
|
|
@@ -682,6 +694,16 @@ export function createSubagentResume(deps) {
|
|
|
682
694
|
const reviveTerminalSummary = failReasonRevive !== undefined
|
|
683
695
|
? `Agent "${reviveName}" (resumed) failed: ${failReasonRevive}${ccElapsedTag(Date.now() - reviveStartedAt)}`.slice(0, 300) + errorKindClause(errClassRevive)
|
|
684
696
|
: `Agent "${reviveName}" (resumed) ${status === "killed" ? "stopped" : child.status === "completed" ? "finished" : String(child.status)}${ccElapsedTag(Date.now() - reviveStartedAt)}`;
|
|
697
|
+
const reviveDurableProbe = deps.registry !== undefined && deps.taskId !== undefined ? deps.registry.durableAgentRowProbe(deps.taskId) : undefined;
|
|
698
|
+
const reviveDurableRow = reviveDurableProbe !== undefined ? await reviveDurableProbe().catch(() => false) : false;
|
|
699
|
+
const resumableRevive = resumabilityClaim({
|
|
700
|
+
status,
|
|
701
|
+
...(stoppedByRevive !== undefined ? { stoppedBy: stoppedByRevive } : {}),
|
|
702
|
+
liveRetainPresent: ledger.get(deps.parentToolCallId) === entry,
|
|
703
|
+
liveRetainHasBudget: entry.resumeCount < SUBAGENT_RESUME_CAP,
|
|
704
|
+
durableRow: reviveDurableRow,
|
|
705
|
+
named: deps.rowName !== undefined,
|
|
706
|
+
});
|
|
685
707
|
const ownsTerminalFacesRevive = deps.registry !== undefined && deps.taskId !== undefined ? deps.registry.claimAgentTerminalNotify(deps.taskId) : true;
|
|
686
708
|
if (ownsTerminalFacesRevive)
|
|
687
709
|
reviveEmit?.({
|
|
@@ -696,7 +718,7 @@ export function createSubagentResume(deps) {
|
|
|
696
718
|
...(stoppedByRevive !== undefined ? { stoppedBy: stoppedByRevive } : {}),
|
|
697
719
|
summary: reviveTerminalSummary,
|
|
698
720
|
...resumeResidual(),
|
|
699
|
-
resumable:
|
|
721
|
+
resumable: resumableRevive,
|
|
700
722
|
usage: { tokens: child.stats.tokens, turns: child.stats.turns, ...delegatedCostField(child.stats) },
|
|
701
723
|
...(completionIdRevive !== undefined ? { completionId: completionIdRevive } : {}),
|
|
702
724
|
});
|
|
@@ -714,7 +736,7 @@ export function createSubagentResume(deps) {
|
|
|
714
736
|
...(resumeHandbackResult ? { result: notifyResultField(resumeHandbackResult) } : {}),
|
|
715
737
|
...(status === "killed" && child.result ? { partial: true } : {}),
|
|
716
738
|
...resumeResidual(),
|
|
717
|
-
resumable:
|
|
739
|
+
resumable: resumableRevive,
|
|
718
740
|
usage: { tokens: child.stats.tokens, turns: child.stats.turns, ...delegatedCostField(child.stats) },
|
|
719
741
|
...(completionIdRevive !== undefined ? { completionId: completionIdRevive } : {}),
|
|
720
742
|
};
|
|
@@ -1589,8 +1611,12 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1589
1611
|
return { ...(recentSteps ? { recentSteps } : {}), ...(editedFiles ? { editedFiles } : {}) };
|
|
1590
1612
|
};
|
|
1591
1613
|
const treeScope = reviveClaim?.row.scope ?? ctx.principal ?? opts.background?.scope;
|
|
1614
|
+
const provenanceRequest = ctx.delegationProvenanceForChildren?.();
|
|
1615
|
+
const childProvenanceRef = provenanceRequest !== undefined ? { current: newDelegationProvenanceAggregate() } : undefined;
|
|
1616
|
+
const childAttestation = (status) => childProvenanceRef !== undefined ? reduceDelegationAttestation(childProvenanceRef.current, { completed: status === "completed" }) : undefined;
|
|
1592
1617
|
const childInternals = {
|
|
1593
1618
|
...(inheritedManifestScope ? { inheritedManifestScope } : {}),
|
|
1619
|
+
...(childProvenanceRef !== undefined && provenanceRequest !== undefined ? { delegationProvenance: { ref: childProvenanceRef, contentSafety: provenanceRequest } } : {}),
|
|
1594
1620
|
...(treeScope !== undefined ? { registryScope: treeScope } : {}),
|
|
1595
1621
|
...(ctx.inheritedGateForChildren ? { inheritedGate: ctx.inheritedGateForChildren() } : {}),
|
|
1596
1622
|
ownOrgAdmissionRef: { current: reviveClaim !== undefined ? readDurableOrgAdmission(reviveClaim.row) : undefined },
|
|
@@ -2274,6 +2300,10 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2274
2300
|
details: completedAgentCard(forkChild, {
|
|
2275
2301
|
subagentType: FORK_SUBAGENT_TYPE,
|
|
2276
2302
|
error: forkErr,
|
|
2303
|
+
...(() => {
|
|
2304
|
+
const a = childAttestation(forkChild.status);
|
|
2305
|
+
return a !== undefined ? { attestation: a } : {};
|
|
2306
|
+
})(),
|
|
2277
2307
|
...(forkHandbackWarning !== undefined ? { handbackWarning: forkHandbackWarning } : {}),
|
|
2278
2308
|
...(worktreeKeptPath !== undefined ? { worktreePath: worktreeKeptPath } : {}),
|
|
2279
2309
|
...(() => {
|
|
@@ -2795,7 +2825,15 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2795
2825
|
const completionIdBg = bg.registry.getCompletionId(taskId);
|
|
2796
2826
|
const observerNote = observerNoteFor(await startBoundedObserverDrain());
|
|
2797
2827
|
const residual = residualFields();
|
|
2798
|
-
const
|
|
2828
|
+
const bgDurableRow = await bgRowConfirmed();
|
|
2829
|
+
const resumableBg = resumabilityClaim({
|
|
2830
|
+
status: settled,
|
|
2831
|
+
...(stoppedBy !== undefined ? { stoppedBy } : {}),
|
|
2832
|
+
liveRetainPresent: settled !== "killed" && bgRetain !== undefined && bgRetainLedger?.get(ctx.toolCallId) === bgRetain,
|
|
2833
|
+
liveRetainHasBudget: bgRetain !== undefined && bgRetain.resumeCount < SUBAGENT_RESUME_CAP,
|
|
2834
|
+
durableRow: bgDurableRow,
|
|
2835
|
+
named: agentName !== undefined,
|
|
2836
|
+
});
|
|
2799
2837
|
const failReasonBg = settled === "failed" ? unparkedPauseReason ?? child.errorMessage : undefined;
|
|
2800
2838
|
const bgTerminalSummary = failReasonBg !== undefined
|
|
2801
2839
|
? `Agent "${shortDesc}" failed: ${failReasonBg}${ccElapsedTag(Date.now() - bgStartedAt)}${observerNote}`.slice(0, 300) + errorKindClause(errClassBg)
|
|
@@ -3106,6 +3144,10 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
3106
3144
|
details: completedAgentCard(child, {
|
|
3107
3145
|
...(def !== undefined ? { subagentType: def.name } : {}),
|
|
3108
3146
|
error: errClass,
|
|
3147
|
+
...(() => {
|
|
3148
|
+
const a = childAttestation(child.status);
|
|
3149
|
+
return a !== undefined ? { attestation: a } : {};
|
|
3150
|
+
})(),
|
|
3109
3151
|
...(handbackWarning !== undefined ? { handbackWarning } : {}),
|
|
3110
3152
|
...(modelFallback !== undefined ? { modelFallback } : {}),
|
|
3111
3153
|
...(worktreeKeptPath !== undefined ? { worktreePath: worktreeKeptPath } : {}),
|
package/dist/brain/errors.d.ts
CHANGED
|
@@ -45,6 +45,26 @@ export declare function extractErrorCode(errorMessage: string | undefined): Brai
|
|
|
45
45
|
* human text. Pairs with {@link extractErrorCode} so a caller can lift the code and clean the message
|
|
46
46
|
* without re-hardcoding the code list (the Runner does both when assembling `TaskResult`). */
|
|
47
47
|
export declare function stripErrorCodePrefix(errorMessage: string): string;
|
|
48
|
+
/**
|
|
49
|
+
* Whether a transport-level connect failure is DETERMINISTIC (see {@link DETERMINISTIC_CONNECT_CODES})
|
|
50
|
+
* or gives no such verdict. Classification is by `code` along the error graph — never by message text,
|
|
51
|
+
* which providers and runtimes reword freely.
|
|
52
|
+
*
|
|
53
|
+
* The graph, not just the chain: undici nests the actionable error under `cause`, and a dual-stack
|
|
54
|
+
* connect nests one `AggregateError` whose `errors[]` hold the per-family results. A code outside the
|
|
55
|
+
* set ANYWHERE in the graph returns `"indeterminate"`, and every AGGREGATE LEG must reach a
|
|
56
|
+
* deterministic verdict of its own — so a hostname whose IPv4 leg is refused while its IPv6 leg timed
|
|
57
|
+
* out, or whose second leg says nothing this classifier can read, keeps the full ladder. Position
|
|
58
|
+
* decides how silence is read: a WRAPPER with no code (the `TypeError("fetch failed")` shell, an
|
|
59
|
+
* `AbortSignal.timeout` DOMException whose `code` is a number) is structural and defers to what it
|
|
60
|
+
* wraps, while a LEG with nothing readable is an outcome nobody accounted for and blocks the verdict.
|
|
61
|
+
*
|
|
62
|
+
* Fail-safe by construction — every uncertain outcome (empty aggregate, an unreadable leg, a graph
|
|
63
|
+
* larger than the node budget, no deterministic code found at all) answers `"indeterminate"`, i.e. the
|
|
64
|
+
* pre-existing behavior.
|
|
65
|
+
*/
|
|
66
|
+
export type ConnectFailureClass = "deterministic" | "indeterminate";
|
|
67
|
+
export declare function classifyConnectFailure(e: unknown): ConnectFailureClass;
|
|
48
68
|
/** RB-386③ ([2090]) — render a transport-level error WITHOUT folding away its cause chain. undici's
|
|
49
69
|
* `fetch` rejects with `TypeError("fetch failed", { cause })` where the CAUSE carries the actually
|
|
50
70
|
* actionable fact (`connect ECONNREFUSED …` / `getaddrinfo ENOTFOUND …` / `SocketError: other side
|
package/dist/brain/errors.js
CHANGED
|
@@ -61,6 +61,46 @@ function hasStaleConnectionCode(e) {
|
|
|
61
61
|
}
|
|
62
62
|
return false;
|
|
63
63
|
}
|
|
64
|
+
const DETERMINISTIC_CONNECT_CODES = new Set(["ECONNREFUSED", "ENOTFOUND"]);
|
|
65
|
+
const CONNECT_GRAPH_NODE_BUDGET = 16;
|
|
66
|
+
function walkConnectFailure(node, budget) {
|
|
67
|
+
if (budget.left <= 0)
|
|
68
|
+
return "indeterminate";
|
|
69
|
+
budget.left--;
|
|
70
|
+
if (node === undefined || node === null)
|
|
71
|
+
return "silent";
|
|
72
|
+
let verdict = "silent";
|
|
73
|
+
const code = node.code;
|
|
74
|
+
if (typeof code === "string") {
|
|
75
|
+
if (!DETERMINISTIC_CONNECT_CODES.has(code))
|
|
76
|
+
return "indeterminate";
|
|
77
|
+
verdict = "deterministic";
|
|
78
|
+
}
|
|
79
|
+
if (!(node instanceof Error))
|
|
80
|
+
return verdict;
|
|
81
|
+
const members = node.errors;
|
|
82
|
+
if (Array.isArray(members)) {
|
|
83
|
+
if (members.length === 0)
|
|
84
|
+
return "indeterminate";
|
|
85
|
+
for (const member of members) {
|
|
86
|
+
if (walkConnectFailure(member, budget) !== "deterministic")
|
|
87
|
+
return "indeterminate";
|
|
88
|
+
}
|
|
89
|
+
verdict = "deterministic";
|
|
90
|
+
}
|
|
91
|
+
const cause = node.cause;
|
|
92
|
+
if (cause !== undefined && cause !== null) {
|
|
93
|
+
const below = walkConnectFailure(cause, budget);
|
|
94
|
+
if (below === "indeterminate")
|
|
95
|
+
return "indeterminate";
|
|
96
|
+
if (below === "deterministic")
|
|
97
|
+
verdict = "deterministic";
|
|
98
|
+
}
|
|
99
|
+
return verdict;
|
|
100
|
+
}
|
|
101
|
+
export function classifyConnectFailure(e) {
|
|
102
|
+
return walkConnectFailure(e, { left: CONNECT_GRAPH_NODE_BUDGET }) === "deterministic" ? "deterministic" : "indeterminate";
|
|
103
|
+
}
|
|
64
104
|
export function describeNetworkError(e) {
|
|
65
105
|
const top = e instanceof Error ? e.message : String(e);
|
|
66
106
|
const parts = [];
|
package/dist/brain/retry.d.ts
CHANGED
|
@@ -46,9 +46,23 @@ export interface ProviderWaitHint {
|
|
|
46
46
|
* ask as if nothing had been truncated at all, even though the pair's honored wait is identical either
|
|
47
47
|
* way. */
|
|
48
48
|
export declare function providerWaitHint(res: Response | undefined): ProviderWaitHint | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* Ceiling on the exponential term for the SHORT lane that serves deterministic connect failures
|
|
51
|
+
* (`ECONNREFUSED` / a name with no address — see `errors.ts` `classifyConnectFailure`).
|
|
52
|
+
*
|
|
53
|
+
* That lane exists because waiting does not change the answer: the only thing that can is the peer
|
|
54
|
+
* itself coming up. So its ladder is sized to cover a service that is already mid-restart, not to
|
|
55
|
+
* out-wait an overloaded provider — which is what the 32s ceiling above is for. The two are separate
|
|
56
|
+
* constants rather than one parameter because they answer different questions, and collapsing them
|
|
57
|
+
* would make either lane's number look arbitrary.
|
|
58
|
+
*/
|
|
59
|
+
export declare const FAST_MAX_BACKOFF_MS = 1000;
|
|
49
60
|
/**
|
|
50
61
|
* Backoff (ms) for retry `attempt` (0-based) of a transient failure: an increasing capped exponential
|
|
51
62
|
* plus ≤25% additive jitter, floored by the response's provider wait hints (see the module note).
|
|
52
|
-
* `rand` is injectable for deterministic tests.
|
|
63
|
+
* `rand` is injectable for deterministic tests. `maxBackoffMs` overrides the exponential ceiling for a
|
|
64
|
+
* caller running a deliberately shorter lane; it bounds only the COMPUTED term — a provider wait hint
|
|
65
|
+
* still floors the result, because a lane choosing to be short is not a licence to ignore a server that
|
|
66
|
+
* said when to come back (and such a lane's failures carry no response to read a hint off anyway).
|
|
53
67
|
*/
|
|
54
|
-
export declare function retryBackoffMs(baseDelayMs: number, attempt: number, res?: Response, rand?: () => number): number;
|
|
68
|
+
export declare function retryBackoffMs(baseDelayMs: number, attempt: number, res?: Response, rand?: () => number, maxBackoffMs?: number): number;
|
package/dist/brain/retry.js
CHANGED
|
@@ -48,8 +48,9 @@ export function providerWaitHint(res) {
|
|
|
48
48
|
function providerWaitHintMs(res) {
|
|
49
49
|
return providerWaitHint(res)?.ms;
|
|
50
50
|
}
|
|
51
|
-
export
|
|
52
|
-
|
|
51
|
+
export const FAST_MAX_BACKOFF_MS = 1_000;
|
|
52
|
+
export function retryBackoffMs(baseDelayMs, attempt, res, rand = Math.random, maxBackoffMs = MAX_BACKOFF_MS) {
|
|
53
|
+
const exp = Math.min(maxBackoffMs, baseDelayMs * 2 ** attempt);
|
|
53
54
|
const computed = Math.round(exp + rand() * JITTER_FRACTION * exp);
|
|
54
55
|
const hint = providerWaitHintMs(res);
|
|
55
56
|
return hint === undefined ? computed : Math.max(hint, computed);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BrainStatus } from "../core/types.js";
|
|
1
|
+
import type { BrainRetryErrClass, BrainStatus } from "../core/types.js";
|
|
2
2
|
/** Run `fn` with a per-task brain-status sink in scope. ALS propagates it through the async brain calls
|
|
3
3
|
* inside `fn` (the harness's prompt → brain.stream → connect/retry loop), so {@link emitBrainStatus}
|
|
4
4
|
* reaches THIS task's sink and nothing else. */
|
|
@@ -34,10 +34,17 @@ export type BrainTelemetry = {
|
|
|
34
34
|
key: string;
|
|
35
35
|
phase: "open" | "half-open" | "closed";
|
|
36
36
|
failures: number;
|
|
37
|
-
}
|
|
37
|
+
}
|
|
38
|
+
/** A retried provider call. `errClass` is the same closed bucket the user-facing status frame carries
|
|
39
|
+
* (so an operator reading traces and a user reading a progress line agree on why); `nextDelayMs` is
|
|
40
|
+
* the backoff actually about to be slept — 0 for the immediate re-sends that take no backoff. Both
|
|
41
|
+
* optional so a caller that has no verdict to report is not forced to invent one. */
|
|
42
|
+
| {
|
|
38
43
|
kind: "retry";
|
|
39
44
|
attempt: number;
|
|
40
45
|
phase: "connect" | "midstream";
|
|
46
|
+
errClass?: BrainRetryErrClass;
|
|
47
|
+
nextDelayMs?: number;
|
|
41
48
|
} | {
|
|
42
49
|
kind: "vision_placeholder";
|
|
43
50
|
count: number;
|
|
@@ -25,6 +25,21 @@ export interface StreamEngineConfig extends BrainTimeoutConfig {
|
|
|
25
25
|
maxRetries?: number;
|
|
26
26
|
/** First-retry backoff ms; each further attempt doubles it (capped, +≤25% jitter). Default 500. */
|
|
27
27
|
retryDelayMs?: number;
|
|
28
|
+
/**
|
|
29
|
+
* Retries allowed for a DETERMINISTIC connect failure — one whose error codes say the target itself
|
|
30
|
+
* is not there (`ECONNREFUSED`, or a name with no address; see `errors.ts` `classifyConnectFailure`).
|
|
31
|
+
* Absent ⇒ {@link DEFAULT_DETERMINISTIC_CONNECT_RETRIES}.
|
|
32
|
+
*
|
|
33
|
+
* These retries exist for one case only: a peer that is mid-restart and about to accept. Waiting
|
|
34
|
+
* longer cannot change any other outcome — re-sending at an address where nothing listens gets the
|
|
35
|
+
* same refusal in milliseconds — so spending the full transient budget on it buys nothing and costs
|
|
36
|
+
* an unattended caller minutes of silence.
|
|
37
|
+
*
|
|
38
|
+
* The value is capped by {@link maxRetries} (this lane can only ever be the SHORTER of the two).
|
|
39
|
+
* Setting it AT or ABOVE `maxRetries` turns the lane off entirely: budget and backoff both revert to
|
|
40
|
+
* the shared transient ladder, which is the opt-out for a deployment that wants the old behavior.
|
|
41
|
+
*/
|
|
42
|
+
deterministicConnectRetries?: number;
|
|
28
43
|
}
|
|
29
44
|
/**
|
|
30
45
|
* Resolve the transient-failure retry budget — explicit config > `SEMA_MAX_RETRIES` env > default 10
|
|
@@ -32,6 +47,13 @@ export interface StreamEngineConfig extends BrainTimeoutConfig {
|
|
|
32
47
|
* value is ignored rather than silently meaning "zero retries"; an oversized one is clamped.
|
|
33
48
|
*/
|
|
34
49
|
export declare function resolveMaxRetries(configured: number | undefined): number;
|
|
50
|
+
/**
|
|
51
|
+
* Resolve the deterministic-connect retry budget. Same posture as {@link resolveMaxRetries}: a chosen
|
|
52
|
+
* NUMBER is honored, a non-finite or negative one falls back to the default rather than silently
|
|
53
|
+
* meaning something (a NaN here would compare false against every budget gate and disable the lane by
|
|
54
|
+
* accident, which is the sentinel-inversion shape CLS-A-8 pinned for the shared budget).
|
|
55
|
+
*/
|
|
56
|
+
export declare function resolveDeterministicConnectRetries(configured: number | undefined): number;
|
|
35
57
|
/**
|
|
36
58
|
* Is `rebuilt` still THE SAME REQUEST as `original`, differing only in the output cap and the values
|
|
37
59
|
* that follow from it (ruled 2026-08-05)?
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createAssistantMessageEventStream, } from "../internal/llm.js";
|
|
2
2
|
import { FLOOR_OUTPUT_TOKENS, parseContextOverflow, planOutputCapAdjustment } from "./context-overflow.js";
|
|
3
|
-
import { BrainError, classifyHttp, describeNetworkError } from "./errors.js";
|
|
4
|
-
import { providerWaitHint, retryBackoffMs } from "./retry.js";
|
|
3
|
+
import { BrainError, classifyConnectFailure, classifyHttp, describeNetworkError } from "./errors.js";
|
|
4
|
+
import { FAST_MAX_BACKOFF_MS, providerWaitHint, retryBackoffMs } from "./retry.js";
|
|
5
5
|
import { emitBrainStatus, emitBrainTelemetry } from "./status-sink.js";
|
|
6
6
|
import { createConnectController, resolveStallTimeoutMs } from "./timeout.js";
|
|
7
7
|
const DEFAULT_MAX_RETRIES = 10;
|
|
@@ -18,6 +18,12 @@ export function resolveMaxRetries(configured) {
|
|
|
18
18
|
}
|
|
19
19
|
return DEFAULT_MAX_RETRIES;
|
|
20
20
|
}
|
|
21
|
+
const DEFAULT_DETERMINISTIC_CONNECT_RETRIES = 2;
|
|
22
|
+
export function resolveDeterministicConnectRetries(configured) {
|
|
23
|
+
if (configured !== undefined && Number.isFinite(configured) && configured >= 0)
|
|
24
|
+
return Math.floor(configured);
|
|
25
|
+
return DEFAULT_DETERMINISTIC_CONNECT_RETRIES;
|
|
26
|
+
}
|
|
21
27
|
const SHOULD_RETRY_HEADER = "x-should-retry";
|
|
22
28
|
function sameHeaders(a, b) {
|
|
23
29
|
const aKeys = Object.keys(a);
|
|
@@ -272,6 +278,8 @@ export function runStreamingBrain(args) {
|
|
|
272
278
|
let maxOutputTokensOverride;
|
|
273
279
|
const maxRetries = resolveMaxRetries(config.maxRetries);
|
|
274
280
|
const baseDelay = config.retryDelayMs ?? 500;
|
|
281
|
+
const deterministicConnectBudget = Math.min(resolveDeterministicConnectRetries(config.deterministicConnectRetries), maxRetries);
|
|
282
|
+
const deterministicLaneActive = deterministicConnectBudget < maxRetries;
|
|
275
283
|
const firstTokenTimeoutMs = resolveStallTimeoutMs(config.firstTokenTimeoutMs ?? stallTimeouts?.firstTokenMs, "firstTokenTimeoutMs");
|
|
276
284
|
const idleTimeoutMs = resolveStallTimeoutMs(config.idleTimeoutMs ?? stallTimeouts?.idleMs, "idleTimeoutMs");
|
|
277
285
|
const connectTimeoutMs = resolveStallTimeoutMs(config.connectTimeoutMs ?? stallTimeouts?.connectMs, "connectTimeoutMs");
|
|
@@ -342,13 +350,14 @@ export function runStreamingBrain(args) {
|
|
|
342
350
|
maxOutputTokensOverride = plan.maxOutputTokens;
|
|
343
351
|
req = rebuilt;
|
|
344
352
|
req.onCommitted?.();
|
|
345
|
-
emitBrainTelemetry({ kind: "retry", attempt: attempt + 1, phase: "connect" });
|
|
353
|
+
emitBrainTelemetry({ kind: "retry", attempt: attempt + 1, phase: "connect", errClass: "output_cap", nextDelayMs: 0 });
|
|
346
354
|
announcedRetry = true;
|
|
347
355
|
emitBrainStatus({
|
|
348
356
|
phase: "retrying",
|
|
349
357
|
detail: `output limit lowered from ${previousCap} to ${plan.maxOutputTokens} tokens, retrying`,
|
|
350
358
|
attempt: attempt + 1,
|
|
351
359
|
maxRetries,
|
|
360
|
+
errClass: "output_cap",
|
|
352
361
|
});
|
|
353
362
|
continue;
|
|
354
363
|
}
|
|
@@ -370,14 +379,33 @@ export function runStreamingBrain(args) {
|
|
|
370
379
|
const retryableByStatus = netErr !== undefined ||
|
|
371
380
|
(r !== undefined && (r.status >= 500 || r.status === 429 || r.status === 408 || r.status === 409));
|
|
372
381
|
const retryable = shouldRetryHeaderVerdict(r) ?? retryableByStatus;
|
|
373
|
-
|
|
374
|
-
|
|
382
|
+
const deterministicConnect = netErr !== undefined && classifyConnectFailure(netErr) === "deterministic";
|
|
383
|
+
const fastLane = deterministicConnect && deterministicLaneActive;
|
|
384
|
+
const laneMaxRetries = fastLane ? deterministicConnectBudget : maxRetries;
|
|
385
|
+
const errClass = netErr !== undefined
|
|
386
|
+
? deterministicConnect
|
|
387
|
+
? "connect_refused"
|
|
388
|
+
: "transport"
|
|
389
|
+
: r?.status === 429
|
|
390
|
+
? "rate_limit"
|
|
391
|
+
: r !== undefined && (r.status >= 500 || r.status === 408 || r.status === 409)
|
|
392
|
+
? "server"
|
|
393
|
+
: "http";
|
|
394
|
+
if (retryable && attempt < laneMaxRetries) {
|
|
395
|
+
const delayMs = fastLane
|
|
396
|
+
? retryBackoffMs(baseDelay, attempt, r, Math.random, FAST_MAX_BACKOFF_MS)
|
|
397
|
+
: retryBackoffMs(baseDelay, attempt, r);
|
|
375
398
|
const waitHint = providerWaitHint(r);
|
|
376
399
|
const truncationNote = waitHint?.rawMs !== undefined
|
|
377
400
|
? ` (the requested wait was longer than the cap; honoring ${Math.ceil(waitHint.ms / 1000)}s instead of ${Math.ceil(waitHint.rawMs / 1000)}s)`
|
|
378
401
|
: "";
|
|
379
402
|
const statusPhase = r?.status === 429 ? "rate_limited" : netErr !== undefined ? "reconnecting" : "retrying";
|
|
380
|
-
|
|
403
|
+
const reconnectDetail = errClass !== "connect_refused"
|
|
404
|
+
? "connection lost, reconnecting"
|
|
405
|
+
: fastLane
|
|
406
|
+
? "no endpoint answering, retrying briefly"
|
|
407
|
+
: "no endpoint answering, retrying";
|
|
408
|
+
emitBrainTelemetry({ kind: "retry", attempt: attempt + 1, phase: "connect", errClass, nextDelayMs: delayMs });
|
|
381
409
|
announcedRetry = true;
|
|
382
410
|
discardResponseBody(r);
|
|
383
411
|
cc.abort();
|
|
@@ -387,12 +415,13 @@ export function runStreamingBrain(args) {
|
|
|
387
415
|
detail: statusPhase === "rate_limited"
|
|
388
416
|
? `rate limited, backing off${truncationNote}`
|
|
389
417
|
: statusPhase === "reconnecting"
|
|
390
|
-
?
|
|
418
|
+
? reconnectDetail
|
|
391
419
|
: `transient error, retrying${truncationNote}`,
|
|
392
420
|
retryInSec: Math.ceil(remainingMs / 1000),
|
|
393
421
|
retryInMs: remainingMs,
|
|
394
422
|
attempt: attempt + 1,
|
|
395
|
-
maxRetries,
|
|
423
|
+
maxRetries: laneMaxRetries,
|
|
424
|
+
errClass,
|
|
396
425
|
}));
|
|
397
426
|
continue;
|
|
398
427
|
}
|
|
@@ -561,7 +590,7 @@ export function runStreamingBrain(args) {
|
|
|
561
590
|
thinkingRetries++;
|
|
562
591
|
parser.sealForRetry();
|
|
563
592
|
const delayMs = 100 * thinkingRetries;
|
|
564
|
-
emitBrainTelemetry({ kind: "retry", attempt: thinkingRetries, phase: "midstream" });
|
|
593
|
+
emitBrainTelemetry({ kind: "retry", attempt: thinkingRetries, phase: "midstream", errClass: "transport", nextDelayMs: delayMs });
|
|
565
594
|
announcedRetry = true;
|
|
566
595
|
await sleepAnnouncingRetry(delayMs, signal, (remainingMs) => ({
|
|
567
596
|
phase: "reconnecting",
|
|
@@ -570,6 +599,7 @@ export function runStreamingBrain(args) {
|
|
|
570
599
|
retryInMs: remainingMs,
|
|
571
600
|
attempt: thinkingRetries,
|
|
572
601
|
maxRetries: THINKING_RETRY_BUDGET,
|
|
602
|
+
errClass: "transport",
|
|
573
603
|
}));
|
|
574
604
|
continue;
|
|
575
605
|
}
|
|
@@ -577,7 +607,7 @@ export function runStreamingBrain(args) {
|
|
|
577
607
|
throw failure.err;
|
|
578
608
|
const delayMs = retryBackoffMs(baseDelay, attempt);
|
|
579
609
|
attempt++;
|
|
580
|
-
emitBrainTelemetry({ kind: "retry", attempt, phase: "midstream" });
|
|
610
|
+
emitBrainTelemetry({ kind: "retry", attempt, phase: "midstream", errClass: "transport", nextDelayMs: delayMs });
|
|
581
611
|
announcedRetry = true;
|
|
582
612
|
await sleepAnnouncingRetry(delayMs, signal, (remainingMs) => ({
|
|
583
613
|
phase: "reconnecting",
|
|
@@ -586,6 +616,7 @@ export function runStreamingBrain(args) {
|
|
|
586
616
|
retryInMs: remainingMs,
|
|
587
617
|
attempt,
|
|
588
618
|
maxRetries,
|
|
619
|
+
errClass: "transport",
|
|
589
620
|
}));
|
|
590
621
|
}
|
|
591
622
|
}
|