@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
@@ -19,8 +19,6 @@ import { createSendMessageTool, SEND_MESSAGE_TOOL_NAME } from "../../agents/send
19
19
  import { SubagentRetainLedger } from "../../agents/retain-ledger.js";
20
20
  import { combinePolicies, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, refuseOutOfContractDecision, resolveAsk, toolPolicyNameSets } from "../tool-policy.js";
21
21
  import { ActiveSkillScope, createActiveSkillScopePolicy } from "./active-skill-scope.js";
22
- import { computeCallCap, createCallCapRef, softExecDeadlineMs, toolCutDeadlineMs, WALLTIME_STALL_CONNECT_MS, WALLTIME_STALL_FIRST_TOKEN_MS, WALLTIME_STALL_IDLE_MS } from "./call-cap.js";
23
- import { createCutKillRegistry } from "./cut-kill.js";
24
22
  import { CHANGED_FILES_MTIME_EPS_MS, fenceMcpServerInstructions, renderAgentListingDelta } from "./turn-attachments.js";
25
23
  import { inlineUntrusted } from "../untrusted-text.js";
26
24
  import { emitTrace } from "../trace.js";
@@ -29,7 +27,7 @@ import { cloneObserverInput, formatHookFeedback, runToolGate } from "../hooks.js
29
27
  import { reconcileInterruptedSession } from "../session-reconcile.js";
30
28
  import { CacheBreakDetector, toolsToFingerprintInputs } from "../cache-break-detector.js";
31
29
  import { reservedCollisions, reservedFor } from "../../brain/request-params.js";
32
- import { resolveBrainCallGuardrailMs, withBrainCallGuardrail } from "../../brain/timeout.js";
30
+ import { resolveBrainCallGuardrailMs, STALL_CONNECT_MS, STALL_FIRST_TOKEN_MS, STALL_IDLE_MS, withBrainCallGuardrail } from "../../brain/timeout.js";
33
31
  import { defineTool, isDefineToolProduct } from "../tools.js";
34
32
  import { RETIRED_TOOL_NAMES } from "../tool-name-aliases.js";
35
33
  import { protocolOf, PROTOCOL_TABLE } from "../protocol-table.js";
@@ -69,13 +67,96 @@ import { createRunWorkflowTool, RUN_WORKFLOW_TOOL_NAME } from "../../orchestrati
69
67
  import { resolveWorkflowSizeGuideline } from "../../orchestration/workflow-size-guideline.js";
70
68
  import { createLspTool, gitCheckIgnoreFilter, resolveLspPath } from "../lsp.js";
71
69
  import { resolveKey } from "../../tools/fs/safety.js";
72
- import { BINDING_CHECKPOINT_VERSION, mintCheckpointToken, RESOURCE_CHECKPOINT_VERSION, WALLTIME_CHECKPOINT_VERSION, buildRiskDescriptor, debitLedger, remainingBudgetMicroUsd, } from "../checkpoint-store.js";
70
+ import { BINDING_CHECKPOINT_VERSION, mintCheckpointToken, RESOURCE_CHECKPOINT_VERSION, TOKEN_CHECKPOINT_VERSION, buildRiskDescriptor, debitLedger, remainingBudgetMicroUsd, } from "../checkpoint-store.js";
73
71
  import { boundInputHashOf } from "../canonical-json.js";
72
+ import { GLOBAL_USAGE_KEY, resolveUsageWindows, usageRetryAfterMs } from "../usage-window-store.js";
74
73
  const RECONCILE_MAX_RETRIES = 3;
75
74
  const DEFAULT_MAX_SUSPENDS = 5;
76
- const DEFAULT_MAX_SLICES = 100;
75
+ const TASK_LIMIT_KEYS = [
76
+ "maxTokens",
77
+ "maxCostUsd",
78
+ "maxTurns",
79
+ "maxWalltimeMs",
80
+ "maxOutputTokens",
81
+ "approachNotice",
82
+ "budgetStreamCancel",
83
+ "degrade",
84
+ "brainCallGuardrailMs",
85
+ ];
86
+ const NUMERIC_TASK_LIMIT_KEYS = ["maxTokens", "maxCostUsd", "maxTurns", "maxWalltimeMs", "maxOutputTokens"];
87
+ const RETIRED_TASK_LIMIT_KEYS = {
88
+ timeoutSec: "maxWalltimeMs (milliseconds, not seconds)",
89
+ deadlineNudge: "approachNotice",
90
+ callCapByDeadline: "(retired — no replacement)",
91
+ gracefulFinalize: "(retired — the approach notice is the only end-of-budget prompt)",
92
+ };
93
+ function limitConfigError(code, message) {
94
+ const e = new Error(message);
95
+ e.code = code;
96
+ return e;
97
+ }
98
+ export function resolveTaskLimits(limits) {
99
+ if (limits === undefined)
100
+ return undefined;
101
+ if (typeof limits !== "object" || Array.isArray(limits)) {
102
+ throw limitConfigError("config.limit_invalid", `TaskSpec.limits must be an object (got ${Array.isArray(limits) ? "an array" : typeof limits})`);
103
+ }
104
+ const raw = limits;
105
+ const legal = TASK_LIMIT_KEYS;
106
+ for (const key of Object.keys(raw)) {
107
+ if (legal.includes(key))
108
+ continue;
109
+ const replacement = RETIRED_TASK_LIMIT_KEYS[key];
110
+ throw limitConfigError("config.limit_unknown_key", `TaskSpec.limits.${key} is not a limit this engine reads` +
111
+ (replacement !== undefined ? ` — it was replaced by \`${replacement}\`` : "") +
112
+ `. Legal keys: ${TASK_LIMIT_KEYS.join(", ")}. Refused rather than ignored: a limit that is silently dropped reads to the caller as an armed ceiling.`);
113
+ }
114
+ for (const key of NUMERIC_TASK_LIMIT_KEYS) {
115
+ const value = raw[key];
116
+ if (value === undefined)
117
+ continue;
118
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
119
+ throw limitConfigError("config.limit_invalid", `TaskSpec.limits.${key} must be a finite, non-negative number (got ${String(value)}) — an unevaluable ceiling is not a ceiling, and folding it to a default would run the task under a limit nobody chose.`);
120
+ }
121
+ }
122
+ const notice = raw.approachNotice;
123
+ if (notice !== undefined && notice !== false) {
124
+ if (typeof notice !== "object" || notice === null || Array.isArray(notice)) {
125
+ throw limitConfigError("config.limit_invalid", `TaskSpec.limits.approachNotice must be \`false\` or { at?: [first, second] } (got ${String(notice)})`);
126
+ }
127
+ const at = notice.at;
128
+ if (at !== undefined) {
129
+ const ok = Array.isArray(at) &&
130
+ at.length === 2 &&
131
+ at.every((n) => typeof n === "number" && Number.isFinite(n) && n > 0 && n <= 1) &&
132
+ at[0] <= at[1];
133
+ if (!ok) {
134
+ throw limitConfigError("config.limit_invalid", `TaskSpec.limits.approachNotice.at must be two fractions in (0, 1] with first <= second (got ${JSON.stringify(at)})`);
135
+ }
136
+ }
137
+ }
138
+ return limits;
139
+ }
77
140
  const ULTRA_REASONING_TIERS = new Set(["xhigh", "max"]);
78
141
  const DEFAULT_RESOURCE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
142
+ export const ENV_LIFETIME_SUSPEND_MARGIN_MS = 60_000;
143
+ export const USAGE_WINDOW_REAP_MARGIN_MS = 60 * 60 * 1000;
144
+ export function resolveEnvLifetimeExpiry(env, observedAt) {
145
+ const lifetimeMs = env.lifetimeMs;
146
+ if (lifetimeMs === undefined)
147
+ return undefined;
148
+ if (typeof lifetimeMs !== "number" || !Number.isFinite(lifetimeMs) || lifetimeMs <= 0) {
149
+ throw limitConfigError("config.env_lifetime_invalid", `ExecutionEnv.lifetimeMs must be a finite number of milliseconds > 0 (got ${String(lifetimeMs)}) — an unevaluable lifetime is not a lifetime, and treating it as absent would run the task with no protection against the platform reclaiming its environment.`);
150
+ }
151
+ const declaredStart = env.lifetimeStartedAt;
152
+ if (declaredStart !== undefined && (typeof declaredStart !== "number" || !Number.isFinite(declaredStart))) {
153
+ throw limitConfigError("config.env_lifetime_invalid", `ExecutionEnv.lifetimeStartedAt must be a finite epoch-ms timestamp (got ${String(declaredStart)}).`);
154
+ }
155
+ const anchor = declaredStart ?? observedAt;
156
+ if (anchor === undefined)
157
+ return { unanchored: true };
158
+ return { expiresAt: anchor + lifetimeMs };
159
+ }
79
160
  const DEFAULT_UNATTENDED_APPROVAL_TTL_MS = 30 * 24 * 60 * 60 * 1000;
80
161
  function sanitizedTtlMs(ttlMs) {
81
162
  if (ttlMs === undefined)
@@ -237,6 +318,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
237
318
  e.code = "config.empty_objective";
238
319
  throw e;
239
320
  }
321
+ resolveTaskLimits(spec.limits);
322
+ const usageWindows = resolveUsageWindows(deps.usageWindows);
240
323
  const brainCallGuardrailRef = {};
241
324
  const brainCallGuardrailMs = resolveBrainCallGuardrailMs(spec.limits?.brainCallGuardrailMs ?? deps.brainCallGuardrailMs);
242
325
  if (spec.agents !== undefined && spec.agents.length > 0) {
@@ -497,6 +580,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
497
580
  throw e;
498
581
  }
499
582
  let ownedEnv;
583
+ let ownedEnvObservedAt;
500
584
  let mcp;
501
585
  let a2a;
502
586
  if (internals?.requestedCwd !== undefined && deps.executionEnvFactory === undefined) {
@@ -514,6 +598,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
514
598
  ...(internals?.parentCwd ? { parentCwd: internals.parentCwd } : {}),
515
599
  })
516
600
  : undefined;
601
+ if (ownedEnv !== undefined)
602
+ ownedEnvObservedAt = Date.now();
517
603
  }
518
604
  catch (factoryErr) {
519
605
  await settleTeardownLeg(() => forgetOnThrow(), "forgetOnThrow (env-factory-throw leg)", (err) => deps.onError?.(err, { phase: "config", sessionId }));
@@ -552,6 +638,11 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
552
638
  if (spec.signal?.aborted)
553
639
  abortController.abort();
554
640
  try {
641
+ const envLifetime = resolveEnvLifetimeExpiry(executionEnv, ownedEnvObservedAt);
642
+ if (envLifetime !== undefined && "unanchored" in envLifetime) {
643
+ deps.onError?.(new Error("ExecutionEnv.lifetimeMs is declared but NOT enforceable: this env was not minted by the run's executionEnvFactory and reports no `lifetimeStartedAt`, so the engine cannot tell how old it is. The run will NOT suspend before the platform reclaims the environment — have the adapter report `lifetimeStartedAt`."), { phase: "config", sessionId });
644
+ }
645
+ const envLifetimeSuspendAt = envLifetime !== undefined && "expiresAt" in envLifetime ? envLifetime.expiresAt - ENV_LIFETIME_SUSPEND_MARGIN_MS : undefined;
555
646
  if (internals?.isolation === "worktree") {
556
647
  const failIsolation = (reason) => {
557
648
  const e = new Error(`isolation "worktree" was requested but cannot be honored: ${reason}. The sub-agent was not started (fail-closed — no silent fallback to the shared working tree).`);
@@ -1034,12 +1125,12 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1034
1125
  };
1035
1126
  const priorSuspendCount = resume?.priorSuspendCount ?? 0;
1036
1127
  const maxSuspends = spec.maxSuspends ?? deps.maxSuspends ?? DEFAULT_MAX_SUSPENDS;
1037
- const maxSlices = spec.resourceSuspend?.maxSlices ?? DEFAULT_MAX_SLICES;
1128
+ const maxSlices = spec.resourceSuspend?.maxSlices;
1038
1129
  const priorLedger = resume?.priorLedger;
1039
1130
  const resourceTotal = spec.resourceSuspend
1040
1131
  ? {
1041
1132
  totalBudgetMicroUsd: spec.resourceSuspend.totalBudgetUsd !== undefined ? Math.round(spec.resourceSuspend.totalBudgetUsd * 1e6) : undefined,
1042
- totalWalltimeSec: spec.resourceSuspend.totalWalltimeSec,
1133
+ totalTokens: spec.resourceSuspend.totalTokens,
1043
1134
  }
1044
1135
  : undefined;
1045
1136
  const budgetSnapshot = {
@@ -1058,6 +1149,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1058
1149
  let worktreeSessionRef;
1059
1150
  let backgroundTaskToolsActive = false;
1060
1151
  let suspendForResource;
1152
+ let suspendForPlatformLimit;
1061
1153
  let suspendForReview;
1062
1154
  const outputRef = {};
1063
1155
  if (resume?.seed.outputRef) {
@@ -1212,29 +1304,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1212
1304
  }
1213
1305
  tools.push(...a2a.tools.map((t) => remoteToolOffload(t)));
1214
1306
  foldProtocolAxes(a2a.toolAxes, "A2A");
1215
- const callCapOn = spec.limits?.callCapByDeadline !== false;
1216
- const callCapRef = spec.limits?.timeoutSec &&
1217
- spec.limits.timeoutSec > 0 &&
1218
- (callCapOn || spec.limits.gracefulFinalize !== false)
1219
- ? createCallCapRef()
1220
- : undefined;
1221
- if (callCapRef) {
1222
- callCapRef.staticCapTokens =
1223
- spec.limits?.maxOutputTokens !== undefined && spec.limits.maxOutputTokens > 0
1224
- ? spec.limits.maxOutputTokens
1225
- : model.maxTokens;
1226
- }
1227
- const cutKills = callCapRef ? createCutKillRegistry() : undefined;
1228
1307
  const callIssuedAtRef = {};
1229
- const execClamp = callCapRef
1230
- ? {
1231
- deadlineMs: () => softExecDeadlineMs(callCapRef),
1232
- onClamp: () => {
1233
- callCapRef.toolClamps = (callCapRef.toolClamps ?? 0) + 1;
1234
- },
1235
- ...(cutKills ? { registerKillHandle: cutKills.register } : {}),
1236
- }
1237
- : undefined;
1238
1308
  const memoryWriteGateRef = {};
1239
1309
  const additionalRootsCanonical = [];
1240
1310
  let attachmentRootCanonical;
@@ -1317,7 +1387,6 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1317
1387
  ...(sessionId !== undefined ? { sessionId } : {}),
1318
1388
  ...(internals?.onTaskNotification !== undefined ? { taskNotification: internals.onTaskNotification } : {}),
1319
1389
  ...(internals?.detachHub !== undefined ? { detachHub: internals.detachHub } : {}),
1320
- ...(execClamp !== undefined ? { execClamp } : {}),
1321
1390
  pdfModelCapabilities: pdfModelCapabilitiesOf(model),
1322
1391
  ...(deps.hands?.bashReadonlyAllow !== undefined ? { bashReadonlyAllow: deps.hands.bashReadonlyAllow } : {}),
1323
1392
  ...(deps.hands?.commitCoAuthor !== undefined ? { commitCoAuthor: deps.hands.commitCoAuthor } : {}),
@@ -1366,7 +1435,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1366
1435
  if (backgroundTaskToolsActive || workflowToolsActive) {
1367
1436
  toolEffects.set("TaskOutput", "read");
1368
1437
  toolEffects.set("TaskStop", "write");
1369
- tools.push(firstPartyOffload(createTaskOutputTool({ registry: defaultTaskRegistry, owner: hostTaskId, scope: taskScope, sessionId, workflowStore: deps.workflowRunStore, agentStore: deps.backgroundAgentStore, notificationWired: internals?.onTaskNotification !== undefined, oneShot: spec.oneShot, toolResultStore: offloadStore, ...(callCapRef ? { deadlineMs: () => toolCutDeadlineMs(callCapRef, Date.now()) } : {}) })), firstPartyOffload(createTaskStopTool({ registry: defaultTaskRegistry, owner: hostTaskId, scope: taskScope, sessionId, workflowStore: deps.workflowRunStore, agentStore: deps.backgroundAgentStore })));
1438
+ tools.push(firstPartyOffload(createTaskOutputTool({ registry: defaultTaskRegistry, owner: hostTaskId, scope: taskScope, sessionId, workflowStore: deps.workflowRunStore, agentStore: deps.backgroundAgentStore, notificationWired: internals?.onTaskNotification !== undefined, oneShot: spec.oneShot, toolResultStore: offloadStore })), firstPartyOffload(createTaskStopTool({ registry: defaultTaskRegistry, owner: hostTaskId, scope: taskScope, sessionId, workflowStore: deps.workflowRunStore, agentStore: deps.backgroundAgentStore })));
1370
1439
  if (runnerSelf && !(spec.tools ?? []).some((t) => t.name === SEND_MESSAGE_TOOL_NAME)) {
1371
1440
  const delegationForRevive = (spec.tools ?? []).find((t) => t.agentListing !== undefined);
1372
1441
  const reviveSpawn = delegationForRevive !== undefined && deps.backgroundAgentStore !== undefined && deps.mailboxStore !== undefined
@@ -2137,46 +2206,10 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2137
2206
  ...(spec.limits?.maxOutputTokens !== undefined && spec.limits.maxOutputTokens > 0
2138
2207
  ? { maxOutputTokens: spec.limits.maxOutputTokens }
2139
2208
  : {}),
2140
- ...(callCapRef !== undefined && callCapOn
2141
- ? {
2142
- maxOutputTokensPerCall: () => {
2143
- if (callCapRef.reasoningObserved === true)
2144
- return undefined;
2145
- const cap = computeCallCap(callCapRef, Date.now(), (thinking ?? "off") !== "off");
2146
- if (cap !== undefined)
2147
- callCapRef.shrinks = (callCapRef.shrinks ?? 0) + 1;
2148
- return cap;
2149
- },
2150
- }
2151
- : {}),
2152
- ...(callCapRef !== undefined
2153
- ? {
2154
- callDeadlineMsPerCall: () => {
2155
- callIssuedAtRef.current = Date.now();
2156
- return softExecDeadlineMs(callCapRef);
2157
- },
2158
- stallTimeoutsPerCall: () => softExecDeadlineMs(callCapRef) !== undefined
2159
- ? { connectMs: WALLTIME_STALL_CONNECT_MS, firstTokenMs: WALLTIME_STALL_FIRST_TOKEN_MS, idleMs: WALLTIME_STALL_IDLE_MS }
2160
- : undefined,
2161
- }
2162
- : {}),
2163
- ...(callCapRef !== undefined
2164
- ? {
2165
- toolDeadline: {
2166
- deadlineMs: () => toolCutDeadlineMs(callCapRef, Date.now()),
2167
- onCut: (info) => {
2168
- callCapRef.toolCuts = (callCapRef.toolCuts ?? 0) + 1;
2169
- if (callCapRef.finalizeMode === true && cutKills !== undefined) {
2170
- cutKills.killFor(info.toolCallId);
2171
- }
2172
- callCapRef.pendingCutTools = (callCapRef.pendingCutTools ?? 0) + 1;
2173
- void info.settled.then(() => {
2174
- callCapRef.pendingCutTools = (callCapRef.pendingCutTools ?? 0) - 1;
2175
- });
2176
- },
2177
- },
2178
- }
2179
- : {}),
2209
+ stallTimeoutsPerCall: () => {
2210
+ callIssuedAtRef.current = Date.now();
2211
+ return { connectMs: STALL_CONNECT_MS, firstTokenMs: STALL_FIRST_TOKEN_MS, idleMs: STALL_IDLE_MS };
2212
+ },
2180
2213
  ...(spec.resilience !== undefined ? { resilience: spec.resilience } : {}),
2181
2214
  loopTrace: (step) => {
2182
2215
  if (step.kind !== "continue")
@@ -2642,11 +2675,11 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2642
2675
  const blockedTracked = Boolean(hooks?.postToolUse || hooks?.preToolUse || hooks?.postToolUseFailure || hooks?.postToolBatch);
2643
2676
  const restoreSurfaceGap = ownedEnv !== undefined && isRemoteExecutionEnv(ownedEnv) && ownedEnv.capabilities.suspendable ? missingRestoreSurface(ownedEnv) : [];
2644
2677
  const incompleteSuspendAdapter = restoreSurfaceGap.length > 0 ? restoreSurfaceGap : undefined;
2645
- const resourceSuspendEligible = spec.resourceSuspend !== undefined &&
2646
- (spec.checkpointStore ?? deps.checkpointStore) !== undefined &&
2678
+ const durableSuspendInfraReady = (spec.checkpointStore ?? deps.checkpointStore) !== undefined &&
2647
2679
  !(offloadStore !== undefined && isVolatileOffloadStore(offloadStore)) &&
2648
2680
  (ownedEnv === undefined || isRemoteExecutionEnv(ownedEnv)) &&
2649
2681
  incompleteSuspendAdapter === undefined;
2682
+ const resourceSuspendEligible = spec.resourceSuspend !== undefined && durableSuspendInfraReady;
2650
2683
  if (spec.resourceSuspend !== undefined && !resourceSuspendEligible) {
2651
2684
  const why = (spec.checkpointStore ?? deps.checkpointStore) === undefined
2652
2685
  ? "no CheckpointStore is wired"
@@ -2660,7 +2693,33 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2660
2693
  sessionId,
2661
2694
  });
2662
2695
  }
2663
- if (effectivePolicy || hooks?.preToolUse || egressTools.size > 0 || irreversibleTools.size > 0 || resourceSuspendEligible || spec.enablePlanMode === true) {
2696
+ const usageGovernance = (() => {
2697
+ if (usageWindows === undefined || usageWindows.length === 0)
2698
+ return undefined;
2699
+ const store = deps.usageWindowStore;
2700
+ if (store === undefined) {
2701
+ deps.onError?.(new Error("RunnerDeps.usageWindows is set but INACTIVE: no `usageWindowStore` is wired, so cross-task usage cannot be counted and no window can be enforced. Wire InMemoryUsageWindowStore (process-local) or FileUsageWindowStore (restart-surviving)."), { phase: "config", sessionId });
2702
+ return undefined;
2703
+ }
2704
+ const key = spec.principal || GLOBAL_USAGE_KEY;
2705
+ const windows = usageWindows;
2706
+ let charged = 0;
2707
+ return {
2708
+ key,
2709
+ async check(now) {
2710
+ return usageRetryAfterMs(await store.read(key, windows, now));
2711
+ },
2712
+ async commit(cumulativeTokens, now) {
2713
+ const delta = cumulativeTokens - charged;
2714
+ if (delta <= 0)
2715
+ return;
2716
+ await store.charge(key, delta, now, windows);
2717
+ charged = cumulativeTokens;
2718
+ },
2719
+ };
2720
+ })();
2721
+ const platformSuspendArmed = durableSuspendInfraReady && (envLifetimeSuspendAt !== undefined || usageGovernance !== undefined);
2722
+ if (effectivePolicy || hooks?.preToolUse || egressTools.size > 0 || irreversibleTools.size > 0 || resourceSuspendEligible || platformSuspendArmed || spec.enablePlanMode === true) {
2664
2723
  const adjudicate = effectivePolicy
2665
2724
  ? (req) => raceAbort(Promise.resolve(effectivePolicy.check({ ...req, budget: budgetSnapshot }, abortController.signal)), abortController.signal, () => ({
2666
2725
  action: "deny",
@@ -2883,6 +2942,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2883
2942
  ref.scope = scope;
2884
2943
  };
2885
2944
  const suspendLoopCapHit = (count, cap, detail) => {
2945
+ if (cap === undefined)
2946
+ return false;
2886
2947
  if (count + 1 <= cap)
2887
2948
  return false;
2888
2949
  suspendLoopRef.hit = true;
@@ -2903,73 +2964,74 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2903
2964
  const { snapshotId: _lineage, ...identity } = env.workspaceHandle();
2904
2965
  return { ...identity, restoreMode: "park_only" };
2905
2966
  };
2906
- suspendForResource = resourceSuspendEligible
2907
- ? async (reason, sliceSpend) => {
2908
- const rs = spec.resourceSuspend;
2909
- if (!checkpointStore || !rs)
2910
- return false;
2911
- if (abortController.signal.aborted)
2912
- return false;
2913
- if (suspendLoopCapHit(priorLedger?.sliceCount ?? 0, maxSlices, " (resource slice limit)"))
2914
- return false;
2915
- const leafId = await session.getLeafId();
2916
- if (!leafId)
2917
- return false;
2918
- if (abortController.signal.aborted)
2919
- return false;
2920
- let remoteHandle;
2921
- if (suspendableEnv !== undefined) {
2922
- if (hasBackgroundShell(suspendableEnv))
2923
- await sweepBackgroundShells(suspendableEnv, defaultTaskRegistry);
2924
- const snap = await suspendableEnv.suspendVM({ abortSignal: abortController.signal });
2925
- if (!snap.ok) {
2926
- remoteEnvFailures.push(remoteEnvFailureNote("suspendVM", snap.error, 1));
2927
- deps.onError?.(new Error(`resource suspendVM failed (${snap.error.code}): ${snap.error.message}`, { cause: snap.error }), { phase: "config", sessionId });
2928
- return false;
2929
- }
2930
- remoteHandle = { ...suspendableEnv.workspaceHandle(), snapshotId: snap.value };
2931
- }
2932
- else if (parkOnlyRemoteEnv !== undefined) {
2933
- if (hasBackgroundShell(parkOnlyRemoteEnv) && spec.retainBackgroundProcesses !== true)
2934
- await sweepBackgroundShells(parkOnlyRemoteEnv, defaultTaskRegistry);
2935
- remoteHandle = parkOnlyHandle(parkOnlyRemoteEnv);
2936
- }
2937
- const token = mintCheckpointToken();
2938
- const gate = { kind: "resource_limit", reason };
2939
- const resourceLedgerOut = debitLedger(priorLedger, sliceSpend, resourceTotal);
2940
- const mintedAt = Date.now();
2941
- const cp = {
2942
- token,
2943
- version: resourceLedgerOut.totalWalltimeSec !== undefined ? WALLTIME_CHECKPOINT_VERSION : RESOURCE_CHECKPOINT_VERSION,
2944
- scope: rs.scope,
2945
- sessionId,
2946
- leafId,
2947
- gate,
2948
- pendingAction: { kind: "resource_limit", reason },
2949
- state: serializeCheckpointState(remoteHandle),
2950
- status: "pending",
2951
- createdAt: mintedAt,
2952
- deadline: mintedAt + (sanitizedTtlMs(rs.ttlMs) ?? DEFAULT_RESOURCE_TTL_MS),
2953
- suspendCount: priorSuspendCount,
2954
- humanReview: humanReviewRef.count > 0
2955
- ? { count: humanReviewRef.count, totalWaitMs: humanReviewRef.totalWaitMs, gates: [...humanReviewRef.gates] }
2956
- : undefined,
2957
- resourceLedger: resourceLedgerOut,
2958
- ...(spec.durableApproval !== undefined ? { durableApproval: { ...spec.durableApproval } } : {}),
2959
- };
2960
- if (!(await commitSuspendSaga(token, cp, suspendableEnv, remoteHandle)))
2967
+ const mintResourceSuspend = async (reason, sliceSpend, hint) => {
2968
+ const rs = spec.resourceSuspend;
2969
+ if (!checkpointStore)
2970
+ return false;
2971
+ const scope = rs?.scope ?? checkpointScopeOf(spec);
2972
+ if (abortController.signal.aborted)
2973
+ return false;
2974
+ if (suspendLoopCapHit(priorLedger?.sliceCount ?? 0, maxSlices, " (resource slice limit)"))
2975
+ return false;
2976
+ const leafId = await session.getLeafId();
2977
+ if (!leafId)
2978
+ return false;
2979
+ if (abortController.signal.aborted)
2980
+ return false;
2981
+ let remoteHandle;
2982
+ if (suspendableEnv !== undefined) {
2983
+ if (hasBackgroundShell(suspendableEnv))
2984
+ await sweepBackgroundShells(suspendableEnv, defaultTaskRegistry);
2985
+ const snap = await suspendableEnv.suspendVM({ abortSignal: abortController.signal });
2986
+ if (!snap.ok) {
2987
+ remoteEnvFailures.push(remoteEnvFailureNote("suspendVM", snap.error, 1));
2988
+ deps.onError?.(new Error(`resource suspendVM failed (${snap.error.code}): ${snap.error.message}`, { cause: snap.error }), { phase: "config", sessionId });
2961
2989
  return false;
2962
- publishCommittedSuspend(token, gate, rs.scope, remoteHandle);
2963
- try {
2964
- await sessions.pin?.(sessionId);
2965
2990
  }
2966
- catch (pinErr) {
2967
- deps.onError?.(pinErr, { phase: "config", sessionId });
2968
- }
2969
- harness.requestStopAfterTurn();
2970
- return true;
2991
+ remoteHandle = { ...suspendableEnv.workspaceHandle(), snapshotId: snap.value };
2992
+ }
2993
+ else if (parkOnlyRemoteEnv !== undefined) {
2994
+ if (hasBackgroundShell(parkOnlyRemoteEnv) && spec.retainBackgroundProcesses !== true)
2995
+ await sweepBackgroundShells(parkOnlyRemoteEnv, defaultTaskRegistry);
2996
+ remoteHandle = parkOnlyHandle(parkOnlyRemoteEnv);
2997
+ }
2998
+ const token = mintCheckpointToken();
2999
+ const gate = { kind: "resource_limit", reason, ...(hint !== undefined ? { resumeAfterMs: hint.resumeAfterMs } : {}) };
3000
+ const resourceLedgerOut = debitLedger(priorLedger, sliceSpend, resourceTotal);
3001
+ const mintedAt = Date.now();
3002
+ const cp = {
3003
+ token,
3004
+ version: resourceLedgerOut.totalTokens !== undefined ? TOKEN_CHECKPOINT_VERSION : RESOURCE_CHECKPOINT_VERSION,
3005
+ scope,
3006
+ sessionId,
3007
+ leafId,
3008
+ gate,
3009
+ pendingAction: { kind: "resource_limit", reason },
3010
+ state: serializeCheckpointState(remoteHandle),
3011
+ status: "pending",
3012
+ createdAt: mintedAt,
3013
+ deadline: mintedAt + Math.max(sanitizedTtlMs(rs?.ttlMs) ?? DEFAULT_RESOURCE_TTL_MS, hint !== undefined ? hint.resumeAfterMs + USAGE_WINDOW_REAP_MARGIN_MS : 0),
3014
+ suspendCount: priorSuspendCount,
3015
+ humanReview: humanReviewRef.count > 0
3016
+ ? { count: humanReviewRef.count, totalWaitMs: humanReviewRef.totalWaitMs, gates: [...humanReviewRef.gates] }
3017
+ : undefined,
3018
+ resourceLedger: resourceLedgerOut,
3019
+ ...(spec.durableApproval !== undefined ? { durableApproval: { ...spec.durableApproval } } : {}),
3020
+ };
3021
+ if (!(await commitSuspendSaga(token, cp, suspendableEnv, remoteHandle)))
3022
+ return false;
3023
+ publishCommittedSuspend(token, gate, scope, remoteHandle);
3024
+ try {
3025
+ await sessions.pin?.(sessionId);
2971
3026
  }
2972
- : undefined;
3027
+ catch (pinErr) {
3028
+ deps.onError?.(pinErr, { phase: "config", sessionId });
3029
+ }
3030
+ harness.requestStopAfterTurn();
3031
+ return true;
3032
+ };
3033
+ suspendForResource = resourceSuspendEligible ? mintResourceSuspend : undefined;
3034
+ suspendForPlatformLimit = durableSuspendInfraReady ? mintResourceSuspend : undefined;
2973
3035
  suspendForReview = checkpointStore
2974
3036
  ? async (reason) => {
2975
3037
  if (!checkpointStore)
@@ -3021,7 +3083,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3021
3083
  const mintedAt = Date.now();
3022
3084
  const cp = {
3023
3085
  token,
3024
- version: reviewLedger.totalWalltimeSec !== undefined ? WALLTIME_CHECKPOINT_VERSION : BINDING_CHECKPOINT_VERSION,
3086
+ version: reviewLedger.totalTokens !== undefined ? TOKEN_CHECKPOINT_VERSION : BINDING_CHECKPOINT_VERSION,
3025
3087
  scope,
3026
3088
  sessionId,
3027
3089
  leafId,
@@ -3146,7 +3208,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3146
3208
  const mintedAt = Date.now();
3147
3209
  cp = {
3148
3210
  token,
3149
- version: approvalLedger.totalWalltimeSec !== undefined ? WALLTIME_CHECKPOINT_VERSION : BINDING_CHECKPOINT_VERSION,
3211
+ version: approvalLedger.totalTokens !== undefined ? TOKEN_CHECKPOINT_VERSION : BINDING_CHECKPOINT_VERSION,
3150
3212
  scope,
3151
3213
  sessionId,
3152
3214
  leafId,
@@ -3537,7 +3599,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3537
3599
  : undefined;
3538
3600
  overheadState.promptChars = systemPrompt.length;
3539
3601
  const preparedHolder = {};
3540
- const buildPrepared = () => ({ harness, session, sessionId, taskRootPath, model, thinking, compModel, mcp: mcp, ...(a2a !== undefined && a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), releaseSignal, cacheBreakDetector, cacheFingerprint, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), ownedEnv, suspendRef, reviewRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(callCapRef ? { callCapRef } : {}), ...(cutKills ? { cutKills } : {}), ...(callCapRef ? { callIssuedAtRef } : {}), brainCallGuardrailRef, suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolEffects, promptOverheadTokens, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit } } : {}), planModeRef, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, listBackgroundTasks, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
3602
+ const buildPrepared = () => ({ harness, session, sessionId, taskRootPath, model, thinking, compModel, mcp: mcp, ...(a2a !== undefined && a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), releaseSignal, cacheBreakDetector, cacheFingerprint, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), ownedEnv, suspendRef, reviewRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(suspendForPlatformLimit !== undefined ? { suspendForPlatformLimit } : {}), ...(envLifetimeSuspendAt !== undefined ? { envLifetimeSuspendAt } : {}), ...(usageGovernance !== undefined ? { usageGovernance } : {}), callIssuedAtRef, brainCallGuardrailRef, suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolEffects, promptOverheadTokens, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit } } : {}), planModeRef, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, listBackgroundTasks, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
3541
3603
  const prepared = buildPrepared();
3542
3604
  preparedHolder.current = prepared;
3543
3605
  return prepared;
@@ -5,8 +5,6 @@ import { type TaskOutcome } from "../task-outcome.js";
5
5
  import { type SideQuerySpec, type SideQueryResult } from "../side-query.js";
6
6
  import type { SessionStore } from "../session.js";
7
7
  import type { AgentDefinition, RunnerDeps, TaskEvent, TaskResult, TaskSpec, TaskStream } from "../types.js";
8
- import { DEFAULT_MAX_TURNS } from "../../config/defaults.js";
9
- export { DEFAULT_MAX_TURNS };
10
8
  export type ResumeTaskConfig = Omit<TaskSpec, "objective" | "sessionId">;
11
9
  interface ResumeRun {
12
10
  cp: Checkpoint;