@sema-agent/core 5.7.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 (84) hide show
  1. package/CHANGELOG.md +37 -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 +10 -8
  26. package/dist/core/checkpoint-store.js +7 -7
  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 +200 -244
  36. package/dist/core/session-policy-store.d.ts +5 -0
  37. package/dist/core/session-policy-store.js +3 -1
  38. package/dist/core/store-contracts/checkpoint-store-contract.js +2 -2
  39. package/dist/core/task-registry-agent.d.ts +2 -0
  40. package/dist/core/task-registry-agent.js +33 -1
  41. package/dist/core/task-registry.d.ts +2 -0
  42. package/dist/core/task-registry.js +9 -1
  43. package/dist/core/tool-errors.js +10 -2
  44. package/dist/core/trace.d.ts +1 -8
  45. package/dist/core/types.d.ts +24 -30
  46. package/dist/core/usage-window-store.d.ts +39 -0
  47. package/dist/core/usage-window-store.js +115 -0
  48. package/dist/engine/execution-env/node-execution-env.js +0 -22
  49. package/dist/engine/harness/agent-harness.d.ts +0 -7
  50. package/dist/engine/harness/agent-harness.js +13 -53
  51. package/dist/engine/harness/types.d.ts +2 -15
  52. package/dist/engine/llm/types.d.ts +1 -2
  53. package/dist/engine/loop/agent-loop.d.ts +1 -1
  54. package/dist/engine/loop/agent-loop.js +5 -171
  55. package/dist/engine/loop/types.d.ts +0 -18
  56. package/dist/index.d.ts +10 -5
  57. package/dist/index.js +8 -4
  58. package/dist/orchestration/goal.js +1 -5
  59. package/dist/orchestration/run-workflow-tool.d.ts +1 -1
  60. package/dist/orchestration/run-workflow-tool.js +1 -2
  61. package/dist/orchestration/workflow-governance.d.ts +6 -6
  62. package/dist/orchestration/workflow-governance.js +33 -47
  63. package/dist/prompt-assembly/event-registry.js +2 -3
  64. package/dist/stores/cc/lockfile.js +9 -11
  65. package/dist/stores/cc/mailbox-store.js +49 -38
  66. package/dist/stores/file/index.d.ts +3 -0
  67. package/dist/stores/file/index.js +4 -0
  68. package/dist/stores/file/usage-window-store.d.ts +9 -0
  69. package/dist/stores/file/usage-window-store.js +86 -0
  70. package/dist/tools/fs/fs-bash.d.ts +1 -10
  71. package/dist/tools/fs/fs-bash.js +20 -60
  72. package/dist/tools/fs/index.d.ts +0 -2
  73. package/dist/tools/fs/index.js +1 -2
  74. package/package.json +1 -4
  75. package/dist/bin/sema-tb.d.ts +0 -31
  76. package/dist/bin/sema-tb.js +0 -448
  77. package/dist/bin/tb-env.d.ts +0 -2
  78. package/dist/bin/tb-env.js +0 -17
  79. package/dist/brain/walltime.d.ts +0 -1
  80. package/dist/brain/walltime.js +0 -1
  81. package/dist/core/runner/call-cap.d.ts +0 -67
  82. package/dist/core/runner/call-cap.js +0 -145
  83. package/dist/core/runner/cut-kill.d.ts +0 -10
  84. package/dist/core/runner/cut-kill.js +0 -37
package/CHANGELOG.md CHANGED
@@ -1,5 +1,42 @@
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
+
3
40
  ## 5.7.0 (2026-08-04)
4
41
 
5
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)._
@@ -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;
@@ -3,7 +3,6 @@ import { BrainError, classifyHttp, describeNetworkError } from "./errors.js";
3
3
  import { retryBackoffMs } from "./retry.js";
4
4
  import { emitBrainStatus, emitBrainTelemetry } from "./status-sink.js";
5
5
  import { createConnectController, resolveStallTimeoutMs } from "./timeout.js";
6
- import { WALLTIME_CUTOFF_MESSAGE } from "./walltime.js";
7
6
  const DEFAULT_MAX_RETRIES = 10;
8
7
  const MAX_RETRIES_ENV_CEILING = 15;
9
8
  const MAX_RETRIES_ENV = "SEMA_MAX_RETRIES";
@@ -43,9 +42,6 @@ function emptyAssistant(model) {
43
42
  function isAbortError(err) {
44
43
  return err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError");
45
44
  }
46
- function isWalltimeCutoff(err) {
47
- return err instanceof Error && err.message === WALLTIME_CUTOFF_MESSAGE;
48
- }
49
45
  function sleep(ms, signal) {
50
46
  return new Promise((resolve) => {
51
47
  if (signal?.aborted)
@@ -78,7 +74,7 @@ async function sleepAnnouncingRetry(totalMs, signal, frame) {
78
74
  }
79
75
  }
80
76
  export function runStreamingBrain(args) {
81
- const { model, doFetch, signal, config, httpLabel, buildRequest, makeParser, callDeadlineMs, stallTimeouts } = args;
77
+ const { model, doFetch, signal, config, httpLabel, buildRequest, makeParser, stallTimeouts } = args;
82
78
  const out = createAssistantMessageEventStream();
83
79
  let cleanup;
84
80
  let announcedRetry = false;
@@ -88,13 +84,10 @@ export function runStreamingBrain(args) {
88
84
  .catch((err) => {
89
85
  const aborted = signal?.aborted === true || isAbortError(err);
90
86
  terminalRetryPhase = "gave_up";
91
- const walltimeCut = isWalltimeCutoff(err);
92
- terminalRetryDetail = aborted ? "cancelled while retrying" : walltimeCut ? "wall-clock deadline reached while retrying" : "retries exhausted";
87
+ terminalRetryDetail = aborted ? "cancelled while retrying" : "retries exhausted";
93
88
  const errorMsg = emptyAssistant(model);
94
89
  errorMsg.stopReason = aborted ? "aborted" : "error";
95
90
  errorMsg.errorMessage = err instanceof Error ? err.message : String(err);
96
- if (walltimeCut)
97
- errorMsg.errorKind = "walltime_cutoff";
98
91
  errorMsg.usageMissing = true;
99
92
  out.push({ type: "error", reason: aborted ? "aborted" : "error", error: errorMsg });
100
93
  })
@@ -128,20 +121,13 @@ export function runStreamingBrain(args) {
128
121
  e.name = "AbortError";
129
122
  throw e;
130
123
  }
131
- if (callDeadlineMs !== undefined && Date.now() >= callDeadlineMs) {
132
- throw new Error(WALLTIME_CUTOFF_MESSAGE);
133
- }
134
- const cc = createConnectController(connectTimeoutMs, signal, callDeadlineMs);
124
+ const cc = createConnectController(connectTimeoutMs, signal);
135
125
  let r;
136
126
  let netErr;
137
127
  try {
138
128
  r = await doFetch(req.url, { method: "POST", headers: req.headers, body: req.body, signal: cc.signal });
139
129
  }
140
130
  catch (e) {
141
- if (cc.deadlineHit()) {
142
- cc.dispose();
143
- throw new Error(WALLTIME_CUTOFF_MESSAGE);
144
- }
145
131
  if (cc.timedOut()) {
146
132
  netErr = new Error(`connect timeout after ${connectTimeoutMs}ms`);
147
133
  }
@@ -153,10 +139,6 @@ export function runStreamingBrain(args) {
153
139
  netErr = e;
154
140
  }
155
141
  }
156
- if (r && cc.deadlineHit()) {
157
- cc.dispose();
158
- throw new Error(WALLTIME_CUTOFF_MESSAGE);
159
- }
160
142
  if (r && r.ok && r.body) {
161
143
  cc.clearTimer();
162
144
  connect = cc;
@@ -168,13 +150,7 @@ export function runStreamingBrain(args) {
168
150
  const retryable = netErr !== undefined ||
169
151
  (r !== undefined && (r.status >= 500 || r.status === 429 || r.status === 408 || r.status === 409));
170
152
  if (retryable && attempt < maxRetries) {
171
- if (callDeadlineMs !== undefined && Date.now() >= callDeadlineMs) {
172
- throw new Error(WALLTIME_CUTOFF_MESSAGE);
173
- }
174
153
  const delayMs = retryBackoffMs(baseDelay, attempt, r);
175
- if (callDeadlineMs !== undefined && Date.now() + delayMs >= callDeadlineMs) {
176
- throw new Error(WALLTIME_CUTOFF_MESSAGE);
177
- }
178
154
  const statusPhase = r?.status === 429 ? "rate_limited" : netErr !== undefined ? "reconnecting" : "retrying";
179
155
  emitBrainTelemetry({ kind: "retry", attempt: attempt + 1, phase: "connect" });
180
156
  announcedRetry = true;
@@ -228,8 +204,6 @@ export function runStreamingBrain(args) {
228
204
  void reader.cancel().catch(() => undefined);
229
205
  }, idleTimeoutMs);
230
206
  };
231
- let dlTimer;
232
- let deadlineCut = false;
233
207
  const sawContentToken = () => {
234
208
  bumpIdle();
235
209
  if (firstTokenSeen)
@@ -250,10 +224,6 @@ export function runStreamingBrain(args) {
250
224
  clearTimeout(idleTimer);
251
225
  idleTimer = undefined;
252
226
  }
253
- if (dlTimer) {
254
- clearTimeout(dlTimer);
255
- dlTimer = undefined;
256
- }
257
227
  connect?.dispose();
258
228
  void reader.cancel().catch(() => undefined);
259
229
  };
@@ -273,14 +243,6 @@ export function runStreamingBrain(args) {
273
243
  sawContentToken,
274
244
  cancel: () => void reader.cancel().catch(() => undefined),
275
245
  });
276
- if (callDeadlineMs !== undefined && parser.onDeadline) {
277
- dlTimer = setTimeout(() => {
278
- dlTimer = undefined;
279
- deadlineCut = parser.onDeadline();
280
- void reader.cancel().catch(() => undefined);
281
- }, Math.max(0, callDeadlineMs - Date.now()));
282
- dlTimer.unref?.();
283
- }
284
246
  const INBAND_OVERLOAD_WARNING = "API overloaded mid-response. The response above may be incomplete.";
285
247
  const onLineOverloadTierA = (line) => {
286
248
  try {
@@ -334,14 +296,10 @@ export function runStreamingBrain(args) {
334
296
  clearTimeout(idleTimer);
335
297
  idleTimer = undefined;
336
298
  }
337
- if (dlTimer) {
338
- clearTimeout(dlTimer);
339
- dlTimer = undefined;
340
- }
341
- if (!failure && firstTokenTimedOut && !deadlineCut) {
299
+ if (!failure && firstTokenTimedOut) {
342
300
  failure = { kind: "stalled", err: new BrainError("network", `first-token timeout after ${firstTokenTimeoutMs}ms`) };
343
301
  }
344
- if (!failure && idleTimedOut && !deadlineCut) {
302
+ if (!failure && idleTimedOut) {
345
303
  failure = { kind: "stalled", err: new BrainError("network", `stream idle timeout after ${idleTimeoutMs}ms`) };
346
304
  }
347
305
  if (!failure) {