@sema-agent/core 5.8.0 → 5.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/agents/cascade.js +24 -0
  3. package/dist/agents/subagent.d.ts +3 -0
  4. package/dist/agents/subagent.js +64 -14
  5. package/dist/brain/open-responses.d.ts +11 -0
  6. package/dist/brain/open-responses.js +721 -0
  7. package/dist/brain/request-params.d.ts +1 -0
  8. package/dist/brain/request-params.js +16 -0
  9. package/dist/core/fs-write-gate-policy.js +2 -2
  10. package/dist/core/lsp-diagnostics.d.ts +3 -2
  11. package/dist/core/lsp-diagnostics.js +20 -7
  12. package/dist/core/runner/assemble-result.d.ts +1 -0
  13. package/dist/core/runner/assemble-result.js +14 -7
  14. package/dist/core/runner/prepare-task.d.ts +1 -0
  15. package/dist/core/runner/prepare-task.js +7 -4
  16. package/dist/core/runner/runtask.js +28 -22
  17. package/dist/core/runner/session-rule-policy.d.ts +1 -0
  18. package/dist/core/runner/session-rule-policy.js +4 -3
  19. package/dist/core/task-registry-shared.d.ts +0 -1
  20. package/dist/core/tool-policy.d.ts +8 -0
  21. package/dist/core/tool-policy.js +11 -0
  22. package/dist/core/trace.d.ts +0 -2
  23. package/dist/core/types.d.ts +3 -0
  24. package/dist/engine/harness/agent-harness.d.ts +1 -0
  25. package/dist/engine/harness/agent-harness.js +3 -0
  26. package/dist/engine/harness/types.d.ts +1 -0
  27. package/dist/engine/llm/types.d.ts +2 -73
  28. package/dist/engine/loop/agent-loop.js +6 -6
  29. package/dist/engine/loop/types.d.ts +1 -0
  30. package/dist/engine/session/repo-utils.d.ts +1 -2
  31. package/dist/engine/session/repo-utils.js +0 -7
  32. package/dist/index.d.ts +3 -2
  33. package/dist/index.js +2 -1
  34. package/dist/internal/llm.d.ts +1 -1
  35. package/dist/orchestration/workflow.js +14 -5
  36. package/dist/tools/web.js +20 -19
  37. package/package.json +4 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,47 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.9.0 (2026-08-04)
4
+
5
+ ### Added
6
+
7
+ - **A delegated child's permission ask reaches the parent's approval channel.** The parent run's effective approver (`spec.onAsk ?? deps.onAsk`) rides the trusted tool context into every child spec (sync / background / fork lanes share the one construction point), wrapped once with frozen provenance: `AskRequest.delegation` (new, optional, readonly) carries `{ parentToolCallId, depth, agentName? }` so an approval UI can render *which* delegation is asking — the innermost (true-origin) frame wins on grandchildren. New exported type `AskDelegationProvenance`. An approver is consulted once per gated call: the single-frame dedup compares approver identity *through* the wrapper (`askApproverIdentity`), so the provenance wrapper cannot split one consultation into two approval cards. **Consumer flips**: a probe pinning "a child's ask resolves at `deps.onAsk` when the parent supplied `spec.onAsk`" now resolves at the parent's approver; the `delegation` field is additive and ignorable.
8
+ - **`tool_end.errorCode` (new, optional, additive)** — present iff `isError` is true and the harness result's `details` carried a string `code`; a machine-readable discriminator so a consumer never parses the result text. Engine vocabulary today: `"tool.not_found"` and `"gate.parked"` (an abort short-circuit poisoned this call because a durable gate parked the batch — the "Operation aborted" family; a plain user interrupt carries NO marker, pinned both ways). The abort texts are byte-identical to before — an exact-equality anchor on `"Operation aborted"` is unaffected. Frame-shape change, additive: this is the [2513]-process disclosure. Library embedders get the seam directly as `abortResultDetails` on the harness options / loop config.
9
+
10
+ - `createOpenResponsesBrain` (+ `OpenResponsesBrainConfig`) — a Brain for the Open Responses wire form (`POST /responses`: typed input/output items, an SSE stream terminated by `response.completed` / `response.incomplete` / `response.failed` with no `[DONE]` sentinel). One adapter for the whole family of endpoints that speak it, instead of one per vendor. It is **stateless by contract**: `store` / `previous_response_id` / `conversation` are never sent, cannot be injected through `Model.extraBody` (all three are reserved for this API family), and every turn replays the full transcript — this engine stays the single transcript authority. Reasoning arrives as first-class `reasoning` items and is stored as normal `thinking` blocks (item id in `thinkingSignature`), replayed on tool-call turns. Terminal events map onto the existing vocabulary — no new `errorKind` or brain-error code is minted: a `max_output_tokens` cut with no answer text is `length_empty`, a content-filter cut and a streamed refusal are `[refusal]`, a stream with no terminal event is `[stream_torn]`, and `response.failed` maps by code onto `rate_limit` / `auth` / `invalid_request` / `server`. Because the wire form offers no capability discovery and endpoints have been observed to accept-and-ignore parameters, the adapter audits the `response.created` echo against what it requested and records divergences (state held that was never asked for, a reasoning tier not honored, serialized tool calls) as a `responses_capability` diagnostic on the final message. `StreamOptions.stop` has no field in this wire form and is REFUSED rather than silently dropped. Type-surface correction from the landing review: `OpenAIResponsesCompat` now declares exactly the knobs the adapter consumes (`supportsReasoningEffort` / `reasoningEffortLevels`); its two former fields (`sendSessionIdHeader` / `supportsLongCacheRetention`) had zero consumers anywhere in the engine and are removed — type-level only, no runtime behavior existed behind them.
11
+
12
+ ### BREAKING
13
+
14
+ - **Declared-only fields removed from the public type surface (type-level only — no runtime behavior existed behind any of them).** A new gate, `gate:field-liveness`, walks every interface reachable from `src/index.ts` (directly exported, or reachable through a field of something that is) and reds on a member no production code under `src/` mentions. Its first run found 59; 34 are removed here. Setting any of them compiled, type-checked, and did nothing.
15
+ - `OpenAICompletionsCompat`: `supportsStore`, `supportsDeveloperRole`, `supportsUsageInStreaming`, `requiresToolResultName`, `requiresAssistantAfterToolResult`, `requiresThinkingAsText`, `openRouterRouting`, `vercelGatewayRouting`, `zaiToolStream`, `supportsStrictMode`, `cacheControlFormat`, `sendSessionAffinityHeaders`, `supportsLongCacheRetention`. Several documented a "Default: auto-detected from URL" that no code performs. The knobs the completions brain does read are unchanged: `supportsReasoningEffort`, `reasoningEffortLevels`, `maxTokensField`, `requiresReasoningContentOnAssistantMessages`, `thinkingFormat`.
16
+ - `AnthropicMessagesCompat`: `supportsEagerToolInputStreaming`, `supportsLongCacheRetention`, `sendSessionAffinityHeaders`, `supportsCacheControlOnTools`. The Anthropic brain performs each of those behaviours unconditionally or not at all, so declaring the opposite changed nothing. `thinkingMode`, `effortLevels`, `supportsTemperature`, `contextManagement`, `interleavedThinking` are unchanged.
17
+ - `OpenRouterRouting` and `VercelGatewayRouting` are removed entirely: both existed only as the types of the two removed routing fields, so nothing they described (fallbacks, ZDR, quantizations, price ceilings, throughput/latency floors, provider order) was ever sent on any request.
18
+ - `TextContent.textSignature` and its payload type `TextSignatureV1` — no brain wrote it and no replay path read it. The live equivalent is `ThinkingContent.thinkingSignature`.
19
+ - `ToolCall.thoughtSignature` — Google-specific, and there is no Google brain in this tree.
20
+ - `Model.mediaInput` — claimed to carry provider media limits "used by attachment preprocessing"; the image pipeline's caps come from `RunnerDeps.mediaByteCapBytes` and the resizer seam.
21
+ - `TraceEvent` (`kind: "brain.call"`): `callCap` and `capThinkingSkipped`, orphans of the design/130 per-call deadline shrink retired in 5.8.0 — the emitter went with the mechanism, the declaration stayed. `stopReason` is unchanged (its JSDoc no longer instructs consumers to judge cap binding by a field that is never emitted).
22
+ - `SemaTaskHandle.outputOffset` — never written by the registry and never read by anything.
23
+ - Migration: setting any removed field is now a compile error at the assignment. There is no runtime change to migrate — the behaviour a consumer sees is exactly what it saw before, because none of these fields ever reached code. The remaining 25 declared-only members are registered with a reason and an owner in `test/field-liveness-allowlist.json` (deployment-implemented adapter contracts, foreign on-disk formats, nominal brand phantoms, and the published recall seam); two of them carry a `PENDING VERDICT` marker with a review date rather than a clearance.
24
+ - `LspDiagnosticsRegistry` is DEPLOYMENT-scoped (one per LSP manager, shared by every task), but its delivered-set had no run boundary: a diagnostic injected into one run was filtered out of every later run's drain for the lifetime of the process — a second session could not see it at all unless it happened to edit that file — and the set grew without bound. The delivered set is now keyed by run: `drain(runIdent)` and `fileEdited(runIdent, uri)` take the run key (both signatures changed), and the new `releaseRun(runIdent)` drops a finished run's keys. The engine passes the session id and releases at the run's terminal; direct callers of the registry must pass a key of their own.
25
+
26
+ ### Changed
27
+
28
+ - Session permission rules: an `allowDirs` confinement now PATH-CONFINES `NotebookEdit` instead of denying it outright. Previously `createSessionRulePolicy` only knew the `file_path`-keyed write tools, so while `allowDirs` was active every `NotebookEdit` call fell into the "mutating but not path-confinable" arm and was denied — even for a notebook inside an allowed directory — while `createFsWriteGatePolicy` had always confined the same tool by its `notebook_path`. Both faces now share one covered set and one target extractor, so a notebook inside `allowDirs` is allowed, one outside is denied, and a decoy `file_path` never outranks the real `notebook_path` target. **A probe pinning "NotebookEdit is denied under `allowDirs`" will now flip** — re-pin it as a path verdict (in-dir allow / out-of-dir deny). Deliberately unchanged: every other mutating tool that cannot be path-confined (bash, deployment-authored write tools, tools with an unknown effect) is still fail-closed denied under `allowDirs`, and the skill-manifest `allowPaths` face still denies `NotebookEdit` — widening that one is a separate decision.
29
+
30
+ ### Changed
31
+
32
+ - **A subagent's durable pause is reported as what it is.** The sync and fork report arms minted `status: failed` with "treat this delegation as failed; do not retry" when the child parked at an approval gate — a fabrication that buried the recovery path. They now mint one honest constructor: `status: suspended`, the checkpoint named, the deployment's approval channel named as the only release path, anti-retry guidance kept, `isError: true` now explicit on the report. `details.error` vocabulary: `unexpected.suspended` → `suspended.awaiting_approval`, `unexpected.needs_review` → `suspended.needs_review` (the TaskResult-level `unexpected.*` codes elsewhere are unchanged). The ineligible-background settle keeps its mechanism byte-for-byte but its row/summary/notification faces now say which disposition happened: new `errorCode` values `suspended.awaiting_approval` (checkpoint still pending, resolvable out-of-band) and `suspended.checkpoint_expired` (destroyed by the no-orphans rule — stop waiting). **Consumer flips**: probes pinning the old `status: failed` wording or the `unexpected.*` codes on delegation reports will red; anything pinning a closed set of background-failure `errorCode`s must admit the two new members.
33
+ - Resumed-batch `tool_end` frames (durable-approval resume) carry the result body — `output` / `truncated` / `totalChars` / `structured` from the same projection the live loop uses; the deferred-sibling closes carry the `[DEFERRED]` body the transcript records. A client rendering tool output from frames no longer shows an empty card for every approved call ([2513] W2; zero shape change — these are the frame's existing optional fields, previously absent). The same frames now also carry `eventId` (+ `parentToolCallId`/`sourceTaskId` when running as a subagent) and reach a parent's `forwardSubagentEvents` pane — a child's durable-approved call no longer leaves the parent's viewing pane spinner open forever.
34
+
35
+ ### Fixed
36
+
37
+ - Workflow resilience knobs (`stallMs` / `agentMaxRetries` / `throttleBackoffMs` / `totalTimeoutMs`) refuse non-finite values loudly instead of silently changing behavior — a NaN used to disarm the stall watchdog, zero the retry budget, or turn the total deadline into an instant abort (`stallMs <= 0` stays the documented explicit-off). `WebFetchConfig.maxBytes` likewise refuses non-finite/non-positive values (`config.web_max_bytes_invalid`) — NaN/Infinity used to disable the byte budget entirely, streaming unbounded into memory (±Infinity gets its own message: the cap cannot be turned off, only widened).
38
+ - The WebFetch binary sniffer consumes the shared magic-byte table (`BINARY_MAGIC_SIGNATURES`) instead of a private eight-format if-chain — one data source; edge behavior tightens slightly (full-length signatures) and labels adopt the table's human-readable names.
39
+ - Platform terminals (`env.lifetime_expired` / `usage.window_exhausted`) now carry `salvagedOutput` like every other salvage-eligible terminal (the eligibility set is one closed construction point) — **a consumer pinning "salvagedOutput is empty on every non-completed result" will flip**. `TaskResult.retryAfterMs` (new, optional) rides a `usage.window_exhausted` terminal with the window's reopen hint; presence condition documented on the field — judge the cause by `errorCode`, never by this field's presence.
40
+ - `assertWorkflowDeterminism` reaches the package root alongside its two conformance siblings (the determinism battery an out-of-repo workflow runner needs; additive).
41
+
42
+
43
+ - `runCascade` no longer reaches its tail with nothing dispatched. A non-finite `config.maxEscalations` is refused at the door (`Error.code = "config.cascade_invalid"`) instead of making the iteration bound `NaN` and running zero rungs; a `totalTimeoutMs` already spent at entry returns the cascade's own terminal (`status: "failed"`, `errorCode: "cascade.budget_exhausted"`, empty `attempts`, `finalRung: -1`) instead of throwing a bare `TypeError`. `CascadeRunResult.finalRung` can therefore be `-1` (no rung produced the result).
44
+
3
45
  ## 5.8.0 (2026-08-04)
4
46
 
5
47
  _The limits restructure: time leaves the task-limit axis, tokens become the primary budget, and every default is empty. One release, five construction stages — there is no intermediate version. Old usage fails loudly (compile errors for removed fields; a typed refusal for unknown limit keys at runtime) — nothing is silently ignored._
@@ -3,6 +3,13 @@ import { mapNestedSuspend, isDurablePause } from "./suspend-guard.js";
3
3
  import { buildCumulativeStats } from "./cumulative-stats.js";
4
4
  import { createSafeNotifier } from "../core/safe-notify.js";
5
5
  const CASCADE_ON_RUNG_SITE = "cascade.onRung";
6
+ const CASCADE_CONFIG_ERROR_CODE = "config.cascade_invalid";
7
+ const CASCADE_NO_DISPATCH_ERROR_CODE = "cascade.budget_exhausted";
8
+ function cascadeConfigError(message) {
9
+ const e = new Error(message);
10
+ e.code = CASCADE_CONFIG_ERROR_CODE;
11
+ return e;
12
+ }
6
13
  function createDefaultGate(spec) {
7
14
  const requiresStructured = spec.outputSchema != null;
8
15
  return (result) => result.status === "completed" && (!requiresStructured || result.structuredOutput !== undefined);
@@ -13,6 +20,9 @@ export async function runCascade(runner, spec, config) {
13
20
  throw new Error("runCascade: config.ladder must have at least one rung");
14
21
  }
15
22
  const maxEscalations = config.maxEscalations ?? ladder.length - 1;
23
+ if (typeof maxEscalations !== "number" || !Number.isFinite(maxEscalations)) {
24
+ throw cascadeConfigError(`runCascade: config.maxEscalations must be a finite number (got ${String(config.maxEscalations)}) — an unevaluable escalation ceiling is not a ceiling, and folding it to a default would run the cascade under a bound nobody chose.`);
25
+ }
16
26
  const maxRungs = Math.min(ladder.length, Math.max(0, maxEscalations) + 1);
17
27
  const gate = config.gate ?? createDefaultGate(spec);
18
28
  const startedAt = Date.now();
@@ -179,6 +189,20 @@ export async function runCascade(runner, spec, config) {
179
189
  break;
180
190
  }
181
191
  }
192
+ if (lastResult === undefined) {
193
+ return {
194
+ taskId: spec.taskId ?? "",
195
+ sessionId: "",
196
+ status: "failed",
197
+ result: `runCascade: the overall wall-clock budget (totalTimeoutMs ${config.totalTimeoutMs}ms) was already spent when the ladder was entered — no rung dispatched`,
198
+ errorCode: CASCADE_NO_DISPATCH_ERROR_CODE,
199
+ stats: { tokens: 0, turns: 0, costMicroUsd: 0 },
200
+ cascadeOutcome: "exhausted",
201
+ escalated: false,
202
+ finalRung: -1,
203
+ attempts,
204
+ };
205
+ }
182
206
  const base = lastResult;
183
207
  const finalRung = attempts.length - 1;
184
208
  return {
@@ -43,6 +43,9 @@ export declare function classifySubagentError(child: {
43
43
  errorKind: SubagentErrorKind;
44
44
  retryable: boolean;
45
45
  } | undefined;
46
+ export declare const SUBAGENT_SUSPENDED_AWAITING_APPROVAL = "suspended.awaiting_approval";
47
+ export declare const SUBAGENT_SUSPENDED_NEEDS_REVIEW = "suspended.needs_review";
48
+ export declare const SUBAGENT_SUSPENDED_CHECKPOINT_EXPIRED = "suspended.checkpoint_expired";
46
49
  export interface SubagentSteerHandle {
47
50
  parentToolCallId: string;
48
51
  agentName?: string;
@@ -1,5 +1,6 @@
1
1
  import { Type } from "typebox";
2
2
  import { isAbsolute } from "node:path";
3
+ import { withDelegationProvenance } from "../core/tool-policy.js";
3
4
  import { resolveModel, resolveModelDisplayLabel } from "../core/roles.js";
4
5
  import { OUTPUT_TOOL_NAME, REPORT_BLOCKED_TOOL_NAME } from "../core/runner/synthetic-tools.js";
5
6
  import { TOOL_SEARCH_NAME } from "../core/runner/tool-disclosure.js";
@@ -231,6 +232,41 @@ export function classifySubagentError(child) {
231
232
  : "logic";
232
233
  return { errorKind, retryable: errorKind !== "logic" };
233
234
  }
235
+ export const SUBAGENT_SUSPENDED_AWAITING_APPROVAL = "suspended.awaiting_approval";
236
+ export const SUBAGENT_SUSPENDED_NEEDS_REVIEW = "suspended.needs_review";
237
+ export const SUBAGENT_SUSPENDED_CHECKPOINT_EXPIRED = "suspended.checkpoint_expired";
238
+ function durablePauseDelegationReport(reportLabel, child) {
239
+ const needsReview = child.status === "needs_review";
240
+ const token = child.checkpointToken;
241
+ return {
242
+ isError: true,
243
+ content: `[Sub-agent report${reportLabel}]\n` +
244
+ `status: suspended\n` +
245
+ `This delegation is PARKED at an approval gate — it did not fail, and it is not finished. ` +
246
+ `${needsReview ? "A human review" : "An approval"} it cannot grant itself is pending` +
247
+ `${token !== undefined ? `, held by checkpoint ${token}` : " (the run reported no checkpoint token)"}. ` +
248
+ `The gated action has NOT run.\n` +
249
+ `Only the deployment's approval channel can release it: you cannot approve it from here, and no ` +
250
+ `result from it will arrive in this turn. Do not re-issue the same approval-gated action — an ` +
251
+ `identical retry parks again and resolves nothing. Carry on with work that does not depend on ` +
252
+ `this delegation, or report that it is awaiting approval.`,
253
+ details: {
254
+ error: needsReview ? SUBAGENT_SUSPENDED_NEEDS_REVIEW : SUBAGENT_SUSPENDED_AWAITING_APPROVAL,
255
+ ...(token !== undefined ? { checkpointToken: token } : {}),
256
+ },
257
+ };
258
+ }
259
+ function unparkedDurablePauseReason(d) {
260
+ return d.kind === "no_park_lane"
261
+ ? `the agent hit an approval gate and paused durably, but this delegation cannot be parked ` +
262
+ `(parking needs an explicitly named child plus a durable agent row, a checkpoint store and the ` +
263
+ `durable-session capability). Its checkpoint ${d.checkpointToken} is still committed and the ` +
264
+ `approval must be resolved through the deployment's approval channel; the gated action never ran.`
265
+ : `the agent hit an approval gate and paused durably, but the park did not take (` +
266
+ `${d.cause === "capability_veto" ? "the durable-session capability refused" : "a concurrent stop won the arbitration"}` +
267
+ `), so its checkpoint was expired under the no-orphans rule. Nothing is pending approval and the ` +
268
+ `gated action never ran — re-run this delegation with an approver attached.`;
269
+ }
234
270
  function errorKindClause(c) {
235
271
  return c !== undefined ? ` (error_kind: ${c.errorKind}, retryable: ${c.retryable})` : "";
236
272
  }
@@ -1403,6 +1439,14 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1403
1439
  ...(ctx.onSubagentSpawn ? { onSubagentSpawn: ctx.onSubagentSpawn } : {}),
1404
1440
  };
1405
1441
  const childThinking = def?.thinking ?? ctx.thinkingLevel;
1442
+ const provenanceAgentName = agentName ?? def?.name;
1443
+ const childOnAsk = ctx.onAsk !== undefined
1444
+ ? withDelegationProvenance(ctx.onAsk, {
1445
+ parentToolCallId: ctx.toolCallId,
1446
+ depth: depth + 1,
1447
+ ...(provenanceAgentName !== undefined ? { agentName: provenanceAgentName } : {}),
1448
+ })
1449
+ : undefined;
1406
1450
  const buildChildSpec = (signal) => ({
1407
1451
  objective: prompt,
1408
1452
  ...(childModel ? { model: childModel } : { modelRole: "subagent" }),
@@ -1412,6 +1456,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1412
1456
  ...(def?.memory ? { memory: def.memory } : {}),
1413
1457
  ...(def?.skills?.length ? { skills: def.skills } : {}),
1414
1458
  ...(ctx.principal !== undefined ? { principal: ctx.principal } : {}),
1459
+ ...(childOnAsk !== undefined ? { onAsk: childOnAsk } : {}),
1415
1460
  ...(ctx.clientContext !== undefined ? { clientContext: ctx.clientContext } : {}),
1416
1461
  ...(ctx.excludeTools !== undefined ? { excludeTools: [...ctx.excludeTools] } : {}),
1417
1462
  ...(ctx.deferTools !== undefined ? { deferTools: [...ctx.deferTools] } : {}),
@@ -1971,11 +2016,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1971
2016
  costMicroUsd: rollupDelegatedCost(forkChild.stats, nsf),
1972
2017
  });
1973
2018
  if (isDurablePause(forkChild.status)) {
1974
- return {
1975
- content: `[Sub-agent report · ${FORK_SUBAGENT_TYPE}]\nstatus: failed\nerror: unexpected durable pause (${forkChild.status}, checkpoint ` +
1976
- `${String(forkChild.checkpointToken ?? "?")}) — treat this fork as failed; do not retry the same gated action.`,
1977
- details: { error: forkChild.status === "needs_review" ? "unexpected.needs_review" : "unexpected.suspended", checkpointToken: forkChild.checkpointToken },
1978
- };
2019
+ return durablePauseDelegationReport(` · ${FORK_SUBAGENT_TYPE}`, forkChild);
1979
2020
  }
1980
2021
  const wtLine = await finishWorktree();
1981
2022
  const forkErr = classifySubagentError(forkChild);
@@ -2408,6 +2449,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2408
2449
  else
2409
2450
  bgRetainLedger?.markSettled(ctx.toolCallId);
2410
2451
  }
2452
+ let unparkedPause;
2411
2453
  if (isDurablePause(child.status) && child.checkpointToken !== undefined) {
2412
2454
  const cpStore = bg.checkpointStore;
2413
2455
  const expireByStoreScope = async (token2) => {
@@ -2452,14 +2494,27 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2452
2494
  if (parked === "parked")
2453
2495
  return;
2454
2496
  await expireByStoreScope(token);
2497
+ unparkedPause = { kind: "checkpoint_expired", cause: attested ? "park_lost" : "capability_veto" };
2455
2498
  }
2456
2499
  }
2500
+ else {
2501
+ unparkedPause = { kind: "no_park_lane", checkpointToken: child.checkpointToken };
2502
+ }
2457
2503
  }
2458
2504
  const ok = child.status === "completed";
2459
2505
  const reaped = !ok && abort.signal.aborted;
2460
2506
  const collateral = reaped && bg.registry.getAccessibleTask(taskId, { ...(bgOwner !== undefined ? { owner: bgOwner } : {}), ...(bgScope !== undefined ? { scope: bgScope } : {}) })?.status === "running";
2461
2507
  const failedBg = !ok && !reaped;
2462
- const errCodeBg = failedBg ? (child.errorCode ?? extractErrorCode(child.errorMessage)) : undefined;
2508
+ const unparkedPauseReason = unparkedPause !== undefined && failedBg ? unparkedDurablePauseReason(unparkedPause) : undefined;
2509
+ const errCodeBg = failedBg
2510
+ ? unparkedPause !== undefined
2511
+ ? unparkedPause.kind === "no_park_lane"
2512
+ ? child.status === "needs_review"
2513
+ ? SUBAGENT_SUSPENDED_NEEDS_REVIEW
2514
+ : SUBAGENT_SUSPENDED_AWAITING_APPROVAL
2515
+ : SUBAGENT_SUSPENDED_CHECKPOINT_EXPIRED
2516
+ : (child.errorCode ?? extractErrorCode(child.errorMessage))
2517
+ : undefined;
2463
2518
  const errClassBg = failedBg
2464
2519
  ? classifySubagentError({ status: "failed", ...(child.errorCode !== undefined ? { errorCode: child.errorCode } : {}), ...(child.errorMessage !== undefined ? { errorMessage: child.errorMessage } : {}) })
2465
2520
  : undefined;
@@ -2468,7 +2523,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2468
2523
  status: ok ? "completed" : reaped ? "killed" : "failed",
2469
2524
  seq: seqAtSettle ?? 1,
2470
2525
  ...resultSettleFields(child.result),
2471
- ...(!ok ? { error: reaped ? (collateral ? BG_AGENT_COLLATERAL_REAP_REASON : BG_AGENT_REAP_STOP_ERROR) : child.errorMessage ?? String(child.status) } : {}),
2526
+ ...(!ok ? { error: reaped ? (collateral ? BG_AGENT_COLLATERAL_REAP_REASON : BG_AGENT_REAP_STOP_ERROR) : unparkedPauseReason ?? child.errorMessage ?? String(child.status) } : {}),
2472
2527
  ...(errCodeBg !== undefined ? { errorCode: errCodeBg } : {}),
2473
2528
  ...(errClassBg !== undefined ? { retryable: errClassBg.retryable, errorKind: errClassBg.errorKind } : {}),
2474
2529
  }) ??
@@ -2486,7 +2541,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2486
2541
  const observerNote = observerNoteFor(await startBoundedObserverDrain());
2487
2542
  const residual = residualFields();
2488
2543
  const resumableBg = bgRetain !== undefined && settled !== "killed";
2489
- const failReasonBg = settled === "failed" ? child.errorMessage : undefined;
2544
+ const failReasonBg = settled === "failed" ? unparkedPauseReason ?? child.errorMessage : undefined;
2490
2545
  const bgTerminalSummary = failReasonBg !== undefined
2491
2546
  ? `Agent "${shortDesc}" failed: ${failReasonBg}${ccElapsedTag(Date.now() - bgStartedAt)}${observerNote}`.slice(0, 300) + errorKindClause(errClassBg)
2492
2547
  : `${ccCompletionText(shortDesc, settled, String(child.status), Date.now() - bgStartedAt)}${observerNote}`;
@@ -2728,12 +2783,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2728
2783
  if (isDurablePause(child.status)) {
2729
2784
  if (retainEntry)
2730
2785
  ctx.subagentRetain?.abandon(ctx.toolCallId);
2731
- return {
2732
- content: `[Sub-agent report${def ? ` · ${def.name}` : ""}]\n` +
2733
- `status: failed\nerror: unexpected durable pause (${child.status}, checkpoint ${String(child.checkpointToken ?? "?")}) — ` +
2734
- `treat this delegation as failed; do not retry with the same approval-gated action.`,
2735
- details: { error: child.status === "needs_review" ? "unexpected.needs_review" : "unexpected.suspended", checkpointToken: child.checkpointToken },
2736
- };
2786
+ return durablePauseDelegationReport(def ? ` · ${def.name}` : "", child);
2737
2787
  }
2738
2788
  let failureRetained = false;
2739
2789
  if (!retainEntry) {
@@ -0,0 +1,11 @@
1
+ import type { Brain } from "../core/types.js";
2
+ import { type StreamEngineConfig } from "./stream-engine.js";
3
+ export interface OpenResponsesBrainConfig extends StreamEngineConfig {
4
+ baseUrl?: string;
5
+ apiKey?: string;
6
+ headers?: Record<string, string>;
7
+ fetchImpl?: typeof fetch;
8
+ replayReasoning?: boolean;
9
+ detectRepetition?: boolean;
10
+ }
11
+ export declare function createOpenResponsesBrain(config?: OpenResponsesBrainConfig): Brain;