@staix/agent-mem 0.20.3 → 0.21.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 (52) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +4 -3
  3. package/package.json +2 -2
  4. package/src/cli.ts +3 -0
  5. package/src/hook-spool.ts +45 -3
  6. package/src/index.ts +25 -2
  7. package/src/memory/bridge.ts +66 -18
  8. package/src/memory/candidates.ts +71 -9
  9. package/src/memory/generation-index.ts +95 -18
  10. package/src/memory/generation-sync.ts +3 -0
  11. package/src/memory/generation.ts +33 -15
  12. package/src/memory/platform.ts +38 -3
  13. package/src/memory/policy.ts +28 -15
  14. package/src/memory/prune-publisher-lineage.ts +50 -12
  15. package/src/memory/public-prune-advance.ts +11 -5
  16. package/src/memory/public-prune-inputs.ts +17 -16
  17. package/src/memory/shard-sync-applications.ts +153 -0
  18. package/src/memory/shard-sync-format.ts +15 -0
  19. package/src/memory/shard-sync-graph.ts +6 -4
  20. package/src/memory/shard-sync-history.ts +238 -0
  21. package/src/memory/shard-sync.ts +67 -151
  22. package/src/memory/shard.ts +32 -6
  23. package/src/memory/store.ts +14 -1
  24. package/src/memory/sync-binding.ts +63 -0
  25. package/src/memory/sync-placement-active.ts +75 -0
  26. package/src/memory/sync-placement-cache.ts +32 -0
  27. package/src/memory/sync-placement-capture.ts +292 -0
  28. package/src/memory/sync-placement-dependencies.ts +255 -0
  29. package/src/memory/sync-placement-format.ts +182 -0
  30. package/src/memory/sync-placement-git.ts +95 -0
  31. package/src/memory/sync-placement-guard.ts +98 -0
  32. package/src/memory/sync-placement-inspection-evidence.ts +257 -0
  33. package/src/memory/sync-placement-inspection-pending.ts +66 -0
  34. package/src/memory/sync-placement-inspection.ts +236 -0
  35. package/src/memory/sync-placement-journal-format.ts +114 -0
  36. package/src/memory/sync-placement-journal-storage.ts +513 -0
  37. package/src/memory/sync-placement-journal.ts +153 -0
  38. package/src/memory/sync-placement-materialize.ts +149 -0
  39. package/src/memory/sync-placement-plan.ts +171 -0
  40. package/src/memory/sync-placement-projection.ts +79 -0
  41. package/src/memory/sync-placement-publication-plan.ts +189 -0
  42. package/src/memory/sync-placement-publication-storage.ts +87 -0
  43. package/src/memory/sync-placement-transport.ts +489 -0
  44. package/src/memory/sync-policy-inspection.ts +126 -0
  45. package/src/memory/sync-screen.ts +128 -33
  46. package/src/memory/sync.ts +20 -64
  47. package/src/ui/inbox-cache.ts +219 -0
  48. package/src/ui/inbox-worker-version.ts +14 -0
  49. package/src/ui/inbox-worker.ts +32 -0
  50. package/src/ui/inbox.ts +281 -0
  51. package/src/ui/panel.js +7 -3
  52. package/src/ui/plugin.ts +26 -53
@@ -0,0 +1,513 @@
1
+ import { types } from "node:util";
2
+ /** Private bounded placement evidence. Caller supplies the live home mutation fence and fresh authority proof. */
3
+ import { createHash } from "node:crypto";
4
+ import { closeSync, constants, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, opendirSync, readSync, unlinkSync, writeSync, type BigIntStats } from "node:fs";
5
+ import { join } from "node:path";
6
+ import { canonicalJSON, hashJSON } from "./generation.ts";
7
+ import { realPath } from "./platform.ts";
8
+ import { assertMutationHandle, type MutationHandle } from "./mutation.ts";
9
+ import { validatePlacementPlan, type SyncPlacementActivationPlan } from "./sync-placement-plan.ts";
10
+ import { validatePlacementProfile, validatePlacementShards } from "./sync-placement-format.ts";
11
+ import { PLACEMENT_ACTIVE_FENCE_SCHEMA, PLACEMENT_JOURNAL_MAX_RECORDS, PLACEMENT_JOURNAL_MAX_OBJECTS, PLACEMENT_JOURNAL_CHUNK_MAX_BYTES, PLACEMENT_JOURNAL_ROOT_MAX_BYTES,
12
+ SyncPlacementJournalError, createPlacementRecord, placementEpochHash, validatePlacementActiveFence, validatePlacementEpoch, validatePlacementEvent, validatePlacementRecord,
13
+ type PlacementActiveFence, type PlacementEpoch, type PlacementEvent, type PlacementRecord, type SyncPlacementJournalErrorCode } from "./sync-placement-journal-format.ts";
14
+ import { validatePlacementPublicationRoot, validatePlacementPublicationBundle, validatePlacementPublicationFact, publicationFileNames,
15
+ PLACEMENT_PUBLICATION_RECORD_MAX_BYTES, type PlacementPublicationRoot, type PlacementPublicationBundle, type PlacementPublicationFact } from "./sync-placement-publication-plan.ts";
16
+ import { PlacementProjection } from "./sync-placement-projection.ts";
17
+
18
+ export type PlacementStoragePhase = "epoch-written" | "epoch-fsync" | "activation-written" | "activation-fsync" | "fence-written" | "fence-fsync"
19
+ | "intent-written" | "intent-fsync" | "chunk-written" | "chunk-fsync" | "intent-cleared"
20
+ | "publication-slot-written" | "publication-slot-fsync" | "publication-chunk-written" | "publication-chunk-fsync"
21
+ | "publication-root-written" | "publication-root-fsync" | "publication-fact-written" | "publication-fact-fsync" | "publication-durability-file-fsync" | "publication-durability-dir-fsync";
22
+ export interface PlacementStorageOptions { mutation: MutationHandle; assertCurrent(): void; phase?(phase: PlacementStoragePhase): void; publicationAttemptHash?: string }
23
+ export interface PlacementJournalRead {
24
+ epoch: PlacementEpoch; activation: SyncPlacementActivationPlan; records: PlacementRecord[]; headHash: string; sequence: number; objects: number; pending: boolean; pendingRecordSequence?: number; pendingRecord?: PlacementRecord; publicationReservation?: { attemptHash: string; reservedRecords: number; consumedRecords: number };
25
+ }
26
+ interface Paths { home: string; root: string; epochs: string; dir: string; hash: string }
27
+ interface Chunk { name: string; size: number; hash: string }
28
+ interface Intent { schema: "agent-mem.sync-placement-append-intent/v1"; epochHash: string; record: PlacementRecord; chunk: string; prefixBytes: number; prefixHash: string; lineHash: string }
29
+ interface Snapshot extends PlacementJournalRead { chunks: Chunk[]; projection: PlacementProjection; intent?: Intent; intentPending: boolean; fencePending: boolean }
30
+ const inflight = new WeakSet<MutationHandle>(), hex = (value: unknown, width = 64): value is string => typeof value === "string" && value.length === width && new RegExp(`^[a-f0-9]{${width}}$`).test(value);
31
+ function fail(code: SyncPlacementJournalErrorCode = "corrupt"): never { throw new SyncPlacementJournalError(code); }
32
+ const digest = (bytes: Uint8Array) => createHash("sha256").update(bytes).digest("hex");
33
+ const line = (value: unknown) => Buffer.from(canonicalJSON(value) + "\n");
34
+ const same = (a: BigIntStats, b: BigIntStats) => a.dev === b.dev && a.ino === b.ino && a.size === b.size && a.mtimeNs === b.mtimeNs && a.ctimeNs === b.ctimeNs;
35
+ const knownError = (error: unknown): error is SyncPlacementJournalError => !!error && typeof error === "object" && !types.isProxy(error) && error instanceof SyncPlacementJournalError;
36
+ function protectedIO<T>(work: () => T): T { try { return work(); } catch (error) { if (knownError(error)) throw error; return fail(); } }
37
+ function owned(stat: BigIntStats, directory = false): void {
38
+ const uid = process.getuid?.();
39
+ if (uid === undefined || stat.uid !== BigInt(uid) || (stat.mode & 0o077n) !== 0n || stat.isSymbolicLink()
40
+ || (directory ? !stat.isDirectory() : !stat.isFile() || stat.nlink !== 1n)) fail();
41
+ }
42
+ function state(file: string, directory = false): BigIntStats | undefined {
43
+ const stat = lstatSync(file, { bigint: true, throwIfNoEntry: false }); if (stat) owned(stat, directory); return stat;
44
+ }
45
+ function paths(home: string, epochHash: string): Paths {
46
+ if (!hex(epochHash)) return fail("invalid");
47
+ const canonical = realPath(home); owned(lstatSync(home, { bigint: true }), true);
48
+ return { home: canonical, root: join(canonical, "sync-placement"), epochs: join(canonical, "sync-placement", "epochs"), dir: join(canonical, "sync-placement", "epochs", epochHash), hash: epochHash };
49
+ }
50
+ function names(dir: string, maximum: number): string[] {
51
+ const reader = opendirSync(dir), result: string[] = [];
52
+ try { for (;;) { const entry = reader.readSync(); if (!entry) break; if (result.length >= maximum) fail("bound"); result.push(entry.name); } }
53
+ finally { reader.closeSync(); }
54
+ return result.sort();
55
+ }
56
+ function inventory(p: Paths): { files: Map<string, BigIntStats>; token: string } {
57
+ const parents: unknown[] = [];
58
+ for (const dir of [p.home, p.root, p.epochs, p.dir]) { const stat = state(dir, true); parents.push([dir, stat ? [String(stat.dev), String(stat.ino)] : null]); }
59
+ if (state(p.root, true)) for (const name of names(p.root, 2)) {
60
+ if (name === "epochs") state(p.epochs, true); else if (name === "active.json") { const stat = state(join(p.root, name))!; if (stat.size > BigInt(PLACEMENT_JOURNAL_ROOT_MAX_BYTES)) fail("bound"); }
61
+ else fail();
62
+ }
63
+ const files = new Map<string, BigIntStats>();
64
+ if (state(p.dir, true)) for (const name of names(p.dir, PLACEMENT_JOURNAL_MAX_OBJECTS - 1)) {
65
+ const chunk = /^records-\d{5}\.jsonl$/.test(name);
66
+ if (!chunk && !publicationName(name) && !["epoch.json", "activation.json", "append-intent.json"].includes(name)) fail();
67
+ const stat = state(join(p.dir, name))!;
68
+ if (stat.size > BigInt(chunk || /-chunk-\d{5}\.json$/.test(name) ? PLACEMENT_JOURNAL_CHUNK_MAX_BYTES : PLACEMENT_JOURNAL_ROOT_MAX_BYTES)) fail("bound");
69
+ files.set(name, stat);
70
+ }
71
+ // One global active.json slot is always reserved, whether that fence exists yet or not.
72
+ if (files.size + 1 > PLACEMENT_JOURNAL_MAX_OBJECTS) fail("bound");
73
+ const fence = state(join(p.root, "active.json"));
74
+ return { files, token: canonicalJSON({ parents, files: [...files].map(([name, stat]) => [name, String(stat.dev), String(stat.ino), String(stat.size), String(stat.mtimeNs), String(stat.ctimeNs)]),
75
+ fence: fence ? [String(fence.dev), String(fence.ino), String(fence.size), String(fence.mtimeNs), String(fence.ctimeNs)] : null }) };
76
+ }
77
+ function readOwned(file: string, maximum: number): Buffer | undefined {
78
+ const before = state(file); if (!before) return undefined;
79
+ if (before.size > BigInt(maximum)) fail("bound");
80
+ const fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
81
+ try {
82
+ const opened = fstatSync(fd, { bigint: true }); owned(opened); if (!same(before, opened)) fail("changed");
83
+ const bytes = Buffer.alloc(Number(opened.size) + 1); let count = 0;
84
+ while (count < bytes.length) { const size = readSync(fd, bytes, count, bytes.length - count, count); if (!size) break; count += size; }
85
+ const named = state(file); if (count !== Number(opened.size) || !named || !same(opened, fstatSync(fd, { bigint: true })) || !same(opened, named)) fail("changed");
86
+ return bytes.subarray(0, count);
87
+ } finally { closeSync(fd); }
88
+ }
89
+ function decode<T>(bytes: Buffer | undefined, validate: (value: unknown) => T): T {
90
+ if (!bytes || !bytes.length || bytes.at(-1) !== 10) return fail("pending");
91
+ const text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes), value = validate(JSON.parse(text));
92
+ if (canonicalJSON(value) + "\n" !== text) return fail(); return value;
93
+ }
94
+ function activation(epoch: PlacementEpoch, epochHash: string, value: unknown): SyncPlacementActivationPlan {
95
+ const plan = validatePlacementPlan(value);
96
+ if (plan.generation !== epoch.generation || plan.contextHash !== epoch.contextHash || plan.targetProfile.placementHead !== epochHash) return fail("conflict");
97
+ return plan;
98
+ }
99
+ function intent(value: unknown, epochHash: string): Intent {
100
+ if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).sort().join() !== "chunk,epochHash,lineHash,prefixBytes,prefixHash,record,schema") return fail();
101
+ const input = value as Intent, record = validatePlacementRecord(input.record);
102
+ if (input.schema !== "agent-mem.sync-placement-append-intent/v1" || input.epochHash !== epochHash || record.epochHash !== epochHash
103
+ || !/^records-\d{5}\.jsonl$/.test(input.chunk) || !Number.isSafeInteger(input.prefixBytes) || input.prefixBytes < 0 || !hex(input.prefixHash) || !hex(input.lineHash)
104
+ || input.lineHash !== digest(line(record)) || input.prefixBytes + line(record).length > PLACEMENT_JOURNAL_CHUNK_MAX_BYTES) return fail();
105
+ return { schema: input.schema, epochHash, record, chunk: input.chunk, prefixBytes: input.prefixBytes, prefixHash: input.prefixHash, lineHash: input.lineHash };
106
+ }
107
+ function snapshot(p: Paths): Snapshot {
108
+ const before = inventory(p);
109
+ const epoch = decode(readOwned(join(p.dir, "epoch.json"), PLACEMENT_JOURNAL_ROOT_MAX_BYTES), validatePlacementEpoch);
110
+ if (placementEpochHash(epoch) !== p.hash) return fail("conflict");
111
+ const plan = decode(readOwned(join(p.dir, "activation.json"), PLACEMENT_JOURNAL_ROOT_MAX_BYTES), value => activation(epoch, p.hash, value));
112
+ const pendingBytes = readOwned(join(p.dir, "append-intent.json"), PLACEMENT_JOURNAL_ROOT_MAX_BYTES);
113
+ const intentPending = pendingBytes !== undefined && (!pendingBytes.length || pendingBytes.at(-1) !== 10);
114
+ const pending = pendingBytes && !intentPending ? decode(pendingBytes, value => intent(value, p.hash)) : undefined;
115
+ const projection = new PlacementProjection(epoch), records: PlacementRecord[] = [], chunks: Chunk[] = [];
116
+ const apply = (bytes: Buffer) => {
117
+ let start = 0;
118
+ while (start < bytes.length) {
119
+ const end = bytes.indexOf(10, start); if (end < 0 || end === start) fail();
120
+ if (records.length >= PLACEMENT_JOURNAL_MAX_RECORDS) fail("bound");
121
+ const row = decode(bytes.subarray(start, end + 1), validatePlacementRecord); projection.apply(row); records.push(row); start = end + 1;
122
+ }
123
+ };
124
+ const chunkNames = [...before.files.keys()].filter(name => /^records-\d{5}\.jsonl$/.test(name));
125
+ for (const [index, name] of chunkNames.entries()) {
126
+ if (name !== `records-${String(index).padStart(5, "0")}.jsonl`) fail();
127
+ const bytes = readOwned(join(p.dir, name), PLACEMENT_JOURNAL_CHUNK_MAX_BYTES)!;
128
+ chunks.push({ name, size: bytes.length, hash: digest(bytes) });
129
+ if (pending?.chunk === name) {
130
+ if (index !== chunkNames.length - 1 || bytes.length < pending.prefixBytes || digest(bytes.subarray(0, pending.prefixBytes)) !== pending.prefixHash) fail();
131
+ apply(bytes.subarray(0, pending.prefixBytes));
132
+ if (projection.headHash !== pending.record.previousHash || projection.sequence + 1 !== pending.record.sequence) fail();
133
+ const suffix = bytes.subarray(pending.prefixBytes), expected = line(pending.record);
134
+ if (suffix.length > expected.length || !expected.subarray(0, suffix.length).equals(suffix)) fail();
135
+ if (suffix.length === expected.length) apply(suffix);
136
+ } else { if (!bytes.length) fail(); apply(bytes); }
137
+ }
138
+ if (pending) {
139
+ const found = chunkNames.includes(pending.chunk);
140
+ if (!found && (pending.chunk !== `records-${String(chunkNames.length).padStart(5, "0")}.jsonl` || pending.prefixBytes !== 0 || pending.prefixHash !== digest(Buffer.alloc(0)))) fail();
141
+ if (!found && (projection.headHash !== pending.record.previousHash || projection.sequence + 1 !== pending.record.sequence)) fail();
142
+ if (pending.record.sequence > PLACEMENT_JOURNAL_MAX_RECORDS) fail("bound");
143
+ }
144
+ let fencePending = false, activeFence: PlacementActiveFence | undefined;
145
+ const fenceBytes = readOwned(join(p.root, "active.json"), PLACEMENT_JOURNAL_ROOT_MAX_BYTES);
146
+ if (fenceBytes !== undefined) {
147
+ if (!fenceBytes.length || fenceBytes.at(-1) !== 10) fencePending = true;
148
+ else activeFence = decode(fenceBytes, validatePlacementActiveFence);
149
+ }
150
+ // A durable append always follows the immutable fence's fsync. Missing/wrong fence evidence cannot be repaired
151
+ // by inventing publication history. Only a completely empty, intent-free epoch can have a recoverable fence prefix.
152
+ if ((records.length > 0 || pendingBytes !== undefined) && (!activeFence || fencePending || activeFence.epochHash !== p.hash
153
+ || activeFence.homeHash !== epoch.homeHash || activeFence.generation !== epoch.generation)) fail();
154
+ if (inventory(p).token !== before.token) fail("changed");
155
+ return { epoch, activation: plan, records, projection, headHash: projection.headHash, sequence: projection.sequence, objects: before.files.size + 1, pending: !!pending || intentPending || fencePending, chunks, intentPending, fencePending, ...(pending ? { pendingRecordSequence: pending.record.sequence, pendingRecord: validatePlacementRecord(pending.record) } : {}), ...(pending ? { intent: pending } : {}) };
156
+ }
157
+ export function readPlacementJournal(home: string, epochHash: string): PlacementJournalRead {
158
+ return protectedIO(() => {
159
+ const p = paths(home, epochHash), before = inventory(p).token;
160
+ const { chunks: _chunks, projection: _projection, intent: _intent, intentPending: _intentPending, fencePending: _fencePending, ...result } = snapshot(p);
161
+ const reservation = pendingPublication(p);
162
+ if (reservation) {
163
+ const root = reservation.bundle.root, consumedRecords = result.sequence - root.journal.sequence;
164
+ const frontier = root.journal.sequence ? result.records[root.journal.sequence - 1]?.seal : p.hash;
165
+ if (frontier !== root.journal.headHash || consumedRecords < 0 || consumedRecords > root.reservedRecords) fail("conflict");
166
+ result.publicationReservation = { attemptHash: root.seal, reservedRecords: root.reservedRecords, consumedRecords };
167
+ }
168
+ if (inventory(p).token !== before) fail("changed"); return result;
169
+ });
170
+ }
171
+ function current(p: Paths, opts: PlacementStorageOptions, required: readonly string[] = []): void {
172
+ try { assertMutationHandle(opts.mutation, p.home); } catch { return fail("busy"); }
173
+ try { opts.assertCurrent(); } catch (error) { if (knownError(error)) throw error; return fail("changed"); }
174
+ try { assertMutationHandle(opts.mutation, p.home); } catch { return fail("busy"); }
175
+ const found = inventory(p), names = new Set([...found.files.keys(), ...required, ...reservedPublicationJournalNames(p)]);
176
+ if (names.size + 1 > PLACEMENT_JOURNAL_MAX_OBJECTS) fail("bound");
177
+ }
178
+ function writer<T>(home: string, epochHash: string, opts: PlacementStorageOptions, work: (p: Paths) => T, incompleteAttempt?: string, preflight?: readonly string[] | ((p: Paths) => readonly string[])): T {
179
+ return protectedIO(() => {
180
+ const p = paths(home, epochHash); current(p, opts);
181
+ if (preflight) current(p, opts, typeof preflight === "function" ? preflight(p) : preflight);
182
+ if (inflight.has(opts.mutation)) return fail("busy"); inflight.add(opts.mutation);
183
+ try { reestablishPublicationDurability(p, opts, incompleteAttempt); const result = work(p); reestablishPublicationDurability(p, opts, incompleteAttempt); return result; } finally { inflight.delete(opts.mutation); }
184
+ });
185
+ }
186
+ function syncDirectory(p: Paths, opts: PlacementStorageOptions, dir: string, required: readonly string[]): void {
187
+ current(p, opts, required); const before = state(dir, true)!;
188
+ const fd = openSync(dir, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
189
+ try { const opened = fstatSync(fd, { bigint: true }); owned(opened, true); if (!same(before, opened)) fail("changed"); current(p, opts, required); if (!same(opened, fstatSync(fd, { bigint: true })) || !same(opened, state(dir, true)!)) fail("changed"); fsyncSync(fd); if (!same(opened, state(dir, true)!)) fail("changed"); }
190
+ finally { closeSync(fd); }
191
+ }
192
+ function directories(p: Paths, opts: PlacementStorageOptions, required: readonly string[]): void {
193
+ for (const dir of [p.root, p.epochs, p.dir]) if (!state(dir, true)) {
194
+ current(p, opts, required); mkdirSync(dir, { mode: 0o700 }); owned(lstatSync(dir, { bigint: true }), true);
195
+ syncDirectory(p, opts, dir === p.root ? p.home : dir === p.epochs ? p.root : p.epochs, required);
196
+ }
197
+ }
198
+ /** Exact prefix completion is the sole recovery write; no file is ever truncated or replaced. */
199
+ function complete(p: Paths, opts: PlacementStorageOptions, file: string, expected: Buffer, maximum: number, required: readonly string[], written: PlacementStoragePhase, synced: PlacementStoragePhase): boolean {
200
+ if (expected.length > maximum) return fail("bound");
201
+ current(p, opts, required); const prior = readOwned(file, maximum);
202
+ if (prior && (prior.length > expected.length || !expected.subarray(0, prior.length).equals(prior))) return fail("conflict");
203
+ const created = prior === undefined; current(p, opts, required);
204
+ const fd = openSync(file, constants.O_RDWR | constants.O_NOFOLLOW | constants.O_NONBLOCK | (created ? constants.O_CREAT | constants.O_EXCL : 0), 0o600);
205
+ try {
206
+ let stat = fstatSync(fd, { bigint: true }); owned(stat);
207
+ if (!same(stat, state(file)!)) fail("changed");
208
+ let offset = prior?.length ?? 0;
209
+ if (Number(stat.size) !== offset || !readOwned(file, maximum)?.equals(prior ?? Buffer.alloc(0))) fail("changed");
210
+ while (offset < expected.length) {
211
+ current(p, opts, required); owned(fstatSync(fd, { bigint: true })); if (!same(stat, state(file)!)) fail("changed");
212
+ const size = writeSync(fd, expected, offset, expected.length - offset, offset); if (size < 1) fail(); offset += size; stat = fstatSync(fd, { bigint: true });
213
+ }
214
+ if (created || (prior?.length ?? 0) !== expected.length) opts.phase?.(written);
215
+ current(p, opts, required);
216
+ if (!same(stat, state(file)!) || !readOwned(file, maximum)?.equals(expected)) fail("changed");
217
+ current(p, opts, required); owned(fstatSync(fd, { bigint: true }));
218
+ if (!same(stat, state(file)!) || !same(stat, fstatSync(fd, { bigint: true }))) fail("changed");
219
+ fsyncSync(fd);
220
+ } finally { closeSync(fd); }
221
+ syncDirectory(p, opts, file.startsWith(p.dir + "/") ? p.dir : p.root, required); opts.phase?.(synced);
222
+ current(p, opts, required); if (!readOwned(file, maximum)?.equals(expected)) fail("changed");
223
+ return prior?.equals(expected) === true;
224
+ }
225
+ export function preparePlacementEpoch(home: string, value: PlacementEpoch, activationPlan: unknown, opts: PlacementStorageOptions): void {
226
+ const epoch = validatePlacementEpoch(value), epochHash = placementEpochHash(epoch);
227
+ writer(home, epochHash, opts, p => {
228
+ const plan = activation(epoch, epochHash, activationPlan), required = ["epoch.json", "activation.json"];
229
+ current(p, opts, required); directories(p, opts, required);
230
+ complete(p, opts, join(p.dir, "epoch.json"), line(epoch), PLACEMENT_JOURNAL_ROOT_MAX_BYTES, required, "epoch-written", "epoch-fsync");
231
+ complete(p, opts, join(p.dir, "activation.json"), line(plan), PLACEMENT_JOURNAL_ROOT_MAX_BYTES, required, "activation-written", "activation-fsync");
232
+ current(p, opts); const read = snapshot(p); if (canonicalJSON(read.epoch) !== canonicalJSON(epoch) || canonicalJSON(read.activation) !== canonicalJSON(plan) || read.records.length || read.pending) fail("conflict");
233
+ });
234
+ }
235
+ function ensureFence(p: Paths, epoch: PlacementEpoch, opts: PlacementStorageOptions, required: readonly string[]): void {
236
+ const profile = decode(readOwned(join(p.home, "profile.json"), PLACEMENT_JOURNAL_ROOT_MAX_BYTES), validatePlacementProfile);
237
+ decode(readOwned(join(p.home, "shards.json"), PLACEMENT_JOURNAL_ROOT_MAX_BYTES), value => validatePlacementShards(value, profile));
238
+ if (profile.generation !== epoch.generation || profile.placementHead !== p.hash) fail("changed");
239
+ const fence: PlacementActiveFence = validatePlacementActiveFence({ schema: PLACEMENT_ACTIVE_FENCE_SCHEMA, epochHash: p.hash, homeHash: epoch.homeHash, generation: epoch.generation });
240
+ complete(p, opts, join(p.root, "active.json"), line(fence), PLACEMENT_JOURNAL_ROOT_MAX_BYTES, required, "fence-written", "fence-fsync");
241
+ }
242
+ export function ensurePlacementFence(home: string, epochHash: string, opts: PlacementStorageOptions): void {
243
+ writer(home, epochHash, opts, p => { const read = snapshot(p); ensureFence(p, read.epoch, opts, []); current(p, opts); });
244
+ }
245
+ function sameEvent(record: PlacementRecord, value: PlacementEvent): boolean {
246
+ const { schema: _schema, epochHash: _epoch, sequence: _seq, previousHash: _previous, seal: _seal, ...event } = record;
247
+ return canonicalJSON(validatePlacementEvent(event)) === canonicalJSON(value);
248
+ }
249
+ export function appendPlacementEvent(home: string, epochHash: string, value: PlacementEvent, expectedHead: string, opts: PlacementStorageOptions): { record: PlacementRecord; already: boolean } {
250
+ const event = validatePlacementEvent(value); if (!hex(expectedHead)) return fail("invalid");
251
+ return writer(home, epochHash, opts, p => {
252
+ const read = snapshot(p);
253
+ checkPublicationAppend(p, read, event, opts);
254
+ if (read.intent) {
255
+ const pending = read.intent;
256
+ checkPublicationRecord(p, pending.record, opts);
257
+ if (pending.record.previousHash !== expectedHead || !sameEvent(pending.record, event)) return fail("pending");
258
+ if (read.projection.headHash !== pending.record.seal) read.projection.apply(pending.record);
259
+ const required = ["append-intent.json", pending.chunk]; current(p, opts, required); ensureFence(p, read.epoch, opts, required);
260
+ const chunk = readOwned(join(p.dir, pending.chunk), PLACEMENT_JOURNAL_CHUNK_MAX_BYTES) ?? Buffer.alloc(0);
261
+ const expected = Buffer.concat([chunk.subarray(0, pending.prefixBytes), line(pending.record)]);
262
+ const already = complete(p, opts, join(p.dir, pending.chunk), expected, PLACEMENT_JOURNAL_CHUNK_MAX_BYTES, required, "chunk-written", "chunk-fsync");
263
+ current(p, opts, required); const ownedIntent = readOwned(join(p.dir, "append-intent.json"), PLACEMENT_JOURNAL_ROOT_MAX_BYTES);
264
+ if (!ownedIntent?.equals(line(pending))) fail("changed");
265
+ unlinkSync(join(p.dir, "append-intent.json")); syncDirectory(p, opts, p.dir, []); opts.phase?.("intent-cleared");
266
+ current(p, opts); const done = snapshot(p); if (done.pending || done.headHash !== pending.record.seal) fail("changed");
267
+ return { record: pending.record, already };
268
+ }
269
+ const existing = read.projection.request(event.requestId);
270
+ if (existing) {
271
+ if (!sameEvent(existing, event) || existing.previousHash !== expectedHead || read.headHash !== existing.seal) return fail("conflict");
272
+ current(p, opts); const latest = snapshot(p); if (latest.pending || latest.headHash !== existing.seal) fail("changed"); return { record: existing, already: true };
273
+ }
274
+ if (read.intent || read.headHash !== expectedHead) return fail(read.intent ? "pending" : "conflict");
275
+ const record = createPlacementRecord(epochHash, read.sequence + 1, expectedHead, event), bytes = line(record);
276
+ checkPublicationRecord(p, record, opts);
277
+ read.projection.apply(record); // Conflict/source/outcome validation must precede even the immutable fence write.
278
+ if (bytes.length > PLACEMENT_JOURNAL_CHUNK_MAX_BYTES) fail("bound");
279
+ const prior = read.chunks.at(-1), reuse = prior !== undefined && prior.size + bytes.length <= PLACEMENT_JOURNAL_CHUNK_MAX_BYTES;
280
+ const chunk = reuse ? prior.name : `records-${String(read.chunks.length).padStart(5, "0")}.jsonl`;
281
+ const transaction: Intent = { schema: "agent-mem.sync-placement-append-intent/v1", epochHash, record, chunk, prefixBytes: reuse ? prior.size : 0,
282
+ prefixHash: reuse ? prior.hash : digest(Buffer.alloc(0)), lineHash: digest(bytes) };
283
+ const required = ["append-intent.json", chunk], transactionBytes = line(transaction);
284
+ if (transactionBytes.length > PLACEMENT_JOURNAL_ROOT_MAX_BYTES) fail("bound");
285
+ // Torn intent has no acknowledged effect. Only the freshly proved request may complete an exact owned prefix;
286
+ // snapshot already required every existing chunk to be complete and in-chain when the intent was unparseable.
287
+ const savedIntent = readOwned(join(p.dir, "append-intent.json"), PLACEMENT_JOURNAL_ROOT_MAX_BYTES);
288
+ if (savedIntent && (savedIntent.length > transactionBytes.length || !transactionBytes.subarray(0, savedIntent.length).equals(savedIntent))) fail("conflict");
289
+ current(p, opts, required); // Reserve both future objects before even the immutable active fence is written.
290
+ ensureFence(p, read.epoch, opts, required);
291
+ complete(p, opts, join(p.dir, "append-intent.json"), line(transaction), PLACEMENT_JOURNAL_ROOT_MAX_BYTES, required, "intent-written", "intent-fsync");
292
+ const prefix = reuse ? readOwned(join(p.dir, chunk), PLACEMENT_JOURNAL_CHUNK_MAX_BYTES)! : Buffer.alloc(0);
293
+ if (prefix.length !== transaction.prefixBytes || digest(prefix) !== transaction.prefixHash) fail("changed");
294
+ complete(p, opts, join(p.dir, chunk), Buffer.concat([prefix, bytes]), PLACEMENT_JOURNAL_CHUNK_MAX_BYTES, required, "chunk-written", "chunk-fsync");
295
+ current(p, opts, required); if (!readOwned(join(p.dir, "append-intent.json"), PLACEMENT_JOURNAL_ROOT_MAX_BYTES)?.equals(line(transaction))) fail("changed");
296
+ unlinkSync(join(p.dir, "append-intent.json")); syncDirectory(p, opts, p.dir, []); opts.phase?.("intent-cleared");
297
+ current(p, opts); const done = snapshot(p); if (done.pending || done.headHash !== record.seal) fail("changed");
298
+ return { record, already: false };
299
+ }, undefined, p => placementAppendRequired(p, event, expectedHead, opts));
300
+ }
301
+
302
+ const publicationPattern = /^publication-([a-f0-9]{64})-(?:root|chunk-\d{5}|artifact|intent|receipt)\.json$/;
303
+ function publicationName(name: string): boolean { const m = publicationPattern.exec(name); return !!m && m[0] === name; }
304
+ export interface PlacementPublicationStored {
305
+ bundle: PlacementPublicationBundle; phase: "prepared" | "artifact-built" | "publication-intent" | "completed";
306
+ facts: PlacementPublicationFact[]; pending: boolean; pendingPhase?: PlacementPublicationFact["phase"];
307
+ }
308
+ function publicationRead(p: Paths, attemptHash: string): PlacementPublicationStored {
309
+ const stem = `publication-${attemptHash}-`, root = decode(readOwned(join(p.dir, stem + "root.json"), PLACEMENT_JOURNAL_ROOT_MAX_BYTES), validatePlacementPublicationRoot);
310
+ const epoch = decode(readOwned(join(p.dir, "epoch.json"), PLACEMENT_JOURNAL_ROOT_MAX_BYTES), validatePlacementEpoch);
311
+ if (root.seal !== attemptHash || root.epochHash !== p.hash || root.generation !== epoch.generation || root.homeHash !== epoch.homeHash) fail("corrupt");
312
+ const chunks = root.chunks.map(c => decode(readOwned(join(p.dir, `${stem}chunk-${String(c.index).padStart(5, "0")}.json`), PLACEMENT_JOURNAL_CHUNK_MAX_BYTES), value => value));
313
+ const bundle = validatePlacementPublicationBundle({ root, chunks });
314
+ const allowed = new Set(publicationFileNames(root));
315
+ for (const name of inventory(p).files.keys()) if (name.startsWith(stem) && !allowed.has(name)) fail("corrupt");
316
+ const facts: PlacementPublicationFact[] = []; let phase: PlacementPublicationStored["phase"] = "prepared", pending = false, absent = false; let pendingPhase: PlacementPublicationFact["phase"] | undefined;
317
+ for (const [index, suffix] of ["artifact", "intent", "receipt"].entries()) {
318
+ const bytes = readOwned(join(p.dir, stem + suffix + ".json"), PLACEMENT_JOURNAL_ROOT_MAX_BYTES);
319
+ if (bytes === undefined) fail("corrupt"); // Every future phase slot was reserved before root acknowledgement.
320
+ if (!bytes.length) { absent = true; continue; }
321
+ if (absent && (suffix !== "receipt" || pendingPhase)) fail("corrupt");
322
+ if (bytes.at(-1) !== 10) { pending = true; pendingPhase = (["artifact-built", "publication-intent", "completed"] as const)[index]; absent = true; continue; }
323
+ const fact = decode(bytes, validatePlacementPublicationFact), previous = facts.at(-1)?.seal ?? root.seal;
324
+ if (fact.attemptHash !== attemptHash || fact.previousHash !== previous || fact.phase !== (["artifact-built", "publication-intent", "completed"] as const)[index]) fail("corrupt");
325
+ const intentFact = facts.find(fact => fact.phase === "publication-intent");
326
+ if (fact.phase === "publication-intent" && (fact.artifactFactHash !== previous || fact.policyHash !== root.policyHash || fact.policyCheckUnixMs !== root.policySnapshotUnixMs)
327
+ || fact.phase === "completed" && (fact.intentFactHash !== (intentFact?.seal ?? null) || fact.journal.sequence < root.journal.sequence || fact.journal.sequence > root.journal.sequence + root.reservedRecords
328
+ || fact.outcome === "committed" && (!intentFact || fact.journal.sequence !== root.journal.sequence + root.reservedRecords))) fail("corrupt");
329
+ facts.push(fact); phase = fact.phase;
330
+ }
331
+ return { bundle, phase, facts, pending, ...(pendingPhase ? { pendingPhase } : {}) };
332
+ }
333
+ function publicationRoots(p: Paths): string[] {
334
+ const names = [...inventory(p).files.keys()].filter(publicationName), roots = names.filter(n => n.endsWith("-root.json"));
335
+ const attempts = new Set(roots.map(n => n.slice(12, 76)));
336
+ for (const name of names) if (!attempts.has(name.slice(12, 76))) fail("pending");
337
+ return roots.map(name => name.slice(12, 76));
338
+ }
339
+ function pendingPublication(p: Paths): PlacementPublicationStored | undefined {
340
+ let pending: PlacementPublicationStored | undefined;
341
+ for (const hash of publicationRoots(p)) {
342
+ const read = publicationRead(p, hash);
343
+ if (read.phase !== "completed" || read.pending) { if (pending) fail("corrupt"); pending = read; }
344
+ }
345
+ return pending;
346
+ }
347
+ /** Simulate whole-record appends exactly; no 2048*N approximation lowers the existing record ceiling. */
348
+ export function placementPublicationReservedJournalNames(root: PlacementPublicationRoot, read: PlacementJournalRead, chunkSizes: readonly number[]): string[] {
349
+ const consumed = read.sequence - root.journal.sequence;
350
+ if (consumed < 0 || consumed > root.reservedRecords || root.journal.sequence === read.sequence && root.journal.headHash !== read.headHash) fail("conflict");
351
+ let count = chunkSizes.length, size = chunkSizes.at(-1) ?? 0; const names: string[] = [];
352
+ for (const bytes of root.reservedRecordBytes.slice(consumed)) {
353
+ if (!count || size + bytes > PLACEMENT_JOURNAL_CHUNK_MAX_BYTES) { names.push(`records-${String(count).padStart(5, "0")}.jsonl`); count++; size = 0; }
354
+ size += bytes;
355
+ }
356
+ if (consumed < root.reservedRecords) names.push("append-intent.json");
357
+ return names;
358
+ }
359
+ function reservedPublicationJournalNames(p: Paths): string[] {
360
+ // A partial root has no acknowledgement and blocks append through checkPublicationAppend. Its exact preparer
361
+ // passes its entire required-name reservation on every write. Complete roots enforce reservations globally.
362
+ const complete = publicationRootsForReservation(p); if (!complete.length) return [];
363
+ const read = snapshot(p), names: string[] = [], sizes = read.chunks.map(c => c.size);
364
+ if (read.intent && read.projection.headHash !== read.intent.record.seal && read.chunks.at(-1)?.name === read.intent.chunk) sizes[sizes.length - 1] = read.intent.prefixBytes;
365
+ for (const root of complete) names.push(...placementPublicationReservedJournalNames(root, read, sizes));
366
+ return names;
367
+ }
368
+ function publicationRootsForReservation(p: Paths): PlacementPublicationRoot[] {
369
+ const roots: PlacementPublicationRoot[] = [];
370
+ for (const name of inventory(p).files.keys()) if (/^publication-[a-f0-9]{64}-root\.json$/.test(name)) {
371
+ const bytes = readOwned(join(p.dir, name), PLACEMENT_JOURNAL_ROOT_MAX_BYTES);
372
+ if (!bytes?.length || bytes.at(-1) !== 10) continue;
373
+ const root = decode(bytes, validatePlacementPublicationRoot), receiptBytes = readOwned(join(p.dir, `publication-${root.seal}-receipt.json`), PLACEMENT_JOURNAL_ROOT_MAX_BYTES);
374
+ if (receiptBytes?.length && receiptBytes.at(-1) === 10) {
375
+ const read = publicationRead(p, root.seal); if (read.phase === "completed" && !read.pending) continue;
376
+ }
377
+ roots.push(root);
378
+ }
379
+ if (roots.length > 1) fail("corrupt"); return roots;
380
+ }
381
+ function checkPublicationAppend(p: Paths, read: Snapshot, event: PlacementEvent, opts: PlacementStorageOptions): void {
382
+ const pending = pendingPublication(p);
383
+ if (!pending) { if (opts.publicationAttemptHash) fail("conflict"); return; }
384
+ const root = pending.bundle.root;
385
+ if (pending.pending || opts.publicationAttemptHash !== root.seal || event.kind === "release") fail("busy");
386
+ const frontier = root.journal.sequence ? read.records[root.journal.sequence - 1]?.seal : p.hash;
387
+ if (frontier !== root.journal.headHash || read.sequence < root.journal.sequence || read.sequence > root.journal.sequence + root.reservedRecords) fail("conflict");
388
+ if (read.sequence === root.journal.sequence + root.reservedRecords && !read.projection.request(event.requestId)) fail("bound");
389
+ }
390
+ function checkPublicationRecord(p: Paths, record: PlacementRecord, opts: PlacementStorageOptions): void {
391
+ if (!opts.publicationAttemptHash) return;
392
+ const read = publicationRead(p, opts.publicationAttemptHash), root = read.bundle.root, index = record.sequence - root.journal.sequence - 1;
393
+ if (record.kind === "release" || index < 0 || index >= root.reservedRecordBytes.length || line(record).length > PLACEMENT_PUBLICATION_RECORD_MAX_BYTES
394
+ || line(record).length !== root.reservedRecordBytes[index]) fail("conflict");
395
+ }
396
+ export interface PlacementPublicationMetadataIO {
397
+ names(): string[]; read(name: string): Buffer | undefined; journal(): PlacementJournalRead;
398
+ reservedJournalNames(root: PlacementPublicationRoot): string[];
399
+ publication(attemptHash: string): PlacementPublicationStored;
400
+ complete(name: string, bytes: Buffer, written: PlacementStoragePhase, synced: PlacementStoragePhase): boolean;
401
+ check(): void;
402
+ }
403
+ function metadataIO(p: Paths, opts?: PlacementStorageOptions, required: readonly string[] = []): PlacementPublicationMetadataIO {
404
+ const valid = (name: string) => { if (!publicationName(name)) fail("invalid"); return /-chunk-\d{5}\.json$/.test(name) ? PLACEMENT_JOURNAL_CHUNK_MAX_BYTES : PLACEMENT_JOURNAL_ROOT_MAX_BYTES; };
405
+ return {
406
+ names: () => [...inventory(p).files.keys()], read: name => readOwned(join(p.dir, name), valid(name)), journal: () => snapshot(p),
407
+ reservedJournalNames: root => { const read = snapshot(p); return placementPublicationReservedJournalNames(root, read, read.chunks.map(c => c.size)); },
408
+ publication: hash => { if (!hex(hash)) fail("invalid"); return publicationRead(p, hash); },
409
+ complete: (name, bytes, written, synced) => { const maximum = valid(name); if (!opts || !required.includes(name)) fail("invalid"); return complete(p, opts, join(p.dir, name), bytes, maximum, required, written, synced); },
410
+ check: () => { if (opts) current(p, opts, required); else inventory(p); },
411
+ };
412
+ }
413
+ /** A read-only metadata snapshot can be rechecked after asynchronous object verification. No writer fence or recovery is acquired. */
414
+ export function capturePlacementPublicationMetadata<T>(home: string, epochHash: string, work: (io: PlacementPublicationMetadataIO) => T): { value: T; assertCurrent(): void } {
415
+ return protectedIO(() => {
416
+ const p = paths(home, epochHash), before = inventory(p).token, value = work(metadataIO(p));
417
+ const assertCurrent = () => protectedIO(() => { if (inventory(p).token !== before) fail("changed"); });
418
+ assertCurrent(); return { value, assertCurrent };
419
+ });
420
+ }
421
+ export function readPlacementPublicationMetadata<T>(home: string, epochHash: string, work: (io: PlacementPublicationMetadataIO) => T): T {
422
+ return capturePlacementPublicationMetadata(home, epochHash, work).value;
423
+ }
424
+ function assertPublicationActive(p: Paths): void {
425
+ const epoch = decode(readOwned(join(p.dir, "epoch.json"), PLACEMENT_JOURNAL_ROOT_MAX_BYTES), validatePlacementEpoch);
426
+ const profile = decode(readOwned(join(p.home, "profile.json"), PLACEMENT_JOURNAL_ROOT_MAX_BYTES), validatePlacementProfile);
427
+ decode(readOwned(join(p.home, "shards.json"), PLACEMENT_JOURNAL_ROOT_MAX_BYTES), value => validatePlacementShards(value, profile));
428
+ const fence = decode(readOwned(join(p.root, "active.json"), PLACEMENT_JOURNAL_ROOT_MAX_BYTES), validatePlacementActiveFence);
429
+ if (profile.generation !== epoch.generation || profile.placementHead !== p.hash || fence.epochHash !== p.hash || fence.generation !== epoch.generation || fence.homeHash !== epoch.homeHash) fail("changed");
430
+ }
431
+ export function withPlacementPublicationMetadata<T>(home: string, epochHash: string, opts: PlacementStorageOptions, required: readonly string[], work: (io: PlacementPublicationMetadataIO) => T): T {
432
+ if (required.some(name => !publicationName(name) && !/^records-\d{5}\.jsonl$/.test(name) && name !== "append-intent.json")) fail("invalid");
433
+ const p = paths(home, epochHash), guarded = { ...opts, assertCurrent() { opts.assertCurrent(); assertPublicationActive(p); } };
434
+ const attempts = new Set(required.filter(publicationName).map(name => name.slice(12, 76)));
435
+ if (attempts.size > 1) fail("invalid");
436
+ return writer(home, epochHash, guarded, p => { current(p, guarded, required); return work(metadataIO(p, guarded, required)); }, [...attempts][0], required);
437
+ }
438
+
439
+
440
+ /** A fingerprint is useful only while this exact kernel-backed mutation handle remains live. */
441
+ const publicationDurability = new WeakMap<MutationHandle, Map<string, string>>();
442
+ function publicationGroupToken(p: Paths, hash: string): string {
443
+ const found = inventory(p), dir = state(p.dir, true)!;
444
+ return canonicalJSON({ dir: [String(dir.dev), String(dir.ino)], files: [...found.files].filter(([name]) => name.startsWith(`publication-${hash}-`))
445
+ .map(([name, s]) => [name, String(s.dev), String(s.ino), String(s.size), String(s.mtimeNs), String(s.ctimeNs), String(s.mode)]) });
446
+ }
447
+ function reestablishPublicationDurability(p: Paths, opts: PlacementStorageOptions, incompleteAttempt?: string): void {
448
+ const groups = new Set([...inventory(p).files.keys()].filter(publicationName).map(name => name.slice(12, 76)));
449
+ if (!groups.size) return;
450
+ let cache = publicationDurability.get(opts.mutation);
451
+ if (!cache) { cache = new Map(); publicationDurability.set(opts.mutation, cache); }
452
+ for (const hash of groups) {
453
+ const token = publicationGroupToken(p, hash); if (cache.get(hash) === token) continue;
454
+ let read: PlacementPublicationStored;
455
+ try { read = publicationRead(p, hash); }
456
+ catch (error) {
457
+ // Only an exact preparer may finish its own unacknowledged root/chunk prefixes. Ordinary journal
458
+ // writers and explicit durability recovery refuse them, never treating them as a free reservation.
459
+ if (hash === incompleteAttempt && knownError(error) && error.code === "pending") { cache.delete(hash); continue; }
460
+ throw error;
461
+ }
462
+ const partial = read.pendingPhase ? `publication-${hash}-${read.pendingPhase === "artifact-built" ? "artifact" : read.pendingPhase === "publication-intent" ? "intent" : "receipt"}.json` : undefined;
463
+ for (const name of publicationFileNames(read.bundle.root)) {
464
+ if (name === partial) continue; // A known incomplete fact has no acknowledgement until exact prefix completion.
465
+ current(p, opts); const file = join(p.dir, name), before = state(file)!;
466
+ const fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
467
+ try {
468
+ const opened = fstatSync(fd, { bigint: true }); owned(opened);
469
+ if (!same(opened, before) || !same(opened, state(file)!)) fail("changed");
470
+ current(p, opts); if (!same(opened, fstatSync(fd, { bigint: true })) || !same(opened, state(file)!)) fail("changed");
471
+ fsyncSync(fd);
472
+ if (!same(opened, state(file)!)) fail("changed"); opts.phase?.("publication-durability-file-fsync");
473
+ } finally { closeSync(fd); }
474
+ }
475
+ syncDirectory(p, opts, p.dir, []); opts.phase?.("publication-durability-dir-fsync"); current(p, opts);
476
+ if (publicationGroupToken(p, hash) !== token) fail("changed"); cache.set(hash, token);
477
+ }
478
+ }
479
+ /** Fresh-owner barrier before transport effects or use of a terminal receipt to free journal capacity.
480
+ * Complete canonical bytes left by a dead owner are not a substitute for this fsync acknowledgement. */
481
+ export function ensurePlacementPublicationDurability(home: string, epochHash: string, opts: PlacementStorageOptions): void {
482
+ protectedIO(() => {
483
+ const p = paths(home, epochHash), guarded = { ...opts, assertCurrent() { opts.assertCurrent(); assertPublicationActive(p); } };
484
+ writer(home, epochHash, guarded, () => undefined);
485
+ });
486
+ }
487
+
488
+ function placementAppendRequired(p: Paths, event: PlacementEvent, expectedHead: string, opts: PlacementStorageOptions): string[] {
489
+ const read = snapshot(p); checkPublicationAppend(p, read, event, opts);
490
+ if (read.intent) {
491
+ const pending = read.intent; checkPublicationRecord(p, pending.record, opts);
492
+ if (pending.record.previousHash !== expectedHead || !sameEvent(pending.record, event)) fail("pending");
493
+ if (read.projection.headHash !== pending.record.seal) read.projection.apply(pending.record);
494
+ return ["append-intent.json", pending.chunk];
495
+ }
496
+ const existing = read.projection.request(event.requestId);
497
+ if (existing) {
498
+ if (!sameEvent(existing, event) || existing.previousHash !== expectedHead || read.headHash !== existing.seal) fail("conflict");
499
+ return [];
500
+ }
501
+ if (read.headHash !== expectedHead) fail("conflict");
502
+ const record = createPlacementRecord(p.hash, read.sequence + 1, expectedHead, event), bytes = line(record);
503
+ checkPublicationRecord(p, record, opts); read.projection.apply(record);
504
+ if (bytes.length > PLACEMENT_JOURNAL_CHUNK_MAX_BYTES) fail("bound");
505
+ const prior = read.chunks.at(-1), reuse = prior !== undefined && prior.size + bytes.length <= PLACEMENT_JOURNAL_CHUNK_MAX_BYTES;
506
+ const chunk = reuse ? prior.name : `records-${String(read.chunks.length).padStart(5, "0")}.jsonl`;
507
+ const transaction: Intent = { schema: "agent-mem.sync-placement-append-intent/v1", epochHash: p.hash, record, chunk, prefixBytes: reuse ? prior.size : 0,
508
+ prefixHash: reuse ? prior.hash : digest(Buffer.alloc(0)), lineHash: digest(bytes) };
509
+ const expected = line(transaction), saved = readOwned(join(p.dir, "append-intent.json"), PLACEMENT_JOURNAL_ROOT_MAX_BYTES);
510
+ if (expected.length > PLACEMENT_JOURNAL_ROOT_MAX_BYTES) fail("bound");
511
+ if (saved && (saved.length > expected.length || !expected.subarray(0, saved.length).equals(saved))) fail("conflict");
512
+ return ["append-intent.json", chunk];
513
+ }