@sema-agent/core 5.6.0 → 5.8.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 (88) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/dist/agents/cascade.d.ts +1 -1
  3. package/dist/agents/subagent.d.ts +4 -7
  4. package/dist/agents/subagent.js +10 -19
  5. package/dist/agents/teacher.js +3 -3
  6. package/dist/agents/team.d.ts +1 -1
  7. package/dist/agents/team.js +1 -1
  8. package/dist/agents/verify.js +1 -1
  9. package/dist/brain/anthropic.js +24 -35
  10. package/dist/brain/openai.js +22 -33
  11. package/dist/brain/stream-engine.d.ts +0 -2
  12. package/dist/brain/stream-engine.js +5 -47
  13. package/dist/brain/stream-shared.d.ts +0 -10
  14. package/dist/brain/stream-shared.js +0 -23
  15. package/dist/brain/terminal-cause.d.ts +0 -1
  16. package/dist/brain/terminal-cause.js +0 -3
  17. package/dist/brain/timeout.d.ts +4 -2
  18. package/dist/brain/timeout.js +4 -14
  19. package/dist/config/catalog.d.ts +3 -11
  20. package/dist/config/catalog.js +30 -69
  21. package/dist/config/defaults.d.ts +0 -3
  22. package/dist/config/defaults.js +0 -3
  23. package/dist/core/auto-compaction.d.ts +2 -11
  24. package/dist/core/auto-compaction.js +3 -67
  25. package/dist/core/checkpoint-store.d.ts +15 -10
  26. package/dist/core/checkpoint-store.js +13 -10
  27. package/dist/core/memory-engine/file-backend.js +20 -17
  28. package/dist/core/memory-engine/layout.d.ts +5 -0
  29. package/dist/core/memory-engine/layout.js +72 -47
  30. package/dist/core/runner/assemble-result.d.ts +2 -8
  31. package/dist/core/runner/assemble-result.js +15 -16
  32. package/dist/core/runner/prepare-task.d.ts +26 -6
  33. package/dist/core/runner/prepare-task.js +203 -141
  34. package/dist/core/runner/runtask.d.ts +0 -2
  35. package/dist/core/runner/runtask.js +267 -253
  36. package/dist/core/secret-env.d.ts +2 -0
  37. package/dist/core/secret-env.js +16 -4
  38. package/dist/core/session-policy-store.d.ts +5 -0
  39. package/dist/core/session-policy-store.js +3 -1
  40. package/dist/core/store-contracts/checkpoint-store-contract.js +2 -2
  41. package/dist/core/task-notification.d.ts +1 -0
  42. package/dist/core/task-notification.js +3 -0
  43. package/dist/core/task-registry-agent.d.ts +2 -0
  44. package/dist/core/task-registry-agent.js +33 -1
  45. package/dist/core/task-registry.d.ts +2 -0
  46. package/dist/core/task-registry.js +9 -1
  47. package/dist/core/tool-errors.js +10 -2
  48. package/dist/core/trace.d.ts +1 -8
  49. package/dist/core/types.d.ts +24 -30
  50. package/dist/core/usage-window-store.d.ts +39 -0
  51. package/dist/core/usage-window-store.js +115 -0
  52. package/dist/engine/execution-env/node-execution-env.js +0 -22
  53. package/dist/engine/harness/agent-harness.d.ts +0 -7
  54. package/dist/engine/harness/agent-harness.js +13 -53
  55. package/dist/engine/harness/types.d.ts +2 -15
  56. package/dist/engine/llm/types.d.ts +1 -2
  57. package/dist/engine/loop/agent-loop.d.ts +1 -1
  58. package/dist/engine/loop/agent-loop.js +5 -171
  59. package/dist/engine/loop/types.d.ts +0 -18
  60. package/dist/index.d.ts +10 -5
  61. package/dist/index.js +8 -4
  62. package/dist/orchestration/goal.js +1 -5
  63. package/dist/orchestration/run-workflow-tool.d.ts +1 -1
  64. package/dist/orchestration/run-workflow-tool.js +1 -2
  65. package/dist/orchestration/workflow-governance.d.ts +6 -6
  66. package/dist/orchestration/workflow-governance.js +33 -47
  67. package/dist/prompt-assembly/event-registry.js +2 -3
  68. package/dist/stores/cc/lockfile.js +9 -11
  69. package/dist/stores/cc/mailbox-store.js +49 -38
  70. package/dist/stores/file/index.d.ts +3 -0
  71. package/dist/stores/file/index.js +4 -0
  72. package/dist/stores/file/usage-window-store.d.ts +9 -0
  73. package/dist/stores/file/usage-window-store.js +86 -0
  74. package/dist/tools/fs/fs-bash.d.ts +1 -10
  75. package/dist/tools/fs/fs-bash.js +20 -60
  76. package/dist/tools/fs/index.d.ts +0 -2
  77. package/dist/tools/fs/index.js +1 -2
  78. package/package.json +1 -4
  79. package/dist/bin/sema-tb.d.ts +0 -31
  80. package/dist/bin/sema-tb.js +0 -448
  81. package/dist/bin/tb-env.d.ts +0 -2
  82. package/dist/bin/tb-env.js +0 -17
  83. package/dist/brain/walltime.d.ts +0 -1
  84. package/dist/brain/walltime.js +0 -1
  85. package/dist/core/runner/call-cap.d.ts +0 -67
  86. package/dist/core/runner/call-cap.js +0 -145
  87. package/dist/core/runner/cut-kill.d.ts +0 -10
  88. package/dist/core/runner/cut-kill.js +0 -37
package/CHANGELOG.md CHANGED
@@ -1,5 +1,60 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.8.0 (2026-08-04)
4
+
5
+ _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._
6
+
7
+ ### BREAKING — task limits (`TaskSpec.limits` is the one budget surface)
8
+
9
+ - `limits` now holds every budget knob: `maxTokens` (primary, cumulative per-slice window — promoted from report-only to a GATE) / `maxCostUsd` / `maxTurns` / `maxWalltimeMs` (opt-in, per-slice active clock, **milliseconds**) / `maxOutputTokens` (unchanged, per-request shape) / `approachNotice` / `budgetStreamCancel` / `degrade` / `brainCallGuardrailMs`. The former top-level `maxTokens` / `maxCostUsd` / `budgetStreamCancel` / `degrade` fields are gone.
10
+ - `limits.timeoutSec` is REMOVED (unit and semantics changed → `maxWalltimeMs`). An unknown key on `limits` is refused at prepare with `config.limit_unknown_key` naming the replacement and the unit; a non-finite or negative value refuses with `config.limit_invalid`. Never silently ignored.
11
+ - EVERY default is empty: `DEFAULT_MAX_TURNS=1000`, `FORK_DEFAULT_MAX_TURNS=200`, the session-scoped background child's injected 30-minute wall clock, the resource-suspend `maxSlices=100` default, and the workflow per-agent 300s cap are all retired. No limit is armed unless configured. Anti-zombie coverage for session-scoped background children moved to the stale-running reaper (`TaskRegistry.reapStaleSessionBackgroundAgents` — collects by STALL, not by wall age). Honest consequence, stated plainly: a zero-config task with a non-terminating brain loop now has no task-level backstop — the engineering guardrails (brain-call guardrail, stall detection) keep it from wedging silently, and the deployment governance windows are the burn ceiling.
12
+ - To-limit behavior is two-tier: WITHOUT `resourceSuspend`, hitting any limit is a loud terminal failure — `TaskResult.status` loses `"timeout"`; all four axes end as `failed` with `limits.max_tokens_exceeded` / `limits.max_cost_exceeded` / `limits.max_turns_exceeded` / `limits.max_walltime_exceeded` (the code names the AXIS, replacing `limit.timeout` / `limit.max_turns` / `budget.exceeded` / `budget.precall`), salvage attached. WITH `resourceSuspend` (+ checkpoint infrastructure), a per-slice window exhaustion suspends with a checkpoint and resume grants the next window; only the optional frozen totals (`resourceSuspend.totalTokens` / `totalBudgetUsd`) are hard stops. `ResourceLimitReason` splits `"budget"` into `"budget_tokens"` / `"budget_cost"` — consumers pinning either closed set are affected and named here.
13
+ - Priority when several axes trip at one boundary: tokens > cost > turns > walltime (attribution only — every configured axis stays armed).
14
+ - `limits.approachNotice` replaces `deadlineNudge`: one "start converging" frame at ~80% and one "deliver now" at ~95% of the tightest configured budget, measured in BUDGET (tokens/cost/turns), never in clock; one-shot; `stats.mechanisms.approachNoticesSent`; a continuation gate keeps the notice from extending a run the model had already ended.
15
+
16
+ ### BREAKING — retired mechanisms
17
+
18
+ - The design/128/130 deadline family is gone: `deadlineNudge` / `callCapByDeadline` / `gracefulFinalize`, the write-out cushion mathematics, and the per-call deadline shrink.
19
+ - tool-cut is gone end to end: `stats.mechanisms.{toolCuts,toolCutKills,pendingCutTools,finalizeInjected,nudgesSent,capShrinks,callCutoffs,toolClamps}`, `engineCut:"walltime_writeout"`, the bash exec deadline clamp, and `cut-kill.ts`.
20
+ - The brain soft-cutoff is gone: `errorKind:"walltime_cutoff"`, `WALLTIME_CUTOFF_MESSAGE`, `StreamOptions.callDeadlineMs`, the loop's `walltime_cutoff_recovery`.
21
+ - auto-compaction's walltime gate / abort race / throughput EWMA are gone (`clampReason` loses `"walltime"`).
22
+ - Stall detection (connect / first-token / idle) is DECOUPLED from the retired walltime lane and now arms unconditionally (guardrail class; constants renamed `STALL_*`; knob semantics unchanged).
23
+ - The `ExecutionEnv.exec` `onSpawn` / `onDetachAdopted` seam is retired (its only consumer left with tool-cut; zero producers remained).
24
+ - The Terminal-Bench surface is retired: the `sema-tb` bin, `tb-env`, the `tb/` adapter tree, and all thirteen `TB_*` env knobs. No switch survives.
25
+
26
+ ### BREAKING — checkpoint / ledger / config
27
+
28
+ - `ResourceLedger.totalWalltimeSec` and `remainingWalltimeMs()` retired; `totalTokens` + `remainingTokens()` added; `spentWalltimeMs` REMAINS (accounting only). Checkpoint version 5 (`TOKEN_CHECKPOINT_VERSION`); a v4 checkpoint carrying the retired wall-clock allocation is refused pre-CAS with `checkpoint.walltime_axis_retired` (the checkpoint stays pending — finish it on the previous release, or start fresh; no silent migration). `resourceSuspend.totalWalltimeSec` → `resourceSuspend.totalTokens`.
29
+ - Config catalog: `limits.{timeoutSec,deadlineNudge,callCapByDeadline,gracefulFinalize}`, `budget.{maxCostUsd,maxTokens,totalWalltimeSec}`, `delegation.{forkMaxTurns,sessionBgTimeoutSec}` retired/moved; `limits.{maxWalltimeMs,approachNotice,maxTokens,maxCostUsd}`, `budget.totalTokens` added. Workflow script `limits` whitelist: `{maxTurns,timeoutSec}` → `{maxTurns,maxWalltimeMs,maxTokens,maxCostUsd}`; `WorkflowChildCaps.perAgentTimeoutSec` → `perAgentMaxWalltimeMs` (no default).
30
+ - Workflow resume journals from 5.7 cache-miss entirely on this version (the spec shape participates in the call identity) — reruns, not errors.
31
+ - Trace: the eight mechanisms fields above are gone; `timerLatenessMs` / `walltimeSyncBackstopFired` REMAIN (the per-slice wall timer survives as `maxWalltimeMs`, so its starvation evidence keeps a producer).
32
+
33
+ ### Fixed
34
+
35
+ - memory-engine: partition adoption is a single atomic rename (the delete-then-rename window could destroy a concurrently adopted partition and then disclose the opposite of what happened); fileless-scaffolding cleanup uses `rmdir` only, so a peer's files are unremovable by construction. Both control-plane lock waits are bounded and yield every round — a permanent errno can no longer starve the event loop in a silent spin (the fail-open sidecar lock now honors its documented "proceed anyway" by going lockless after three post-deadline steal rounds).
36
+ - cc stores: the mailbox sweep AND `drop` now judge and delete inside the box's cross-process lock (a compliant writer's freshly committed message can no longer be deleted unseen, and another holder's lock directory is never torn down); the cc lockfile's retry bound is read before every round (three `continue` shortcuts skipped it — a permanently failing reclaim spun forever); the lease table's entry keys fold the same way the table itself does (two store instances over one directory via a symlink could each grant the same lease).
37
+ - session policy: the tighten gate's path-containment fold is win-family-conditional, matching the runtime's — a POSIX sibling directory whose literal name embeds backslashes no longer reads as "within" the confined root (a loosening the gate exists to refuse). The transcript-dir gate's write-tool literal gains a mechanical sync pin against `PATH_WRITE_TOOLS`.
38
+ - `tool-name-aliases` docstring named a compile-time enforcement symbol that never existed; it now states the real division of labor (the prepare-time audit escalates, the compile pass deliberately does not).
39
+
40
+ ## 5.7.0 (2026-08-04)
41
+
42
+ _Three collected cars: a parked question is answered by the decision itself (behavior face — see the consumer note), a descendant's terminal notification anchors on the delegation tree, and the env scrub stops deleting infrastructure key names (behavior face)._
43
+
44
+ ### Fixed
45
+
46
+ - A descendant background agent's terminal notification now anchors on the delegation tree instead of dying with its spawner. A background agent notifies through the injector of the run that spawned it, and that injector parked on that run's OWN session once its lane was gone — so a grandchild launched from a delegated run (root → child A → background grandchild B; A completes, B settles later) parked its terminal on a throwaway child session that never runs again, and the root conversation was never told (the data was never lost — `TaskOutput` still answered; the gap was purely the push face). A delegated agent's TERMINAL frame now also escalates one hop up the delegation chain: a live ancestor takes it in its current run, a dead one repeats the decision one level higher, and the walk ends at the tree root; without a chain link the recorded root anchor is used directly. Strictly additive: the own-session park is kept (a retained child session later resumed still receives what it always received), the live lane is untouched, and every other notification class (shell completions, SendMessage deliveries, external `notify()` events) stays addressed to its own session. A delegated run that can reach neither an uplink nor a root anchor still parks (fail-open) and discloses once (`degraded` / `descendant-terminal-unrouted`).
47
+
48
+ - The env scrub no longer deletes infrastructure key names. `SECRET_ENV_RE`'s bare `KEY` word matched `PARTITION_KEY` / `SORT_KEY` / `RANGE_KEY` / `IDEMPOTENCY_KEY` / `CACHE_KEY` / `ROUTING_KEY`, and `scrubSecretEnv` DELETES what it matches — a spawned child silently lost variables that carry no credential (writes to a wrong partition, a routing key nothing binds). The word itself cannot be withdrawn (`API_KEY` / `SECRET_KEY` / `PRIVATE_KEY` / `ACCESS_KEY` are the bulk of real credential names), so the fix is the mirror of the existing positive exact-name list: `NON_SECRET_ENV_EXACT_NAMES`, exact names (never a shape — a pattern exemption would be a bypass) confirmed non-credential, consulted before the shape rule with the credential list read first (a name on both lists resolves to the credential reading). An exempted entry is kept and mints no finding. Decorated variants (`USERS_SORT_KEY`, `SORT_KEY_2`) are still dropped. **Consumers pinning the old shape**: a probe asserting `isSecretEnvKey("SORT_KEY") === true`, or that a scrub drops one of the six exempt names, now reads the opposite answer.
49
+
50
+ ### Added
51
+
52
+ - A durable question can be answered by the decision itself. `ResumeOutcome`'s `policy_ask` arm takes an optional `answer` (a `QuestionAnswer`) when the checkpoint's pending call is the reserved `AskUserQuestion` tool; the resume binds it as the resumed leg's answering face, so the pending call executes against the operator's actual selections. This is the same wiring the documented live arm builds by hand (`resume(token, outcome, { ...config, onQuestion: async () => answer })`) — but minted from the payload, which is the only route available to a caller redeeming an **offline background child**: the revive drive rebuilds that leg's config from the parked row and has no seat to put a closure in, so approving a parked question used to feed the model a fabricated "no human is available" default. The answer is untrusted wire data throughout: it goes through the existing answer fence unchanged (`selected ⊆ options` for the trusted line, off-list values and free-text `note` in a data fence), so multi-select and "Other" free text behave identically on both arms. The binding is one-shot — it answers the decided call only; a new question on the resumed leg is not fed the old answer. The answer is also recorded on the persisted winner, so an `env_failed` reopen must replay the identical answer (a replay may not keep the verdict and swap the text).
53
+
54
+ ### Changed
55
+
56
+ - **Behavior face** — approving a durable question without an answer is now refused instead of consumed. A `policy_ask` `allow` whose pending call is the reserved question tool is rejected **pre-CAS** (`checkpoint.invalid_outcome`, `detail.field: "answer"`, the checkpoint stays `pending` and redeemable) when it carries neither an `answer` nor a live answering face — where "live" excludes the reserved `QUESTION_AWAITS_RESUME` placeholder, whose contract is that it must never run. Previously that shape consumed the human's approval and let the run finish on a fabricated default (or, with the placeholder, on its config error) with nothing left to redeem. A resume that supplies its own real `onQuestion` is unaffected; so is every non-question approval. Symmetrically, an `answer` attached to a decision that cannot carry one (a `deny`, or a pending call that is not the question tool) is rejected the same way rather than silently dropped. **Consumers pinning the old shape**: a probe that approved a parked question with a bare allow now gets a typed rejection — supply the answer on the outcome, or deny.
57
+
3
58
  ## 5.6.0 (2026-08-03)
4
59
 
5
60
  _A hardening tail: numeric knobs refuse what they cannot honor, the binary gate reads bytes instead of trusting statistics, and the MCP SDK takes its last zero-cost step before the v2 jump._
@@ -6,7 +6,7 @@ export type GateVerdict = boolean | {
6
6
  };
7
7
  export interface CascadeRung {
8
8
  model: ModelRef;
9
- overrides?: Partial<Pick<TaskSpec, "limits" | "degrade" | "systemPrompt">>;
9
+ overrides?: Partial<Pick<TaskSpec, "limits" | "systemPrompt">>;
10
10
  }
11
11
  export interface CascadeConfig {
12
12
  onNotifyError?: (failure: import("../core/safe-notify.js").SafeNotifyFailure) => void;
@@ -1,10 +1,10 @@
1
1
  import type { Runner } from "../core/runner/runtask.js";
2
- import type { AgentDefinition, Model, ModelRef, ToolSpec } from "../core/types.js";
2
+ import type { AgentDefinition, Model, ModelRef, TaskLimits, ToolSpec } from "../core/types.js";
3
3
  import { type ExecutionEnv } from "../internal/harness.js";
4
4
  import type { RunInternals } from "../core/runner/prepare-task.js";
5
5
  import type { TaskNotificationPayload } from "../core/task-notification.js";
6
- import { FORK_DEFAULT_MAX_TURNS, SESSION_BG_DEFAULT_TIMEOUT_SEC, RETAIN_DEFAULT_TTL_MS, RETAIN_DEFAULT_MAX } from "../config/defaults.js";
7
- export { FORK_DEFAULT_MAX_TURNS, SESSION_BG_DEFAULT_TIMEOUT_SEC, RETAIN_DEFAULT_TTL_MS, RETAIN_DEFAULT_MAX };
6
+ import { RETAIN_DEFAULT_TTL_MS, RETAIN_DEFAULT_MAX } from "../config/defaults.js";
7
+ export { RETAIN_DEFAULT_TTL_MS, RETAIN_DEFAULT_MAX };
8
8
  import { SubagentRetainLedger } from "./retain-ledger.js";
9
9
  export type { SubagentStep, SubagentEditedFile } from "./subagent-steps.js";
10
10
  export declare function notifyResultField(result: string | undefined): string | undefined;
@@ -101,10 +101,7 @@ export interface SubagentToolOptions {
101
101
  systemPrompt?: string;
102
102
  name?: string;
103
103
  purpose?: string;
104
- limits?: {
105
- maxTurns?: number;
106
- timeoutSec?: number;
107
- };
104
+ limits?: TaskLimits;
108
105
  maxDepth?: number;
109
106
  retainFailedSessions?: boolean;
110
107
  onObserverError?: (err: unknown, info: {
@@ -17,7 +17,7 @@ import { addWorktree } from "../core/git-worktree-env.js";
17
17
  import { shellQuote } from "../tools/fs/search.js";
18
18
  import { BG_AGENT_REAP_STOP_ERROR } from "../core/task-registry.js";
19
19
  import { extractErrorCode } from "../brain/errors.js";
20
- import { FORK_DEFAULT_MAX_TURNS, SESSION_BG_DEFAULT_TIMEOUT_SEC, RETAIN_DEFAULT_TTL_MS, RETAIN_DEFAULT_MAX, RUNNING_AGENT_OBSERVE_EVERY_BEATS } from "../config/defaults.js";
20
+ import { RETAIN_DEFAULT_TTL_MS, RETAIN_DEFAULT_MAX, RUNNING_AGENT_OBSERVE_EVERY_BEATS } from "../config/defaults.js";
21
21
  function rollupDelegatedCost(stats, nested) {
22
22
  if (stats.costMicroUsd === undefined)
23
23
  return undefined;
@@ -29,7 +29,7 @@ function delegatedCostField(stats) {
29
29
  const total = rollupDelegatedCost(stats, stats.nested);
30
30
  return total !== undefined ? { costMicroUsd: total } : {};
31
31
  }
32
- export { FORK_DEFAULT_MAX_TURNS, SESSION_BG_DEFAULT_TIMEOUT_SEC, RETAIN_DEFAULT_TTL_MS, RETAIN_DEFAULT_MAX };
32
+ export { RETAIN_DEFAULT_TTL_MS, RETAIN_DEFAULT_MAX };
33
33
  import { SUBAGENT_RESUME_CAP, SubagentRetainLedger, getOrCreateSessionRetainLedger, ensureSessionReapHook, createResumePrompt, } from "./retain-ledger.js";
34
34
  import { recordRosterSpawn } from "./roster-store.js";
35
35
  import { ObserverDigestTap, ObserverPairing, createObserverReportToolSpec, markObserverTaskId, unmarkObserverTaskId, isObserverTaskId, ObserverResumeStateError, ObserverStoppedByUserError, observerFramingPrompt, observerSlug, resolveObserverDeclaration, } from "./observer.js";
@@ -224,7 +224,7 @@ export function classifySubagentError(child) {
224
224
  ? "rate_limit"
225
225
  : code === "server" && (msg.includes("overloaded") || msg.includes("529"))
226
226
  ? "overloaded"
227
- : code === "limit.timeout" || ((code === "network" || code === "server") && msg.includes("timeout"))
227
+ : code === "limits.max_walltime_exceeded" || ((code === "network" || code === "server") && msg.includes("timeout"))
228
228
  ? "timeout"
229
229
  : code === "network" || code === "server"
230
230
  ? "network"
@@ -237,7 +237,7 @@ function errorKindClause(c) {
237
237
  const FAILED_SESSION_RETAIN_TTL_MS = 15 * 60 * 1000;
238
238
  const PARTIAL_FINDINGS_MAX_CHARS = 1200;
239
239
  const BG_NOTIFY_DRAIN_WINDOW_MS = 2_000;
240
- function createBgActivityBeat(parentToolCallId, emitTick) {
240
+ function createBgActivityBeat(parentToolCallId, emitTick, noteActivity) {
241
241
  let beats = 0;
242
242
  let starts = 0;
243
243
  return (e) => {
@@ -248,6 +248,7 @@ function createBgActivityBeat(parentToolCallId, emitTick) {
248
248
  if (e.type === "tool_start")
249
249
  starts += 1;
250
250
  beats += 1;
251
+ noteActivity?.();
251
252
  if (beats === 1 || beats % RUNNING_AGENT_OBSERVE_EVERY_BEATS === 0)
252
253
  emitTick(starts);
253
254
  };
@@ -395,7 +396,7 @@ export function createSubagentResume(deps) {
395
396
  ...(currentTool !== undefined ? { currentTool } : {}),
396
397
  usage: { toolUses: toolStarts },
397
398
  });
398
- });
399
+ }, deps.registry !== undefined && deps.taskId !== undefined ? () => deps.registry.noteBackgroundAgentActivity(deps.taskId) : undefined);
399
400
  stream = childRunner.runTaskStream(resumeSpec, undefined, {
400
401
  ...entry.internalsSnapshot,
401
402
  ...(true
@@ -1149,11 +1150,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1149
1150
  const childSystemPrompt = def?.systemPrompt ?? opts.systemPrompt;
1150
1151
  const childDefaultPersona = childSystemPrompt === undefined && !wantsFork ? SUBAGENT_PROMPT : undefined;
1151
1152
  const defMaxTurns = typeof def?.maxTurns === "number" && Number.isFinite(def.maxTurns) && def.maxTurns > 0 ? def.maxTurns : undefined;
1152
- const childLimits = defMaxTurns !== undefined
1153
- ? { ...opts.limits, maxTurns: defMaxTurns }
1154
- : wantsFork && opts.limits?.maxTurns === undefined
1155
- ? { ...opts.limits, maxTurns: FORK_DEFAULT_MAX_TURNS }
1156
- : opts.limits;
1153
+ const childLimits = defMaxTurns !== undefined ? { ...opts.limits, maxTurns: defMaxTurns } : opts.limits;
1157
1154
  const nextExcluded = def ? new Set([...excluded, def.name]) : excluded;
1158
1155
  const defAllowsDelegation = def === undefined || toolNameAllowed(opts.name ?? DEFAULT_SUBAGENT_TOOL_NAME, def.allowTools, def.denyTools);
1159
1156
  if (depth + 1 < maxDepth && defAllowsDelegation) {
@@ -1233,7 +1230,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1233
1230
  enableBlockedReport: false,
1234
1231
  limits: {
1235
1232
  maxTurns: typeof observerDef.maxTurns === "number" && Number.isFinite(observerDef.maxTurns) && observerDef.maxTurns > 0 ? observerDef.maxTurns : 8,
1236
- timeoutSec: 120,
1233
+ maxWalltimeMs: 120_000,
1237
1234
  },
1238
1235
  });
1239
1236
  const runObserverLeg = async (spec) => {
@@ -1722,9 +1719,6 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1722
1719
  });
1723
1720
  const forkBgStartedAt = Date.now();
1724
1721
  const bgForkSpec = { ...buildChildSpec(abort.signal), sessionId: forkedId, requireExistingSession: true, objective: forkObjective };
1725
- if (sessionScopedBg && bgForkSpec.limits?.timeoutSec === undefined) {
1726
- bgForkSpec.limits = { ...(bgForkSpec.limits ?? {}), timeoutSec: SESSION_BG_DEFAULT_TIMEOUT_SEC };
1727
- }
1728
1722
  const s2ForkNotifyReady = (inject) => {
1729
1723
  bg.registry.attachAgentNotify(taskId, inject);
1730
1724
  };
@@ -1744,7 +1738,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1744
1738
  ...(currentTool !== undefined ? { currentTool } : {}),
1745
1739
  usage: { toolUses: toolStarts },
1746
1740
  });
1747
- });
1741
+ }, () => bg.registry.noteBackgroundAgentActivity(taskId));
1748
1742
  const bgForkInternals = bgSink
1749
1743
  ? {
1750
1744
  ...forkInternals,
@@ -2195,9 +2189,6 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2195
2189
  ...(forwardDurableApproval ? { durableApproval: { ...ctx.durableApprovalForChildren } } : {}),
2196
2190
  ...(reviveRow !== undefined ? { requireExistingSession: true } : {}),
2197
2191
  };
2198
- if ((sessionScopedBg || reviveRow !== undefined) && bgSpec.limits?.timeoutSec === undefined) {
2199
- bgSpec.limits = { ...(bgSpec.limits ?? {}), timeoutSec: SESSION_BG_DEFAULT_TIMEOUT_SEC };
2200
- }
2201
2192
  let reviveAttachedResolve;
2202
2193
  const reviveAttached = reviveRow !== undefined ? new Promise((r) => (reviveAttachedResolve = r)) : undefined;
2203
2194
  const s2NotifyReady = (inject) => {
@@ -2222,7 +2213,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2222
2213
  ...(currentTool !== undefined ? { currentTool } : {}),
2223
2214
  usage: { toolUses: toolStarts },
2224
2215
  });
2225
- });
2216
+ }, () => bg.registry.noteBackgroundAgentActivity(taskId));
2226
2217
  const bgInternals = bgSink
2227
2218
  ? {
2228
2219
  ...childInternals,
@@ -192,7 +192,7 @@ async function runTeacherCore(runner, studentSpec, teacher) {
192
192
  ...helperBase(),
193
193
  objective: helperObjective,
194
194
  enableBlockedReport: false,
195
- limits: { maxTurns: 1, timeoutSec: studentSpec.limits?.timeoutSec },
195
+ limits: { maxTurns: 1, ...(studentSpec.limits?.maxWalltimeMs !== undefined ? { maxWalltimeMs: studentSpec.limits.maxWalltimeMs } : {}) },
196
196
  signal: extSignal,
197
197
  });
198
198
  await runner.sessions.release(r.sessionId).catch(() => undefined);
@@ -341,7 +341,7 @@ async function runTeacherCore(runner, studentSpec, teacher) {
341
341
  ...teacherModelFields(),
342
342
  systemPrompt: teacher.prompts?.teacher ?? TEACHER_PROMPT,
343
343
  enableBlockedReport: false,
344
- limits: { maxTurns: 4, timeoutSec: studentSpec.limits?.timeoutSec },
344
+ limits: { maxTurns: 4, ...(studentSpec.limits?.maxWalltimeMs !== undefined ? { maxWalltimeMs: studentSpec.limits.maxWalltimeMs } : {}) },
345
345
  signal: extSignal,
346
346
  });
347
347
  await runner.sessions.release(r.sessionId).catch(() => undefined);
@@ -363,7 +363,7 @@ async function runTeacherCore(runner, studentSpec, teacher) {
363
363
  else if (result.status === "blocked") {
364
364
  trigger = "blocked";
365
365
  }
366
- else if (result.status === "failed" || result.status === "timeout") {
366
+ else if (result.status === "failed") {
367
367
  trigger = "failed";
368
368
  }
369
369
  else if (!verifyOutput) {
@@ -56,7 +56,7 @@ export interface TeamDiscussionOptions {
56
56
  };
57
57
  onEvent?: (e: TeamEvent) => void;
58
58
  limits?: {
59
- timeoutSec?: number;
59
+ maxWalltimeMs?: number;
60
60
  maxTurns?: number;
61
61
  maxTokens?: number;
62
62
  maxCostUsd?: number;
@@ -63,7 +63,7 @@ export async function runTeamDiscussion(opts) {
63
63
  : undefined;
64
64
  const memberLimits = opts.limits
65
65
  ? {
66
- ...(opts.limits.timeoutSec !== undefined ? { timeoutSec: opts.limits.timeoutSec } : {}),
66
+ ...(opts.limits.maxWalltimeMs !== undefined ? { maxWalltimeMs: opts.limits.maxWalltimeMs } : {}),
67
67
  ...(opts.limits.maxTurns !== undefined ? { maxTurns: opts.limits.maxTurns } : {}),
68
68
  }
69
69
  : undefined;
@@ -101,7 +101,7 @@ export async function verifyCompleted(runner, result, specBase, objective, confi
101
101
  handsReadOnly: config.verifierHandsReadOnly ?? true,
102
102
  outputSchema: VerdictSchema,
103
103
  enableBlockedReport: false,
104
- limits: { timeoutSec: specBase.limits?.timeoutSec },
104
+ limits: { ...(specBase.limits?.maxWalltimeMs !== undefined ? { maxWalltimeMs: specBase.limits.maxWalltimeMs } : {}) },
105
105
  getApiKeyAndHeaders: specBase.getApiKeyAndHeaders,
106
106
  signal: specBase.signal,
107
107
  });
@@ -1,9 +1,8 @@
1
1
  import {} from "../internal/llm.js";
2
2
  import { BrainError } from "./errors.js";
3
3
  import { DEGENERATE_MESSAGE, trimDegenerateTail } from "./repetition.js";
4
- import { createRepetitionPoll, createWalltimeGate, parseStreamedToolArgs } from "./stream-shared.js";
4
+ import { createRepetitionPoll, parseStreamedToolArgs } from "./stream-shared.js";
5
5
  import { mintFallbackToolCallId } from "./tool-call-id.js";
6
- import { WALLTIME_CUTOFF_MESSAGE } from "./walltime.js";
7
6
  import { emitBrainTelemetry } from "./status-sink.js";
8
7
  import { errorResultMediaNote, IMAGE_OMITTED_NO_VISION, modelSupportsVision, sendableImages } from "./media-degrade.js";
9
8
  import { ANTHROPIC_RESERVED, applyExtraBody, stripAuthHeaders } from "./request-params.js";
@@ -233,7 +232,6 @@ export function createAnthropicBrain(config = {}) {
233
232
  signal: options?.signal,
234
233
  config,
235
234
  httpLabel: "anthropic",
236
- callDeadlineMs: options?.callDeadlineMs,
237
235
  stallTimeouts: options?.stallTimeouts,
238
236
  buildRequest: () => {
239
237
  const apiKey = options?.apiKey ?? config.apiKey;
@@ -342,7 +340,6 @@ export function createAnthropicBrain(config = {}) {
342
340
  let degenerate = false;
343
341
  let degenTextBlock;
344
342
  const rep = createRepetitionPoll();
345
- const walltime = createWalltimeGate({ callDeadlineMs: options?.callDeadlineMs, isDegenerate: () => degenerate, cancel: () => ctrl.cancel() });
346
343
  let lastDegenCheck = 0;
347
344
  let lastDegenCheckThinking = 0;
348
345
  const detectRep = config.detectRepetition !== false;
@@ -497,10 +494,7 @@ export function createAnthropicBrain(config = {}) {
497
494
  }
498
495
  }
499
496
  },
500
- onDeadline: () => walltime.onDeadline(),
501
497
  onLine(rawLine) {
502
- if (walltime.shouldDropLine())
503
- return;
504
498
  const line = rawLine.trim();
505
499
  if (!line.startsWith("data:"))
506
500
  return;
@@ -576,40 +570,35 @@ export function createAnthropicBrain(config = {}) {
576
570
  const staticReasoningCut = truncatedEmpty && !dynamicCut && reasoningSeen && options?.staticReasoningCutDowngrade === true;
577
571
  const safetyCut = stopReason === "refusal";
578
572
  const contextWindowExceeded = stopReason === "model_context_window_exceeded";
579
- const emptyNoFinish = noUsableContent && stopReason == null && !degenerate && !walltime.cut;
573
+ const emptyNoFinish = noUsableContent && stopReason == null && !degenerate;
580
574
  const maxTokensNote = `effective max_tokens=${sentMaxTokens ?? "?"} (from ${sentMaxTokensLane === "options" ? "options.maxTokens" : sentMaxTokensLane === "model" ? "model.maxTokens" : "config default"})`;
581
- const errorMessage = walltime.cut
582
- ? WALLTIME_CUTOFF_MESSAGE
583
- : degenerate
584
- ? DEGENERATE_MESSAGE
585
- : safetyCut
586
- ? `[refusal] response refused by the model (stop_reason="refusal"${stopDetails?.category ? `, category="${stopDetails.category}"` : ""}) — ${stopDetails?.explanation ?? "the output was withheld for policy and is unreliable"}`
587
- : contextWindowExceeded
588
- ? `[invalid_request] model context window exceeded (stop_reason="model_context_window_exceeded") the conversation no longer fits the model's context window`
589
- : (toolError ??
590
- (truncatedEmpty && !dynamicCut && !staticReasoningCut
591
- ? thinkingRequested
592
- ? `[length_empty] response truncated at max_tokens with no answer text (${maxTokensNote}) — extended thinking likely consumed the entire budget; raise max_tokens or lower thinkingBudgetShare`
593
- : reasoningSeen
594
- ? `[length_empty] response truncated at max_tokens with no answer text (${maxTokensNote}) — the model emitted reasoning although no thinking was requested (a budget-ignoring gateway?) and it consumed the entire output budget; raise max_tokens`
595
- : `[length_empty] response truncated at max_tokens with no answer text (${maxTokensNote}); raise max_tokens`
596
- : emptyNoFinish
597
- ? `[stream_torn] model stream ended with no content and no stop_reason — the response was lost (a torn stream or an in-band provider error frame)${malformedFrames > 0 ? ` — ${malformedFrames} unparseable data frame(s) were dropped on this stream (first sample: ${JSON.stringify(malformedSample)})` : ""}`
598
- : undefined));
599
- const errored = walltime.cut ||
600
- degenerate ||
575
+ const errorMessage = degenerate
576
+ ? DEGENERATE_MESSAGE
577
+ : safetyCut
578
+ ? `[refusal] response refused by the model (stop_reason="refusal"${stopDetails?.category ? `, category="${stopDetails.category}"` : ""}) — ${stopDetails?.explanation ?? "the output was withheld for policy and is unreliable"}`
579
+ : contextWindowExceeded
580
+ ? `[invalid_request] model context window exceeded (stop_reason="model_context_window_exceeded") — the conversation no longer fits the model's context window`
581
+ : (toolError ??
582
+ (truncatedEmpty && !dynamicCut && !staticReasoningCut
583
+ ? thinkingRequested
584
+ ? `[length_empty] response truncated at max_tokens with no answer text (${maxTokensNote}) extended thinking likely consumed the entire budget; raise max_tokens or lower thinkingBudgetShare`
585
+ : reasoningSeen
586
+ ? `[length_empty] response truncated at max_tokens with no answer text (${maxTokensNote}) — the model emitted reasoning although no thinking was requested (a budget-ignoring gateway?) and it consumed the entire output budget; raise max_tokens`
587
+ : `[length_empty] response truncated at max_tokens with no answer text (${maxTokensNote}); raise max_tokens`
588
+ : emptyNoFinish
589
+ ? `[stream_torn] model stream ended with no content and no stop_reason — the response was lost (a torn stream or an in-band provider error frame)${malformedFrames > 0 ? ` — ${malformedFrames} unparseable data frame(s) were dropped on this stream (first sample: ${JSON.stringify(malformedSample)})` : ""}`
590
+ : undefined));
591
+ const errored = degenerate ||
601
592
  safetyCut ||
602
593
  contextWindowExceeded ||
603
594
  emptyNoFinish ||
604
595
  (toolError !== undefined && noUsableContent) ||
605
596
  (truncatedEmpty && !dynamicCut && !staticReasoningCut);
606
- const errorKind = walltime.cut
607
- ? "walltime_cutoff"
608
- : degenerate
609
- ? "degenerate"
610
- : !safetyCut && toolError === undefined && truncatedEmpty && !dynamicCut && !staticReasoningCut
611
- ? "length_empty"
612
- : undefined;
597
+ const errorKind = degenerate
598
+ ? "degenerate"
599
+ : !safetyCut && toolError === undefined && truncatedEmpty && !dynamicCut && !staticReasoningCut
600
+ ? "length_empty"
601
+ : undefined;
613
602
  const finalMessage = {
614
603
  ...partial,
615
604
  content: finalContent.length > 0 ? finalContent : [{ type: "text", text: "" }],
@@ -1,9 +1,8 @@
1
1
  import {} from "../internal/llm.js";
2
2
  import { repairTextToolCalls } from "./tool-call-repair.js";
3
3
  import { DEGENERATE_MESSAGE, trimDegenerateTail } from "./repetition.js";
4
- import { createRepetitionPoll, createWalltimeGate, parseStreamedToolArgs } from "./stream-shared.js";
4
+ import { createRepetitionPoll, parseStreamedToolArgs } from "./stream-shared.js";
5
5
  import { mintFallbackToolCallId } from "./tool-call-id.js";
6
- import { WALLTIME_CUTOFF_MESSAGE } from "./walltime.js";
7
6
  import { emitBrainTelemetry } from "./status-sink.js";
8
7
  import { errorResultMediaNote, IMAGE_OMITTED_NO_VISION, imagesOmittedNoVisionNote, modelSupportsVision, sendableImages } from "./media-degrade.js";
9
8
  import { OPENAI_RESERVED, applyExtraBody, stripAuthHeaders } from "./request-params.js";
@@ -275,7 +274,6 @@ export function createOpenAIBrain(config = {}) {
275
274
  signal: options?.signal,
276
275
  config,
277
276
  httpLabel: "gateway",
278
- callDeadlineMs: options?.callDeadlineMs,
279
277
  stallTimeouts: options?.stallTimeouts,
280
278
  buildRequest: () => {
281
279
  const apiKey = options?.apiKey ?? config.apiKey;
@@ -330,7 +328,6 @@ export function createOpenAIBrain(config = {}) {
330
328
  let degenerate = false;
331
329
  let degenFace;
332
330
  const rep = createRepetitionPoll();
333
- const walltime = createWalltimeGate({ callDeadlineMs: options?.callDeadlineMs, isDegenerate: () => degenerate, cancel: () => ctrl.cancel() });
334
331
  let lastDegenCheck = 0;
335
332
  let lastDegenCheckReasoning = 0;
336
333
  const detectRep = config.detectRepetition !== false;
@@ -410,10 +407,7 @@ export function createOpenAIBrain(config = {}) {
410
407
  sealForRetry() {
411
408
  closeThinking();
412
409
  },
413
- onDeadline: () => walltime.onDeadline(),
414
410
  onLine(rawLine) {
415
- if (walltime.shouldDropLine())
416
- return;
417
411
  const line = rawLine.trim();
418
412
  if (!line.startsWith("data:"))
419
413
  return;
@@ -534,40 +528,35 @@ export function createOpenAIBrain(config = {}) {
534
528
  const dynamicCut = truncatedEmpty && options?.maxTokensDynamic === true;
535
529
  const safetyCut = finishReason === "content_filter";
536
530
  const refused = accumRefusal.trim().length > 0;
537
- const emptyNoFinish = noUsableContent && finishReason == null && !degenerate && !walltime.cut;
531
+ const emptyNoFinish = noUsableContent && finishReason == null && !degenerate;
538
532
  const maxTokensNote = sentMaxTokens !== undefined
539
533
  ? `effective max_tokens=${sentMaxTokens} (from ${sentMaxTokensLane === "options" ? "options.maxTokens" : "model.maxTokens"})`
540
534
  : "max_tokens not set (neither options.maxTokens nor model.maxTokens — the provider's default cap applied)";
541
- const errorMessage = walltime.cut
542
- ? WALLTIME_CUTOFF_MESSAGE
543
- : degenerate
544
- ? DEGENERATE_MESSAGE
545
- : safetyCut
546
- ? `[refusal] response cut by the provider content filter (finish_reason="content_filter") — the output was censored/truncated for policy and is unreliable`
547
- : refused
548
- ? `[refusal] response refused by the model for policy reasons: ${accumRefusal.trim()}`
549
- : (toolError ??
550
- (truncatedEmpty && !dynamicCut
551
- ? accumReasoning.trim()
552
- ? `[length_empty] response truncated at max_tokens (finish_reason="length", ${maxTokensNote}) with no answer text — the model spent the whole budget on reasoning (${accumReasoning.length} reasoning chars). Raise max_tokens, or disable thinking for this task.`
553
- : `[length_empty] response truncated at max_tokens (finish_reason="length", ${maxTokensNote}) with no answer text raise max_tokens`
554
- : emptyNoFinish
555
- ? `[stream_torn] model stream ended with no content and no finish_reason — the response was lost (a torn stream or an in-band provider error frame)${malformedFrames > 0 ? ` — ${malformedFrames} unparseable data frame(s) were dropped on this stream (first sample: ${JSON.stringify(malformedSample)})` : ""}`
556
- : undefined));
557
- const errored = walltime.cut ||
558
- degenerate ||
535
+ const errorMessage = degenerate
536
+ ? DEGENERATE_MESSAGE
537
+ : safetyCut
538
+ ? `[refusal] response cut by the provider content filter (finish_reason="content_filter") — the output was censored/truncated for policy and is unreliable`
539
+ : refused
540
+ ? `[refusal] response refused by the model for policy reasons: ${accumRefusal.trim()}`
541
+ : (toolError ??
542
+ (truncatedEmpty && !dynamicCut
543
+ ? accumReasoning.trim()
544
+ ? `[length_empty] response truncated at max_tokens (finish_reason="length", ${maxTokensNote}) with no answer text — the model spent the whole budget on reasoning (${accumReasoning.length} reasoning chars). Raise max_tokens, or disable thinking for this task.`
545
+ : `[length_empty] response truncated at max_tokens (finish_reason="length", ${maxTokensNote}) with no answer text — raise max_tokens`
546
+ : emptyNoFinish
547
+ ? `[stream_torn] model stream ended with no content and no finish_reason — the response was lost (a torn stream or an in-band provider error frame)${malformedFrames > 0 ? ` — ${malformedFrames} unparseable data frame(s) were dropped on this stream (first sample: ${JSON.stringify(malformedSample)})` : ""}`
548
+ : undefined));
549
+ const errored = degenerate ||
559
550
  safetyCut ||
560
551
  refused ||
561
552
  emptyNoFinish ||
562
553
  (toolError !== undefined && noUsableContent) ||
563
554
  (truncatedEmpty && !dynamicCut);
564
- const errorKind = walltime.cut
565
- ? "walltime_cutoff"
566
- : degenerate
567
- ? "degenerate"
568
- : !safetyCut && !refused && toolError === undefined && truncatedEmpty && !dynamicCut
569
- ? "length_empty"
570
- : undefined;
555
+ const errorKind = degenerate
556
+ ? "degenerate"
557
+ : !safetyCut && !refused && toolError === undefined && truncatedEmpty && !dynamicCut
558
+ ? "length_empty"
559
+ : undefined;
571
560
  const finalMessage = {
572
561
  ...partial,
573
562
  content: finalContent.length > 0 ? finalContent : [{ type: "text", text: "" }],
@@ -18,7 +18,6 @@ export interface StreamControls {
18
18
  }
19
19
  export interface StreamParser {
20
20
  onLine(line: string): void;
21
- onDeadline?(): boolean;
22
21
  finalize(): void;
23
22
  snapshot(): {
24
23
  hasSubstantiveText: boolean;
@@ -35,7 +34,6 @@ export declare function runStreamingBrain(args: {
35
34
  httpLabel: string;
36
35
  buildRequest: () => SSERequest;
37
36
  makeParser: (ctrl: StreamControls) => StreamParser;
38
- callDeadlineMs?: number;
39
37
  stallTimeouts?: {
40
38
  connectMs?: number;
41
39
  firstTokenMs?: number;