@sema-agent/core 7.28.1 → 7.30.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 (162) hide show
  1. package/CHANGELOG.md +473 -1
  2. package/dist/agents/child-model-seat.d.ts +59 -7
  3. package/dist/agents/child-model-seat.js +33 -7
  4. package/dist/agents/cross-session-judge.d.ts +27 -5
  5. package/dist/agents/cross-session-judge.js +23 -12
  6. package/dist/agents/list-agents-tool.d.ts +31 -6
  7. package/dist/agents/list-agents-tool.js +66 -14
  8. package/dist/agents/peer-admission.d.ts +20 -8
  9. package/dist/agents/peer-admission.js +21 -12
  10. package/dist/agents/peer-directory.d.ts +29 -0
  11. package/dist/agents/peer-directory.js +20 -0
  12. package/dist/agents/peer-held-queue.d.ts +31 -4
  13. package/dist/agents/peer-held-queue.js +32 -1
  14. package/dist/agents/peer-session-drain.d.ts +91 -13
  15. package/dist/agents/peer-session-drain.js +409 -102
  16. package/dist/agents/read-inbox-tool.d.ts +22 -0
  17. package/dist/agents/read-inbox-tool.js +39 -0
  18. package/dist/agents/roster-store.d.ts +2 -4
  19. package/dist/agents/roster-store.js +1 -1
  20. package/dist/agents/send-message-tool.d.ts +4 -4
  21. package/dist/agents/send-message-tool.js +22 -2
  22. package/dist/agents/subagent.js +62 -44
  23. package/dist/agents/team-role.d.ts +34 -0
  24. package/dist/agents/team-role.js +17 -0
  25. package/dist/agents/verify.js +2 -2
  26. package/dist/brain/anthropic.js +4 -2
  27. package/dist/brain/open-responses.js +4 -3
  28. package/dist/brain/openai.js +2 -1
  29. package/dist/brain/stream-engine.js +6 -0
  30. package/dist/brain/stream-shared.d.ts +8 -0
  31. package/dist/brain/stream-shared.js +8 -0
  32. package/dist/core/auto-compaction.d.ts +42 -0
  33. package/dist/core/auto-compaction.js +35 -2
  34. package/dist/core/child-clamp-seats.d.ts +11 -0
  35. package/dist/core/child-clamp-seats.js +3 -0
  36. package/dist/core/context-edit.d.ts +3 -2
  37. package/dist/core/context-edit.js +6 -5
  38. package/dist/core/context-guard.d.ts +2 -1
  39. package/dist/core/context-guard.js +3 -2
  40. package/dist/core/context-settings.d.ts +103 -0
  41. package/dist/core/context-settings.js +60 -0
  42. package/dist/core/engine-notice.d.ts +18 -1
  43. package/dist/core/frame-settlement.d.ts +97 -0
  44. package/dist/core/frame-settlement.js +36 -0
  45. package/dist/core/governance-codes.d.ts +1 -1
  46. package/dist/core/governance-codes.js +4 -0
  47. package/dist/core/hooks.d.ts +10 -10
  48. package/dist/core/mailbox-store.d.ts +41 -7
  49. package/dist/core/mailbox-store.js +9 -3
  50. package/dist/core/message-utils.d.ts +13 -0
  51. package/dist/core/message-utils.js +6 -0
  52. package/dist/core/parent-spec-seat.d.ts +14 -1
  53. package/dist/core/parent-spec-seat.js +6 -0
  54. package/dist/core/permission-rule-model.js +1 -1
  55. package/dist/core/roles.d.ts +20 -0
  56. package/dist/core/roles.js +35 -16
  57. package/dist/core/runner/assemble-result.d.ts +2 -0
  58. package/dist/core/runner/assemble-result.js +3 -1
  59. package/dist/core/runner/compaction-call-options.d.ts +26 -2
  60. package/dist/core/runner/compaction-call-options.js +9 -0
  61. package/dist/core/runner/compaction-seams.d.ts +4 -3
  62. package/dist/core/runner/compaction-seams.js +13 -6
  63. package/dist/core/runner/contracts.d.ts +61 -7
  64. package/dist/core/runner/decide-continuation.d.ts +2 -1
  65. package/dist/core/runner/decide-continuation.js +11 -10
  66. package/dist/core/runner/initial-run-state.js +2 -2
  67. package/dist/core/runner/prepare-caps-and-workflow.js +9 -1
  68. package/dist/core/runner/prepare-config-doors.js +4 -1
  69. package/dist/core/runner/prepare-context-lane.d.ts +9 -2
  70. package/dist/core/runner/prepare-context-lane.js +15 -10
  71. package/dist/core/runner/prepare-delegation-surface.d.ts +3 -2
  72. package/dist/core/runner/prepare-delegation-surface.js +9 -3
  73. package/dist/core/runner/prepare-file-history.js +10 -8
  74. package/dist/core/runner/prepare-gate-stations.d.ts +4 -0
  75. package/dist/core/runner/prepare-gate-stations.js +3 -1
  76. package/dist/core/runner/prepare-inherited-gate.js +1 -0
  77. package/dist/core/runner/prepare-prompt-inputs.d.ts +6 -0
  78. package/dist/core/runner/prepare-prompt-inputs.js +2 -1
  79. package/dist/core/runner/prepare-task.js +22 -9
  80. package/dist/core/runner/prepare-wiring-manifest.d.ts +6 -1
  81. package/dist/core/runner/prepare-wiring-manifest.js +6 -1
  82. package/dist/core/runner/response-model-seat.d.ts +16 -0
  83. package/dist/core/runner/response-model-seat.js +27 -0
  84. package/dist/core/runner/retired-runner-deps.d.ts +3 -1
  85. package/dist/core/runner/retired-runner-deps.js +1 -0
  86. package/dist/core/runner/run-compaction-machinery.d.ts +4 -2
  87. package/dist/core/runner/run-compaction-machinery.js +5 -3
  88. package/dist/core/runner/run-harness-handlers.js +9 -2
  89. package/dist/core/runner/run-identity-wiring.js +19 -1
  90. package/dist/core/runner/run-leg.js +1 -1
  91. package/dist/core/runner/run-notification-lane.js +8 -2
  92. package/dist/core/runner/run-recovery-lanes.js +9 -5
  93. package/dist/core/runner/run-settle-and-teardown.d.ts +4 -22
  94. package/dist/core/runner/run-settle-and-teardown.js +6 -10
  95. package/dist/core/runner/run-stop-and-final-verify.js +1 -0
  96. package/dist/core/runner/run-terminal-adoption.js +5 -5
  97. package/dist/core/runner/run-turn-boundary.js +32 -5
  98. package/dist/core/runner/runtask.d.ts +8 -1
  99. package/dist/core/runner/runtask.js +33 -2
  100. package/dist/core/runner-deps.d.ts +44 -13
  101. package/dist/core/side-query.d.ts +6 -5
  102. package/dist/core/side-query.js +3 -1
  103. package/dist/core/swappable-deps.d.ts +20 -4
  104. package/dist/core/swappable-deps.js +6 -1
  105. package/dist/core/task-event.d.ts +13 -0
  106. package/dist/core/task-notification.d.ts +144 -2
  107. package/dist/core/task-notification.js +154 -5
  108. package/dist/core/task-result.d.ts +27 -3
  109. package/dist/core/task-spec.d.ts +7 -0
  110. package/dist/core/tool-catalog-entries.js +2 -1
  111. package/dist/core/tool-conformance.js +1 -0
  112. package/dist/core/tool-policy.d.ts +9 -4
  113. package/dist/core/tool-spec.d.ts +14 -0
  114. package/dist/core/wiring-manifest.d.ts +12 -0
  115. package/dist/core/wiring-manifest.js +5 -1
  116. package/dist/engine/harness/agent-harness.d.ts +14 -1
  117. package/dist/engine/harness/agent-harness.js +28 -3
  118. package/dist/engine/harness/types.d.ts +3 -1
  119. package/dist/engine/llm/types.d.ts +8 -7
  120. package/dist/engine/llm/types.js +1 -1
  121. package/dist/engine/session/import-validate.js +4 -0
  122. package/dist/index.d.ts +10 -7
  123. package/dist/index.js +7 -5
  124. package/dist/orchestration/run-workflow-tool.d.ts +4 -4
  125. package/dist/orchestration/workflow.d.ts +1 -1
  126. package/dist/orchestration/workflow.js +8 -5
  127. package/dist/shell/lex.d.ts +30 -51
  128. package/dist/shell/lex.js +237 -284
  129. package/dist/stores/cc/mailbox-store.js +3 -2
  130. package/dist/stores/file/corrupt-read-sink.d.ts +28 -2
  131. package/dist/stores/file/file-history-store.d.ts +2 -4
  132. package/dist/stores/file/file-history-store.js +1 -1
  133. package/dist/stores/file/fs-atomic.d.ts +16 -11
  134. package/dist/stores/file/fs-atomic.js +17 -6
  135. package/dist/stores/file/index.d.ts +5 -6
  136. package/dist/stores/file/index.js +1 -1
  137. package/dist/stores/file/mailbox-store.d.ts +4 -9
  138. package/dist/stores/file/mailbox-store.js +29 -30
  139. package/dist/stores/file/session-policy-store.d.ts +2 -0
  140. package/dist/stores/file/session-policy-store.js +1 -1
  141. package/dist/stores/file/session-store.d.ts +11 -7
  142. package/dist/stores/file/session-store.js +23 -16
  143. package/dist/stores/file/shared-ledger.js +3 -14
  144. package/dist/stores/file/tool-result-store.d.ts +2 -0
  145. package/dist/stores/file/tool-result-store.js +5 -2
  146. package/dist/stores/file/workflow-journal-store.js +14 -9
  147. package/dist/tools/fs/bash-program-position.d.ts +15 -24
  148. package/dist/tools/fs/bash-program-position.js +11 -29
  149. package/dist/tools/fs/bash-readonly-classifier.d.ts +125 -117
  150. package/dist/tools/fs/bash-readonly-classifier.js +400 -272
  151. package/dist/tools/fs/shell-read-check.js +3 -3
  152. package/dist/tools/fs/shell-read-establish.js +1 -1
  153. package/dist/tools/fs/shell-read-plan.d.ts +11 -6
  154. package/dist/tools/fs/shell-read-plan.js +167 -183
  155. package/dist/tools/fs/shell-read-word.d.ts +50 -0
  156. package/dist/tools/fs/shell-read-word.js +59 -0
  157. package/dist/tools/fs/shell-reentry-text.d.ts +10 -0
  158. package/dist/tools/fs/shell-reentry-text.js +73 -0
  159. package/package.json +1 -1
  160. package/test/export-surface.snapshot.json +58 -8
  161. package/wire-changes/core@7.29.0.json +185 -0
  162. package/wire-changes/core@7.30.0.json +1821 -0
@@ -202,9 +202,9 @@ export function createCcFileMailboxStore(opts) {
202
202
  const path = inboxPath(handle);
203
203
  const held = leases.get(leaseKey(handle));
204
204
  if (held === undefined || held.owner !== owner)
205
- return Promise.resolve();
205
+ return Promise.resolve({ acked: false });
206
206
  if (!existsSync(path))
207
- return Promise.resolve();
207
+ return Promise.resolve({ acked: false });
208
208
  const notes = deferredNotes();
209
209
  return withCcLock(path, () => {
210
210
  const box = loadBox(path, notes.note);
@@ -219,6 +219,7 @@ export function createCcFileMailboxStore(opts) {
219
219
  saveBox(path, box);
220
220
  if (held.maxSeq <= upToSeq)
221
221
  leases.delete(leaseKey(handle));
222
+ return { acked: true };
222
223
  }).finally(() => {
223
224
  notes.flush();
224
225
  });
@@ -4,8 +4,34 @@
4
4
  * hand-written four times. Three copies contained an async sink's rejection; the mailbox's caught only a
5
5
  * synchronous throw, so an `async` sink there became an unhandled rejection.
6
6
  */
7
- /** Coordinates of one durable read a store answered as absent because the bytes were corrupt. */
8
- export interface CorruptReadInfo {
7
+ import type { CorruptBodyForm } from "../../core/store-contracts/read-back-shape-contract.js";
8
+ /** One disclosure. Every level carries `path` + `reason`; `kind` names the level. */
9
+ export type CorruptReadInfo = CorruptRecordRead | CorruptReplayTally | CorruptFileRead;
10
+ /** One JSONL record a replay skipped: fired per skipped record, in file order, before its read's tally. */
11
+ export interface CorruptRecordRead {
12
+ kind: "record";
13
+ path: string;
14
+ reason: string;
15
+ /** 1-based line of the skipped record. */
16
+ line: number;
17
+ form: CorruptBodyForm;
18
+ }
19
+ /**
20
+ * The count of one replay that skipped at least one record, fired once after its record disclosures; a
21
+ * read that skipped nothing fires none. `total` = `kept` + `dropped` (an unterminated crash tail is not a
22
+ * record); `kept === 0` = nothing in the file was readable.
23
+ */
24
+ export interface CorruptReplayTally {
25
+ kind: "replay";
26
+ path: string;
27
+ reason: string;
28
+ kept: number;
29
+ dropped: number;
30
+ total: number;
31
+ }
32
+ /** A disclosure about a file or directory as a whole, outside any JSONL replay. */
33
+ export interface CorruptFileRead {
34
+ kind: "file";
9
35
  path: string;
10
36
  reason: string;
11
37
  }
@@ -1,6 +1,7 @@
1
1
  import type { ExecutionEnv } from "../../internal/harness-types.js";
2
2
  import { type FileHistoryDiffStats, type FileHistoryExport, type FileHistoryResult, type FileHistoryRestoreResult, type FileHistoryStore, type FileHistoryTrackResult } from "../../core/file-history-store.js";
3
3
  import { type FileHistoryRetentionPolicy } from "../../core/file-history-retention.js";
4
+ import { type CorruptReadInfo } from "./corrupt-read-sink.js";
4
5
  /**
5
6
  * design/381 slice 2 — the DURABLE file-backed {@link FileHistoryStore}: the per-edited-file rewind
6
7
  * history that survives a process restart (slice 1 shipped the contract + the in-memory reference,
@@ -75,10 +76,7 @@ export interface FileFileHistoryStoreOptions {
75
76
  * are then kept — leaked, never lost). Never fires on plain ENOENT: a scope with no history yet
76
77
  * really is empty. Swallow-guarded.
77
78
  */
78
- onCorruptRead?: (info: {
79
- path: string;
80
- reason: string;
81
- }) => void;
79
+ onCorruptRead?: (info: CorruptReadInfo) => void;
82
80
  /**
83
81
  * design/381 §片2 — the built-in boundary bound and its override. Omitted ⇒ the engine default
84
82
  * ({@link import("../../core/file-history-retention.js").DEFAULT_FILE_HISTORY_BOUNDARY_KEEP}
@@ -195,7 +195,7 @@ export class FileFileHistoryStore {
195
195
  }
196
196
  }
197
197
  disclose(path, reason) {
198
- this.onCorruptRead?.({ path, reason });
198
+ this.onCorruptRead?.({ kind: "file", path, reason });
199
199
  }
200
200
  scopeDir(scope) {
201
201
  return join(this.scopesDir, sanitizeScope(scope));
@@ -1,3 +1,4 @@
1
+ import type { CorruptRecordRead, CorruptReplayTally } from "./corrupt-read-sink.js";
1
2
  /**
2
3
  * Validate an engine-minted id (sessionId / tool-result ref) before using it as a path component. These are
3
4
  * `uuidv7` / an encoded tool-result ref — already filename-safe — but a strict guard is cheap
@@ -42,19 +43,23 @@ export declare function atomicWriteFile(tmpDir: string, target: string, bytes: s
42
43
  *
43
44
  * SHAPE (`@contract store.read_back_shape_judged`): parseability is not the judgement. Every store on this
44
45
  * format writes ONE JSON OBJECT per line, so a line parsing to a number, string, boolean, `null` or array
45
- * takes the road a torn line takes — disclosed, skipped, the replay intact — because it is the same fact:
46
- * these bytes are not what this store wrote. Without it `null` made a `line.kind` test throw.
46
+ * takes the road a torn line takes — disclosed and skipped — because it is the same fact: these bytes are
47
+ * not what this store wrote. `judge` (optional) is the consuming store's own floor on an object ("does it
48
+ * carry my members at all?"); a record it rejects is skipped as `missing-required` on the same road, so
49
+ * the count below covers every skip.
47
50
  *
48
- * `onCorrupt` (optional, review-510 disclosure-seat family): fired once per skipped INTERIOR record so a
49
- * caller-side seat can observe real corruption — a silently-skipped record is otherwise indistinguishable
50
- * from "less data"; its `reason` opens with the {@link CorruptBodyForm} word, so a sink classifies without
51
- * reading prose. The torn TAIL stays out by design: dying mid-write is the crash shape this format exists
52
- * to absorb. The callback is invoked as given — a seat that must not throw swallow-guards on ITS side.
51
+ * `onCorrupt` (optional): one `record` disclosure per skipped INTERIOR record, then — iff any was skipped —
52
+ * one `replay` tally. The torn TAIL stays out of both by design: dying mid-write is the crash shape this
53
+ * format exists to absorb. The callback is invoked as given — a seat that must not throw swallow-guards on
54
+ * ITS side.
53
55
  */
54
- export declare function readJsonlRecords<T>(path: string, onCorrupt?: (info: {
55
- path: string;
56
- reason: string;
57
- }) => void): T[];
56
+ export declare function readJsonlRecords<T>(path: string, onCorrupt?: (info: CorruptRecordRead | CorruptReplayTally) => void, judge?: JsonlRecordJudge): T[];
57
+ /** A consuming store's floor on a parsed JSONL object: `accepts` = one of its records; `what` names a
58
+ * rejected one in the disclosure's words. */
59
+ export interface JsonlRecordJudge {
60
+ accepts(record: object): boolean;
61
+ what: string;
62
+ }
58
63
  /**
59
64
  * An append-only JSONL log opened once with `O_APPEND` and held for the file's lifetime. One record =
60
65
  * one `JSON.stringify(...) + "\n"` written in a SINGLE `writeSync` call (never split across writes → the
@@ -105,7 +105,7 @@ function basenameOf(p) {
105
105
  function dirnameOf(p) {
106
106
  return dirname(p);
107
107
  }
108
- export function readJsonlRecords(path, onCorrupt) {
108
+ export function readJsonlRecords(path, onCorrupt, judge) {
109
109
  let raw;
110
110
  try {
111
111
  raw = readFileSync(path, "utf8");
@@ -120,6 +120,11 @@ export function readJsonlRecords(path, onCorrupt) {
120
120
  const parts = raw.split("\n");
121
121
  const endedClean = parts[parts.length - 1] === "";
122
122
  const out = [];
123
+ let dropped = 0;
124
+ const discloseSkip = (form, line, what) => {
125
+ dropped++;
126
+ onCorrupt?.({ kind: "record", path, reason: `${form}: ${what} at line ${line} (skipped)`, line, form });
127
+ };
123
128
  for (let i = 0; i < parts.length; i++) {
124
129
  if (i === parts.length - 1 && !endedClean)
125
130
  break;
@@ -131,20 +136,26 @@ export function readJsonlRecords(path, onCorrupt) {
131
136
  record = JSON.parse(line);
132
137
  }
133
138
  catch {
134
- onCorrupt?.({ path, reason: corruptRecordReason("torn-json", i + 1, "unparseable jsonl record") });
139
+ discloseSkip("torn-json", i + 1, "unparseable jsonl record");
135
140
  continue;
136
141
  }
137
142
  if (record === null || typeof record !== "object" || Array.isArray(record)) {
138
- onCorrupt?.({ path, reason: corruptRecordReason("not-an-object", i + 1, `jsonl record is ${jsonTypeName(record)}, not an object`) });
143
+ discloseSkip("not-an-object", i + 1, `jsonl record is ${jsonTypeName(record)}, not an object`);
144
+ continue;
145
+ }
146
+ if (judge !== undefined && !judge.accepts(record)) {
147
+ discloseSkip("missing-required", i + 1, judge.what);
139
148
  continue;
140
149
  }
141
150
  out.push(record);
142
151
  }
152
+ if (dropped > 0) {
153
+ const total = out.length + dropped;
154
+ const reason = out.length === 0 ? `replay: none of ${total} records readable (all skipped)` : `replay: ${dropped} of ${total} records skipped, ${out.length} kept`;
155
+ onCorrupt?.({ kind: "replay", path, reason, kept: out.length, dropped, total });
156
+ }
143
157
  return out;
144
158
  }
145
- function corruptRecordReason(form, lineNo, what) {
146
- return `${form}: ${what} at line ${lineNo} (skipped; the rest of the replay is intact)`;
147
- }
148
159
  function jsonTypeName(value) {
149
160
  if (value === null)
150
161
  return "null";
@@ -5,6 +5,7 @@ import { type EvictPolicy } from "../../core/session-store.js";
5
5
  import type { SessionStore } from "../../core/session.js";
6
6
  import { type FileCheckpointStoreOptions } from "./checkpoint-store.js";
7
7
  import { type FileFileHistoryStoreOptions } from "./file-history-store.js";
8
+ import type { CorruptReadInfo } from "./corrupt-read-sink.js";
8
9
  import type { FileHistoryStore } from "../../core/file-history-store.js";
9
10
  import type { StrategyStore } from "../../core/strategy-store.js";
10
11
  import type { BackgroundAgentStore } from "../../core/background-agent-store.js";
@@ -105,17 +106,15 @@ export interface FileStorageBackendOptions {
105
106
  * backend constructs can deliver, so ONE sink reaches all of them (a per-store payload type would make
106
107
  * the backend's forwarding contravariance-illegal, which is what kept the seat single-store).
107
108
  * `SessionPolicyCorruptReadInfo` is assignable to it; the two-field store seats are satisfied by it.
109
+ * `path` (absolute) and `reason` ride every level; `kind` tells a skipped JSONL record (`"record"`) and
110
+ * that replay's count (`"replay"`) from a disclosure about a file or directory as a whole (`"file"`).
108
111
  */
109
- export interface FileStorageCorruptReadInfo {
110
- /** Absolute path of the offending file/directory. */
111
- path: string;
112
- /** What was observed and what the read degraded to. */
113
- reason: string;
112
+ export type FileStorageCorruptReadInfo = CorruptReadInfo & {
114
113
  /** Present only when the reading store keys by session (the session-policy face). */
115
114
  sessionId?: string;
116
115
  /** Present only when the read was keyed by a principal. */
117
116
  principal?: string;
118
- }
117
+ };
119
118
  export declare class FileStorageBackend {
120
119
  /** The resolved, canonical data root all four stores live under. */
121
120
  readonly root: string;
@@ -89,7 +89,7 @@ export class FileStorageBackend {
89
89
  root: strategiesRoot,
90
90
  ...(opts.onCorruptRead !== undefined
91
91
  ? {
92
- onIncident: (i) => opts.onCorruptRead?.({ path: i.path ?? strategiesRoot, reason: `strategy ${i.op}: ${i.error}` }),
92
+ onIncident: (i) => opts.onCorruptRead?.({ kind: "file", path: i.path ?? strategiesRoot, reason: `strategy ${i.op}: ${i.error}` }),
93
93
  }
94
94
  : {}),
95
95
  });
@@ -1,4 +1,5 @@
1
- import { type MailboxAppendMessage, type MailboxLease, type MailboxStore } from "../../core/mailbox-store.js";
1
+ import { type MailboxAckResult, type MailboxAppendMessage, type MailboxLease, type MailboxStore } from "../../core/mailbox-store.js";
2
+ import { type CorruptReadInfo } from "./corrupt-read-sink.js";
2
3
  /** The waiter's refusal when a box lock stays held by a LIVE process past `lockWaitMs`. Carried as
3
4
  * `.code` on a plain Error (consumers branch on the string); the message names the lock and the holder. */
4
5
  export declare const MAILBOX_BOX_LOCK_TIMEOUT_CODE = "mailbox.box_lock_timeout";
@@ -45,10 +46,7 @@ export interface FileMailboxStoreOptions {
45
46
  * INTERIOR record dropped by the JSONL replay is disclosed here instead of silently reading as
46
47
  * "less mail". The torn tail stays silent by design — that is the append-log's documented
47
48
  * crash-recovery shape, not corruption. Swallow-guarded; absent = the old silent tolerance. */
48
- onCorruptRead?: (info: {
49
- path: string;
50
- reason: string;
51
- }) => void;
49
+ onCorruptRead?: (info: CorruptReadInfo) => void;
52
50
  }
53
51
  export declare class FileMailboxStore implements MailboxStore {
54
52
  /** design/385 — the whole cross-process set (box lock + fresh re-read + descriptor reopen +
@@ -105,16 +103,13 @@ export declare class FileMailboxStore implements MailboxStore {
105
103
  * and the cache is replayed from the file. A moved INODE also swap-closes the descriptor (ring 3):
106
104
  * the next write reopens the current file instead of landing on the unlinked one.
107
105
  */
108
- /** Fold ONE replayed event, disclosing a record that is not one of this box's ({@link applyEvent}
109
- * answers `false`): dropped silently it reads exactly like a shorter mailbox. */
110
- private applyReplayed;
111
106
  private reconcile;
112
107
  private load;
113
108
  private commit;
114
109
  private compact;
115
110
  append(scope: string, handle: string, msg: MailboxAppendMessage): Promise<number>;
116
111
  claimLease(scope: string, handle: string, owner: string, ttlMs: number, now?: number): Promise<MailboxLease | null>;
117
- ack(scope: string, handle: string, owner: string, upToSeq: number): Promise<void>;
112
+ ack(scope: string, handle: string, owner: string, upToSeq: number): Promise<MailboxAckResult>;
118
113
  releaseLease(scope: string, handle: string, owner: string): Promise<void>;
119
114
  peekCount(scope: string, handle: string): Promise<number>;
120
115
  drop(scope: string, handle: string): Promise<void>;
@@ -373,23 +373,18 @@ export class FileMailboxStore {
373
373
  return result;
374
374
  });
375
375
  }
376
- applyReplayed(b, ev, path) {
377
- if (applyEvent(b, ev))
378
- return;
379
- this.discloseCorrupt?.({ path, reason: "missing-required: a mailbox record is not one of this box's events — an unknown tag, or a known tag whose payload is missing (skipped; the rest of the replay is intact)" });
380
- }
381
376
  reconcile(b) {
382
377
  const now = diskMarkOf(b.path);
383
378
  if (sameDiskMark(now, b.mark))
384
379
  return;
385
380
  if (now === undefined || b.mark === undefined || now.ino !== b.mark.ino)
386
381
  b.log.closeForSwap();
387
- const events = readJsonlRecords(b.path, this.discloseCorrupt);
382
+ const events = readJsonlRecords(b.path, this.discloseCorrupt, MAILBOX_EVENT_JUDGE);
388
383
  b.messages = [];
389
384
  b.nextSeq = 1;
390
385
  delete b.lease;
391
386
  for (const ev of events)
392
- this.applyReplayed(b, ev, b.path);
387
+ applyEvent(b, ev);
393
388
  b.events = Math.max(0, events.length - (b.messages.length + 1 + (b.lease !== undefined ? 1 : 0)));
394
389
  b.mark = now;
395
390
  }
@@ -406,10 +401,10 @@ export class FileMailboxStore {
406
401
  return b;
407
402
  }
408
403
  ensureDir(join(this.dir, sanitizeScope(scope)));
409
- const events = readJsonlRecords(path, this.discloseCorrupt);
404
+ const events = readJsonlRecords(path, this.discloseCorrupt, MAILBOX_EVENT_JUDGE);
410
405
  const state = { messages: [], nextSeq: 1, log: new AppendLog(path), events: events.length, refs: 1, path, mark: undefined };
411
406
  for (const ev of events)
412
- this.applyReplayed(state, ev, path);
407
+ applyEvent(state, ev);
413
408
  state.mark = diskMarkOf(path);
414
409
  sharedBoxes.set(key, state);
415
410
  this.touched.set(key, state);
@@ -417,7 +412,8 @@ export class FileMailboxStore {
417
412
  }
418
413
  commit(b, path, ev) {
419
414
  b.log.append(ev, this.fsyncEnabled);
420
- applyEvent(b, ev);
415
+ if (MAILBOX_EVENT_JUDGE.accepts(ev))
416
+ applyEvent(b, ev);
421
417
  b.events++;
422
418
  if (b.events >= this.compactEvery) {
423
419
  try {
@@ -475,8 +471,9 @@ export class FileMailboxStore {
475
471
  return this.withBox(this.lockKey(scope, handle), this.boxPath(scope, handle), () => {
476
472
  const b = this.load(scope, handle);
477
473
  if (b.lease === undefined || b.lease.owner !== owner)
478
- return;
474
+ return { acked: false };
479
475
  this.commit(b, this.boxPath(scope, handle), { t: "ack", upToSeq });
476
+ return { acked: true };
480
477
  });
481
478
  }
482
479
  async releaseLease(scope, handle, owner) {
@@ -496,8 +493,8 @@ export class FileMailboxStore {
496
493
  return 0;
497
494
  return withPathLock(this.lockKey(scope, handle), () => {
498
495
  const b = { messages: [], nextSeq: 1 };
499
- for (const ev of readJsonlRecords(path, this.discloseCorrupt))
500
- this.applyReplayed(b, ev, path);
496
+ for (const ev of readJsonlRecords(path, this.discloseCorrupt, MAILBOX_EVENT_JUDGE))
497
+ applyEvent(b, ev);
501
498
  return b.messages.length;
502
499
  });
503
500
  }
@@ -556,8 +553,8 @@ export class FileMailboxStore {
556
553
  if (sharedBoxes.has(key))
557
554
  return false;
558
555
  const b = { messages: [], nextSeq: 1 };
559
- for (const ev of readJsonlRecords(path, this.discloseCorrupt))
560
- this.applyReplayed(b, ev, path);
556
+ for (const ev of readJsonlRecords(path, this.discloseCorrupt, MAILBOX_EVENT_JUDGE))
557
+ applyEvent(b, ev);
561
558
  const newest = newestSentAt(b.messages);
562
559
  if (newest === undefined || newest >= now - maxAgeMs)
563
560
  return false;
@@ -584,35 +581,37 @@ export class FileMailboxStore {
584
581
  }
585
582
  }
586
583
  const num = (v) => typeof v === "number" && Number.isFinite(v);
584
+ const MAILBOX_EVENT_JUDGE = {
585
+ accepts: (r) => {
586
+ const ev = r;
587
+ if (ev.t === "append")
588
+ return ev.m !== null && typeof ev.m === "object" && num(ev.m.seq);
589
+ if (ev.t === "lease")
590
+ return typeof ev.owner === "string" && num(ev.maxSeq) && num(ev.expiresAt);
591
+ if (ev.t === "ack")
592
+ return num(ev.upToSeq);
593
+ return ev.t === "release" && typeof ev.owner === "string";
594
+ },
595
+ what: "a mailbox record is not one of this box's events — an unknown tag, or a known tag whose payload is missing",
596
+ };
587
597
  function applyEvent(b, ev) {
588
598
  if (ev.t === "append") {
589
- if (ev.m === null || typeof ev.m !== "object" || !num(ev.m.seq))
590
- return false;
591
599
  b.messages.push(ev.m);
592
600
  if (ev.m.seq >= b.nextSeq)
593
601
  b.nextSeq = ev.m.seq + 1;
594
- return true;
595
602
  }
596
- if (ev.t === "lease") {
597
- if (typeof ev.owner !== "string" || !num(ev.maxSeq) || !num(ev.expiresAt))
598
- return false;
603
+ else if (ev.t === "lease") {
599
604
  if (ev.maxSeq >= b.nextSeq)
600
605
  b.nextSeq = ev.maxSeq + 1;
601
606
  if (ev.owner !== "")
602
607
  b.lease = { owner: ev.owner, expiresAt: ev.expiresAt, maxSeq: ev.maxSeq };
603
- return true;
604
608
  }
605
- if (ev.t === "ack") {
606
- if (!num(ev.upToSeq))
607
- return false;
609
+ else if (ev.t === "ack") {
608
610
  b.messages = b.messages.filter((m) => m.seq > ev.upToSeq);
609
611
  if (b.lease !== undefined && b.lease.maxSeq <= ev.upToSeq)
610
612
  b.lease = undefined;
611
- return true;
612
613
  }
613
- if (ev.t !== "release" || typeof ev.owner !== "string")
614
- return false;
615
- if (b.lease?.owner === ev.owner)
614
+ else if (b.lease?.owner === ev.owner) {
616
615
  b.lease = undefined;
617
- return true;
616
+ }
618
617
  }
@@ -16,6 +16,8 @@ import { type PutRulesOptions, type SessionPermissionRules, type SessionPolicySt
16
16
  /** Coordinates of one corrupt-treated-as-absent policy read (the {@link FileSessionPolicyStoreOptions.onCorruptRead}
17
17
  * payload). Named rather than inline so the backend option that forwards it names the SAME shape. */
18
18
  export interface SessionPolicyCorruptReadInfo {
19
+ /** Always a whole-file disclosure (the {@link import("./corrupt-read-sink.js").CorruptReadInfo} level). */
20
+ kind: "file";
19
21
  /** The session whose read observed the corrupt row. On the enumeration face this is the session being
20
22
  * ENUMERATED — a corrupt row's own `__sid` is by definition unreadable, so it cannot be attributed. */
21
23
  sessionId: string;
@@ -77,7 +77,7 @@ export class FileSessionPolicyStore {
77
77
  adoption !== undefined && "adopted" in adoption && adoption.adopted.from.kind === "local-owner" ? adoption.adopted.toPrincipal : undefined;
78
78
  }
79
79
  disclose(info) {
80
- this.onCorruptRead?.(info);
80
+ this.onCorruptRead?.({ kind: "file", ...info });
81
81
  }
82
82
  discloseCorrupt(sessionId, principal, reason) {
83
83
  this.disclose({ sessionId, ...(principal !== undefined ? { principal } : {}), path: this.pathFor(sessionId, principal), reason });
@@ -1,5 +1,6 @@
1
1
  import type { Session, SessionCreateOptions, SessionForkOptions, SessionMetadata, SessionPlacementRecord, SessionRepo, SessionTreeEntry } from "../../internal/harness.js";
2
2
  import type { PlacedSessionRow } from "../../core/session.js";
3
+ import { type CorruptReadInfo } from "./corrupt-read-sink.js";
3
4
  export interface FileSessionRepoOptions {
4
5
  /**
5
6
  * Disclosure sink for a durable read this repo treats as ABSENT (ruled 2026-08-03). Same name and
@@ -7,17 +8,14 @@ export interface FileSessionRepoOptions {
7
8
  * "corrupt read as missing" are one failure grammar, so they get one seat name.
8
9
  *
9
10
  * Fires for: an unreadable sessions DIRECTORY (the listing degrades to "no sessions"), a session log
10
- * that could not be replayed during a listing (skipped from the result), and a corrupt interior JSONL
11
- * record dropped on replay (turns silently missing from an otherwise healthy session).
11
+ * that could not be replayed during a listing (skipped from the result), and each corrupt interior JSONL
12
+ * record dropped on replay plus that replay's tally (turns missing from an otherwise healthy session).
12
13
  *
13
14
  * Never fires on plain ENOENT / `not_found` — that is honest absence, and the torn TAIL stays silent
14
15
  * too (dying mid-write is the append log's documented crash-recovery shape, not corruption).
15
16
  * Swallow-guarded: a throwing sink cannot re-introduce the fault the fail-open avoids.
16
17
  */
17
- onCorruptRead?: (info: {
18
- path: string;
19
- reason: string;
20
- }) => void;
18
+ onCorruptRead?: (info: CorruptReadInfo) => void;
21
19
  }
22
20
  export declare class FileSessionRepo implements SessionRepo {
23
21
  private readonly dir;
@@ -31,7 +29,13 @@ export declare class FileSessionRepo implements SessionRepo {
31
29
  /** The one delivery point for {@link FileSessionRepoOptions.onCorruptRead}. */
32
30
  private disclose;
33
31
  private pathFor;
34
- /** Read + torn-tail-recover a session file into (meta, entries); a missing file → not_found. */
32
+ /**
33
+ * Read + torn-tail-recover a session file into (meta, entries). Three answers: no file → `not_found`; a
34
+ * file with no meta header → `corrupt_session`, even when every other record is intact (every write path
35
+ * here lays the header down first and the header alone carries `createdAt` / fork / placement, so the file
36
+ * is not a session this store wrote, or its header line is damaged); otherwise the session, each skipped
37
+ * record disclosed.
38
+ */
35
39
  private read;
36
40
  private storage;
37
41
  /**
@@ -6,6 +6,13 @@ import { guardCorruptSink } from "./corrupt-read-sink.js";
6
6
  import { assertAdoptionBootGate } from "./adoption/marker.js";
7
7
  import { thrownText } from "../../core/thrown-value.js";
8
8
  const SUFFIX = ".jsonl";
9
+ const SESSION_LINE_JUDGE = {
10
+ accepts: (r) => {
11
+ const line = r;
12
+ return line.kind === "meta" || (line.kind === "entry" && line.entry !== null && typeof line.entry === "object");
13
+ },
14
+ what: "a session line is neither a meta header nor an entry record",
15
+ };
9
16
  const sharedSessionStorages = new Map();
10
17
  const sessionStorageFinalizer = new FinalizationRegistry(({ canonical, log }) => {
11
18
  log.close();
@@ -68,7 +75,7 @@ export class FileSessionRepo {
68
75
  this.onCorruptRead = guardCorruptSink(opts?.onCorruptRead);
69
76
  }
70
77
  disclose(path, reason) {
71
- this.onCorruptRead?.({ path, reason });
78
+ this.onCorruptRead?.({ kind: "file", path, reason });
72
79
  }
73
80
  pathFor(id) {
74
81
  return join(this.dir, `${sanitizePathComponent(id)}${SUFFIX}`);
@@ -78,24 +85,24 @@ export class FileSessionRepo {
78
85
  if (!existsSync(path)) {
79
86
  throw new SessionError("not_found", `Session not found: ${id}`);
80
87
  }
81
- const lines = readJsonlRecords(path, (info) => this.disclose(info.path, info.reason));
82
- let createdAt = "";
83
- let forkedFrom;
84
- let placement;
88
+ let tally;
89
+ const lines = readJsonlRecords(path, (info) => {
90
+ if (info.kind === "replay")
91
+ tally = info;
92
+ this.onCorruptRead?.(info);
93
+ }, SESSION_LINE_JUDGE);
94
+ let meta;
85
95
  const entries = [];
86
96
  for (const line of lines) {
87
- if (line.kind === "meta") {
88
- createdAt = line.createdAt;
89
- forkedFrom = line.forkedFrom;
90
- placement = line.placement;
91
- continue;
92
- }
93
- if (line.kind !== "entry" || line.entry === null || typeof line.entry !== "object") {
94
- this.disclose(path, "missing-required: a session line is neither a meta header nor an entry record (skipped; the rest of the replay is intact)");
95
- continue;
96
- }
97
- entries.push(line.entry);
97
+ if (line.kind === "meta")
98
+ meta = line;
99
+ else
100
+ entries.push(line.entry);
101
+ }
102
+ if (meta === undefined) {
103
+ throw new SessionError("corrupt_session", `Session ${id} is unreadable: its log has no meta header (${tally?.dropped ?? 0} of ${tally?.total ?? lines.length} records skipped as corrupt)`);
98
104
  }
105
+ const { createdAt, forkedFrom, placement } = meta;
99
106
  return { createdAt, ...(forkedFrom !== undefined ? { forkedFrom } : {}), ...(placement !== undefined ? { placement } : {}), entries };
100
107
  }
101
108
  storage(id, createdAt, entries, forkedFrom, placement) {
@@ -21,22 +21,11 @@ export class LedgerCore {
21
21
  this.fence = acquireStoreDirLock(this.paths.dir, { label: this.model.label });
22
22
  try {
23
23
  const disclose = this.paths.onCorruptRead;
24
- const mine = (xs, judge, path, what) => {
25
- const kept = [];
26
- for (const x of xs) {
27
- if (judge(x))
28
- kept.push(x);
29
- else
30
- disclose?.({ path, reason: `missing-required: a ${what} carries none of this ${this.model.label}'s own members (skipped; the rest of the replay is intact)` });
31
- }
32
- return kept;
33
- };
34
- const snapPath = this.paths.snapshotPath;
35
- const ledgerPath = this.paths.ledgerPath;
36
- for (const row of mine(readJsonlRecords(snapPath, disclose), this.model.judgeRow, snapPath, "snapshot row")) {
24
+ const judge = (accepts, what) => ({ accepts, what: `a ${what} carries none of this ${this.model.label}'s own members` });
25
+ for (const row of readJsonlRecords(this.paths.snapshotPath, disclose, judge(this.model.judgeRow, "snapshot row"))) {
37
26
  this.rows.set(this.model.keyOf(row), row);
38
27
  }
39
- const events = mine(readJsonlRecords(ledgerPath, disclose), this.model.judgeEvent, ledgerPath, "ledger event");
28
+ const events = readJsonlRecords(this.paths.ledgerPath, disclose, judge(this.model.judgeEvent, "ledger event"));
40
29
  this.events = events.length;
41
30
  for (const ev of events)
42
31
  this.model.apply(this.rows, ev);
@@ -37,6 +37,8 @@ export declare class FileToolResultStore implements ToolResultStore {
37
37
  private readonly dir;
38
38
  private readonly discloseCorrupt;
39
39
  constructor(root: string, opts?: FileToolResultStoreOptions);
40
+ /** The one delivery point for {@link FileToolResultStoreOptions.onCorruptRead}. */
41
+ private disclose;
40
42
  /** The directory a ref's files live in, and their shared stem. See {@link encodeRefFilename} for why
41
43
  * a FOLDED name gets its own directory instead of sharing one with identity names. */
42
44
  private locate;
@@ -14,6 +14,9 @@ export class FileToolResultStore {
14
14
  this.discloseCorrupt = guardCorruptSink(opts?.onCorruptRead);
15
15
  ensureDir(this.dir);
16
16
  }
17
+ disclose(path, reason) {
18
+ this.discloseCorrupt?.({ kind: "file", path, reason });
19
+ }
17
20
  locate(ref) {
18
21
  assertSafeToolResultRef(ref);
19
22
  const encoded = encodeRefFilename(ref);
@@ -81,7 +84,7 @@ export class FileToolResultStore {
81
84
  return undefined;
82
85
  if (opts.loud)
83
86
  throw err;
84
- this.discloseCorrupt?.({ path: ownerPath, reason: `unreadable owner sidecar (${err.code ?? "unknown"}) — read as unowned` });
87
+ this.disclose(ownerPath, `unreadable owner sidecar (${err.code ?? "unknown"}) — read as unowned`);
85
88
  return undefined;
86
89
  }
87
90
  const parsed = parseOwnerRecord(raw);
@@ -90,7 +93,7 @@ export class FileToolResultStore {
90
93
  if (opts.loud) {
91
94
  throw new Error(`tool-result store: owner record for ref ${JSON.stringify(ref)} is present but unreadable — refusing to write over it`);
92
95
  }
93
- this.discloseCorrupt?.({ path: ownerPath, reason: "not-an-object: owner sidecar is present but is not an owner record — read as unowned" });
96
+ this.disclose(ownerPath, "not-an-object: owner sidecar is present but is not an owner record — read as unowned");
94
97
  return undefined;
95
98
  }
96
99
  writeOwnerFile(ref, provenance) {
@@ -19,6 +19,10 @@ export const RESUME_CLAIM_SWEEP_INTERVAL_MS = 60_000;
19
19
  export const RESUME_CLAIM_REAP_GRACE_MS = 5 * 60_000;
20
20
  const sharedJournalDirs = new Map();
21
21
  const MAX_OPEN_JOURNAL_LOGS = 64;
22
+ function isJournalLine(r) {
23
+ const rec = r;
24
+ return typeof rec.callKey === "string" && typeof rec.scope === "string" && Number.isFinite(rec.ordinal);
25
+ }
22
26
  export class FileWorkflowJournalStore {
23
27
  fsyncEnabled;
24
28
  dir;
@@ -75,16 +79,17 @@ export class FileWorkflowJournalStore {
75
79
  return undefined;
76
80
  let scope;
77
81
  const byOrdinal = new Map();
78
- for (const rec of readJsonlRecords(path, this.discloseCorrupt)) {
79
- if (typeof rec.callKey !== "string" || typeof rec.scope !== "string" || !Number.isFinite(rec.ordinal)) {
80
- this.discloseCorrupt?.({ path, reason: "missing-required: a journal line carries no callKey/scope pair (skipped; the rest of the replay is intact)" });
81
- continue;
82
- }
83
- scope = scope ?? rec.scope;
84
- if (rec.scope !== scope)
85
- continue;
82
+ const judge = {
83
+ accepts: (r) => {
84
+ if (!isJournalLine(r))
85
+ return false;
86
+ scope ??= r.scope;
87
+ return r.scope === scope;
88
+ },
89
+ what: "a journal line carries no callKey/scope pair, or a scope other than its run's first line",
90
+ };
91
+ for (const rec of readJsonlRecords(path, this.discloseCorrupt, judge))
86
92
  byOrdinal.set(rec.ordinal, entryOf(rec));
87
- }
88
93
  return scope === undefined ? undefined : { scope, byOrdinal };
89
94
  }
90
95
  async append(runId, scope, entry) {