@sema-agent/core 5.65.0 → 6.0.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 (159) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/agents/subagent.d.ts +2 -2
  3. package/dist/agents/subagent.js +11 -0
  4. package/dist/agents/verify.d.ts +1 -1
  5. package/dist/brain/anthropic.js +1 -1
  6. package/dist/brain/errors.d.ts +29 -0
  7. package/dist/brain/errors.js +20 -0
  8. package/dist/brain/open-responses.js +2 -2
  9. package/dist/brain/route-adjudicator.d.ts +8 -1
  10. package/dist/brain/route-adjudicator.js +1 -0
  11. package/dist/brain/status-sink.js +12 -1
  12. package/dist/brain/stream-engine.js +17 -6
  13. package/dist/core/auto-compaction.d.ts +26 -0
  14. package/dist/core/auto-compaction.js +7 -2
  15. package/dist/core/auto-mode-arming.d.ts +138 -0
  16. package/dist/core/auto-mode-arming.js +181 -0
  17. package/dist/core/auto-mode-defaults.d.ts +13 -0
  18. package/dist/core/auto-mode-defaults.js +5 -0
  19. package/dist/core/auto-mode-prompt.d.ts +14 -3
  20. package/dist/core/auto-mode-prompt.js +10 -7
  21. package/dist/core/auto-mode-rebuild.d.ts +75 -0
  22. package/dist/core/auto-mode-rebuild.js +41 -0
  23. package/dist/core/auto-mode.d.ts +15 -0
  24. package/dist/core/auto-mode.js +4 -2
  25. package/dist/core/checkpoint-store.d.ts +18 -0
  26. package/dist/core/context-edit.d.ts +47 -5
  27. package/dist/core/context-guard.d.ts +1 -1
  28. package/dist/core/file-history-retention.d.ts +106 -0
  29. package/dist/core/file-history-retention.js +36 -0
  30. package/dist/core/file-history-store.d.ts +768 -0
  31. package/dist/core/file-history-store.js +880 -0
  32. package/dist/core/governance-codes.d.ts +2 -1
  33. package/dist/core/governance-codes.js +14 -0
  34. package/dist/core/hooks.d.ts +39 -7
  35. package/dist/core/hooks.js +38 -21
  36. package/dist/core/lsp.d.ts +2 -2
  37. package/dist/core/mcp.d.ts +29 -7
  38. package/dist/core/memory-engine/consolidation-driver.d.ts +11 -0
  39. package/dist/core/memory-engine/consolidation-driver.js +71 -4
  40. package/dist/core/memory-engine/consolidation.d.ts +25 -2
  41. package/dist/core/memory-engine/consolidation.js +4 -1
  42. package/dist/core/memory-engine/distiller.d.ts +84 -1
  43. package/dist/core/memory-engine/distiller.js +68 -0
  44. package/dist/core/memory-engine/dual-root.js +3 -0
  45. package/dist/core/memory-engine/engine.d.ts +328 -15
  46. package/dist/core/memory-engine/engine.js +355 -29
  47. package/dist/core/memory-engine/file-backend.d.ts +30 -0
  48. package/dist/core/memory-engine/file-backend.js +14 -13
  49. package/dist/core/memory-engine/frontmatter.d.ts +22 -1
  50. package/dist/core/memory-engine/frontmatter.js +3 -0
  51. package/dist/core/memory-engine/header-hints.d.ts +5 -0
  52. package/dist/core/memory-engine/index.d.ts +5 -4
  53. package/dist/core/memory-engine/index.js +5 -4
  54. package/dist/core/memory-engine/layout.d.ts +88 -2
  55. package/dist/core/memory-engine/layout.js +112 -3
  56. package/dist/core/memory-engine/provenance-wording.d.ts +7 -0
  57. package/dist/core/memory-engine/provenance-wording.js +3 -0
  58. package/dist/core/memory-engine/tools.d.ts +89 -8
  59. package/dist/core/memory-engine/tools.js +263 -22
  60. package/dist/core/memory-engine/types.d.ts +64 -1
  61. package/dist/core/memory-recall.d.ts +6 -0
  62. package/dist/core/memory.d.ts +27 -1
  63. package/dist/core/memory.js +16 -2
  64. package/dist/core/permission-rule-consent.d.ts +20 -0
  65. package/dist/core/permission-rule-consent.js +12 -3
  66. package/dist/core/permission-rule-model.d.ts +67 -7
  67. package/dist/core/permission-rule-model.js +53 -7
  68. package/dist/core/permission-rule-store.js +15 -10
  69. package/dist/core/permission-rule-sync.js +15 -11
  70. package/dist/core/retention-policy.d.ts +9 -0
  71. package/dist/core/retention-policy.js +5 -2
  72. package/dist/core/retention.d.ts +13 -2
  73. package/dist/core/runner/assemble-result.d.ts +19 -1
  74. package/dist/core/runner/assemble-result.js +17 -2
  75. package/dist/core/runner/compaction-call-options.d.ts +93 -0
  76. package/dist/core/runner/compaction-call-options.js +3 -0
  77. package/dist/core/runner/memory-capture-optout.d.ts +80 -0
  78. package/dist/core/runner/memory-capture-optout.js +53 -0
  79. package/dist/core/runner/prepare-config-doors.d.ts +5 -0
  80. package/dist/core/runner/prepare-config-doors.js +16 -0
  81. package/dist/core/runner/prepare-hands-readface.d.ts +110 -5
  82. package/dist/core/runner/prepare-hands-readface.js +99 -7
  83. package/dist/core/runner/prepare-memory.d.ts +88 -0
  84. package/dist/core/runner/prepare-memory.js +305 -24
  85. package/dist/core/runner/prepare-task.d.ts +141 -1
  86. package/dist/core/runner/prepare-task.js +443 -79
  87. package/dist/core/runner/runtask.d.ts +9 -20
  88. package/dist/core/runner/runtask.js +133 -96
  89. package/dist/core/runner/session-file-state-replay.d.ts +18 -10
  90. package/dist/core/runner/session-file-state-replay.js +52 -1
  91. package/dist/core/runner/tool-disclosure.js +2 -1
  92. package/dist/core/runner/turn-attachments.d.ts +22 -12
  93. package/dist/core/session-store.d.ts +1 -1
  94. package/dist/core/session-store.js +6 -1
  95. package/dist/core/session.d.ts +34 -1
  96. package/dist/core/store-contracts/file-history-store-contract.d.ts +3 -0
  97. package/dist/core/store-contracts/file-history-store-contract.js +720 -0
  98. package/dist/core/task-registry-shared.js +11 -1
  99. package/dist/core/tool-errors.js +1 -0
  100. package/dist/core/tool-policy.d.ts +172 -1
  101. package/dist/core/tool-policy.js +32 -1
  102. package/dist/core/tool-result-store.js +2 -1
  103. package/dist/core/trace.d.ts +24 -0
  104. package/dist/core/types.d.ts +784 -89
  105. package/dist/core/types.js +4 -3
  106. package/dist/core/untrusted-text.d.ts +1 -1
  107. package/dist/core/untrusted-text.js +8 -0
  108. package/dist/core/workflow-run-store-contract.js +6 -2
  109. package/dist/core/workflow-run-store.d.ts +4 -1
  110. package/dist/engine/compaction/compaction.d.ts +88 -10
  111. package/dist/engine/compaction/compaction.js +109 -30
  112. package/dist/engine/execution-env/node-execution-env.d.ts +9 -1
  113. package/dist/engine/execution-env/node-execution-env.js +28 -0
  114. package/dist/engine/harness/agent-harness.d.ts +52 -1
  115. package/dist/engine/harness/agent-harness.js +36 -1
  116. package/dist/engine/harness/types.d.ts +26 -1
  117. package/dist/engine/llm/types.d.ts +50 -4
  118. package/dist/engine/loop/agent-loop.d.ts +5 -1
  119. package/dist/engine/loop/agent-loop.js +25 -0
  120. package/dist/engine/loop/types.d.ts +19 -0
  121. package/dist/engine/lsp/node-lsp-manager.d.ts +1 -1
  122. package/dist/engine/session/session.js +1 -1
  123. package/dist/index.d.ts +18 -8
  124. package/dist/index.js +14 -6
  125. package/dist/orchestration/run-workflow-tool.d.ts +20 -2
  126. package/dist/orchestration/run-workflow-tool.js +22 -3
  127. package/dist/orchestration/workflow-governance.d.ts +59 -1
  128. package/dist/orchestration/workflow-governance.js +61 -8
  129. package/dist/orchestration/workflow-meta.d.ts +4 -2
  130. package/dist/orchestration/workflow-primitives.js +56 -13
  131. package/dist/orchestration/workflow-types.d.ts +78 -2
  132. package/dist/orchestration/workflow.d.ts +20 -0
  133. package/dist/orchestration/workflow.js +163 -14
  134. package/dist/prompt-assembly/event-registry.js +1 -1
  135. package/dist/prompts/default.d.ts +7 -7
  136. package/dist/stores/file/file-history-store.d.ts +368 -0
  137. package/dist/stores/file/file-history-store.js +1248 -0
  138. package/dist/stores/file/index.d.ts +22 -13
  139. package/dist/stores/file/index.js +4 -4
  140. package/dist/stores/file/permission-rule-store.js +1 -0
  141. package/dist/stores/file/strategy-store.d.ts +3 -3
  142. package/dist/tools/fs/bash-readonly-classifier.d.ts +87 -3
  143. package/dist/tools/fs/bash-readonly-classifier.js +106 -4
  144. package/dist/tools/fs/fs-bash.js +9 -5
  145. package/dist/tools/fs/fs-shared.d.ts +52 -1
  146. package/dist/tools/fs/fs-shared.js +14 -0
  147. package/dist/tools/fs/fs-write.d.ts +5 -5
  148. package/dist/tools/fs/fs-write.js +71 -14
  149. package/dist/tools/fs/index.d.ts +6 -1
  150. package/dist/tools/fs/index.js +1 -1
  151. package/dist/tools/web.js +2 -1
  152. package/package.json +5 -1
  153. package/test/export-surface.snapshot.json +155 -23
  154. package/dist/core/file-snapshot-store.d.ts +0 -165
  155. package/dist/core/file-snapshot-store.js +0 -259
  156. package/dist/core/store-contracts/file-snapshot-store-contract.d.ts +0 -13
  157. package/dist/core/store-contracts/file-snapshot-store-contract.js +0 -134
  158. package/dist/stores/file/file-snapshot-store.d.ts +0 -58
  159. package/dist/stores/file/file-snapshot-store.js +0 -353
@@ -0,0 +1,368 @@
1
+ import type { ExecutionEnv } from "../../internal/harness-types.js";
2
+ import { type FileHistoryDiffStats, type FileHistoryExport, type FileHistoryResult, type FileHistoryRestoreResult, type FileHistoryStore, type FileHistoryTrackResult } from "../../core/file-history-store.js";
3
+ import { type FileHistoryRetentionPolicy } from "../../core/file-history-retention.js";
4
+ /**
5
+ * design/381 slice 2 — the DURABLE file-backed {@link FileHistoryStore}: the per-edited-file rewind
6
+ * history that survives a process restart (slice 1 shipped the contract + the in-memory reference,
7
+ * and the file backend carried no rewind store in between — this closes that hole).
8
+ *
9
+ * It REUSES core's exported halves rather than re-deriving them, which is the §3 分工 clause, not a
10
+ * style preference: `captureFileBackupViaEnv`/`applyFileRestoreViaEnv` own every env-touching and
11
+ * security-relevant decision (content-hash equality, deletion markers, the parent-identity guard),
12
+ * and `trackKeyOf`/`resolveTrackKey` own the path coordinate (root-relative under the task root,
13
+ * verbatim-absolute outside it, family-conditional, '..' fail-closed). A backend that hand-rolls
14
+ * either is how symlink-escape and moved-root bugs come back.
15
+ *
16
+ * ON-DISK LAYOUT under `<root>/file-history/`:
17
+ * ```
18
+ * blobs/<sha256> write-ONCE content-addressed bytes, shared by
19
+ * every scope and version (EEXIST = dedup no-op)
20
+ * scopes/<sanitizeScope(scope)>/
21
+ * tracked.json the tracked set + version chains (pair array)
22
+ * boundaries/<entryId>.json one COMMITTED boundary = one file (its existence
23
+ * IS canRestore; written LAST, so a boundary is
24
+ * visible only once everything it references is)
25
+ * staging/<attempt>.jsonl the crash-residue journal of an in-flight capture
26
+ * ```
27
+ *
28
+ * ATOMIC VISIBILITY (§3, the F5/G11a family): blobs are published first (atomic write-then-link),
29
+ * the version chains second (one atomic whole-file replace), the boundary file LAST. A crash in the
30
+ * middle therefore leaves at worst orphan blobs and orphan version records — both collectable — and
31
+ * NEVER a boundary whose bytes are missing. `canRestore` only ever answers true for a boundary file
32
+ * that exists and parses.
33
+ *
34
+ * CRASH RESIDUE IS REAPER-JUDGEABLE (the slice-1 adversarial round's registered leak, closed here):
35
+ * an in-memory backend that dies mid-capture leaks its staged blobs with nothing left to judge them.
36
+ * Here every capture opens a staging journal naming the blobs it published; `reap` unions the
37
+ * journals of THIS process's live attempts into the live set (so a concurrent reap cannot collect a
38
+ * capture's bytes) and treats every OTHER journal as residue of a dead attempt — it removes the
39
+ * journal, and the blobs it protected fall out of the live set in the same pass.
40
+ *
41
+ * WRITER COORDINATES (stated rather than assumed; the two clauses answer different questions):
42
+ * · WITHIN one process, the durable face is the FILE, not the instance — several store instances
43
+ * over one data root are a supported configuration (this suite pins it): committed history is
44
+ * mutually visible, verb bodies re-read disk after every await and commit synchronously (one
45
+ * event loop = no torn interleave), and `reap` judges another instance's in-flight staging by
46
+ * proof-of-death, never by "not in my map". There is deliberately no shared in-process table —
47
+ * cross-instance coordination all rides the on-disk artifacts.
48
+ * · ACROSS OS processes the premise stays SINGLE-WRITER per data directory (the
49
+ * {@link import("./index.js").FileStorageBackend} boot lock in the assembled deployment; a
50
+ * directly-constructed store takes no fence of its own — the contract has no dispose verb a
51
+ * lease could ride): `tracked.json` is a whole-file replace with no cross-process merge, so two
52
+ * WRITER processes can lose each other's rows. Only the GC path is hardened beyond the premise
53
+ * (a foreign journal is protected by owner-pid liveness + age), because deleting bytes is the
54
+ * one mistake that cannot be walked back. The capture-lease sets
55
+ * (`spentAttempts`/`activeCaptures`) are deliberately in-process and NOT persisted (§10 S-6:
56
+ * the lease close is the calling Runner's discipline, and after a crash no new process has a
57
+ * capture path for the old entryId), so cross-replica fencing is a multi-writer backend's own
58
+ * job (the SQL twin's DB-level claim), not something this backend pretends to provide.
59
+ *
60
+ * RETENTION IS BUILT IN (design/381 §片2): a scope's boundary directory grows by one file per turn,
61
+ * so this backend trims it after every commit — the newest
62
+ * {@link import("../../core/file-history-retention.js").DEFAULT_FILE_HISTORY_BOUNDARY_KEEP} survive
63
+ * by default, and {@link FileFileHistoryStoreOptions.retention} is how a deployment says otherwise
64
+ * (including handing the schedule back with `keep: "unbounded"`). Of the two bundled backends this
65
+ * is the one where the absence of a bound was a disk that fills, which is why the bound is the
66
+ * store's own and not a chore delegated to whoever wires it.
67
+ */
68
+ export interface FileFileHistoryStoreOptions {
69
+ /**
70
+ * Disclosure sink for a durable read this store treats as ABSENT because the bytes were corrupt
71
+ * or unreadable — same name and `{path, reason}` shape as every sibling file store, because
72
+ * "unreadable read as missing" is one failure grammar. Fires for: a corrupt/unparseable
73
+ * `tracked.json` or boundary file collapsed to "no history" (the never-throw seam's documented
74
+ * fail-open), and a blob-GC pass that ABORTED because its live set could not be completed (blobs
75
+ * are then kept — leaked, never lost). Never fires on plain ENOENT: a scope with no history yet
76
+ * really is empty. Swallow-guarded.
77
+ */
78
+ onCorruptRead?: (info: {
79
+ path: string;
80
+ reason: string;
81
+ }) => void;
82
+ /**
83
+ * design/381 §片2 — the built-in boundary bound and its override. Omitted ⇒ the engine default
84
+ * ({@link import("../../core/file-history-retention.js").DEFAULT_FILE_HISTORY_BOUNDARY_KEEP}
85
+ * newest boundaries per scope, trimmed after each commit); `{ keep: "unbounded" }` switches the
86
+ * self-trim off and hands the GC schedule back to the deployment. A malformed policy is REFUSED
87
+ * at construction — this store writes to a disk that a silently-substituted retention policy
88
+ * would fill.
89
+ */
90
+ retention?: FileHistoryRetentionPolicy;
91
+ }
92
+ export declare class FileFileHistoryStore implements FileHistoryStore {
93
+ private readonly base;
94
+ private readonly blobsDir;
95
+ private readonly scopesDir;
96
+ private readonly onCorruptRead;
97
+ /**
98
+ * scope → entryIds whose capture attempt failed: the lease is spent, so a retry refuses with ZERO
99
+ * env reads (§10 S-6 — in-process, deliberately not persisted; see {@link reapedCache} for the
100
+ * neighbouring ledger that IS durable and why the two differ).
101
+ *
102
+ * NESTED, not the joined `scope\0entryId` key this used to be (slice 3) — and the honest reason is
103
+ * defence in depth rather than a live bug. A joined key is only unambiguous while NEITHER component
104
+ * can contain the separator, and the entry id cannot: `historyPathComponent` runs FIRST in
105
+ * `snapshot` and rejects anything outside `[A-Za-z0-9_.-]`, so with NUL-free ids the last separator
106
+ * splits the string uniquely and no two (scope, entryId) pairs can share a key. The problem with
107
+ * that argument is where it lives: the ledger's correctness depended on a whitelist in a different
108
+ * function, and a future id spelling that relaxes it (or a second caller that reaches these ledgers
109
+ * without the path guard) would silently make one scope's failed capture close another scope's
110
+ * lease — refusing a good boundary AND explaining it with a "prior attempt" that scope never made.
111
+ * The nested form does not need the argument. It is the shape the reaped ledger beside it already
112
+ * uses, for the reason stated there.
113
+ */
114
+ private readonly spentAttempts;
115
+ /** scope → entryId → staging journal path, for the CAPTURES in flight right now: a concurrent
116
+ * same-entryId capture is refused, and `reap` treats their journals' blobs as live. */
117
+ private readonly activeCaptures;
118
+ /** scope → the staging journals of the IMPORTS in flight right now. Separate from the captures
119
+ * above rather than sharing one key space with them: an import is keyed by its own journal path
120
+ * and a capture by an entry id, and one map holding both would be a key space where two unrelated
121
+ * vocabularies have to promise never to spell the same string. */
122
+ private readonly activeImports;
123
+ /**
124
+ * The CONSUMED-ID ledger's in-process view — a CACHE over `scopes/<scope>/reaped.log`, not the
125
+ * record itself (design/381 slice 3, closing the residual slice 2 registered here by name).
126
+ *
127
+ * Why it had to become durable. Reaping a boundary deletes the one artifact that proved its
128
+ * entryId had been consumed, and `snapshot`'s create-once check read exactly that artifact; without
129
+ * a tombstone a later capture for a reaped id takes the CAPTURE path and publishes post-turn bytes
130
+ * under a pre-turn key. Slice 2 kept the tombstones in this object, which holds for one store
131
+ * instance and for nothing else: a PEER store over the same data directory (supported in-process)
132
+ * and every restart see neither the deleted boundary file nor another instance's Set, so an old
133
+ * entryId re-presented THERE reached the capture path — the hole, one door over. The ledger is now
134
+ * an append-only log per scope, so every reader of that directory judges the same ids.
135
+ *
136
+ * The cache is keyed by the log's SIZE, which is sound precisely because the log is append-only:
137
+ * a peer's reap can only make it longer, so an unchanged size is an unchanged set. Steady state is
138
+ * therefore one `stat` on the per-turn path, which is what makes the durability affordable — and
139
+ * that sentence is load-bearing rather than decorative: past its bound a scope reaps on EVERY
140
+ * commit, so an entry dropped at each append would make the steady state a full re-read of a log
141
+ * that only grows. {@link markReaped} therefore CARRIES this entry across its own append.
142
+ *
143
+ * Growth is stated rather than hidden: one line (~40 bytes) per boundary ever reaped in the scope,
144
+ * forever — the cheap residue of a bound that throws away complete tracked-set mappings, by two
145
+ * orders of magnitude. It is what makes the bound SAFE rather than merely small.
146
+ */
147
+ private readonly reapedCache;
148
+ /** Resolved boundary cap, or `null` for the explicit `"unbounded"` opt-out (host owns the GC). */
149
+ private readonly boundaryKeep;
150
+ /** Highest {@link BoundaryJson.committedAt} this instance has minted — see {@link nextCommittedAt}. */
151
+ private lastCommittedAt;
152
+ /** Scopes whose on-disk maximum has already been folded into {@link lastCommittedAt} — see there. */
153
+ private readonly seededScopes;
154
+ /**
155
+ * The publication stamp, STRICTLY INCREASING — and increasing against what is already ON DISK, not
156
+ * merely against what this object has minted (cross-review rounds 4 and 5).
157
+ *
158
+ * `Date.now()` has millisecond resolution and turns are faster than that, so several boundaries
159
+ * could legitimately share a stamp — and a tied group falls through to the entry-id tie-break,
160
+ * which is only commit order for MONOTONIC ids. A host minting arbitrary entry ids could therefore
161
+ * have the retention pass drop the genuinely newer of two same-millisecond boundaries and keep the
162
+ * older. Ties are not broken better here; they are made not to happen, which is the difference
163
+ * between a heuristic and an order. (The same technique the engine's own uuidv7 mint uses.)
164
+ *
165
+ * The instance counter alone was not enough, and the reason is a configuration this backend
166
+ * explicitly supports rather than an exotic one: a SECOND store object over the same directory — or
167
+ * the same process restarted, which is the everyday case — starts its counter at zero, so after a
168
+ * clock rollback (or simply within the same millisecond as the previous instance's last commit) it
169
+ * could stamp a NEW boundary at or below an existing one, and retention would then keep the older.
170
+ * So the first commit into a scope SEEDS the counter from that scope's highest resolved order.
171
+ * Cost: one directory read per (instance, scope), amortized to nothing across a session.
172
+ *
173
+ * RESIDUAL, named rather than implied: two live instances interleaving commits into one scope
174
+ * inside a single millisecond can still tie, because each advances its own counter after seeding.
175
+ * A tie is then decided by the entry id, which IS commit order for engine-minted uuidv7 ids.
176
+ */
177
+ private nextCommittedAt;
178
+ constructor(root: string, opts?: FileFileHistoryStoreOptions);
179
+ /**
180
+ * The RETENTION pass, run after a boundary file is published (design/381 §片2 — see
181
+ * {@link FileHistoryStore.reap} for why the backend owns this rather than the host).
182
+ *
183
+ * COMMIT ORDER, in order of preference: the boundary's own {@link BoundaryJson.committedAt} stamp,
184
+ * else the file's mtime, else the entry id. The layering is the point — each fallback is weaker
185
+ * than the one above it and is named so nobody has to reverse-engineer which one is in force:
186
+ * · `committedAt` is written at publication and carried across `adoptScope`, so it survives the
187
+ * ordinary operations that destroy an mtime (a `cp` without `-p`, a restore from backup, the
188
+ * re-serialization adoption does). Absent only on records written before the field and on
189
+ * imported graphs, whose envelope carries no such value.
190
+ * · mtime is the fallback for exactly those, and is honest for a reason this family's standing
191
+ * law makes worth stating: DV-13 forbids metadata as a proof of CONTENT sameness, because
192
+ * deciding a tracked file "did not change" is a correctness and security judgment mtime cannot
193
+ * carry. Ordering records THIS store wrote once and never rewrites is a different question
194
+ * about a different object; being one place off costs a different-but-equally-legal retention
195
+ * choice and can never mis-restore a byte.
196
+ * · the entry id breaks ties, which for engine-minted uuidv7 ids IS commit order.
197
+ * A MIXED population (old records with no stamp beside new ones that have it) sorts coherently for
198
+ * a reason that is a fact rather than a hope, and is written down so nobody "fixes" one side into
199
+ * a different unit: both sources are milliseconds since the epoch — `Date.now()` and `Stats.mtimeMs`
200
+ * — so a stamped and an unstamped record are directly comparable.
201
+ * The residual, stated rather than papered over: a clock stepped BACKWARDS between commits, or a
202
+ * host minting non-monotonic entry ids of its own, can still make the pass drop a boundary that
203
+ * was not the oldest. It cannot make the pass drop THIS turn's boundary —
204
+ * {@link fileHistoryBoundariesToKeep} pins the just-committed id — and it cannot cost the pristine
205
+ * v1 fallback, which `reap` exempts permanently.
206
+ *
207
+ * WHAT IT REFUSES TO JUDGE — FAIL-CLOSED, WHOLE (cross-review rounds 1+2): if ANY `.json` in the
208
+ * scope fails to parse or fails its own digest, this pass does NOTHING for that scope. Not "skip
209
+ * that file", not "keep it and trim around it": nothing. The intermediate form is a trap, and it
210
+ * is worth writing down because it is the one this pass shipped with for a round. `reap` rebuilds
211
+ * its version-reference set from the READABLE boundaries, so retaining an unreadable boundary
212
+ * while trimming its neighbours still destroys the version records and blobs that only the
213
+ * unreadable one referenced — the file survives as "evidence" of history whose bytes the same
214
+ * pass deleted. And counting it toward the bound is the other half of the same mistake: one
215
+ * damaged record would push a healthy, still-restorable boundary out of the window. A scope with a
216
+ * record nobody can judge is a scope in a FAULT state, and an automatic, per-turn, irreversible
217
+ * deletion pass is exactly the wrong thing to keep running there. It grows past its bound instead,
218
+ * loudly (disclosed every pass), until an operator looks — the safe direction of a choice between
219
+ * "too much disk" and "history destroyed". The one caveat a deployment has to hear: that
220
+ * disclosure is only as loud as the sink it wired. A deployment that registers no
221
+ * {@link FileFileHistoryStoreOptions.onCorruptRead} gets the unbounded growth WITHOUT the notice —
222
+ * the same limitation every other corrupt-read fact in this store carries, and the reason the sink
223
+ * is not optional equipment in a durable deployment.
224
+ *
225
+ * COST, stated rather than left to be measured: the common path is ONE `readdir` — a scope under
226
+ * its bound parses nothing, stats nothing and calls no GC. A scope OVER the bound reads each
227
+ * boundary file (validation, then its stamp) and runs the whole `reap` pass, which ends in the
228
+ * cross-scope blob GC that walks every scope under this data root. Past the bound that is
229
+ * per-commit work proportional to the root, not to this scope — the price of the bound, and the
230
+ * reason the cheap path is kept genuinely cheap.
231
+ *
232
+ * Never throws and never fails the commit that triggered it (the boundary is already durable; a
233
+ * GC that could not run leaves a graph larger than its bound, the safer of the two directions).
234
+ */
235
+ private applyBoundaryRetention;
236
+ /** The single delivery point for the disclosure sink; swallow-guarded here so no call site has to
237
+ * remember, and so a host's throwing sink cannot break the never-throw seam. */
238
+ private disclose;
239
+ private scopeDir;
240
+ private trackedPath;
241
+ private boundaryDir;
242
+ /** A boundary file path, or null when `entryId` fails the strict path-component guard — the
243
+ * never-throw seam turns that into a typed result at the caller, never an exception. */
244
+ private tryBoundaryPath;
245
+ private blobPath;
246
+ /**
247
+ * Read a scope's tracked set, distinguishing the three states that matter (adversarial round 1):
248
+ * ABSENT (no history yet — the ordinary empty case), READ (the graph), and CORRUPT (bytes exist
249
+ * but cannot be understood). Collapsing CORRUPT into "empty" was a false-green machine: a readable
250
+ * boundary beside an unreadable tracked set made `restore` iterate zero files and report
251
+ * `complete` — a successful rewind that rewound nothing — and let the next `trackEdit` re-mint
252
+ * possibly-already-edited bytes as pristine. Callers must branch on `corrupt`.
253
+ */
254
+ private readTrackedState;
255
+ /** The tracked set alone, for the read paths that have already handled the corrupt state. */
256
+ private readTracked;
257
+ /** Replace a scope's tracked set in ONE atomic step (the DV-14 "one atomic durable record" and
258
+ * the version-chain commit both ride this). */
259
+ private writeTracked;
260
+ private readBoundary;
261
+ /**
262
+ * A committed boundary's own publication stamp ({@link BoundaryJson.committedAt}), or `undefined`
263
+ * when the record predates the field, came from an import, or cannot be read. Callers treat
264
+ * `undefined` as "no stamp" and fall back — never as a value, and never as a reason to refuse: a
265
+ * boundary with no stamp is perfectly restorable, it just cannot state its own place in the order.
266
+ */
267
+ private boundaryCommittedAt;
268
+ /**
269
+ * A boundary's place in commit order, resolved through the whole fallback chain in ONE place so
270
+ * the retention sort and the adoption copy cannot disagree about it (cross-review round 3): its
271
+ * own stamp, else the file's mtime, else `undefined` when neither is available. Both sources are
272
+ * milliseconds since the epoch, so a mixed population compares directly.
273
+ */
274
+ /**
275
+ * UNION the source scope's consumed-id ledger into the target's — the adoption invariant, in ONE
276
+ * place because it has to hold on EVERY successful arm of `adoptScope` and the arm it was first
277
+ * missing from was the early one ("nothing to adopt", which is a statement about the GRAPH and not
278
+ * about the ledger). A fork is the source's history continued under a new name, so an id the
279
+ * source spent must not become mintable there.
280
+ */
281
+ /**
282
+ * The boundary ids in `scope` that exist on disk but cannot be judged — `null` means the whole set
283
+ * is judgeable and safe to reason about. A directory that cannot be LISTED answers with an empty
284
+ * array rather than `null`, because "I cannot see them" and "I saw them and they are fine" must
285
+ * not be the same answer; ENOENT (no boundaries yet) is genuinely the second.
286
+ */
287
+ private unjudgeableBoundaryIdsOf;
288
+ private carryConsumedIds;
289
+ /**
290
+ * The consumed-id ledger's path — a SIBLING of the scope directory, deliberately NOT a file inside
291
+ * it (cross-car review, adopted). `adoptScope` publishes by REPLACING the scope directory, and a
292
+ * ledger that lives inside it dies with every such replacement: the version that did had to read
293
+ * the ids out, carry them through the staging directory and write them back, which works right up
294
+ * until a crash lands between the removal and the rename — at which point the directory (and the
295
+ * only record of that scope's spent ids) is gone and an old entry id becomes mintable again.
296
+ * Outside, the ledger is simply never moved: the publish cannot lose it, and the carry-and-rewrite
297
+ * that used to be necessary is gone with it.
298
+ *
299
+ * A scope's sanitized directory name can never contain a `.` (non-alphanumerics collapse to `-`,
300
+ * then a hash is appended), so the `.reaped.log` suffix cannot collide with another scope's dir.
301
+ */
302
+ private reapedLogPath;
303
+ /**
304
+ * The scope's consumed ids, cached by log size (see {@link reapedCache}). `null` = the log EXISTS
305
+ * but could not be read: a state where this store cannot tell a fresh entryId from a spent one, and
306
+ * the callers below take the fail-CLOSED arm rather than guess — refusing a boundary is a turn that
307
+ * cannot be rewound to, while guessing wrong is post-turn bytes published under a pre-turn key.
308
+ */
309
+ private readReapedIds;
310
+ /** Append ids to the scope's consumed-id ledger. Append-only and idempotent by construction (a
311
+ * membership test cannot tell a duplicated line from a single one), which is what lets a peer
312
+ * process write it without coordination — the reason this is a log and not a rewritten set. */
313
+ private markReaped;
314
+ private boundaryOrderOf;
315
+ /** Publish a blob write-ONCE (EEXIST = the identical bytes are already stored — dedup no-op). */
316
+ private putBlob;
317
+ trackEdit(scope: string, absPath: string, env: ExecutionEnv, root: string, signal?: AbortSignal): Promise<FileHistoryTrackResult>;
318
+ annulTrack(scope: string, absPath: string, root: string, verify?: {
319
+ env: ExecutionEnv;
320
+ signal?: AbortSignal;
321
+ }): Promise<FileHistoryResult>;
322
+ /**
323
+ * Is this scope free of history RIGHT NOW? Synchronous on purpose (adversarial round 1): the
324
+ * empty-target law is only sound if the observation and the publish happen with no await between
325
+ * them — an emptiness result carried across an await is a claim about the past, and two concurrent
326
+ * imports that both believed it would each delete the other's freshly published graph.
327
+ */
328
+ private scopeIsEmptySync;
329
+ /**
330
+ * The ONE-STEP publish shared by adoptScope and importHistory: verify emptiness and swap the
331
+ * staged directory into place with NOTHING awaited in between, so the whole claim-and-publish is
332
+ * atomic against any other in-flight publish in this process.
333
+ *
334
+ * THREE answers, not two (A-082 rescan): `"published"`, `"not_empty"` (the target stopped being
335
+ * empty — the caller's typed `conflict`), and `"ledger_failed"` (the tombstones for the residue
336
+ * this publish destroys could not be written). The second failure arrived with the tombstone write
337
+ * below and rode the same `false` as the first, so both callers dressed an IO fault as the one
338
+ * code the engine's fork-carry seat is contractually silent about — the exact silence the
339
+ * `conflict`/`adopt_failed` split was introduced to close, re-opened one door over (and its message
340
+ * asserted a fact that was not true: the target had gained nothing). They are separate answers now.
341
+ */
342
+ private publishStagedScopeSync;
343
+ /** Committed boundary ids in a scope (a corrupt boundary is NOT listed — the listing agrees with
344
+ * canRestore/restore, so a half-readable directory never looks like more history than it serves). */
345
+ private listBoundaryIds;
346
+ snapshot(scope: string, entryId: string, env: ExecutionEnv, root: string, signal?: AbortSignal): Promise<FileHistoryResult>;
347
+ restore(scope: string, entryId: string, env: ExecutionEnv, root: string, signal?: AbortSignal): Promise<FileHistoryRestoreResult>;
348
+ canRestore(scope: string, entryId: string): Promise<boolean>;
349
+ diffStats(scope: string, entryId: string, env: ExecutionEnv, root: string): Promise<FileHistoryDiffStats | null>;
350
+ reap(scope: string, keepEntryIds: string[]): Promise<number>;
351
+ /**
352
+ * Blob GC across EVERY scope — FAIL-SAFE by construction: build the COMPLETE live set or keep
353
+ * every blob. An incomplete live set (a swallowed scan error) would delete still-live bytes, so
354
+ * any unexpected read aborts the pass and discloses (a leaked blob is harmless; a deleted live one
355
+ * is data loss). Synchronous end to end, so it cannot interleave with a capture parked at an
356
+ * await; the live set additionally unions the staging journals of THIS process's in-flight
357
+ * attempts (bytes published but not yet chained), while every OTHER journal is dead-attempt
358
+ * residue: it is removed here, and the blobs it was protecting become collectable in this pass.
359
+ */
360
+ private collectBlobs;
361
+ adoptScope(fromScope: string, toScope: string): Promise<FileHistoryResult>;
362
+ exportHistory(scope: string): Promise<FileHistoryExport | null>;
363
+ importHistory(scope: string, data: FileHistoryExport, srcGetBlob: (hash: string) => Promise<Uint8Array | undefined>): Promise<FileHistoryResult>;
364
+ /** The import's body, so EVERY exit (typed refusal, thrown fault, success) passes one `finally`
365
+ * that releases the journal + lease — an early return used to leak both, and a leaked lease is
366
+ * permanent liveness for blobs nothing references (adversarial round 2). */
367
+ private runImport;
368
+ }