@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
@@ -1,3 +1,4 @@
1
+ import { type QuestionAnswer } from "./ask-question.js";
1
2
  import type { ReadEntry } from "../tools/fs/safety.js";
2
3
  import type { RepairBundle } from "../agents/repair-loop.js";
3
4
  import type { WorkspaceHandle } from "./remote-env.js";
@@ -9,7 +10,8 @@ export type CheckpointToken = string & {
9
10
  readonly __brand: "CheckpointToken";
10
11
  };
11
12
  export declare function mintCheckpointToken(): CheckpointToken;
12
- export type ResourceLimitReason = "budget" | "walltime" | "turns" | "preempt";
13
+ export type ResourceLimitReason = "budget_tokens" | "budget_cost" | "walltime" | "turns" | "preempt" | "env_lifetime" | "usage_window";
14
+ export type PlatformLimitReason = Extract<ResourceLimitReason, "env_lifetime" | "usage_window">;
13
15
  export interface SafetyAxis {
14
16
  egress?: boolean;
15
17
  irreversible?: boolean;
@@ -52,6 +54,7 @@ export type CheckpointGate = {
52
54
  } | {
53
55
  kind: "resource_limit";
54
56
  reason: ResourceLimitReason;
57
+ resumeAfterMs?: number;
55
58
  } | {
56
59
  kind: "needs_review";
57
60
  reason: string;
@@ -68,6 +71,7 @@ export type ResumeOutcome = {
68
71
  decision: "allow" | "deny";
69
72
  updatedInput?: unknown;
70
73
  reason?: string;
74
+ answer?: QuestionAnswer;
71
75
  } | {
72
76
  gate: "resource_limit";
73
77
  decision: "continue";
@@ -151,12 +155,12 @@ export type SerializedCheckpointState = {
151
155
  export declare const CURRENT_CHECKPOINT_VERSION = 1;
152
156
  export declare const RESOURCE_CHECKPOINT_VERSION = 2;
153
157
  export declare const BINDING_CHECKPOINT_VERSION = 3;
154
- export declare const WALLTIME_CHECKPOINT_VERSION = 4;
155
- export declare const MAX_SUPPORTED_CHECKPOINT_VERSION = 4;
158
+ export declare const TOKEN_CHECKPOINT_VERSION = 5;
159
+ export declare const MAX_SUPPORTED_CHECKPOINT_VERSION = 5;
156
160
  export declare function checkpointVersionOf(cp: Pick<Checkpoint, "version">): number;
157
161
  export interface ResourceLedger {
158
162
  totalBudgetMicroUsd?: number;
159
- totalWalltimeSec?: number;
163
+ totalTokens?: number;
160
164
  spentMicroUsd: number;
161
165
  spentTokens: number;
162
166
  spentTurns: number;
@@ -170,16 +174,17 @@ export declare function debitLedger(prior: ResourceLedger | undefined, slice: {
170
174
  walltimeMs?: number;
171
175
  }, total?: {
172
176
  totalBudgetMicroUsd?: number;
173
- totalWalltimeSec?: number;
177
+ totalTokens?: number;
174
178
  }, opts?: {
175
179
  countSlice?: boolean;
176
180
  }): ResourceLedger;
177
181
  export declare function remainingBudgetMicroUsd(ledger: ResourceLedger | undefined): number | undefined;
178
- export declare function remainingWalltimeMs(ledger: ResourceLedger | undefined): number | undefined;
182
+ export declare function remainingTokens(ledger: ResourceLedger | undefined): number | undefined;
179
183
  export interface ResolvedOutcome {
180
184
  boundCallId: string;
181
185
  decision: "allow" | "deny" | "approve" | "reject" | "edit";
182
186
  updatedInput?: unknown;
187
+ answer?: QuestionAnswer;
183
188
  }
184
189
  export type ReopenReason = "env_failed" | "tool_unavailable";
185
190
  export interface ResolveExpectation {
@@ -239,12 +244,12 @@ export interface CheckpointSummary {
239
244
  }
240
245
  export declare function summarizeCheckpoint(cp: Checkpoint): CheckpointSummary;
241
246
  export declare class CheckpointError extends Error {
242
- readonly code: "checkpoint.already_exists" | "checkpoint.already_resolved" | "checkpoint.not_found" | "checkpoint.gate_mismatch" | "checkpoint.resume_aborted" | "checkpoint.invalid_outcome" | "checkpoint.unsupported_version" | "checkpoint.reopen_revote" | "checkpoint.reopened_concurrently" | "checkpoint.reopen_failed" | "steering.invalid_content" | "wake.gate_pending" | "wake.nothing_to_deliver" | "resume.parent_constraint_missing" | "resume.parent_constraint_mismatch";
247
+ readonly code: "checkpoint.already_exists" | "checkpoint.already_resolved" | "checkpoint.not_found" | "checkpoint.gate_mismatch" | "checkpoint.resume_aborted" | "checkpoint.invalid_outcome" | "checkpoint.unsupported_version" | "checkpoint.walltime_axis_retired" | "checkpoint.reopen_revote" | "checkpoint.reopened_concurrently" | "checkpoint.reopen_failed" | "steering.invalid_content" | "wake.gate_pending" | "wake.nothing_to_deliver" | "resume.parent_constraint_missing" | "resume.parent_constraint_mismatch";
243
248
  readonly detail?: {
244
- field?: "boundCallId" | "boundInputHash";
249
+ field?: "boundCallId" | "boundInputHash" | "answer";
245
250
  } | undefined;
246
- constructor(code: "checkpoint.already_exists" | "checkpoint.already_resolved" | "checkpoint.not_found" | "checkpoint.gate_mismatch" | "checkpoint.resume_aborted" | "checkpoint.invalid_outcome" | "checkpoint.unsupported_version" | "checkpoint.reopen_revote" | "checkpoint.reopened_concurrently" | "checkpoint.reopen_failed" | "steering.invalid_content" | "wake.gate_pending" | "wake.nothing_to_deliver" | "resume.parent_constraint_missing" | "resume.parent_constraint_mismatch", message: string, detail?: {
247
- field?: "boundCallId" | "boundInputHash";
251
+ constructor(code: "checkpoint.already_exists" | "checkpoint.already_resolved" | "checkpoint.not_found" | "checkpoint.gate_mismatch" | "checkpoint.resume_aborted" | "checkpoint.invalid_outcome" | "checkpoint.unsupported_version" | "checkpoint.walltime_axis_retired" | "checkpoint.reopen_revote" | "checkpoint.reopened_concurrently" | "checkpoint.reopen_failed" | "steering.invalid_content" | "wake.gate_pending" | "wake.nothing_to_deliver" | "resume.parent_constraint_missing" | "resume.parent_constraint_mismatch", message: string, detail?: {
252
+ field?: "boundCallId" | "boundInputHash" | "answer";
248
253
  } | undefined);
249
254
  }
250
255
  export interface CheckpointStore {
@@ -126,8 +126,8 @@ export function buildRiskDescriptor(input) {
126
126
  export const CURRENT_CHECKPOINT_VERSION = 1;
127
127
  export const RESOURCE_CHECKPOINT_VERSION = 2;
128
128
  export const BINDING_CHECKPOINT_VERSION = 3;
129
- export const WALLTIME_CHECKPOINT_VERSION = 4;
130
- export const MAX_SUPPORTED_CHECKPOINT_VERSION = 4;
129
+ export const TOKEN_CHECKPOINT_VERSION = 5;
130
+ export const MAX_SUPPORTED_CHECKPOINT_VERSION = 5;
131
131
  export function checkpointVersionOf(cp) {
132
132
  return cp.version ?? 0;
133
133
  }
@@ -140,7 +140,7 @@ function finiteSpend(v) {
140
140
  export function debitLedger(prior, slice, total, opts) {
141
141
  return {
142
142
  totalBudgetMicroUsd: finiteBound(prior?.totalBudgetMicroUsd) ?? finiteBound(total?.totalBudgetMicroUsd),
143
- totalWalltimeSec: finiteBound(prior?.totalWalltimeSec) ?? finiteBound(total?.totalWalltimeSec),
143
+ totalTokens: finiteBound(prior?.totalTokens) ?? finiteBound(total?.totalTokens),
144
144
  spentMicroUsd: (finiteSpend(prior?.spentMicroUsd) ?? 0) + Math.max(0, finiteSpend(slice.costMicroUsd) ?? 0),
145
145
  spentTokens: (finiteSpend(prior?.spentTokens) ?? 0) + Math.max(0, finiteSpend(slice.tokens) ?? 0),
146
146
  spentTurns: (finiteSpend(prior?.spentTurns) ?? 0) + Math.max(0, finiteSpend(slice.turns) ?? 0),
@@ -157,14 +157,14 @@ export function remainingBudgetMicroUsd(ledger) {
157
157
  return 0;
158
158
  return Math.max(0, total - (spent ?? 0));
159
159
  }
160
- export function remainingWalltimeMs(ledger) {
161
- const total = ledger?.totalWalltimeSec;
160
+ export function remainingTokens(ledger) {
161
+ const total = ledger?.totalTokens;
162
162
  if (total === undefined || !Number.isFinite(total))
163
163
  return undefined;
164
- const spent = ledger?.spentWalltimeMs;
164
+ const spent = ledger?.spentTokens;
165
165
  if (spent !== undefined && !Number.isFinite(spent))
166
166
  return 0;
167
- return Math.max(0, total * 1000 - (spent ?? 0));
167
+ return Math.max(0, total - (spent ?? 0));
168
168
  }
169
169
  export function summarizeCheckpoint(cp) {
170
170
  const riskDescriptor = cp.gate.kind === "human" || cp.gate.kind === "irreversible_ask" ? cp.gate.riskDescriptor : undefined;
@@ -223,9 +223,12 @@ export function winnerFromOutcome(outcome) {
223
223
  }
224
224
  if (outcome.gate !== "policy_ask")
225
225
  return undefined;
226
- return outcome.updatedInput === undefined
227
- ? { boundCallId: outcome.boundCallId, decision: outcome.decision }
228
- : { boundCallId: outcome.boundCallId, decision: outcome.decision, updatedInput: outcome.updatedInput };
226
+ return {
227
+ boundCallId: outcome.boundCallId,
228
+ decision: outcome.decision,
229
+ ...(outcome.updatedInput === undefined ? {} : { updatedInput: outcome.updatedInput }),
230
+ ...(outcome.answer === undefined ? {} : { answer: outcome.answer }),
231
+ };
229
232
  }
230
233
  export function validatePendingSteer(steer) {
231
234
  if (sanitizeUntrustedText(steer.text) !== steer.text) {
@@ -487,28 +487,31 @@ export class FileMemoryEngineBackend {
487
487
  mtimeMs = statSync(lockDir).mtimeMs;
488
488
  }
489
489
  catch {
490
- continue;
490
+ mtimeMs = undefined;
491
491
  }
492
- const staleAt = mtimeMs + staleMs;
493
- deadline = Math.max(deadline, staleAt + stealGraceMs);
494
- if (Date.now() > staleAt) {
495
- let tokenPresent = false;
496
- try {
497
- statSync(ownerPath);
498
- tokenPresent = true;
499
- }
500
- catch {
501
- }
502
- if (tokenPresent) {
503
- const tomb = join(this.controlPlaneRoot, `txn.lock.stale-${randomUUID()}`);
492
+ if (mtimeMs !== undefined) {
493
+ const staleAt = mtimeMs + staleMs;
494
+ deadline = Math.max(deadline, staleAt + stealGraceMs);
495
+ if (Date.now() > staleAt) {
496
+ let tokenPresent = false;
504
497
  try {
505
- renameSync(lockDir, tomb);
498
+ statSync(ownerPath);
499
+ tokenPresent = true;
506
500
  }
507
501
  catch {
508
- continue;
509
502
  }
510
- rmSync(tomb, { recursive: true, force: true });
511
- continue;
503
+ if (tokenPresent) {
504
+ const tomb = join(this.controlPlaneRoot, `txn.lock.stale-${randomUUID()}`);
505
+ let stolen = false;
506
+ try {
507
+ renameSync(lockDir, tomb);
508
+ stolen = true;
509
+ }
510
+ catch {
511
+ }
512
+ if (stolen)
513
+ rmSync(tomb, { recursive: true, force: true });
514
+ }
512
515
  }
513
516
  }
514
517
  if (Date.now() > deadline) {
@@ -41,6 +41,11 @@ export declare const ANNOUNCEMENTS_FILE = "announcements.json";
41
41
  export declare const MEMORY_ANNOUNCEMENTS_MAX = 20;
42
42
  export declare const SCAN_FUSE_FILE = "scan-fuse.json";
43
43
  export declare const SCAN_FUSE_THRESHOLD = 3;
44
+ export interface SidecarLockOptions {
45
+ onDeadline?: "proceed" | "throw";
46
+ waitCapMs?: number;
47
+ }
48
+ export declare function acquireSidecarLock(lockDir: string, opts?: SidecarLockOptions): string;
44
49
  interface AnnouncementsRecord {
45
50
  folded: number;
46
51
  queue: MemoryAnnouncement[];
@@ -1,4 +1,4 @@
1
- import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
1
+ import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { createHash } from "node:crypto";
4
4
  import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
@@ -36,6 +36,37 @@ function subtreeHasFiles(dir) {
36
36
  }
37
37
  return false;
38
38
  }
39
+ function pruneFilelessDirTree(dir) {
40
+ let entries;
41
+ try {
42
+ entries = readdirSync(dir, { withFileTypes: true });
43
+ }
44
+ catch {
45
+ return !existsSync(dir);
46
+ }
47
+ for (const e of entries) {
48
+ if (!e.isDirectory())
49
+ return false;
50
+ if (!pruneFilelessDirTree(join(dir, e.name)))
51
+ return false;
52
+ }
53
+ try {
54
+ rmdirSync(dir);
55
+ return true;
56
+ }
57
+ catch {
58
+ return false;
59
+ }
60
+ }
61
+ function tryAdoptByRename(fromDir, toDir) {
62
+ try {
63
+ renameSync(fromDir, toDir);
64
+ return { ok: true };
65
+ }
66
+ catch (err) {
67
+ return { ok: false, code: err.code, error: err };
68
+ }
69
+ }
39
70
  function migrateKeyedPartition(m) {
40
71
  const disclose = (code, message) => {
41
72
  try {
@@ -52,36 +83,26 @@ function migrateKeyedPartition(m) {
52
83
  return;
53
84
  if (!existsSync(m.fromDir))
54
85
  return;
55
- if (existsSync(m.toDir)) {
56
- if (subtreeHasFiles(m.toDir)) {
57
- disclose("memory.partition_split", splitText);
58
- return;
59
- }
60
- try {
61
- rmSync(m.toDir, { recursive: true });
62
- }
63
- catch (err) {
64
- const code = err.code;
65
- disclose("memory.partition_adopt_failed", `could not clear the empty directory scaffolding at ${m.toDir} before adopting ${m.fromDir} (${code ?? String(err)}): ` +
66
- `this session proceeds on ${m.toDir}; ${m.fromDir} still holds the prior memory — move it by hand.`);
67
- return;
68
- }
86
+ if (existsSync(m.toDir) && subtreeHasFiles(m.toDir)) {
87
+ disclose("memory.partition_split", splitText);
88
+ return;
69
89
  }
70
- try {
71
- ensureDirExists(dirname(m.toDir));
72
- renameSync(m.fromDir, m.toDir);
73
- }
74
- catch (err) {
75
- const code = err.code;
76
- if ((code === "ENOENT" || code === "EEXIST" || code === "ENOTEMPTY") && existsSync(m.toDir)) {
77
- if (!existsSync(m.fromDir))
78
- return;
79
- disclose("memory.partition_split", splitText);
90
+ ensureDirExists(dirname(m.toDir));
91
+ let outcome = tryAdoptByRename(m.fromDir, m.toDir);
92
+ if (!outcome.ok && (outcome.code === "EEXIST" || outcome.code === "ENOTEMPTY") && !subtreeHasFiles(m.toDir) && pruneFilelessDirTree(m.toDir)) {
93
+ outcome = tryAdoptByRename(m.fromDir, m.toDir);
94
+ }
95
+ if (outcome.ok)
96
+ return;
97
+ const { code, error } = outcome;
98
+ if ((code === "ENOENT" || code === "EEXIST" || code === "ENOTEMPTY") && existsSync(m.toDir)) {
99
+ if (!existsSync(m.fromDir))
80
100
  return;
81
- }
82
- disclose("memory.partition_adopt_failed", `could not adopt the memory partition ${m.fromDir} → ${m.toDir} (${code ?? String(err)}): this session proceeds on ${m.toDir}; ` +
83
- `${m.fromDir} still holds the prior memory — move it by hand.`);
101
+ disclose("memory.partition_split", splitText);
102
+ return;
84
103
  }
104
+ disclose("memory.partition_adopt_failed", `could not adopt the memory partition ${m.fromDir} → ${m.toDir} (${code ?? String(error)}): this session proceeds on ${m.toDir}; ` +
105
+ `${m.fromDir} still holds the prior memory — move it by hand.`);
85
106
  }
86
107
  catch {
87
108
  }
@@ -338,8 +359,13 @@ function sleepSync(ms) {
338
359
  }
339
360
  const LOCK_STALE_MS = 1_500;
340
361
  const LOCK_WAIT_CAP_MS = 3_000;
341
- function acquireSidecarLock(lockDir, opts = {}) {
342
- const deadline = Date.now() + LOCK_WAIT_CAP_MS;
362
+ const LOCK_SPIN_PAUSE_MS = 5;
363
+ const MAX_FAIL_OPEN_DEADLINE_ROUNDS = 3;
364
+ export function acquireSidecarLock(lockDir, opts = {}) {
365
+ const waitCapMs = opts.waitCapMs ?? LOCK_WAIT_CAP_MS;
366
+ const deadline = Date.now() + waitCapMs;
367
+ const mintToken = () => `${process.pid}-${Math.random().toString(36).slice(2, 10)}`;
368
+ let deadlineRounds = 0;
343
369
  const stealOnce = () => {
344
370
  const tomb = `${lockDir}.stale-${process.pid}-${Math.random().toString(36).slice(2, 8)}`;
345
371
  try {
@@ -354,7 +380,7 @@ function acquireSidecarLock(lockDir, opts = {}) {
354
380
  for (;;) {
355
381
  try {
356
382
  mkdirSync(lockDir);
357
- const token = `${process.pid}-${Math.random().toString(36).slice(2, 10)}`;
383
+ const token = mintToken();
358
384
  let fd;
359
385
  try {
360
386
  fd = openSync(join(lockDir, "owner"), "w");
@@ -374,25 +400,24 @@ function acquireSidecarLock(lockDir, opts = {}) {
374
400
  return token;
375
401
  }
376
402
  catch {
377
- try {
378
- const age = Date.now() - statSync(lockDir).mtimeMs;
379
- if (age > LOCK_STALE_MS) {
380
- stealOnce();
381
- continue;
382
- }
383
- }
384
- catch {
385
- continue;
386
- }
387
- if (Date.now() > deadline) {
388
- if (opts.onDeadline === "throw") {
389
- throw new Error(`sidecar lock wait timed out (${LOCK_WAIT_CAP_MS}ms) at ${lockDir} — refusing to proceed without the lock (fail-closed sidecar)`);
390
- }
403
+ }
404
+ try {
405
+ const age = Date.now() - statSync(lockDir).mtimeMs;
406
+ if (age > LOCK_STALE_MS)
391
407
  stealOnce();
392
- continue;
408
+ }
409
+ catch {
410
+ }
411
+ if (Date.now() > deadline) {
412
+ if (opts.onDeadline === "throw") {
413
+ throw new Error(`sidecar lock wait timed out (${waitCapMs}ms) at ${lockDir} — refusing to proceed without the lock (fail-closed sidecar)`);
393
414
  }
394
- sleepSync(5);
415
+ deadlineRounds += 1;
416
+ if (deadlineRounds > MAX_FAIL_OPEN_DEADLINE_ROUNDS)
417
+ return mintToken();
418
+ stealOnce();
395
419
  }
420
+ sleepSync(LOCK_SPIN_PAUSE_MS);
396
421
  }
397
422
  }
398
423
  function rollForwardSidecar(file, journal) {
@@ -30,14 +30,6 @@ export interface Stats {
30
30
  }>;
31
31
  };
32
32
  mechanisms?: {
33
- finalizeInjected?: true;
34
- nudgesSent?: number;
35
- capShrinks?: number;
36
- callCutoffs?: number;
37
- toolClamps?: number;
38
- toolCuts?: number;
39
- toolCutKills?: number;
40
- pendingCutTools?: number;
41
33
  finalVerifyInjected?: true;
42
34
  finalVerifyInjections?: number;
43
35
  attachmentsInjected?: number;
@@ -60,6 +52,7 @@ export interface Stats {
60
52
  compactionMicroUsd: number;
61
53
  };
62
54
  }
55
+ export type BudgetAxis = "tokens" | "cost";
63
56
  export interface ResultFlags {
64
57
  threw: unknown;
65
58
  model?: string;
@@ -70,6 +63,7 @@ export interface ResultFlags {
70
63
  abortedForTurns: boolean;
71
64
  abortedLive?: boolean;
72
65
  budgetHit?: "exceeded" | "precall";
66
+ budgetAxis?: BudgetAxis;
73
67
  blockedReason?: string;
74
68
  conflict?: boolean;
75
69
  outputInvalid?: boolean;
@@ -1,4 +1,4 @@
1
- import { isDegenerateCutMessage, isWalltimeCutMessage } from "../../brain/terminal-cause.js";
1
+ import { isDegenerateCutMessage } from "../../brain/terminal-cause.js";
2
2
  import { extractErrorCode, stripErrorCodePrefix } from "../../brain/errors.js";
3
3
  export function errorCodeOf(err) {
4
4
  let cur = err;
@@ -54,7 +54,6 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
54
54
  let checkpointGate;
55
55
  let workspaceRestoreMode;
56
56
  const isDegenerate = final?.stopReason === "error" && isDegenerateCutMessage(final);
57
- const isWalltimeCutoff = final?.stopReason === "error" && isWalltimeCutMessage(final);
58
57
  if (flags.outputInvalid) {
59
58
  status = "failed";
60
59
  errorCode = "output.invalid";
@@ -66,19 +65,15 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
66
65
  errorMessage = final?.errorMessage;
67
66
  salvagedOutput = text.trim() || undefined;
68
67
  }
69
- else if (isWalltimeCutoff) {
70
- status = "timeout";
71
- errorCode = "limit.timeout";
72
- errorMessage = final?.errorMessage;
73
- salvagedOutput = text.trim() || undefined;
74
- }
75
68
  else if (flags.budgetHit) {
76
69
  status = "failed";
77
- errorCode = flags.budgetHit === "precall" ? "budget.precall" : "budget.exceeded";
70
+ errorCode = flags.budgetAxis === "tokens" ? "limits.max_tokens_exceeded" : "limits.max_cost_exceeded";
71
+ const axisName = flags.budgetAxis === "tokens" ? "limits.maxTokens" : "limits.maxCostUsd";
78
72
  errorMessage =
79
73
  flags.budgetHit === "precall"
80
- ? "estimated cost exceeds maxCostUsd; task not started"
81
- : "cumulative cost or tokens exceeded the task budget";
74
+ ? `the estimated cost of the first call already exceeds ${axisName}; the task was not started`
75
+ : `cumulative usage exceeded ${axisName}`;
76
+ salvagedOutput = text.trim() || undefined;
82
77
  }
83
78
  else if (flags.suspendLoop) {
84
79
  status = "failed";
@@ -86,15 +81,17 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
86
81
  errorMessage = "task suspended too many times (resume/restart loop) — exceeded the suspend limit";
87
82
  }
88
83
  else if (flags.threw) {
89
- status = flags.abortedForTimeout ? "timeout" : "failed";
84
+ status = "failed";
90
85
  errorMessage = flags.threw instanceof Error ? flags.threw.message : String(flags.threw);
91
86
  errorCode = flags.abortedForTimeout
92
- ? "limit.timeout"
87
+ ? "limits.max_walltime_exceeded"
93
88
  : flags.abortedForTurns
94
- ? "limit.max_turns"
89
+ ? "limits.max_turns_exceeded"
95
90
  : flags.conflict
96
91
  ? "conflict"
97
92
  : errorCodeOf(flags.threw);
93
+ if (flags.abortedForTimeout || flags.abortedForTurns)
94
+ salvagedOutput = text.trim() || undefined;
98
95
  }
99
96
  else if (flags.blockedReason) {
100
97
  status = "blocked";
@@ -114,9 +111,11 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
114
111
  workspaceRestoreMode = flags.reviewRef.restoreMode;
115
112
  }
116
113
  else if (flags.abortedLive || final?.stopReason === "aborted") {
117
- status = flags.abortedForTimeout ? "timeout" : "failed";
118
- errorCode = flags.abortedForTimeout ? "limit.timeout" : flags.abortedForTurns ? "limit.max_turns" : undefined;
114
+ status = "failed";
115
+ errorCode = flags.abortedForTimeout ? "limits.max_walltime_exceeded" : flags.abortedForTurns ? "limits.max_turns_exceeded" : undefined;
119
116
  errorMessage = final?.errorMessage ?? (flags.abortedForTurns ? "max turns exceeded" : "run aborted");
117
+ if (flags.abortedForTimeout || flags.abortedForTurns)
118
+ salvagedOutput = text.trim() || undefined;
120
119
  }
121
120
  else if (!final) {
122
121
  status = "failed";
@@ -8,7 +8,6 @@ import type { SessionStore } from "../session.js";
8
8
  import { SubagentRetainLedger } from "../../agents/retain-ledger.js";
9
9
  import type { OnAsk, ToolPolicy } from "../tool-policy.js";
10
10
  import { type ActiveSkillFrame } from "./active-skill-scope.js";
11
- import { type CutKillRegistry } from "./cut-kill.js";
12
11
  import type { SessionPermissionRules } from "../session-policy-store.js";
13
12
  import { CacheBreakDetector, type ToolFingerprintInput } from "../cache-break-detector.js";
14
13
  import { type BrainCallGuardrailRef } from "../../brain/timeout.js";
@@ -19,10 +18,23 @@ import type { TaskNotificationPayload } from "../task-notification.js";
19
18
  import { type CwdRef } from "../../tools/fs/index.js";
20
19
  import { type WorkflowSizeGuideline } from "../../orchestration/workflow-size-guideline.js";
21
20
  import type { Runner } from "./runtask.js";
22
- import { type CheckpointGate, type CheckpointState, type CheckpointToken, type ResourceLedger, type ResourceLimitReason } from "../checkpoint-store.js";
21
+ import { type CheckpointGate, type CheckpointState, type CheckpointToken, type ResourceLedger, type PlatformLimitReason, type ResourceLimitReason } from "../checkpoint-store.js";
23
22
  import type { ActiveWorktreeSession, AgentMessage, AgentTool, ExecutionEnv } from "../../internal/harness.js";
24
- import type { NestedUsageAccum, RunnerDeps, TaskEvent, TaskResult, TaskSpec, ToolActivity, ToolEffect } from "../types.js";
23
+ import type { NestedUsageAccum, RunnerDeps, TaskEvent, TaskLimits, TaskResult, TaskSpec, ToolActivity, ToolEffect } from "../types.js";
25
24
  import type { RepairBundle } from "../../agents/repair-loop.js";
25
+ export declare function resolveTaskLimits(limits: TaskLimits | undefined): TaskLimits | undefined;
26
+ export declare const ENV_LIFETIME_SUSPEND_MARGIN_MS = 60000;
27
+ export declare const USAGE_WINDOW_REAP_MARGIN_MS: number;
28
+ export interface UsageGovernance {
29
+ readonly key: string;
30
+ check(now: number): Promise<number | undefined>;
31
+ commit(cumulativeTokens: number, now: number): Promise<void>;
32
+ }
33
+ export declare function resolveEnvLifetimeExpiry(env: ExecutionEnv, observedAt: number | undefined): {
34
+ expiresAt: number;
35
+ } | {
36
+ unanchored: true;
37
+ } | undefined;
26
38
  export declare const DEFAULT_IRREVERSIBLE_SCOPE = "irreversible";
27
39
  export declare function checkpointScopeOf(spec: {
28
40
  durableApproval?: {
@@ -132,9 +144,17 @@ export interface Prepared {
132
144
  turns: number;
133
145
  walltimeMs: number;
134
146
  }) => Promise<boolean>;
135
- callCapRef?: import("./call-cap.js").CallCapRef;
136
- cutKills?: CutKillRegistry;
137
- callIssuedAtRef?: {
147
+ suspendForPlatformLimit?: (reason: PlatformLimitReason, sliceSpend: {
148
+ costMicroUsd: number;
149
+ tokens: number;
150
+ turns: number;
151
+ walltimeMs: number;
152
+ }, hint?: {
153
+ resumeAfterMs: number;
154
+ }) => Promise<boolean>;
155
+ envLifetimeSuspendAt?: number;
156
+ usageGovernance?: UsageGovernance;
157
+ callIssuedAtRef: {
138
158
  current?: number;
139
159
  };
140
160
  brainCallGuardrailRef: BrainCallGuardrailRef;