@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
@@ -17,7 +17,6 @@ const DEFAULTS = {
17
17
  maxScriptChars: 100_000,
18
18
  maxAgents: 50,
19
19
  totalTimeoutMs: 600_000,
20
- perAgentTimeoutSec: 300,
21
20
  maxResultChars: 100_000,
22
21
  maxLogChars: 10_000,
23
22
  };
@@ -141,7 +140,7 @@ export async function createRunWorkflowTool(d) {
141
140
  const lim = d.limits ?? {};
142
141
  const maxScriptChars = lim.maxScriptChars ?? DEFAULTS.maxScriptChars;
143
142
  const childCaps = {
144
- perAgentTimeoutSec: lim.perAgentTimeoutSec ?? DEFAULTS.perAgentTimeoutSec,
143
+ perAgentMaxWalltimeMs: lim.perAgentMaxWalltimeMs,
145
144
  childMaxCostUsd: lim.childMaxCostUsd,
146
145
  childMaxTokens: lim.childMaxTokens,
147
146
  childMaxTurns: lim.childMaxTurns,
@@ -13,20 +13,20 @@ export interface WorkflowAgentSpec {
13
13
  images?: ImageInput[];
14
14
  limits?: {
15
15
  maxTurns?: number;
16
- timeoutSec?: number;
16
+ maxWalltimeMs?: number;
17
+ maxTokens?: number;
18
+ maxCostUsd?: number;
17
19
  };
18
- maxCostUsd?: number;
19
- maxTokens?: number;
20
20
  }
21
- export declare const WHITELIST_KEYS: readonly ["objective", "modelName", "thinking", "systemPrompt", "images", "limits", "maxCostUsd", "maxTokens"];
21
+ export declare const WHITELIST_KEYS: readonly ["objective", "modelName", "thinking", "systemPrompt", "images", "limits"];
22
22
  export interface WorkflowChildCaps {
23
- perAgentTimeoutSec?: number;
23
+ perAgentMaxWalltimeMs?: number;
24
24
  childMaxCostUsd?: number;
25
25
  childMaxTokens?: number;
26
26
  childMaxTurns?: number;
27
27
  }
28
28
  export interface ResourceClampNote {
29
- field: "maxCostUsd" | "maxTokens" | "timeoutSec" | "maxTurns";
29
+ field: "maxCostUsd" | "maxTokens" | "maxWalltimeMs" | "maxTurns";
30
30
  requested: number | undefined;
31
31
  applied: number;
32
32
  }
@@ -17,8 +17,6 @@ export const WHITELIST_KEYS = [
17
17
  "systemPrompt",
18
18
  "images",
19
19
  "limits",
20
- "maxCostUsd",
21
- "maxTokens",
22
20
  ];
23
21
  const VALID_THINKING = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
24
22
  export function resolveModelName(name, allowlist, models) {
@@ -91,36 +89,26 @@ function pickWhitelist(scriptSpec) {
91
89
  }
92
90
  if (s.limits !== undefined) {
93
91
  if (typeof s.limits !== "object" || s.limits === null || Array.isArray(s.limits)) {
94
- throw new WorkflowScriptError("agent(spec): `limits` must be an object { maxTurns?, timeoutSec? }");
92
+ throw new WorkflowScriptError("agent(spec): `limits` must be an object { maxTurns?, maxWalltimeMs?, maxTokens?, maxCostUsd? }");
95
93
  }
96
94
  const l = s.limits;
97
95
  const limits = {};
98
- if (l.maxTurns !== undefined) {
99
- if (typeof l.maxTurns !== "number" || !Number.isFinite(l.maxTurns))
100
- throw new WorkflowScriptError("agent(spec): `limits.maxTurns` must be a number");
101
- if (l.maxTurns <= 0)
102
- throw new WorkflowScriptError(NON_POSITIVE_LIMIT_MSG("maxTurns", "turn cap"));
103
- limits.maxTurns = l.maxTurns;
104
- }
105
- if (l.timeoutSec !== undefined) {
106
- if (typeof l.timeoutSec !== "number" || !Number.isFinite(l.timeoutSec))
107
- throw new WorkflowScriptError("agent(spec): `limits.timeoutSec` must be a number");
108
- if (l.timeoutSec <= 0)
109
- throw new WorkflowScriptError(NON_POSITIVE_LIMIT_MSG("timeoutSec", "wall-clock window"));
110
- limits.timeoutSec = l.timeoutSec;
111
- }
96
+ const readAxis = (field, what) => {
97
+ const raw = l[field];
98
+ if (raw === undefined)
99
+ return;
100
+ if (typeof raw !== "number" || !Number.isFinite(raw))
101
+ throw new WorkflowScriptError(`agent(spec): \`limits.${field}\` must be a number`);
102
+ if (raw <= 0)
103
+ throw new WorkflowScriptError(NON_POSITIVE_LIMIT_MSG(field, what));
104
+ limits[field] = raw;
105
+ };
106
+ readAxis("maxTurns", "turn cap");
107
+ readAxis("maxWalltimeMs", "wall-clock window");
108
+ readAxis("maxTokens", "token budget");
109
+ readAxis("maxCostUsd", "spend ceiling");
112
110
  safe.limits = limits;
113
111
  }
114
- if (s.maxCostUsd !== undefined) {
115
- if (typeof s.maxCostUsd !== "number" || !Number.isFinite(s.maxCostUsd))
116
- throw new WorkflowScriptError("agent(spec): `maxCostUsd` must be a number");
117
- safe.maxCostUsd = s.maxCostUsd;
118
- }
119
- if (s.maxTokens !== undefined) {
120
- if (typeof s.maxTokens !== "number" || !Number.isFinite(s.maxTokens))
121
- throw new WorkflowScriptError("agent(spec): `maxTokens` must be a number");
122
- safe.maxTokens = s.maxTokens;
123
- }
124
112
  let modelName;
125
113
  if (s.modelName !== undefined) {
126
114
  if (typeof s.modelName !== "string")
@@ -131,37 +119,35 @@ function pickWhitelist(scriptSpec) {
131
119
  }
132
120
  function clampResourceLimits(safe, base, caps) {
133
121
  const notes = [];
134
- const requestedCost = safe.maxCostUsd;
135
- const cost = minDefined(safe.maxCostUsd, base.maxCostUsd, caps?.childMaxCostUsd);
136
- if (cost !== undefined) {
137
- if (cost !== requestedCost)
138
- notes.push({ field: "maxCostUsd", requested: requestedCost, applied: cost });
139
- safe.maxCostUsd = cost;
140
- }
141
- const requestedTokens = safe.maxTokens;
142
- const tokens = minDefined(safe.maxTokens, base.maxTokens, caps?.childMaxTokens);
143
- if (tokens !== undefined) {
144
- if (tokens !== requestedTokens)
145
- notes.push({ field: "maxTokens", requested: requestedTokens, applied: tokens });
146
- safe.maxTokens = tokens;
147
- }
148
- const requestedTimeoutSec = safe.limits?.timeoutSec;
122
+ const requestedCost = safe.limits?.maxCostUsd;
123
+ const requestedTokens = safe.limits?.maxTokens;
124
+ const requestedWalltimeMs = safe.limits?.maxWalltimeMs;
149
125
  const requestedMaxTurns = safe.limits?.maxTurns;
150
- const timeoutSec = minPositiveDefined(safe.limits?.timeoutSec, base.limits?.timeoutSec, caps?.perAgentTimeoutSec);
126
+ const cost = minDefined(safe.limits?.maxCostUsd, base.limits?.maxCostUsd, caps?.childMaxCostUsd);
127
+ const tokens = minDefined(safe.limits?.maxTokens, base.limits?.maxTokens, caps?.childMaxTokens);
128
+ const maxWalltimeMs = minPositiveDefined(safe.limits?.maxWalltimeMs, base.limits?.maxWalltimeMs, caps?.perAgentMaxWalltimeMs);
151
129
  const maxTurns = minPositiveDefined(safe.limits?.maxTurns, base.limits?.maxTurns, caps?.childMaxTurns);
152
- if (timeoutSec !== undefined && timeoutSec !== requestedTimeoutSec) {
153
- notes.push({ field: "timeoutSec", requested: requestedTimeoutSec, applied: timeoutSec });
130
+ if (cost !== undefined && cost !== requestedCost)
131
+ notes.push({ field: "maxCostUsd", requested: requestedCost, applied: cost });
132
+ if (tokens !== undefined && tokens !== requestedTokens)
133
+ notes.push({ field: "maxTokens", requested: requestedTokens, applied: tokens });
134
+ if (maxWalltimeMs !== undefined && maxWalltimeMs !== requestedWalltimeMs) {
135
+ notes.push({ field: "maxWalltimeMs", requested: requestedWalltimeMs, applied: maxWalltimeMs });
154
136
  }
155
137
  if (maxTurns !== undefined && maxTurns !== requestedMaxTurns) {
156
138
  notes.push({ field: "maxTurns", requested: requestedMaxTurns, applied: maxTurns });
157
139
  }
158
140
  const baseOtherLimits = { ...base.limits };
159
141
  delete baseOtherLimits.maxTurns;
160
- delete baseOtherLimits.timeoutSec;
142
+ delete baseOtherLimits.maxWalltimeMs;
143
+ delete baseOtherLimits.maxTokens;
144
+ delete baseOtherLimits.maxCostUsd;
161
145
  const rebuilt = {
162
146
  ...baseOtherLimits,
163
147
  ...(maxTurns !== undefined ? { maxTurns } : {}),
164
- ...(timeoutSec !== undefined ? { timeoutSec } : {}),
148
+ ...(maxWalltimeMs !== undefined ? { maxWalltimeMs } : {}),
149
+ ...(tokens !== undefined ? { maxTokens: tokens } : {}),
150
+ ...(cost !== undefined ? { maxCostUsd: cost } : {}),
165
151
  };
166
152
  if (base.limits !== undefined || Object.keys(rebuilt).length > 0)
167
153
  safe.limits = rebuilt;
@@ -13,9 +13,8 @@ export const EVENT_PROMPT_REGISTRY = new Map([
13
13
  { kind: "skills_listing", carrier: "message.user-prefix", trust: "operator", dedupe: "replace-by-key", defaultPolicy: "on", rendererRef: "turn-attachments.ts#renderSkillsListingDelta" },
14
14
  { kind: "mcp_instructions", carrier: "message.user-prefix", trust: "external", dedupe: "replace-by-key", defaultPolicy: "off", rendererRef: "turn-attachments.ts#renderMcpInstructionsDelta" },
15
15
  { kind: "mcp_dropped_tools", carrier: "message.user-prefix", trust: "external", dedupe: "replace-by-key", defaultPolicy: "off", rendererRef: "turn-attachments.ts#renderMcpDroppedTools" },
16
- { kind: "deadline_converge", carrier: "message.user-prefix", trust: "operator", dedupe: "once-per-run", defaultPolicy: "always", rendererRef: "runtask.ts#nudgeSchedule" },
17
- { kind: "deadline_deliver_now", carrier: "message.user-prefix", trust: "operator", dedupe: "once-per-run", defaultPolicy: "always", rendererRef: "runtask.ts#nudgeSchedule" },
18
- { kind: "deadline_finalize", carrier: "message.user-prefix", trust: "operator", dedupe: "once-per-run", defaultPolicy: "always", rendererRef: "runtask.ts#buildFinalizeText" },
16
+ { kind: "limit_approach_converge", carrier: "message.user-prefix", trust: "operator", dedupe: "once-per-run", defaultPolicy: "always", rendererRef: "runtask.ts#limitApproachFrames" },
17
+ { kind: "limit_approach_deliver", carrier: "message.user-prefix", trust: "operator", dedupe: "once-per-run", defaultPolicy: "always", rendererRef: "runtask.ts#limitApproachFrames" },
19
18
  { kind: "final_verification", carrier: "message.user-prefix", trust: "operator", dedupe: "once-per-run", defaultPolicy: "always", rendererRef: "runtask.ts#final-verification" },
20
19
  ].map((e) => [e.kind, e]));
21
20
  export function eventDefaultOn(kind) {
@@ -20,6 +20,9 @@ export async function acquireCcLock(target, opts) {
20
20
  }
21
21
  };
22
22
  for (let attempt = 0;; attempt++) {
23
+ if (attempt >= RETRIES) {
24
+ throw new Error(`cc lockfile: could not acquire ${lockPath} after ${RETRIES} retries`);
25
+ }
23
26
  try {
24
27
  mkdirSync(lockPath);
25
28
  writeFileSync(ownerPath, nonce);
@@ -60,21 +63,16 @@ export async function acquireCcLock(target, opts) {
60
63
  const st = statSync(lockPath);
61
64
  if (Date.now() - st.mtimeMs > staleMs) {
62
65
  const st2 = statSync(lockPath);
63
- if (st2.mtimeMs !== st.mtimeMs)
64
- continue;
65
- try {
66
- rmSync(lockPath, { recursive: true, force: true });
67
- }
68
- catch {
66
+ if (st2.mtimeMs === st.mtimeMs) {
67
+ try {
68
+ rmSync(lockPath, { recursive: true, force: true });
69
+ }
70
+ catch {
71
+ }
69
72
  }
70
- continue;
71
73
  }
72
74
  }
73
75
  catch {
74
- continue;
75
- }
76
- if (attempt >= RETRIES) {
77
- throw new Error(`cc lockfile: could not acquire ${lockPath} after ${RETRIES} retries (held and fresh)`);
78
76
  }
79
77
  const backoff = Math.min(MAX_TIMEOUT_MS, MIN_TIMEOUT_MS * 2 ** attempt);
80
78
  await sleep(backoff / 2 + Math.random() * (backoff / 2));
@@ -3,6 +3,7 @@ import { join, resolve } from "node:path";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { atomicWriteFile, canonicalStoreKey } from "../file/fs-atomic.js";
5
5
  import { withCcLock } from "./lockfile.js";
6
+ const INBOX_FILE_EXT = ".json";
6
7
  export function sanitizeCcAgentName(name) {
7
8
  return name.replace(/[^a-zA-Z0-9]/g, "-").toLowerCase();
8
9
  }
@@ -23,10 +24,8 @@ export function createCcFileMailboxStore(opts) {
23
24
  throw new Error(`CcFileMailboxStore: CC inbox files carry no tenant axis — scope:"default" only (D-1); got ${JSON.stringify(scope)}`);
24
25
  }
25
26
  };
26
- const inboxPath = (handle) => {
27
- const name = opts.agentNameOf?.(handle) ?? handle;
28
- return join(inboxDir, `${sanitizeCcAgentName(name)}.json`);
29
- };
27
+ const leaseKey = (handle) => sanitizeCcAgentName(opts.agentNameOf?.(handle) ?? handle);
28
+ const inboxPath = (handle) => join(inboxDir, `${leaseKey(handle)}${INBOX_FILE_EXT}`);
30
29
  const discloseCorrupt = (path, reason) => {
31
30
  try {
32
31
  opts.onCorruptRead?.({ path, reason });
@@ -98,6 +97,42 @@ export function createCcFileMailboxStore(opts) {
98
97
  const saveBox = (path, box) => {
99
98
  atomicWriteFile(inboxDir, path, JSON.stringify(box, null, 2));
100
99
  };
100
+ const isColderThan = (box, now, maxAgeMs) => {
101
+ if (box.length === 0)
102
+ return false;
103
+ const newest = box.reduce((acc, e) => {
104
+ const t = typeof e.timestamp === "string" ? Date.parse(e.timestamp) : Number.NaN;
105
+ return Number.isFinite(t) ? Math.max(acc, t) : Number.POSITIVE_INFINITY;
106
+ }, 0);
107
+ return newest !== Number.POSITIVE_INFINITY && now - newest > maxAgeMs;
108
+ };
109
+ const sweepColdBoxes = async (now, maxAgeMs) => {
110
+ let dropped = 0;
111
+ for (const f of readdirSync(inboxDir)) {
112
+ if (!f.endsWith(INBOX_FILE_EXT))
113
+ continue;
114
+ const path = join(inboxDir, f);
115
+ const boxKey = f.slice(0, -INBOX_FILE_EXT.length);
116
+ const preNotes = deferredNotes();
117
+ if (!isColderThan(loadBox(path, preNotes.note), now, maxAgeMs)) {
118
+ preNotes.flush();
119
+ continue;
120
+ }
121
+ const notes = deferredNotes();
122
+ const swept = await withCcLock(path, () => {
123
+ if (!isColderThan(loadBox(path, notes.note), now, maxAgeMs))
124
+ return false;
125
+ leases.delete(boxKey);
126
+ rmSync(path, { force: true });
127
+ return true;
128
+ }).finally(() => {
129
+ notes.flush();
130
+ });
131
+ if (swept)
132
+ dropped += 1;
133
+ }
134
+ return dropped;
135
+ };
101
136
  return {
102
137
  append: (scope, handle, msg) => {
103
138
  requireDefaultScope(scope);
@@ -124,11 +159,12 @@ export function createCcFileMailboxStore(opts) {
124
159
  claimLease: (scope, handle, owner, ttlMs, now) => {
125
160
  requireDefaultScope(scope);
126
161
  const t = now ?? opts.now?.() ?? Date.now();
127
- const key = inboxPath(handle);
162
+ const key = leaseKey(handle);
163
+ const path = inboxPath(handle);
128
164
  const held = leases.get(key);
129
165
  if (held !== undefined && held.expiresAt > t && held.owner !== owner)
130
166
  return Promise.resolve(null);
131
- const box = loadBox(key, discloseCorrupt);
167
+ const box = loadBox(path, discloseCorrupt);
132
168
  const maxSeq = box.length;
133
169
  const pending = box
134
170
  .map((e, i) => ({ e, seq: i + 1 }))
@@ -147,7 +183,7 @@ export function createCcFileMailboxStore(opts) {
147
183
  ack: (scope, handle, owner, upToSeq) => {
148
184
  requireDefaultScope(scope);
149
185
  const path = inboxPath(handle);
150
- const held = leases.get(path);
186
+ const held = leases.get(leaseKey(handle));
151
187
  if (held === undefined || held.owner !== owner)
152
188
  return Promise.resolve();
153
189
  if (!existsSync(path))
@@ -165,14 +201,14 @@ export function createCcFileMailboxStore(opts) {
165
201
  if (changed)
166
202
  saveBox(path, box);
167
203
  if (held.maxSeq <= upToSeq)
168
- leases.delete(path);
204
+ leases.delete(leaseKey(handle));
169
205
  }).finally(() => {
170
206
  notes.flush();
171
207
  });
172
208
  },
173
209
  releaseLease: (scope, handle, owner) => {
174
210
  requireDefaultScope(scope);
175
- const key = inboxPath(handle);
211
+ const key = leaseKey(handle);
176
212
  const held = leases.get(key);
177
213
  if (held !== undefined && held.owner === owner)
178
214
  leases.delete(key);
@@ -185,42 +221,17 @@ export function createCcFileMailboxStore(opts) {
185
221
  drop: (scope, handle) => {
186
222
  requireDefaultScope(scope);
187
223
  const path = inboxPath(handle);
188
- leases.delete(path);
189
- try {
224
+ leases.delete(leaseKey(handle));
225
+ return withCcLock(path, () => {
190
226
  rmSync(path, { force: true });
191
- rmSync(`${path}.lock`, { recursive: true, force: true });
192
- }
193
- catch {
194
- }
195
- return Promise.resolve();
227
+ });
196
228
  },
197
229
  reap: (scope, now, reapOpts) => {
198
230
  requireDefaultScope(scope);
199
231
  const maxAgeMs = reapOpts?.maxAgeMs;
200
232
  if (maxAgeMs === undefined || !existsSync(inboxDir))
201
233
  return Promise.resolve(0);
202
- let dropped = 0;
203
- for (const f of readdirSync(inboxDir)) {
204
- if (!f.endsWith(".json"))
205
- continue;
206
- const path = join(inboxDir, f);
207
- const box = loadBox(path, discloseCorrupt);
208
- const newest = box.reduce((acc, e) => {
209
- const t = typeof e.timestamp === "string" ? Date.parse(e.timestamp) : Number.NaN;
210
- return Number.isFinite(t) ? Math.max(acc, t) : Number.POSITIVE_INFINITY;
211
- }, 0);
212
- if (box.length > 0 && newest !== Number.POSITIVE_INFINITY && now - newest > maxAgeMs) {
213
- leases.delete(path);
214
- try {
215
- rmSync(path, { force: true });
216
- rmSync(`${path}.lock`, { recursive: true, force: true });
217
- dropped += 1;
218
- }
219
- catch {
220
- }
221
- }
222
- }
223
- return Promise.resolve(dropped);
234
+ return sweepColdBoxes(now, maxAgeMs);
224
235
  },
225
236
  };
226
237
  }
@@ -7,6 +7,7 @@ import { type FileCheckpointStoreOptions } from "./checkpoint-store.js";
7
7
  import type { SessionPolicyStore } from "../../core/session-policy-store.js";
8
8
  import type { FileSnapshotStore } from "../../core/file-snapshot-store.js";
9
9
  import type { WorkflowJournalStore } from "../../core/workflow-journal-store.js";
10
+ import type { UsageWindowStore } from "../../core/usage-window-store.js";
10
11
  export { FileCheckpointStore, type FileCheckpointStoreOptions } from "./checkpoint-store.js";
11
12
  export { FileMemoryStore } from "./memory-store.js";
12
13
  export { FileSessionRepo, type FileSessionRepoOptions } from "./session-store.js";
@@ -14,6 +15,7 @@ export { FileToolResultStore } from "./tool-result-store.js";
14
15
  export { FileSessionPolicyStore, type FileSessionPolicyStoreOptions, type SessionPolicyCorruptReadInfo } from "./session-policy-store.js";
15
16
  export { FileFileSnapshotStore, type FileFileSnapshotStoreOptions } from "./file-snapshot-store.js";
16
17
  export { FileWorkflowJournalStore, MAX_JOURNAL_RESULT_BYTES, oversizeJournalResult } from "./workflow-journal-store.js";
18
+ export { FileUsageWindowStore } from "./usage-window-store.js";
17
19
  export { resolveDataRoot, sanitizeScope, sanitizePathComponent, createFileConsolidationLock } from "./fs-atomic.js";
18
20
  export { atomicWriteFile, writeThenLink, ensureDir, readJsonlRecords, AppendLog } from "./fs-atomic.js";
19
21
  export interface FileStorageBackendOptions {
@@ -40,6 +42,7 @@ export declare class FileStorageBackend {
40
42
  readonly sessionPolicyStore: SessionPolicyStore;
41
43
  readonly fileSnapshotStore: FileSnapshotStore;
42
44
  readonly workflowJournalStore: WorkflowJournalStore;
45
+ readonly usageWindowStore: UsageWindowStore;
43
46
  readonly consolidationLock: (scope: string) => (() => void) | undefined;
44
47
  private readonly lock;
45
48
  private readonly fileCheckpoints;
@@ -9,6 +9,7 @@ import { FileToolResultStore } from "./tool-result-store.js";
9
9
  import { FileSessionPolicyStore } from "./session-policy-store.js";
10
10
  import { FileFileSnapshotStore } from "./file-snapshot-store.js";
11
11
  import { FileWorkflowJournalStore } from "./workflow-journal-store.js";
12
+ import { FileUsageWindowStore } from "./usage-window-store.js";
12
13
  export { FileCheckpointStore } from "./checkpoint-store.js";
13
14
  export { FileMemoryStore } from "./memory-store.js";
14
15
  export { FileSessionRepo } from "./session-store.js";
@@ -16,6 +17,7 @@ export { FileToolResultStore } from "./tool-result-store.js";
16
17
  export { FileSessionPolicyStore } from "./session-policy-store.js";
17
18
  export { FileFileSnapshotStore } from "./file-snapshot-store.js";
18
19
  export { FileWorkflowJournalStore, MAX_JOURNAL_RESULT_BYTES, oversizeJournalResult } from "./workflow-journal-store.js";
20
+ export { FileUsageWindowStore } from "./usage-window-store.js";
19
21
  export { resolveDataRoot, sanitizeScope, sanitizePathComponent, createFileConsolidationLock } from "./fs-atomic.js";
20
22
  export { atomicWriteFile, writeThenLink, ensureDir, readJsonlRecords, AppendLog } from "./fs-atomic.js";
21
23
  export class FileStorageBackend {
@@ -27,6 +29,7 @@ export class FileStorageBackend {
27
29
  sessionPolicyStore;
28
30
  fileSnapshotStore;
29
31
  workflowJournalStore;
32
+ usageWindowStore;
30
33
  consolidationLock;
31
34
  lock;
32
35
  fileCheckpoints;
@@ -53,6 +56,7 @@ export class FileStorageBackend {
53
56
  this.fileSnapshotStore = new FileFileSnapshotStore(this.root, opts.snapshotBounds, corruptRead);
54
57
  this.fileWorkflowJournal = new FileWorkflowJournalStore(this.root);
55
58
  this.workflowJournalStore = this.fileWorkflowJournal;
59
+ this.usageWindowStore = new FileUsageWindowStore(this.root);
56
60
  this.consolidationLock = createFileConsolidationLock(join(this.root, "consolidation-locks"));
57
61
  }
58
62
  catch (err) {
@@ -0,0 +1,9 @@
1
+ import { type UsageWindow, type UsageWindowReading, type UsageWindowStore } from "../../core/usage-window-store.js";
2
+ export declare class FileUsageWindowStore implements UsageWindowStore {
3
+ private readonly dir;
4
+ constructor(root: string);
5
+ private pathFor;
6
+ private loadRecord;
7
+ charge(key: string, tokens: number, at: number, windows: readonly UsageWindow[]): Promise<void>;
8
+ read(key: string, windows: readonly UsageWindow[], now: number): Promise<readonly UsageWindowReading[]>;
9
+ }
@@ -0,0 +1,86 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { chargeUsageRecord, readUsageRecord, EMPTY_USAGE_WINDOW_RECORD, } from "../../core/usage-window-store.js";
4
+ import { atomicWriteFile, ensureDir, sanitizeScope } from "./fs-atomic.js";
5
+ export class FileUsageWindowStore {
6
+ dir;
7
+ constructor(root) {
8
+ this.dir = join(root, "usage-windows");
9
+ ensureDir(this.dir);
10
+ }
11
+ pathFor(key) {
12
+ return join(this.dir, `${sanitizeScope(key)}.json`);
13
+ }
14
+ loadRecord(key) {
15
+ const path = this.pathFor(key);
16
+ let raw;
17
+ try {
18
+ raw = readFileSync(path, "utf8");
19
+ }
20
+ catch (err) {
21
+ if (err.code === "ENOENT")
22
+ return EMPTY_USAGE_WINDOW_RECORD;
23
+ throw err;
24
+ }
25
+ let parsed;
26
+ try {
27
+ parsed = JSON.parse(raw);
28
+ }
29
+ catch {
30
+ throw corrupt(path, "unparseable JSON (torn/zero-byte write?)");
31
+ }
32
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
33
+ throw corrupt(path, "not a JSON object");
34
+ const rec = parsed;
35
+ const slots = validateSlots(rec.slots, path);
36
+ const buckets = validateBuckets(rec.buckets, path);
37
+ return { slots, buckets };
38
+ }
39
+ async charge(key, tokens, at, windows) {
40
+ const next = chargeUsageRecord(this.loadRecord(key), tokens, at, windows);
41
+ atomicWriteFile(join(this.dir, "tmp"), this.pathFor(key), JSON.stringify(next));
42
+ }
43
+ async read(key, windows, now) {
44
+ return readUsageRecord(this.loadRecord(key), windows, now);
45
+ }
46
+ }
47
+ function corrupt(path, reason) {
48
+ const e = new Error(`usage-window ledger at ${path} is unreadable (${reason}). Refused rather than treated as empty: an unreadable ceiling would grant the key an unlimited, silent allowance.`);
49
+ e.code = "usage_window.store_corrupt";
50
+ return e;
51
+ }
52
+ function validateSlots(value, path) {
53
+ if (value === undefined)
54
+ return [];
55
+ if (!Array.isArray(value))
56
+ throw corrupt(path, "`slots` is not an array");
57
+ return value.map((s) => {
58
+ if (s === null || typeof s !== "object")
59
+ throw corrupt(path, "`slots` entry is not an object");
60
+ const { at, tokens } = s;
61
+ if (typeof at !== "number" || !Number.isFinite(at) || typeof tokens !== "number" || !Number.isFinite(tokens)) {
62
+ throw corrupt(path, "`slots` entry has a non-numeric `at`/`tokens`");
63
+ }
64
+ return { at, tokens };
65
+ });
66
+ }
67
+ function validateBuckets(value, path) {
68
+ if (value === undefined)
69
+ return [];
70
+ if (!Array.isArray(value))
71
+ throw corrupt(path, "`buckets` is not an array");
72
+ return value.map((b) => {
73
+ if (b === null || typeof b !== "object")
74
+ throw corrupt(path, "`buckets` entry is not an object");
75
+ const { windowMs, openedAt, tokens } = b;
76
+ if (typeof windowMs !== "number" ||
77
+ !Number.isFinite(windowMs) ||
78
+ typeof openedAt !== "number" ||
79
+ !Number.isFinite(openedAt) ||
80
+ typeof tokens !== "number" ||
81
+ !Number.isFinite(tokens)) {
82
+ throw corrupt(path, "`buckets` entry has a non-numeric `windowMs`/`openedAt`/`tokens`");
83
+ }
84
+ return { windowMs, openedAt, tokens };
85
+ });
86
+ }
@@ -5,14 +5,6 @@ import { type BashReadonlyRootBoundary } from "./bash-readonly-classifier.js";
5
5
  export declare function bashReversibilityProbe(allow?: readonly string[], boundary?: BashReadonlyRootBoundary | (() => BashReadonlyRootBoundary | undefined)): (args: unknown) => {
6
6
  reversible: boolean;
7
7
  };
8
- export interface ExecClampOption {
9
- deadlineMs: () => number | undefined;
10
- onClamp?: (requestedSec: number, clampedSec: number) => void;
11
- registerKillHandle?: (toolCallId: string, handle: {
12
- pid: number;
13
- kill: () => void;
14
- }) => () => void;
15
- }
16
8
  export declare function bashExitOneInterpretation(command: string): string | undefined;
17
9
  export declare function canAutoBackground(command: string, depth?: number): boolean;
18
10
  export declare function createBashTool(env: ExecutionEnv, rootCanonical: string, coAuthor?: string | false, cwdRef?: CwdRef, taskOpts?: {
@@ -24,14 +16,13 @@ export declare function createBashTool(env: ExecutionEnv, rootCanonical: string,
24
16
  priority?: "now" | "next" | "later";
25
17
  }) => void;
26
18
  detachHub?: import("../../core/tool-detach.js").ToolDetachHub;
27
- execClamp?: ExecClampOption;
28
19
  autoBackgroundOnTimeout?: boolean;
29
20
  oneShot?: boolean;
30
21
  additionalRoots?: readonly string[];
31
22
  bashDefaultTimeoutMs?: number;
32
23
  bashMaxTimeoutMs?: number;
33
24
  }): AgentTool;
34
- export declare function createBashReadonlyTool(env: ExecutionEnv, rootCanonical: string, allow: ReadonlySet<string>, execClamp?: ExecClampOption, opts?: {
25
+ export declare function createBashReadonlyTool(env: ExecutionEnv, rootCanonical: string, allow: ReadonlySet<string>, opts?: {
35
26
  bashDefaultTimeoutMs?: number;
36
27
  bashMaxTimeoutMs?: number;
37
28
  additionalRoots?: readonly string[];