@sema-agent/core 7.40.1 → 7.41.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 (134) hide show
  1. package/CHANGELOG.md +188 -0
  2. package/dist/agents/agent-transcript-tool.d.ts +10 -2
  3. package/dist/agents/agent-transcript-tool.js +52 -26
  4. package/dist/agents/list-agents-tool.d.ts +17 -2
  5. package/dist/agents/list-agents-tool.js +28 -16
  6. package/dist/agents/peer-session-drain.d.ts +3 -1
  7. package/dist/agents/peer-session-drain.js +1 -0
  8. package/dist/agents/subagent-steps.d.ts +12 -2
  9. package/dist/agents/subagent-steps.js +8 -4
  10. package/dist/agents/subagent.d.ts +1 -17
  11. package/dist/agents/subagent.js +12 -24
  12. package/dist/core/agent-definition.d.ts +19 -40
  13. package/dist/core/agent-state.d.ts +91 -0
  14. package/dist/core/agent-state.js +79 -0
  15. package/dist/core/ask-origin.d.ts +28 -6
  16. package/dist/core/ask-origin.js +9 -5
  17. package/dist/core/ask-unresolvable-notice.d.ts +27 -4
  18. package/dist/core/ask-unresolvable-notice.js +8 -0
  19. package/dist/core/auto-promote.d.ts +14 -27
  20. package/dist/core/auto-promote.js +5 -5
  21. package/dist/core/checkpoint-store.d.ts +11 -12
  22. package/dist/core/decision-receipt.d.ts +5 -2
  23. package/dist/core/decision-receipt.js +1 -1
  24. package/dist/core/engine-notice.d.ts +7 -1
  25. package/dist/core/gate-crash-trace.d.ts +1 -1
  26. package/dist/core/gate-fold.js +7 -3
  27. package/dist/core/gate-lanes.js +22 -3
  28. package/dist/core/governance-codes.d.ts +3 -1
  29. package/dist/core/governance-codes.js +3 -0
  30. package/dist/core/hooks.d.ts +20 -6
  31. package/dist/core/hooks.js +28 -28
  32. package/dist/core/mcp-tool-meta.d.ts +24 -0
  33. package/dist/core/mcp-tool-meta.js +17 -0
  34. package/dist/core/mcp.d.ts +1 -6
  35. package/dist/core/mcp.js +6 -7
  36. package/dist/core/mode-policy.d.ts +25 -0
  37. package/dist/core/mode-policy.js +8 -0
  38. package/dist/core/protocol-table.d.ts +3 -0
  39. package/dist/core/protocol-table.js +3 -0
  40. package/dist/core/reminder-mint.d.ts +21 -1
  41. package/dist/core/reminder-mint.js +20 -0
  42. package/dist/core/retired-keys.d.ts +2 -1
  43. package/dist/core/retired-keys.js +13 -2
  44. package/dist/core/rule-denial-sentence.d.ts +2 -2
  45. package/dist/core/runner/call-hook-stations.d.ts +106 -0
  46. package/dist/core/runner/call-hook-stations.js +118 -0
  47. package/dist/core/runner/contracts.d.ts +35 -25
  48. package/dist/core/runner/gate-exit.d.ts +5 -1
  49. package/dist/core/runner/gate-exit.js +10 -6
  50. package/dist/core/runner/prepare-delegation-surface.d.ts +1 -0
  51. package/dist/core/runner/prepare-delegation-surface.js +4 -4
  52. package/dist/core/runner/prepare-gate-stations.d.ts +8 -3
  53. package/dist/core/runner/prepare-gate-stations.js +13 -70
  54. package/dist/core/runner/prepare-inherited-gate.d.ts +6 -3
  55. package/dist/core/runner/prepare-inherited-gate.js +5 -3
  56. package/dist/core/runner/prepare-park-ask.d.ts +4 -0
  57. package/dist/core/runner/prepare-park-ask.js +4 -1
  58. package/dist/core/runner/prepare-policy-chain.d.ts +6 -5
  59. package/dist/core/runner/prepare-policy-chain.js +14 -25
  60. package/dist/core/runner/prepare-project-context.d.ts +15 -16
  61. package/dist/core/runner/prepare-project-context.js +15 -11
  62. package/dist/core/runner/prepare-run-refs.d.ts +8 -8
  63. package/dist/core/runner/prepare-run-refs.js +3 -3
  64. package/dist/core/runner/prepare-safety-scan.js +3 -3
  65. package/dist/core/runner/prepare-suspend-saga.d.ts +2 -1
  66. package/dist/core/runner/prepare-suspend-saga.js +2 -1
  67. package/dist/core/runner/prepare-task.js +14 -12
  68. package/dist/core/runner/prepare-turn-wiring.d.ts +3 -1
  69. package/dist/core/runner/prepare-turn-wiring.js +16 -13
  70. package/dist/core/runner/resume-admission.js +2 -1
  71. package/dist/core/runner/resume-apply.js +12 -23
  72. package/dist/core/runner/resume-checkpoint-screen.js +3 -0
  73. package/dist/core/runner/run-leg.js +61 -47
  74. package/dist/core/runner/runtask.d.ts +1 -1
  75. package/dist/core/runner/session-rule-policy.d.ts +7 -19
  76. package/dist/core/runner/session-rule-policy.js +1 -2
  77. package/dist/core/runner/skill-base-dir.d.ts +10 -0
  78. package/dist/core/runner/skill-base-dir.js +4 -0
  79. package/dist/core/runner/synthetic-tools.d.ts +11 -17
  80. package/dist/core/runner/synthetic-tools.js +7 -8
  81. package/dist/core/safety-axis-vocab.d.ts +2 -2
  82. package/dist/core/safety-axis-vocab.js +2 -0
  83. package/dist/core/session-policy-store.d.ts +63 -11
  84. package/dist/core/session-policy-store.js +52 -6
  85. package/dist/core/skill-grant.d.ts +239 -0
  86. package/dist/core/skill-grant.js +313 -0
  87. package/dist/core/skills-directory.d.ts +5 -34
  88. package/dist/core/skills-directory.js +9 -58
  89. package/dist/core/task-registry-agent.d.ts +14 -2
  90. package/dist/core/task-registry-agent.js +23 -8
  91. package/dist/core/task-registry-shared.d.ts +6 -0
  92. package/dist/core/task-registry.d.ts +16 -0
  93. package/dist/core/task-registry.js +19 -6
  94. package/dist/core/task-spec.d.ts +10 -0
  95. package/dist/core/tool-face.d.ts +3 -2
  96. package/dist/core/tool-name-aliases.d.ts +38 -12
  97. package/dist/core/tool-name-aliases.js +16 -0
  98. package/dist/core/tool-name-readings.d.ts +6 -2
  99. package/dist/core/tool-name-readings.js +6 -4
  100. package/dist/core/tool-policy.d.ts +2 -2
  101. package/dist/core/tool-registry.d.ts +1 -4
  102. package/dist/core/tool-registry.js +0 -3
  103. package/dist/core/tool-spec.d.ts +11 -13
  104. package/dist/core/tools.d.ts +1 -1
  105. package/dist/core/trace.d.ts +15 -0
  106. package/dist/core/types.d.ts +1 -1
  107. package/dist/engine/harness/agent-harness.d.ts +12 -1
  108. package/dist/engine/harness/agent-harness.js +17 -13
  109. package/dist/engine/loop/agent-loop.d.ts +16 -1
  110. package/dist/engine/loop/agent-loop.js +9 -15
  111. package/dist/engine/loop/types.d.ts +2 -0
  112. package/dist/index.d.ts +9 -4
  113. package/dist/index.js +4 -2
  114. package/dist/stores/file/adoption/adopt.js +23 -16
  115. package/dist/stores/file/index.d.ts +8 -2
  116. package/dist/stores/file/index.js +2 -2
  117. package/dist/stores/file/mailbox-store.js +2 -2
  118. package/dist/stores/file/permission-rule-adopt.js +10 -1
  119. package/dist/stores/file/permission-rule-store.d.ts +6 -4
  120. package/dist/stores/file/permission-rule-store.js +127 -59
  121. package/dist/stores/file/session-policy-store.d.ts +86 -14
  122. package/dist/stores/file/session-policy-store.js +135 -118
  123. package/dist/stores/file/task-list-store.d.ts +5 -0
  124. package/dist/stores/file/task-list-store.js +18 -4
  125. package/dist/stores/file/usage-window-store.d.ts +9 -1
  126. package/dist/stores/file/usage-window-store.js +47 -33
  127. package/dist/tools/fs/safety.d.ts +1 -1
  128. package/package.json +1 -1
  129. package/test/export-surface.snapshot.json +45 -5
  130. package/wire-changes/core@7.41.0.json +1865 -0
  131. package/dist/core/runner/active-skill-scope.d.ts +0 -58
  132. package/dist/core/runner/active-skill-scope.js +0 -133
  133. package/dist/core/skill-tool-specifier.d.ts +0 -80
  134. package/dist/core/skill-tool-specifier.js +0 -57
@@ -1,6 +1,6 @@
1
1
  import { readFileSync, readdirSync, unlinkSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { loosenReasons, normalizeRules, stripRev, SessionPolicyError, } from "../../core/session-policy-store.js";
3
+ import { assertSessionRuleNamesCurrent, loosenReasons, normalizeRules, readMountedToolNames, stripRev, SessionPolicyError, } from "../../core/session-policy-store.js";
4
4
  import { atomicWriteFile, ensureDir, sanitizeScope } from "./fs-atomic.js";
5
5
  import { guardCorruptSink } from "./corrupt-read-sink.js";
6
6
  import { assertAdoptionBootGate, readRootAdoptionFile } from "./adoption/marker.js";
@@ -13,27 +13,25 @@ export function readSessionPolicyMark(path) {
13
13
  }
14
14
  catch (err) {
15
15
  if (err.code === "ENOENT")
16
- return null;
16
+ return { mark: null };
17
17
  throw err;
18
18
  }
19
- const refuse = (fault) => {
20
- throw new SessionPolicyError("corrupt", `session rules high-water mark ${fault}: ${path} — refusing rather than restart this key's rev from zero. Deleting that file is the repair, and it gives up the number it was holding.`);
21
- };
19
+ const corrupt = (reason) => ({ corrupt: { reason } });
22
20
  let parsed;
23
21
  try {
24
22
  parsed = JSON.parse(raw);
25
23
  }
26
24
  catch {
27
- return refuse("is unparseable");
25
+ return corrupt("is unparseable");
28
26
  }
29
27
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
30
- return refuse("is not a JSON object");
28
+ return corrupt("is not a JSON object");
31
29
  const mark = parsed;
32
30
  if (typeof mark.__hwm !== "number" || !Number.isFinite(mark.__hwm))
33
- return refuse("carries no numeric value");
31
+ return corrupt("carries no numeric value");
34
32
  if (mark.__gen !== undefined && (typeof mark.__gen !== "number" || !Number.isInteger(mark.__gen) || mark.__gen < 0))
35
- return refuse("carries a lineage that is not a whole number");
36
- return mark;
33
+ return corrupt("carries a lineage that is not a whole number");
34
+ return { mark };
37
35
  }
38
36
  export function judgeSessionPolicyRowObject(raw) {
39
37
  let parsed;
@@ -63,12 +61,22 @@ export function judgeSessionPolicyRowShape(row) {
63
61
  void _principal;
64
62
  return { rules: clean };
65
63
  }
64
+ export function attributeSessionPolicyRow(dir, file, row) {
65
+ const spell = (p) => (typeof p === "string" || p === null ? p : undefined);
66
+ if (row !== undefined && typeof row.__sid === "string")
67
+ return { owner: { sessionId: row.__sid, principal: spell(row.__principal) } };
68
+ const markRead = readSessionPolicyMark(join(dir, `${file.slice(0, -".json".length)}${MARK_SUFFIX}`));
69
+ const mark = "corrupt" in markRead ? null : markRead.mark;
70
+ return { owner: mark !== null && typeof mark.__sid === "string" ? { sessionId: mark.__sid, principal: spell(mark.__principal) } : undefined, markRead };
71
+ }
66
72
  export class FileSessionPolicyStore {
67
73
  dir;
68
74
  onCorruptRead;
75
+ mountedToolNames;
69
76
  anonymousAlias;
70
77
  constructor(root, opts) {
71
78
  assertAdoptionBootGate(root, "FileSessionPolicyStore");
79
+ this.mountedToolNames = readMountedToolNames(opts?.mountedToolNames);
72
80
  this.dir = join(root, "session-policy");
73
81
  ensureDir(this.dir);
74
82
  this.onCorruptRead = guardCorruptSink(opts?.onCorruptRead);
@@ -79,11 +87,57 @@ export class FileSessionPolicyStore {
79
87
  disclose(info) {
80
88
  this.onCorruptRead?.({ kind: "file", ...info });
81
89
  }
82
- refuseDamaged(sessionId, principal, grade, reason) {
83
- const path = this.pathFor(sessionId, principal);
90
+ refuseDamaged(sessionId, principal, path, grade, reason) {
84
91
  this.disclose({ sessionId, ...(principal !== undefined ? { principal } : {}), path, reason });
85
92
  throw new SessionPolicyError("corrupt", `session rules file for ${JSON.stringify(sessionId)} is ${grade} (${reason}) — refusing rather than read the rules it held as none (session rules only tighten). Repair it: fix the file, or delete it (deleting it = explicitly no rules, and gives up the rules it held). The file's location is reported to the store's corrupt-read disclosure (onCorruptRead), not in this message.`);
86
93
  }
94
+ refuseDamagedMark(sessionId, principal, reason) {
95
+ this.disclose({ sessionId, ...(principal !== undefined ? { principal } : {}), path: this.markPathFor(sessionId, principal), reason: `high-water mark ${reason}` });
96
+ throw new SessionPolicyError("corrupt", `session rules high-water mark for ${JSON.stringify(sessionId)} ${reason} — refusing rather than restart this key's rev from zero. Repair it: fix the mark file, or delete it (deleting it gives up the number it was holding). The file's location is reported to the store's corrupt-read disclosure (onCorruptRead), not in this message.`);
97
+ }
98
+ refuseUnreadable(sessionId, path, what, err) {
99
+ const reason = `${what} could not be read (${err.code ?? err.name})`;
100
+ this.disclose({ sessionId, path, reason });
101
+ throw new SessionPolicyError("corrupt", `session rules for ${JSON.stringify(sessionId)}: ${reason} — refusing rather than guess whose rules it holds. Retry; if it persists, repair access to the file. The file's location is reported to the store's corrupt-read disclosure (onCorruptRead), not in this message.`);
102
+ }
103
+ ownerOf(sessionId, file, row) {
104
+ try {
105
+ return attributeSessionPolicyRow(this.dir, file, row);
106
+ }
107
+ catch (err) {
108
+ return this.refuseUnreadable(sessionId, join(this.dir, `${file.slice(0, -".json".length)}${MARK_SUFFIX}`), "a rules row's high-water mark", err);
109
+ }
110
+ }
111
+ sweepMark(sessionId, markPath) {
112
+ try {
113
+ return readSessionPolicyMark(markPath);
114
+ }
115
+ catch (err) {
116
+ return this.refuseUnreadable(sessionId, markPath, "a rules row's high-water mark", err);
117
+ }
118
+ }
119
+ sweepEntries(sessionId) {
120
+ try {
121
+ return readdirSync(this.dir);
122
+ }
123
+ catch (err) {
124
+ if (err.code === "ENOENT")
125
+ return [];
126
+ return this.refuseUnreadable(sessionId, this.dir, "the session rules directory", err);
127
+ }
128
+ }
129
+ readMark(sessionId, principal) {
130
+ let judged;
131
+ try {
132
+ judged = readSessionPolicyMark(this.markPathFor(sessionId, principal));
133
+ }
134
+ catch (err) {
135
+ return this.refuseDamagedMark(sessionId, principal, `could not be read (${err.code ?? err.name})`);
136
+ }
137
+ if ("corrupt" in judged)
138
+ return this.refuseDamagedMark(sessionId, principal, judged.corrupt.reason);
139
+ return judged.mark;
140
+ }
87
141
  pathFor(sessionId, principal) {
88
142
  return join(this.dir, `${this.basenameFor(sessionId, principal)}.json`);
89
143
  }
@@ -102,22 +156,28 @@ export class FileSessionPolicyStore {
102
156
  atomicWriteFile(join(this.dir, "tmp"), path, JSON.stringify(mark));
103
157
  }
104
158
  read(sessionId, principal) {
159
+ const path = this.pathFor(sessionId, principal);
160
+ const judged = this.readRowFile(path);
161
+ if (judged === null)
162
+ return null;
163
+ if ("corrupt" in judged)
164
+ return this.refuseDamaged(sessionId, principal, path, judged.corrupt.grade, judged.corrupt.reason);
165
+ const shaped = judgeSessionPolicyRowShape(judged.row);
166
+ if ("corrupt" in shaped)
167
+ return this.refuseDamaged(sessionId, principal, path, shaped.corrupt.grade, shaped.corrupt.reason);
168
+ return shaped.rules;
169
+ }
170
+ readRowFile(path) {
105
171
  let raw;
106
172
  try {
107
- raw = readFileSync(this.pathFor(sessionId, principal), "utf8");
173
+ raw = readFileSync(path, "utf8");
108
174
  }
109
175
  catch (err) {
110
176
  if (err.code === "ENOENT")
111
177
  return null;
112
- return this.refuseDamaged(sessionId, principal, "unreadable", `read failed (${err.code ?? err.name})`);
178
+ return { corrupt: { grade: "unreadable", reason: `read failed (${err.code ?? err.name})` } };
113
179
  }
114
- const judged = judgeSessionPolicyRowObject(raw);
115
- if ("corrupt" in judged)
116
- return this.refuseDamaged(sessionId, principal, judged.corrupt.grade, judged.corrupt.reason);
117
- const shaped = judgeSessionPolicyRowShape(judged.row);
118
- if ("corrupt" in shaped)
119
- return this.refuseDamaged(sessionId, principal, shaped.corrupt.grade, shaped.corrupt.reason);
120
- return shaped.rules;
180
+ return judgeSessionPolicyRowObject(raw);
121
181
  }
122
182
  async getRules(sessionId, principal) {
123
183
  return this.read(sessionId, principal);
@@ -128,13 +188,14 @@ export class FileSessionPolicyStore {
128
188
  throw new SessionPolicyError("conflict", `session rules rev mismatch (expected ${opts.expectedRev}, have ${prior?.rev ?? 0})`);
129
189
  }
130
190
  const clean = normalizeRules(stripRev(rules));
191
+ assertSessionRuleNamesCurrent(clean, this.mountedToolNames);
131
192
  if (prior && opts?.operator !== true) {
132
- const loosens = loosenReasons(prior, clean);
193
+ const loosens = loosenReasons(prior, clean, this.mountedToolNames);
133
194
  if (loosens.length > 0) {
134
195
  throw new SessionPolicyError("loosen_forbidden", `a non-operator session-rule write may only tighten; refused: ${loosens.join("; ")}`);
135
196
  }
136
197
  }
137
- const mark = readSessionPolicyMark(this.markPathFor(sessionId, principal));
198
+ const mark = this.readMark(sessionId, principal);
138
199
  if (mark?.__lineage === "lost") {
139
200
  throw new SessionPolicyError("corrupt", `session rules for ${JSON.stringify(sessionId)} have no lineage evidence (the erase left a lost-lineage mark) — refusing to restart the key's lineage by guessing; delete the mark to restart it`);
140
201
  }
@@ -147,15 +208,7 @@ export class FileSessionPolicyStore {
147
208
  return next;
148
209
  }
149
210
  async listBySession(sessionId) {
150
- let files;
151
- try {
152
- files = readdirSync(this.dir);
153
- }
154
- catch (err) {
155
- if (err.code === "ENOENT")
156
- return [];
157
- throw err;
158
- }
211
+ const files = this.sweepEntries(sessionId);
159
212
  const out = [];
160
213
  for (const file of files) {
161
214
  if (file.endsWith(MARK_SUFFIX))
@@ -163,116 +216,80 @@ export class FileSessionPolicyStore {
163
216
  if (!file.endsWith(".json"))
164
217
  continue;
165
218
  const full = join(this.dir, file);
166
- let raw;
167
- try {
168
- raw = readFileSync(full, "utf8");
169
- }
170
- catch (err) {
171
- if (err.code !== "ENOENT") {
172
- this.disclose({ sessionId, path: full, reason: `read failed (non-ENOENT): ${err.message}` });
173
- }
219
+ const judged = this.readRowFile(full);
220
+ if (judged === null)
174
221
  continue;
175
- }
176
- const judged = judgeSessionPolicyRowObject(raw);
177
- if ("corrupt" in judged) {
178
- this.disclose({ sessionId, path: full, reason: judged.corrupt.reason });
222
+ const { owner } = this.ownerOf(sessionId, file, "row" in judged ? judged.row : undefined);
223
+ if (owner === undefined) {
224
+ this.disclose({ sessionId, path: full, reason: "corrupt" in judged ? judged.corrupt.reason : "session metadata is not a string" });
179
225
  continue;
180
226
  }
181
- if (typeof judged.row.__sid !== "string") {
182
- this.disclose({ sessionId, path: full, reason: "session metadata is not a string" });
183
- continue;
184
- }
185
- if (judged.row.__sid !== sessionId)
227
+ if (owner.sessionId !== sessionId)
186
228
  continue;
229
+ if ("corrupt" in judged)
230
+ this.refuseDamaged(sessionId, undefined, full, judged.corrupt.grade, judged.corrupt.reason);
231
+ if (typeof judged.row.__sid !== "string")
232
+ this.refuseDamaged(sessionId, undefined, full, "malformed", "session metadata is not a string");
187
233
  const shaped = judgeSessionPolicyRowShape(judged.row);
188
- if ("corrupt" in shaped) {
189
- this.disclose({ sessionId, path: full, reason: shaped.corrupt.reason });
190
- continue;
191
- }
234
+ if ("corrupt" in shaped)
235
+ this.refuseDamaged(sessionId, undefined, full, shaped.corrupt.grade, shaped.corrupt.reason);
192
236
  const attributed = judged.row.__principal;
193
237
  if (attributed !== undefined && attributed !== null && typeof attributed !== "string") {
194
- this.disclose({ sessionId, path: full, reason: "principal metadata is not a string" });
195
- continue;
238
+ this.refuseDamaged(sessionId, undefined, full, "malformed", "principal metadata is not a string");
196
239
  }
197
240
  out.push({ principal: attributed ?? undefined, rules: shaped.rules });
198
241
  }
199
242
  return out;
200
243
  }
201
244
  async deleteBySession(sessionId) {
202
- let files;
203
- try {
204
- files = readdirSync(this.dir);
205
- }
206
- catch (err) {
207
- if (err.code === "ENOENT")
208
- return;
209
- throw err;
210
- }
211
- for (const file of files) {
245
+ for (const file of this.sweepEntries(sessionId)) {
212
246
  if (file.endsWith(MARK_SUFFIX))
213
247
  continue;
214
248
  if (!file.endsWith(".json"))
215
249
  continue;
216
- let raw;
217
- try {
218
- raw = readFileSync(join(this.dir, file), "utf8");
219
- }
220
- catch {
250
+ const full = join(this.dir, file);
251
+ const judged = this.readRowFile(full);
252
+ if (judged === null)
221
253
  continue;
222
- }
223
- const judged = judgeSessionPolicyRowObject(raw);
224
- if ("corrupt" in judged) {
225
- if (judged.corrupt.grade !== "unparseable")
226
- continue;
227
- const tornMarkPath = join(this.dir, `${file.slice(0, -".json".length)}${MARK_SUFFIX}`);
228
- try {
229
- const tornMark = readSessionPolicyMark(tornMarkPath);
230
- if (tornMark === null) {
231
- this.writeMark(tornMarkPath, { __hwm: 0, __gen: 0, __lineage: "lost" });
232
- this.disclose({ sessionId, path: join(this.dir, file), reason: "erase purged a torn row that had no high-water mark: the key's lineage is lost, writes are refused until the mark is deleted" });
233
- }
234
- else {
235
- this.writeMark(tornMarkPath, { ...tornMark, __gen: (tornMark.__gen ?? 0) + 1 });
236
- }
237
- }
238
- catch (err) {
239
- if (!(err instanceof SessionPolicyError))
240
- throw err;
241
- this.disclose({ sessionId, path: join(this.dir, file), reason: `erase purged a torn row whose high-water mark is corrupt (${err.message}); the mark is left for the operator` });
242
- }
243
- try {
244
- unlinkSync(join(this.dir, file));
245
- }
246
- catch (err) {
247
- if (err.code !== "ENOENT")
248
- throw err;
249
- }
254
+ const row = "row" in judged ? judged.row : undefined;
255
+ const markPath = join(this.dir, `${file.slice(0, -".json".length)}${MARK_SUFFIX}`);
256
+ const { owner, markRead } = this.ownerOf(sessionId, file, row);
257
+ if (owner !== undefined) {
258
+ if (owner.sessionId === sessionId)
259
+ this.eraseRow(full, markPath, sessionId, owner.principal ?? null, row, markRead);
250
260
  continue;
251
261
  }
252
- const row = judged.row;
253
- if (row.__sid !== sessionId)
262
+ if (!("corrupt" in judged) || judged.corrupt.grade !== "unparseable")
254
263
  continue;
255
- const markPath = join(this.dir, `${file.slice(0, -".json".length)}${MARK_SUFFIX}`);
256
- let mark;
257
- try {
258
- mark = readSessionPolicyMark(markPath);
259
- }
260
- catch (err) {
261
- if (!(err instanceof SessionPolicyError))
262
- throw err;
263
- mark = undefined;
264
+ const tornRead = markRead ?? this.sweepMark(sessionId, markPath);
265
+ if ("corrupt" in tornRead) {
266
+ this.disclose({ sessionId, path: markPath, reason: `erase purged a torn row whose high-water mark is corrupt (it ${tornRead.corrupt.reason}); the mark is left for the operator` });
264
267
  }
265
- if (mark !== undefined) {
266
- const rowRev = typeof row.rev === "number" && Number.isFinite(row.rev) ? row.rev : 0;
267
- this.writeMarkFile(markPath, Math.max(mark?.__hwm ?? 0, rowRev), sessionId, row.__principal ?? null, (mark?.__gen ?? 0) + 1);
268
+ else if (tornRead.mark === null) {
269
+ this.writeMark(markPath, { __hwm: 0, __gen: 0, __lineage: "lost" });
270
+ this.disclose({ sessionId, path: full, reason: "erase purged a torn row that had no high-water mark: the key's lineage is lost, writes are refused until the mark is deleted" });
268
271
  }
269
- try {
270
- unlinkSync(join(this.dir, file));
271
- }
272
- catch (err) {
273
- if (err.code !== "ENOENT")
274
- throw err;
272
+ else {
273
+ this.writeMark(markPath, { ...tornRead.mark, __gen: (tornRead.mark.__gen ?? 0) + 1 });
275
274
  }
275
+ unlinkIfPresent(full);
276
+ }
277
+ }
278
+ eraseRow(full, markPath, sessionId, principal, row, observed) {
279
+ const markRead = observed ?? this.sweepMark(sessionId, markPath);
280
+ if (!("corrupt" in markRead)) {
281
+ const rowRev = typeof row?.rev === "number" && Number.isFinite(row.rev) ? row.rev : 0;
282
+ this.writeMarkFile(markPath, Math.max(markRead.mark?.__hwm ?? 0, rowRev), sessionId, principal, (markRead.mark?.__gen ?? 0) + 1);
276
283
  }
284
+ unlinkIfPresent(full);
285
+ }
286
+ }
287
+ function unlinkIfPresent(path) {
288
+ try {
289
+ unlinkSync(path);
290
+ }
291
+ catch (err) {
292
+ if (err.code !== "ENOENT")
293
+ throw err;
277
294
  }
278
295
  }
@@ -1,4 +1,5 @@
1
1
  import type { TaskListStore } from "../../tools/task-list.js";
2
+ import { type CorruptReadInfo } from "./corrupt-read-sink.js";
2
3
  export declare function createFileTaskListStore(root: string, opts?: {
3
4
  /**
4
5
  * The DATA ROOT whose adoption marker gates construction (design/183 I6). Most of the file family
@@ -13,4 +14,8 @@ export declare function createFileTaskListStore(root: string, opts?: {
13
14
  * on `adoptLocalDataRoot` lists that default among the seams it does not cover.
14
15
  */
15
16
  dataRoot?: string;
17
+ /** Disclosure sink for a task-list document that is present but cannot be read as one: it fires WITH the
18
+ * refusal and is the only carrier of the file's location (the refusal's message names none). Never fires on
19
+ * plain absence. Swallow-guarded: a throwing sink cannot change the refusal. */
20
+ onCorruptRead?: (info: CorruptReadInfo) => void;
16
21
  }): TaskListStore;
@@ -3,6 +3,7 @@ import { readFileSync } from "node:fs";
3
3
  import { assertJsonMetadata, normalizeTaskShape } from "../../tools/task-list.js";
4
4
  import { canonicalStoreKey, atomicWriteFile, ensureDir } from "./fs-atomic.js";
5
5
  import { assertAdoptionBootGate } from "./adoption/marker.js";
6
+ import { guardCorruptSink } from "./corrupt-read-sink.js";
6
7
  const dirLocks = new Map();
7
8
  export function createFileTaskListStore(root, opts) {
8
9
  const dir = resolve(root);
@@ -12,6 +13,11 @@ export function createFileTaskListStore(root, opts) {
12
13
  ensureDir(dir);
13
14
  const lockKey = canonicalStoreKey(dir);
14
15
  ensureDir(tmpDir);
16
+ const disclose = guardCorruptSink(opts?.onCorruptRead);
17
+ const refuseDamaged = (fault) => {
18
+ disclose?.({ kind: "file", path, reason: fault });
19
+ throw new Error(`file task-list store: the task list document ${fault} — refusing rather than start an empty list, which would hand out again the task ids it reserved. Repair it: fix the file, or delete it (deleting it = an empty task list whose ids restart at 1). The file's location is reported to the store's corrupt-read disclosure (onCorruptRead), not in this message.`);
20
+ };
15
21
  const load = () => {
16
22
  let raw;
17
23
  try {
@@ -20,14 +26,22 @@ export function createFileTaskListStore(root, opts) {
20
26
  catch (err) {
21
27
  if (err.code === "ENOENT")
22
28
  return { version: 1, nextId: 1, tasks: {} };
23
- throw err;
29
+ return refuseDamaged(`could not be read (${err.code ?? err.name})`);
24
30
  }
25
31
  if (raw === "")
26
32
  return { version: 1, nextId: 1, tasks: {} };
27
- const doc = JSON.parse(raw);
28
- if (typeof doc.nextId !== "number" || typeof doc.tasks !== "object" || doc.tasks === null) {
29
- throw new Error(`file task-list store: corrupt document at ${path}`);
33
+ let parsed;
34
+ try {
35
+ parsed = JSON.parse(raw);
36
+ }
37
+ catch {
38
+ return refuseDamaged("is not valid JSON (torn write?)");
30
39
  }
40
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
41
+ return refuseDamaged("is not a JSON object");
42
+ const doc = parsed;
43
+ if (typeof doc.nextId !== "number" || typeof doc.tasks !== "object" || doc.tasks === null)
44
+ return refuseDamaged("carries no numeric nextId or no tasks map");
31
45
  return doc;
32
46
  };
33
47
  const save = (doc) => {
@@ -1,4 +1,11 @@
1
1
  import { type UsageWindow, type UsageWindowReading, type UsageWindowStore } from "../../core/usage-window-store.js";
2
+ import { type CorruptReadInfo } from "./corrupt-read-sink.js";
3
+ export interface FileUsageWindowStoreOptions {
4
+ /** Disclosure sink for a ledger that is present but cannot be read as one: it fires WITH the refusal and is the
5
+ * only carrier of the file's location (the refusal's message, which reaches a run's failure text, names none).
6
+ * Never fires on plain absence. Swallow-guarded: a throwing sink cannot change the refusal. */
7
+ onCorruptRead?: (info: CorruptReadInfo) => void;
8
+ }
2
9
  /**
3
10
  * design/164 件五 — file-backed {@link UsageWindowStore}: ONE JSON record per governed key, semantics
4
11
  * identical to `InMemoryUsageWindowStore` BY CONSTRUCTION (both are shells around the same pure
@@ -17,7 +24,8 @@ import { type UsageWindow, type UsageWindowReading, type UsageWindowStore } from
17
24
  */
18
25
  export declare class FileUsageWindowStore implements UsageWindowStore {
19
26
  private readonly dir;
20
- constructor(root: string);
27
+ private readonly onCorruptRead;
28
+ constructor(root: string, opts?: FileUsageWindowStoreOptions);
21
29
  /** key → a safe, INJECTIVE filename (`sanitizeScope` appends the full sha256 of the raw key, so two
22
30
  * distinct principals can never share a ledger file). */
23
31
  private pathFor;
@@ -3,40 +3,30 @@ import { join } from "node:path";
3
3
  import { chargeUsageRecord, readUsageRecord, EMPTY_USAGE_WINDOW_RECORD, } from "../../core/usage-window-store.js";
4
4
  import { atomicWriteFile, ensureDir, sanitizeScope } from "./fs-atomic.js";
5
5
  import { assertAdoptionBootGate } from "./adoption/marker.js";
6
+ import { guardCorruptSink } from "./corrupt-read-sink.js";
6
7
  export class FileUsageWindowStore {
7
8
  dir;
8
- constructor(root) {
9
+ onCorruptRead;
10
+ constructor(root, opts) {
9
11
  assertAdoptionBootGate(root, "FileUsageWindowStore");
10
12
  this.dir = join(root, "usage-windows");
11
13
  ensureDir(this.dir);
14
+ this.onCorruptRead = guardCorruptSink(opts?.onCorruptRead);
12
15
  }
13
16
  pathFor(key) {
14
17
  return join(this.dir, `${sanitizeScope(key)}.json`);
15
18
  }
16
19
  loadRecord(key) {
17
20
  const path = this.pathFor(key);
18
- let raw;
19
21
  try {
20
- raw = readFileSync(path, "utf8");
22
+ return judgeRecord(path);
21
23
  }
22
24
  catch (err) {
23
- if (err.code === "ENOENT")
24
- return EMPTY_USAGE_WINDOW_RECORD;
25
- throw err;
25
+ if (!(err instanceof LedgerFault))
26
+ throw err;
27
+ this.onCorruptRead?.({ kind: "file", path, reason: err.message });
28
+ throw corrupt(key, err.message);
26
29
  }
27
- let parsed;
28
- try {
29
- parsed = JSON.parse(raw);
30
- }
31
- catch {
32
- throw corrupt(path, "unparseable JSON (torn/zero-byte write?)");
33
- }
34
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
35
- throw corrupt(path, "not a JSON object");
36
- const rec = parsed;
37
- const slots = validateSlots(rec.slots, path);
38
- const buckets = validateBuckets(rec.buckets, path);
39
- return { slots, buckets };
40
30
  }
41
31
  async charge(key, tokens, at, windows, costMicroUsd) {
42
32
  const next = chargeUsageRecord(this.loadRecord(key), tokens, at, windows, costMicroUsd);
@@ -46,28 +36,52 @@ export class FileUsageWindowStore {
46
36
  return readUsageRecord(this.loadRecord(key), windows, now);
47
37
  }
48
38
  }
49
- function corrupt(path, reason) {
50
- 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.`);
39
+ class LedgerFault extends Error {
40
+ }
41
+ function judgeRecord(path) {
42
+ let raw;
43
+ try {
44
+ raw = readFileSync(path, "utf8");
45
+ }
46
+ catch (err) {
47
+ if (err.code === "ENOENT")
48
+ return EMPTY_USAGE_WINDOW_RECORD;
49
+ throw new LedgerFault(`read failed (${err.code ?? err.name})`);
50
+ }
51
+ let parsed;
52
+ try {
53
+ parsed = JSON.parse(raw);
54
+ }
55
+ catch {
56
+ throw new LedgerFault("unparseable JSON (torn/zero-byte write?)");
57
+ }
58
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
59
+ throw new LedgerFault("not a JSON object");
60
+ const rec = parsed;
61
+ return { slots: validateSlots(rec.slots), buckets: validateBuckets(rec.buckets) };
62
+ }
63
+ function corrupt(key, reason) {
64
+ const e = new Error(`usage-window ledger for key ${JSON.stringify(key)} is unreadable (${reason}). Refused rather than treated as empty: an unreadable ceiling would grant the key an unlimited, silent allowance. Repair it: fix the file, or delete it (deleting it = no usage recorded for the key). The file's location is reported to the store's corrupt-read disclosure (onCorruptRead), not in this message.`);
51
65
  e.code = "usage_window.store_corrupt";
52
66
  return e;
53
67
  }
54
- function validateSlots(value, path) {
68
+ function validateSlots(value) {
55
69
  if (value === undefined)
56
70
  return [];
57
71
  if (!Array.isArray(value))
58
- throw corrupt(path, "`slots` is not an array");
72
+ throw new LedgerFault("`slots` is not an array");
59
73
  return value.map((s) => {
60
74
  if (s === null || typeof s !== "object")
61
- throw corrupt(path, "`slots` entry is not an object");
75
+ throw new LedgerFault("`slots` entry is not an object");
62
76
  const { at, tokens, costMicroUsd, costUnknown } = s;
63
77
  if (typeof at !== "number" || !Number.isFinite(at) || typeof tokens !== "number" || !Number.isFinite(tokens)) {
64
- throw corrupt(path, "`slots` entry has a non-numeric `at`/`tokens`");
78
+ throw new LedgerFault("`slots` entry has a non-numeric `at`/`tokens`");
65
79
  }
66
80
  if (costMicroUsd !== undefined && (typeof costMicroUsd !== "number" || !Number.isFinite(costMicroUsd))) {
67
- throw corrupt(path, "`slots` entry has a non-numeric `costMicroUsd`");
81
+ throw new LedgerFault("`slots` entry has a non-numeric `costMicroUsd`");
68
82
  }
69
83
  if (costUnknown !== undefined && costUnknown !== true)
70
- throw corrupt(path, "`slots` entry has a `costUnknown` that is not `true`");
84
+ throw new LedgerFault("`slots` entry has a `costUnknown` that is not `true`");
71
85
  return {
72
86
  at,
73
87
  tokens,
@@ -76,14 +90,14 @@ function validateSlots(value, path) {
76
90
  };
77
91
  });
78
92
  }
79
- function validateBuckets(value, path) {
93
+ function validateBuckets(value) {
80
94
  if (value === undefined)
81
95
  return [];
82
96
  if (!Array.isArray(value))
83
- throw corrupt(path, "`buckets` is not an array");
97
+ throw new LedgerFault("`buckets` is not an array");
84
98
  return value.map((b) => {
85
99
  if (b === null || typeof b !== "object")
86
- throw corrupt(path, "`buckets` entry is not an object");
100
+ throw new LedgerFault("`buckets` entry is not an object");
87
101
  const { windowMs, openedAt, tokens, costMicroUsd, costUnknown } = b;
88
102
  if (typeof windowMs !== "number" ||
89
103
  !Number.isFinite(windowMs) ||
@@ -91,13 +105,13 @@ function validateBuckets(value, path) {
91
105
  !Number.isFinite(openedAt) ||
92
106
  typeof tokens !== "number" ||
93
107
  !Number.isFinite(tokens)) {
94
- throw corrupt(path, "`buckets` entry has a non-numeric `windowMs`/`openedAt`/`tokens`");
108
+ throw new LedgerFault("`buckets` entry has a non-numeric `windowMs`/`openedAt`/`tokens`");
95
109
  }
96
110
  if (costMicroUsd !== undefined && (typeof costMicroUsd !== "number" || !Number.isFinite(costMicroUsd))) {
97
- throw corrupt(path, "`buckets` entry has a non-numeric `costMicroUsd`");
111
+ throw new LedgerFault("`buckets` entry has a non-numeric `costMicroUsd`");
98
112
  }
99
113
  if (costUnknown !== undefined && costUnknown !== true)
100
- throw corrupt(path, "`buckets` entry has a `costUnknown` that is not `true`");
114
+ throw new LedgerFault("`buckets` entry has a `costUnknown` that is not `true`");
101
115
  return {
102
116
  windowMs,
103
117
  openedAt,
@@ -5,7 +5,7 @@ import { protectivePathTargetOf, type ToolCallLike } from "../../core/tool-regis
5
5
  /**
6
6
  * CC parity: Read/Edit/Write take the file path as `file_path`. Read the target the same way
7
7
  * EVERYWHERE the path is consumed from a tool-call's args — not just the tool body but ALSO every gate that
8
- * extracts the write target to confine it (sensitive-path, skill allowPaths, session allowDirs). A gate that only
8
+ * extracts the write target to confine it (sensitive-path, session allowDirs, the fs-write gate). A gate that only
9
9
  * read `args.path` would see `undefined` for a `file_path` call and either fail-OPEN (circumvent the guard — a real
10
10
  * hole) or fail-closed (deny a legitimate write). Single source.
11
11
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "7.40.1",
3
+ "version": "7.41.0",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",