@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
@@ -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;