@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
@@ -1,3 +1,6 @@
1
+ import { readActivePlacementBinding } from "./sync-placement-active.ts";
2
+ import { validatePlacementProfile, validatePlacementShards } from "./sync-placement-format.ts";
3
+ import { assertPlacementRuntime, SyncPlacementRuntimeError } from "./sync-placement-guard.ts";
1
4
  import { assertNoPublicPruneIntent, readPublicPruneView } from "./public-prune-view.ts";
2
5
  import { indexFileName } from "./index-file.ts";
3
6
  import { createHash, randomBytes } from "node:crypto";
@@ -84,31 +87,46 @@ export function captureShardDir(base: string, key: string, machine: string): str
84
87
  export const SHARDED_SCHEMA = "agent-mem.shards/v1";
85
88
  export const SHARDED_V2_SCHEMA = "agent-mem.shards/v2";
86
89
  export const SHARDED_V2_FIELDS = ["schema", "generation", "phase", "selectorRevision", "sourceGeneration", "sources", "machine", "legacyCommit", "deadLeasesRemoved"] as const;
87
- export interface ShardedSelection { schema: typeof SHARDED_SCHEMA | typeof SHARDED_V2_SCHEMA; generation: string; phase: "active"; selectorRevision: string }
90
+ export interface ShardedSelection { schema: typeof SHARDED_SCHEMA | typeof SHARDED_V2_SCHEMA | "agent-mem.shards/v3"; generation: string; phase: "active"; selectorRevision: string }
88
91
 
89
92
  export function userShardDir(base: string): string {
90
93
  return join(base, "user");
91
94
  }
92
95
 
93
96
  type ShardedJournal = Record<string, unknown> & ShardedSelection;
97
+ /** Keep the existing active reader shape and the migration's captured-v1/v2 state distinct from v3. */
98
+ export function legacyShardMarkerShape(value: unknown): boolean {
99
+ if (value === undefined) return true;
100
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
101
+ const row = value as Record<string, unknown>;
102
+ return (row.schema === SHARDED_SCHEMA || row.schema === SHARDED_V2_SCHEMA) && validGeneration(row.generation)
103
+ && typeof row.phase === "string" && ["captured", "active"].includes(row.phase) && typeof row.selectorRevision === "string"
104
+ && (row.schema !== SHARDED_V2_SCHEMA || !Object.keys(row).some(key => !(SHARDED_V2_FIELDS as readonly string[]).includes(key)));
105
+ }
94
106
  function shardJournal(memoryDir: string): ShardedJournal {
95
107
  const value = readProfileJSON(join(memoryDir, "shards.json")) as Record<string, unknown> | undefined;
108
+ if (value?.schema === "agent-mem.shards/v3") {
109
+ const profile = validatePlacementProfile(readProfileJSON(join(memoryDir, "profile.json")));
110
+ return validatePlacementShards(value, profile) as unknown as ShardedJournal;
111
+ }
96
112
  if (!value || (value.schema !== SHARDED_SCHEMA && value.schema !== SHARDED_V2_SCHEMA) || !validGeneration(value.generation) || value.phase !== "active" || typeof value.selectorRevision !== "string") throw new Error("no active curated shard migration");
97
113
  if (value.schema === SHARDED_V2_SCHEMA && Object.keys(value).some(key => !(SHARDED_V2_FIELDS as readonly string[]).includes(key))) throw new Error("unsupported curated shard view");
98
114
  return value as ShardedJournal;
99
115
  }
100
116
  function compatibleShardAdvance(before: ShardedJournal, after: ShardedJournal): boolean {
101
- if (before.schema !== SHARDED_V2_SCHEMA || after.schema !== SHARDED_V2_SCHEMA) return false;
117
+ if (![SHARDED_V2_SCHEMA, "agent-mem.shards/v3"].includes(before.schema) || after.schema !== before.schema) return false;
102
118
  const { selectorRevision: _old, ...old } = before, { selectorRevision: _next, ...next } = after;
103
119
  return canonicalJSON(old) === canonicalJSON(next);
104
120
  }
105
121
  function shardSnapshot(memoryDir: string, deadline: number) {
122
+ assertPlacementRuntime(memoryDir);
106
123
  assertNoPublicPruneIntent(memoryDir, deadline);
107
124
  let journal = shardJournal(memoryDir);
108
125
  for (let attempt = 0; attempt < 6 && Date.now() <= deadline; attempt++) {
109
126
  let selector: ReturnType<typeof readSelector>;
110
127
  try { selector = readSelector(memoryDir, deadline); }
111
128
  catch (error) {
129
+ if (error instanceof SyncPlacementRuntimeError) throw error;
112
130
  // The writer's spent wait budget or unavailable proof is never restarted here.
113
131
  if (error instanceof Error && error.message === "public prune view unavailable") throw error;
114
132
  const current = shardJournal(memoryDir);
@@ -120,8 +138,8 @@ function shardSnapshot(memoryDir: string, deadline: number) {
120
138
  if (!compatibleShardAdvance(journal, current)) throw new Error("curated shard selector changed");
121
139
  journal = current; continue;
122
140
  }
123
- if ((journal.schema === SHARDED_V2_SCHEMA) !== (selector.selector?.schema === "agent-mem.profile/v2")) throw new Error("curated shard selector changed");
124
- if (journal.schema === SHARDED_V2_SCHEMA && (selector.revision !== journal.selectorRevision || selector.selector?.generation !== journal.generation)) continue;
141
+ if ((journal.schema === SHARDED_V2_SCHEMA || journal.schema === "agent-mem.shards/v3") !== (selector.selector?.schema === "agent-mem.profile/v2" || selector.selector?.schema === "agent-mem.profile/v3")) throw new Error("curated shard selector changed");
142
+ if ((journal.schema === SHARDED_V2_SCHEMA || journal.schema === "agent-mem.shards/v3") && (selector.revision !== journal.selectorRevision || selector.selector?.generation !== journal.generation)) continue;
125
143
  return { journal, selector };
126
144
  }
127
145
  throw new Error("curated shard selector changed");
@@ -135,6 +153,14 @@ export function readShardedSelection(memoryDir: string): ShardedSelection {
135
153
  * ponytail: same-user concurrent parent replacement needs descriptor-relative opens; fixed preflight does not claim that fence.
136
154
  */
137
155
  export function assertCuratedShardPaths(memoryDir: string, opts: { project?: string; user?: boolean }): void {
156
+ assertPlacementRuntime(memoryDir);
157
+ const selector = readProfileJSON(join(memoryDir, "profile.json")) as {schema?:unknown} | undefined;
158
+ if (selector?.schema === "agent-mem.profile/v3") {
159
+ const active = readActivePlacementBinding(memoryDir, selector, readProfileJSON(join(memoryDir, "shards.json")));
160
+ const keyHash = opts.user ? undefined : createHash("sha256").update(opts.project!).digest("hex");
161
+ if (!active || !active.activation.realms.some(realm => opts.user ? realm.scope === "user" : realm.scope === "project" && realm.keyHash === keyHash))
162
+ throw new Error("sync placement realm is outside the activated inventory; review its activation before opening it");
163
+ }
138
164
  const dir = opts.user === true ? userShardDir(memoryDir) : shardDir(memoryDir, opts.project!);
139
165
  const check = (path: string, directory: boolean) => {
140
166
  try {
@@ -169,10 +195,10 @@ export function resolveShardProfile(memoryDir: string, opts: { project?: string;
169
195
  const final = shardSnapshot(memoryDir, deadline), selector = final.selector;
170
196
  if (canonicalJSON(final.journal) !== canonicalJSON(initial.journal) && !compatibleShardAdvance(initial.journal, final.journal)
171
197
  || selector.revision !== final.journal.selectorRevision) throw new Error("curated shard selector changed; resume or inspect migration state");
172
- if (selector.selector?.schema === "agent-mem.profile/v2") readPublicPruneView(memoryDir, selector.selector.pruneInputSetHash, generation, deadline);
198
+ if (selector.selector?.schema === "agent-mem.profile/v2" || selector.selector?.schema === "agent-mem.profile/v3") readPublicPruneView(memoryDir, selector.selector.pruneInputSetHash, generation, deadline);
173
199
  const legacyMainHome = join(dir, "legacy-main");
174
200
  const hasLegacyMain = existsSync(legacyMainHome) || sources.sources.some(source => !!source && typeof source === "object" && "source" in source && source.source === "legacy-main");
175
201
  return { kind: "generation", home: memoryDir, dir, generation, selectorRevision: selector.revision,
176
- ...(selector.selector?.schema === "agent-mem.profile/v2" ? { foreignPruneFormat: selector.selector.foreignPruneFormat, pruneInputSetHash: selector.selector.pruneInputSetHash } : {}),
202
+ ...((selector.selector?.schema === "agent-mem.profile/v2" || selector.selector?.schema === "agent-mem.profile/v3") ? { foreignPruneFormat: selector.selector.foreignPruneFormat, pruneInputSetHash: selector.selector.pruneInputSetHash, ...(selector.selector.schema === "agent-mem.profile/v3" ? { publicationFormat: selector.selector.publicationFormat, placementHead: selector.selector.placementHead } : {}) } : {}),
177
203
  legacyHome: join(dir, "legacy"), ...(hasLegacyMain ? { legacyMainHome } : {}), sharded: true, ...(opts.user === true ? { shardUser: true } : { shardKey: opts.project! }) };
178
204
  }
@@ -1,3 +1,4 @@
1
+ import { assertPlacementRuntime, placementSelectorRefusal, SyncPlacementRuntimeError } from "./sync-placement-guard.ts";
1
2
  import { assertFormatFloor } from "./formats.ts";
2
3
  import { readPublicPruneView } from "./public-prune-view.ts";
3
4
  import { INDEX_VERSION, indexFileName } from "./index-file.ts";
@@ -234,7 +235,10 @@ export class MemoryStore {
234
235
  if (opts.memoryDir !== undefined && (opts.home !== undefined || typeof opts.memoryDir !== "string" || !opts.memoryDir.trim())) throw new Error("choose a memory home or one explicit memory directory");
235
236
  const home = bootstrap?.profile.home ?? opts.memoryDir ?? (opts.home === undefined ? memoryHome() : join(opts.home, "memory"));
236
237
  assertFormatFloor(home, "curated");
238
+ const placementRefusal = placementSelectorRefusal(home);
239
+ if (placementRefusal) throw new SyncPlacementRuntimeError(placementRefusal);
237
240
  if (!bootstrap && lstatSync(join(home, "shards.json"), { throwIfNoEntry: false })) throw new Error("memory home has a curated split; hosts must use openShardStore({ project }) or openShardStore({ user: true }); finish an incomplete split with amem migrate --shards --resume");
241
+ if (bootstrap) assertPlacementRuntime(home);
238
242
  this.profile = bootstrap?.profile ?? resolveProfile(home, opts.profile);
239
243
  if (bootstrap && (this.profile.kind !== "generation" || !this.profile.generation
240
244
  || (!this.profile.sharded && this.profile.dir !== generationDir(this.profile.home, this.profile.generation))
@@ -1561,6 +1565,14 @@ export class MemoryStore {
1561
1565
  });
1562
1566
  }
1563
1567
 
1568
+ /** Canonical generation outbound sources only; no refresh, rebind or derived-state write.
1569
+ * Call the returned bounded recheck after visiting all opened targets, before declaring completeness.
1570
+ */
1571
+ inspectOutboundRows(visit: (row: Record<string, unknown>) => void, limits: { maxBytes: number; maxRows: number }): () => void {
1572
+ if (!this.generationIndex) throw Object.assign(new Error("outbound generation input unavailable"), { code: "input-unavailable" });
1573
+ return this.generationIndex.inspectOutboundRows(visit, limits);
1574
+ }
1575
+
1564
1576
  /** Person-local diagnostic receipt; malformed legacy bodies are never included. */
1565
1577
  bridgeStatus(): { skippedRows: number } | undefined {
1566
1578
  return this.generationIndex ? { skippedRows: this.generationIndex.skippedRows() } : undefined;
@@ -1622,7 +1634,7 @@ function initializeShard(home: string, opts: { project?: string; user?: boolean
1622
1634
  writeProfileJSON(join(dir, "migration-sources.json"), { schema: "agent-mem.migration-sources/v1", sources });
1623
1635
  const selected = readSelector(home).selector;
1624
1636
  const profile: BoundProfile = { kind: "generation", home, dir, generation, selectorRevision: selection.selectorRevision,
1625
- ...(selected?.schema === "agent-mem.profile/v2" ? {foreignPruneFormat: selected.foreignPruneFormat, pruneInputSetHash: selected.pruneInputSetHash} : {}),
1637
+ ...((selected?.schema === "agent-mem.profile/v2" || selected?.schema === "agent-mem.profile/v3") ? {foreignPruneFormat: selected.foreignPruneFormat, pruneInputSetHash: selected.pruneInputSetHash, ...(selected.schema === "agent-mem.profile/v3" ? {publicationFormat:selected.publicationFormat,placementHead:selected.placementHead} : {})} : {}),
1626
1638
  legacyHome, legacyMainHome: join(dir, "legacy-main"), sharded: true, ...(opts.user === true ? { shardUser: true } : { shardKey: opts.project! }) };
1627
1639
  buildGenerationIndex(profile, sources);
1628
1640
  writeProfileJSON(join(dir, "generation-seal.json"), { schema: "agent-mem.generation-seal/v1", generation,
@@ -1645,6 +1657,7 @@ export function openShardStore(opts: StoreOptions & { project?: string; user?: b
1645
1657
  const missing = !existsSync(join(dir, "generation.json"));
1646
1658
  if (missing) {
1647
1659
  const selected = readSelector(home).selector;
1660
+ if (selected?.schema === "agent-mem.profile/v3") throw new Error("sync placement realm was not activated; review a complete realm inventory before opening a new curated shard");
1648
1661
  if (selected?.schema === "agent-mem.profile/v2") {
1649
1662
  const view = readPublicPruneView(home, selected.pruneInputSetHash, selected.generation), key = opts.user ? "user" : `project:${opts.project}`;
1650
1663
  if (view.realms[key] || ["generation-seal.json", "migration-sources.json", "machine.json"].some(name => existsSync(join(dir, name)))) throw new Error("v2 generation input unavailable; reader cannot repair committed inputs");
@@ -0,0 +1,63 @@
1
+ import { closeSync, constants, fstatSync, lstatSync, openSync, readSync, type BigIntStats } from "node:fs";
2
+ import { createHash } from "node:crypto";
3
+ import { dirname, join, relative, sep } from "node:path";
4
+ import type { MemoryStore } from "./store.ts";
5
+
6
+ /** Metadata-only sharded sync epoch. Hash complete source bytes, never notes or unopened shards. */
7
+ export function shardSyncBinding(store: MemoryStore): string { return captureShardSyncBinding(store, true); }
8
+ /** Selection bytes may change only after this sync commits; source/policy identity remains pinned across that commit. */
9
+ export function shardSyncSourceBinding(store: MemoryStore): string { return captureShardSyncBinding(store, false); }
10
+ function captureShardSyncBinding(store: MemoryStore, selection: boolean): string {
11
+ const unavailable = (): never => { throw new Error("sharded sync binding unavailable"); };
12
+ if (!store.profile.sharded) return unavailable();
13
+ const uid = process.getuid?.(); if (uid === undefined) return unavailable();
14
+ 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;
15
+ const owned = (stat: BigIntStats, directory: boolean, privateMode = true) => {
16
+ if (stat.isSymbolicLink() || (directory ? !stat.isDirectory() : !stat.isFile() || stat.nlink !== 1n)
17
+ || stat.uid !== BigInt(uid) || privateMode && (stat.mode & 0o077n) !== 0n) unavailable();
18
+ };
19
+ const home = store.profile.home, digest = createHash("sha256"), directories = new Map<string, BigIntStats>(), checks: (() => void)[] = [];
20
+ const emit = (value: unknown) => { digest.update(JSON.stringify(value) + "\n"); };
21
+ const directory = (dir: string): void => {
22
+ if (directories.has(dir)) return;
23
+ const rel = relative(home, dir); if (rel === ".." || rel.startsWith(".." + sep)) unavailable();
24
+ if (dir !== home) directory(dirname(dir));
25
+ const before = lstatSync(dir, { bigint: true }); owned(before, true); directories.set(dir, before);
26
+ emit(["directory", dir, String(before.dev), String(before.ino)]);
27
+ checks.push(() => { const now = lstatSync(dir, { bigint: true }); owned(now, true); if (!same(before, now)) unavailable(); });
28
+ };
29
+ // Policy config keeps its established file modes inside this already-private owned home. Its complete bytes/inode still bind the epoch.
30
+ const source = (file: string, privateMode = true): void => {
31
+ directory(dirname(file));
32
+ let before: BigIntStats;
33
+ try { before = lstatSync(file, { bigint: true }); }
34
+ catch (error) {
35
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
36
+ emit(["source", file, "absent"]);
37
+ checks.push(() => { if (lstatSync(file, { bigint: true, throwIfNoEntry: false }) !== undefined) unavailable(); }); return;
38
+ }
39
+ owned(before, false, privateMode);
40
+ const fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
41
+ try {
42
+ const opened = fstatSync(fd, { bigint: true }); owned(opened, false, privateMode); if (!same(before, opened)) unavailable();
43
+ const content = createHash("sha256"), buffer = Buffer.allocUnsafe(65_536); let bytes = 0n;
44
+ for (;;) { const n = readSync(fd, buffer, 0, buffer.length, null); if (!n) break; content.update(buffer.subarray(0, n)); bytes += BigInt(n); }
45
+ const after = fstatSync(fd, { bigint: true }); owned(after, false, privateMode);
46
+ if (bytes !== before.size || !same(before, after) || !same(before, lstatSync(file, { bigint: true }))) unavailable();
47
+ emit(["source", file, "present", String(before.dev), String(before.ino), String(before.size), String(before.mtimeNs), String(before.ctimeNs), content.digest("hex")]);
48
+ } finally { closeSync(fd); }
49
+ checks.push(() => { const now = lstatSync(file, { bigint: true }); owned(now, false, privateMode); if (!same(before, now)) unavailable(); });
50
+ };
51
+ try {
52
+ directory(home);
53
+ for (const name of [...(selection ? ["profile.json", "shards.json"] : []), "aliases.jsonl", "amem.config.json", "machine.json"]) source(join(home, name), name !== "amem.config.json");
54
+ const targets = [...store.openedSyncTargets()].sort((a, b) => a.dir.localeCompare(b.dir));
55
+ for (const target of targets) {
56
+ directory(target.dir);
57
+ emit(["target", target.dir, target.profile.generation ?? null, target.machine.id, target.profile.shardUser ?? false, target.profile.shardKey ?? null]);
58
+ for (const name of ["project.json", "generation.json", "generation-seal.json", "machine.json"]) source(join(target.dir, name));
59
+ }
60
+ for (const check of checks) check();
61
+ return digest.digest("hex");
62
+ } catch { return unavailable(); }
63
+ }
@@ -0,0 +1,75 @@
1
+ /** Metadata-only v3 admission. Publication still requires the complete journal/source/policy proof. */
2
+ import { closeSync, constants, fstatSync, lstatSync, openSync, readSync, type BigIntStats } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { canonicalJSON, hashJSON } from "./generation.ts";
5
+ import { realPath } from "./platform.ts";
6
+ import { validatePlacementProfile, validatePlacementShards, type PlacementProfile, type PlacementShards } from "./sync-placement-format.ts";
7
+ import { validatePlacementPlan, type SyncPlacementActivationPlan } from "./sync-placement-plan.ts";
8
+ import { placementEpochHash, validatePlacementEpoch, validatePlacementActiveFence, type PlacementEpoch } from "./sync-placement-journal-format.ts";
9
+ import { readPublicPruneViewForActivation } from "./public-prune-view.ts";
10
+ import { readPublicPruneInputSetChain } from "./public-prune-input-chain.ts";
11
+ import { shardDir, userShardDir } from "./shard.ts";
12
+
13
+ export interface ActivePlacementBinding { profile: PlacementProfile; shards: PlacementShards; epoch: PlacementEpoch; activation: SyncPlacementActivationPlan }
14
+ function fail(): never { throw new Error("sync placement activation metadata unavailable or changed; inspect the retained activation before continuing"); }
15
+ 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;
16
+ function owned(s: BigIntStats, directory = false): void {
17
+ if (s.uid !== BigInt(process.getuid?.() ?? -1) || (s.mode & 0o077n) !== 0n || s.isSymbolicLink()
18
+ || (directory ? !s.isDirectory() : !s.isFile() || s.nlink !== 1n)) fail();
19
+ }
20
+ /** Initial activation commitments remain authoritative; maintenance may only grow the same-generation prune view. */
21
+ export function assertPlacementViewAdvance(home: string, activation: SyncPlacementActivationPlan, profile: PlacementProfile, shards: PlacementShards): void {
22
+ const initial = activation.targetProfile;
23
+ const { pruneInputSetHash: _old, ...before } = initial, { pruneInputSetHash: _next, ...after } = profile;
24
+ if (canonicalJSON(before) !== canonicalJSON(after)) fail();
25
+ const initialShards = { ...shards, selectorRevision: hashJSON(initial) };
26
+ if (hashJSON(initialShards) !== activation.targetShardsHash) fail();
27
+ if (initial.pruneInputSetHash === profile.pruneInputSetHash) return;
28
+ const oldView = readPublicPruneViewForActivation(home, initial.pruneInputSetHash, profile.generation);
29
+ const nextView = readPublicPruneViewForActivation(home, profile.pruneInputSetHash, profile.generation);
30
+ if (!Object.keys(oldView.realms).every(key => Object.hasOwn(nextView.realms, key))) fail();
31
+ for (const [key, hash] of Object.entries(nextView.realms)) {
32
+ const old = oldView.realms[key]; if (old === hash) continue;
33
+ const dir = key === "user" ? userShardDir(home) : shardDir(home, key.slice(8));
34
+ const chain = readPublicPruneInputSetChain({ dir, receivingGeneration: profile.generation, inputSetHash: hash });
35
+ if (old !== undefined && !chain.commitments.includes(old)) fail();
36
+ }
37
+ }
38
+ /** No mutation/reader registration/index open. Missing preparation is distinct from malformed retained evidence. */
39
+ export function readActivePlacementBinding(homeInput: string, profileInput: unknown, shardsInput: unknown): ActivePlacementBinding | undefined {
40
+ const profile = validatePlacementProfile(profileInput), shards = validatePlacementShards(shardsInput, profile);
41
+ const home = realPath(homeInput), root = lstatSync(home, { bigint: true }); owned(root, true);
42
+ const checks: (() => void)[] = [() => { const now = lstatSync(home, { bigint: true }); owned(now, true); if (now.dev !== root.dev || now.ino !== root.ino) fail(); }];
43
+ const directory = (dir: string): boolean => {
44
+ const s = lstatSync(dir, { bigint: true, throwIfNoEntry: false }); if (!s) return false; owned(s, true);
45
+ checks.push(() => { const now = lstatSync(dir, { bigint: true }); owned(now, true); if (s.dev !== now.dev || s.ino !== now.ino) fail(); }); return true;
46
+ };
47
+ const read = (file: string): { value: unknown; text: string } | undefined => {
48
+ const before = lstatSync(file, { bigint: true, throwIfNoEntry: false }); if (!before) return undefined;
49
+ owned(before); if (before.size <= 0n || before.size > 262144n) fail();
50
+ const fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
51
+ try {
52
+ const s = fstatSync(fd, { bigint: true }); owned(s); if (!same(before, s)) fail();
53
+ const bytes = Buffer.alloc(Number(s.size) + 1); let at = 0;
54
+ while (at < bytes.length) { const n = readSync(fd, bytes, at, bytes.length - at, at); if (!n) break; at += n; }
55
+ if (at !== Number(s.size) || !same(s, fstatSync(fd, { bigint: true })) || !same(s, lstatSync(file, { bigint: true }))) fail();
56
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(0, at)), value: unknown = JSON.parse(text);
57
+ if (canonicalJSON(value) + "\n" !== text) fail();
58
+ checks.push(() => { const now = lstatSync(file, { bigint: true }); owned(now); if (!same(s, now)) fail(); }); return { value, text };
59
+ } finally { closeSync(fd); }
60
+ };
61
+ if (!directory(join(home, "sync-placement")) || !directory(join(home, "sync-placement", "epochs"))) return undefined;
62
+ const dir = join(home, "sync-placement", "epochs", profile.placementHead);
63
+ if (!directory(dir)) return undefined;
64
+ const epochFile = read(join(dir, "epoch.json")), activationFile = read(join(dir, "activation.json")), fenceFile = read(join(home, "sync-placement", "active.json"));
65
+ if (!epochFile || !activationFile || !fenceFile) return undefined;
66
+ const epoch = validatePlacementEpoch(epochFile.value), activation = validatePlacementPlan(activationFile.value), fence = validatePlacementActiveFence(fenceFile.value);
67
+ const homeHash = hashJSON([home, String(root.dev), String(root.ino), String(root.uid)]);
68
+ if (epoch.homeHash !== homeHash || epoch.generation !== profile.generation || placementEpochHash(epoch) !== profile.placementHead
69
+ || activation.contextHash !== epoch.contextHash || activation.generation !== epoch.generation || activation.targetProfile.placementHead !== profile.placementHead
70
+ || fence.epochHash !== profile.placementHead || fence.homeHash !== homeHash || fence.generation !== profile.generation) fail();
71
+ if (read(join(home, "profile.json"))?.text !== canonicalJSON(profile) + "\n" || read(join(home, "shards.json"))?.text !== canonicalJSON(shards) + "\n") fail();
72
+ assertPlacementViewAdvance(home, activation, profile, shards);
73
+ for (const check of checks) check();
74
+ return { profile, shards, epoch, activation };
75
+ }
@@ -0,0 +1,32 @@
1
+ /** Shared private cache metadata validation. These checks never initialize, repair or publish a repository. */
2
+ import { lstatSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { canonicalJSON } from "./generation.ts";
5
+ import { SyncPlacementJournalError } from "./sync-placement-journal-format.ts";
6
+
7
+ function fail(): never { throw new SyncPlacementJournalError("corrupt"); }
8
+ export function placementCacheRootStamp(home: string): string {
9
+ const stat = lstatSync(join(home, "shard-sync-placement"), { bigint: true });
10
+ if (!stat.isDirectory() || stat.isSymbolicLink() || stat.uid !== BigInt(process.getuid?.() ?? -1) || (stat.mode & 0o077n) !== 0n) fail();
11
+ return canonicalJSON([String(stat.dev), String(stat.ino), String(stat.uid), String(stat.mode)]);
12
+ }
13
+ export function placementCacheConfigStamp(cache: string): string {
14
+ const stat = lstatSync(join(cache, "config"), { bigint: true });
15
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1n || stat.uid !== BigInt(process.getuid?.() ?? -1)
16
+ || (stat.mode & 0o077n) !== 0n || stat.size > 65536n) fail();
17
+ return canonicalJSON([String(stat.dev), String(stat.ino), String(stat.size), String(stat.mtimeNs), String(stat.ctimeNs)]);
18
+ }
19
+ export function validatePlacementCacheConfig(output: string): ReadonlyMap<string, string> {
20
+ const seen = new Map<string, string>();
21
+ const booleans = new Set(["core.filemode", "core.bare", "core.ignorecase", "core.precomposeunicode", "core.protecthfs", "core.protectntfs", "core.logallrefupdates", "core.symlinks"]);
22
+ for (const row of output.split("\0").filter(Boolean)) {
23
+ const at = row.indexOf("\n"); if (at < 1) fail();
24
+ const key = row.slice(0, at), value = row.slice(at + 1);
25
+ if (seen.has(key)) fail(); seen.set(key, value);
26
+ if (booleans.has(key)) { if (value !== "true" && value !== "false") fail(); }
27
+ else if (key === "core.repositoryformatversion") { if (value !== "0" && value !== "1") fail(); }
28
+ else if (key === "extensions.objectformat") { if (value !== "sha1") fail(); }
29
+ else if (!["remote.origin.url", "remote.origin.fetch", "remote.origin.pushurl"].includes(key)) fail();
30
+ }
31
+ return seen;
32
+ }
@@ -0,0 +1,292 @@
1
+ import type { PlacementPublicationSource } from "./sync-placement-publication-plan.ts";
2
+ import { validatePlacementKey, type PlacementRealm } from "./sync-placement-journal-format.ts";
3
+ import { types } from "node:util";
4
+ import { createHash } from "node:crypto";
5
+ import { closeSync, constants, fstatSync, lstatSync, openSync, opendirSync, readSync, type BigIntStats } from "node:fs";
6
+ import { join } from "node:path";
7
+ import { canonicalJSON, generationDescriptor, hashJSON, validateVaultLinkRow, type BoundProfile, type VaultLinkRow } from "./generation.ts";
8
+ import { validatePlacementBaseV2, validatePlacementProfile, validatePlacementShards, PLACEMENT_PROFILE_SCHEMA } from "./sync-placement-format.ts";
9
+ import { planSyncPlacementActivation, type SyncPlacementActivationContext, type SyncPlacementWriterAttestation } from "./sync-placement-plan.ts";
10
+ import { readPublicPruneViewForActivation, PUBLIC_PRUNE_INTENT } from "./public-prune-view.ts";
11
+ import { realPath, DENY_NAMES, DENY_SEGMENTS } from "./platform.ts";
12
+ import { loadAmemConfig } from "./config.ts";
13
+ import { memoryPolicySnapshot } from "./policy.ts";
14
+ import { SegmentReader, SegmentReadBudgetError, checkedEntry, completeLines } from "./bridge.ts";
15
+ import { readShardLegacySnapshot } from "./shard-bridge.ts";
16
+ import { coalesceGenerationSource } from "./generation-index.ts";
17
+ import { validatePruneRow, type PruneSourceRow, type PruneRow } from "./prune.ts";
18
+ import { shardKeyHash, SHARD_SCHEMA } from "./shard.ts";
19
+ import { SyncPlacementJournalError, placementKeyId, type PlacementKey } from "./sync-placement-journal-format.ts";
20
+ import type { Entry } from "./store.ts";
21
+
22
+ /** Host authentication/compatibility facts are re-read at every effect boundary, never inferred from a session marker. */
23
+ export interface PlacementCapturePort {
24
+ attest(): { writers: readonly SyncPlacementWriterAttestation[]; legacySyncQuiesced: true };
25
+ /** Required when a caller adds a policy callback; must change whenever its policy changes. */
26
+ policyVersion?(): string;
27
+ }
28
+ /** Internal actual-transport proof, admitted only after the native pending plan and fetched original were validated. */
29
+ export interface PlacementPendingSource { realm: PlacementRealm; pathHash: string; offset: number; prefixHash: string; line: string }
30
+ export interface PlacementSourceRow { key: PlacementKey; row: Entry; sourcePlacement: "local" | "synced"; intrinsicLocal: boolean }
31
+ export interface PlacementSpecialSourceRow { key: PlacementKey; row: PruneRow | VaultLinkRow; sourcePlacement: "local" | "synced"; intrinsicLocal: boolean }
32
+ export interface PlacementCapture {
33
+ home: string; context: SyncPlacementActivationContext; activeHead?: string;
34
+ markerHash: string; fingerprint: string; directories: string[]; prefixes: PlacementPublicationSource[]; rows: Map<string, PlacementSourceRow>;
35
+ /** Diagnostic inventory only; these rows never enter the ordinary transport/capture row map implicitly. */
36
+ specialRows: Map<string, PlacementSpecialSourceRow>;
37
+ sourceRowsByRealm: Map<string, readonly PruneSourceRow[]>;
38
+ assertCurrent(): void;
39
+ /** Under live writer fences, inode/ctime/size checks protect the already-hashed prefixes across callbacks without reparsing rows. */
40
+ assertUnchanged(): void;
41
+ }
42
+ function fail(code: "changed" | "invalid" | "bound" = "changed"): never { throw new SyncPlacementJournalError(code); }
43
+ const digest = (bytes: Uint8Array | string) => createHash("sha256").update(bytes).digest("hex");
44
+ const stamp = (s: BigIntStats) => [s.dev, s.ino, s.size, s.mtimeNs, s.ctimeNs].map(String);
45
+ const same = (a: BigIntStats, b: BigIntStats) => canonicalJSON(stamp(a)) === canonicalJSON(stamp(b));
46
+ function ownedDirectory(dir: string): BigIntStats {
47
+ const s = lstatSync(dir, { bigint: true });
48
+ if (!s.isDirectory() || s.isSymbolicLink() || s.uid !== BigInt(process.getuid!()) || (s.mode & 0o022n) !== 0n) fail();
49
+ return s;
50
+ }
51
+ /** ponytail: capture is capped at 256 metadata objects and 8 MiB/20000 source rows across all realms.
52
+ * Larger homes need a reviewed chunked activation proof; no input is silently omitted at this ceiling. */
53
+ function capture(homeInput: string, port: PlacementCapturePort, selected?: readonly PlacementRealm[], pending: readonly PlacementPendingSource[] = []): Omit<PlacementCapture, "assertCurrent"> {
54
+ let selection: PlacementRealm[] | undefined;
55
+ if (selected !== undefined) {
56
+ if (!Array.isArray(selected) || types.isProxy(selected) || selected.length > 256) fail("bound");
57
+ const copied = memoryPolicySnapshot(selected); if (!copied) fail();
58
+ selection = copied.map(realm => validatePlacementKey({ realm, id: "0".repeat(32), rowHash: "0".repeat(64) }).realm);
59
+ if (!selection.length || new Set(selection.map(realm => canonicalJSON(realm))).size !== selection.length) fail();
60
+ }
61
+ // All host callouts precede filesystem capture: a callback cannot alter input after its final proof.
62
+ const attestation = memoryPolicySnapshot(port.attest()), policyVersion = port.policyVersion?.();
63
+ if (!attestation) fail();
64
+ if (port.policyVersion && typeof policyVersion !== "string") fail();
65
+ const home = realPath(homeInput), root = ownedDirectory(home);
66
+ let metadataObjects = 0, metadataBytes = 0;
67
+ const facts: unknown[] = [], sourceChecks: (() => void)[] = [];
68
+ const pendingByPath = new Map(pending.map(item => [canonicalJSON(item.realm)+":"+item.pathHash,item]));
69
+ if (pendingByPath.size !== pending.length) fail();
70
+ const directoryFact = (dir: string) => {
71
+ const stat = ownedDirectory(dir); facts.push([dir, "directory", String(stat.dev), String(stat.ino), String(stat.uid)]);
72
+ sourceChecks.push(() => { const now=ownedDirectory(dir); if(now.dev!==stat.dev||now.ino!==stat.ino) fail(); });
73
+ };
74
+ const bytes = (file: string, optional = false): Buffer | undefined => {
75
+ const before = lstatSync(file, { bigint: true, throwIfNoEntry: false });
76
+ if (!before) { if (!optional) fail(); facts.push([file, null]); sourceChecks.push(() => { if(lstatSync(file,{throwIfNoEntry:false})) fail(); }); return undefined; }
77
+ if (++metadataObjects > 256 || before.size > 262144n || metadataBytes + Number(before.size) > 8 * 1024 * 1024) fail("bound");
78
+ if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1n || before.uid !== root.uid || (before.mode & 0o022n) !== 0n) fail();
79
+ const fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
80
+ try {
81
+ const opened = fstatSync(fd, { bigint: true }); if (!same(before, opened)) fail();
82
+ const out = Buffer.alloc(Number(opened.size)); let at = 0;
83
+ while (at < out.length) { const count = readSync(fd, out, at, out.length - at, at); if (!count) fail(); at += count; }
84
+ const after = lstatSync(file, { bigint: true }); if (!same(opened, fstatSync(fd, { bigint: true })) || !same(opened, after)) fail();
85
+ metadataBytes += out.length; facts.push([file, stamp(opened), digest(out)]);
86
+ sourceChecks.push(() => { const now=lstatSync(file,{bigint:true}); if(!now.isFile()||now.isSymbolicLink()||now.nlink!==1n||!same(opened,now)) fail(); }); return out;
87
+ } finally { closeSync(fd); }
88
+ };
89
+ const json = (file: string, optional = false): any => {
90
+ const input = bytes(file, optional); if (!input) return undefined;
91
+ return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(input));
92
+ };
93
+ const rawProfile = json(join(home, "profile.json")), rawShards = json(join(home, "shards.json"));
94
+ let activeHead: string | undefined;
95
+ let base: ReturnType<typeof validatePlacementBaseV2>;
96
+ if (rawProfile?.schema === PLACEMENT_PROFILE_SCHEMA) {
97
+ const profile = validatePlacementProfile(rawProfile), shards = validatePlacementShards(rawShards, profile);
98
+ activeHead = profile.placementHead;
99
+ const { publicationFormat: _pf, placementHead: _ph, ...p } = profile;
100
+ const profile2 = { ...p, schema: "agent-mem.profile/v2" };
101
+ const { publicationFormat: _sf, placementHead: _sh, ...s } = shards;
102
+ base = validatePlacementBaseV2(profile2, { ...s, schema: "agent-mem.shards/v2", selectorRevision: hashJSON(profile2) });
103
+ } else base = validatePlacementBaseV2(rawProfile, rawShards);
104
+ const markerHash = hashJSON({ profile: rawProfile, shards: rawShards });
105
+ bytes(join(home, "public-prune-input-sets", `${base.profile.pruneInputSetHash}.json`));
106
+ readPublicPruneViewForActivation(home, base.profile.pruneInputSetHash, base.profile.generation);
107
+ const aliases = bytes(join(home, "aliases.jsonl"), true);
108
+ bytes(join(home, "amem.config.json"), true);
109
+ const machine = json(join(home, "machine.json"));
110
+ if (machine?.id !== base.shards.machine.id) fail();
111
+ // An in-flight migration/prune is never accepted as a stable placement source.
112
+ for (const name of ["profile.lock", PUBLIC_PRUNE_INTENT]) if (lstatSync(join(home, name), { throwIfNoEntry: false })) fail();
113
+ const reader = new SegmentReader(16 * 1024 * 1024, { maxSourceBytes: 8 * 1024 * 1024, maxRows: 20000 });
114
+ const rows = new Map<string, PlacementSourceRow>(), specialRows = new Map<string, PlacementSpecialSourceRow>(), sourceRowsByRealm = new Map<string, readonly PruneSourceRow[]>();
115
+ const directories = [home], prefixes: PlacementPublicationSource[] = [];
116
+ const realms: SyncPlacementActivationContext["realms"][number][] = [];
117
+ const listing = (dir: string, optional = false): string[] => {
118
+ const s = lstatSync(dir, { throwIfNoEntry: false });
119
+ if (!s) { if (!optional) fail(); facts.push([dir, null]); sourceChecks.push(() => {if(lstatSync(dir,{throwIfNoEntry:false}))fail();}); return []; }
120
+ directoryFact(dir); const listStamp = lstatSync(dir,{bigint:true});
121
+ sourceChecks.push(() => { if(!same(listStamp,lstatSync(dir,{bigint:true}))) fail(); });
122
+ const iterator = opendirSync(dir), names: string[] = [];
123
+ try { for (;;) { const item = iterator.readSync(); if (!item) break; if (names.length >= 256) fail("bound"); names.push(item.name); } }
124
+ finally { iterator.closeSync(); }
125
+ names.sort(); facts.push([dir, names]); return names;
126
+ };
127
+ const realm = (dir: string, identity: PlacementKey["realm"], project?: string) => {
128
+ directories.push(dir);
129
+ const beforeFacts = facts.length; directoryFact(dir);
130
+ if (project !== undefined) {
131
+ const descriptor = json(join(dir, "project.json"));
132
+ if (descriptor?.schema !== SHARD_SCHEMA || descriptor.key !== project || typeof descriptor.local !== "boolean") fail();
133
+ }
134
+ const descriptor = json(join(dir, "generation.json")), sources = json(join(dir, "migration-sources.json")), seal = json(join(dir, "generation-seal.json"));
135
+ if (canonicalJSON(descriptor) !== canonicalJSON(generationDescriptor(base.profile.generation)) || !Array.isArray(sources?.sources)
136
+ || sources.schema !== "agent-mem.migration-sources/v1" || seal?.schema !== "agent-mem.generation-seal/v1"
137
+ || seal.generation !== base.profile.generation || seal.descriptorHash !== hashJSON(descriptor)
138
+ || seal.sourceDigest !== hashJSON({ sources: sources.sources, legacyCommit: sources.legacyCommit })
139
+ || json(join(dir, "machine.json"))?.id !== machine.id) fail();
140
+ const hasMain = !!lstatSync(join(dir, "legacy-main"), { throwIfNoEntry: false });
141
+ const profile: BoundProfile = { kind: "generation", home, dir, generation: base.profile.generation, selectorRevision: hashJSON(base.profile),
142
+ sharded: true, legacyHome: join(dir, "legacy"), ...(hasMain ? { legacyMainHome: join(dir, "legacy-main") } : {}) };
143
+ for (const root of [profile.legacyHome!, ...(hasMain ? [profile.legacyMainHome!] : [])]) {
144
+ directoryFact(root); bytes(join(root, "machine.json"), true);
145
+ for(const folder of ["log","local"]) {
146
+ const file=join(root,folder), s=lstatSync(file,{bigint:true,throwIfNoEntry:false});
147
+ sourceChecks.push(() => { const now=lstatSync(file,{bigint:true,throwIfNoEntry:false}); if(s ? !now||!same(s,now) : now!==undefined) fail(); });
148
+ }
149
+ }
150
+ const segments = readShardLegacySnapshot(profile, { reader });
151
+ for (const folder of ["log", "local"]) {
152
+ directoryFact(join(dir, folder));
153
+ const folderPath=join(dir,folder), folderStamp=lstatSync(folderPath,{bigint:true});
154
+ sourceChecks.push(() => { if(!same(folderStamp,lstatSync(folderPath,{bigint:true}))) fail(); });
155
+ if(folder === "local") bytes(join(dir,folder,`publisher-boundary-${machine.id}.json`),true);
156
+ for (const name of reader.listFiles(join(dir, folder))) {
157
+ if (!name.endsWith(".jsonl")) continue;
158
+ if ( !/^[A-Za-z0-9][A-Za-z0-9._-]*\.jsonl$/.test(name)) fail();
159
+ segments.push({ ...reader.read(join(dir, folder, name), undefined, true), source: "generation", path: `${folder}/${name}`, local: folder === "local" });
160
+ }
161
+ }
162
+ const presence = new Map<string, PruneSourceRow>();
163
+ const special = new Map<string, PlacementSpecialSourceRow>();
164
+ const retainSpecial = (row: PruneRow | VaultLinkRow, local: boolean) => {
165
+ const key = { realm: identity, id: row.id, rowHash: hashJSON(row) }, prior = special.get(row.id);
166
+ if (prior && (prior.key.rowHash !== key.rowHash || prior.sourcePlacement !== (local ? "local" : "synced"))) fail();
167
+ special.set(row.id, { key, row, sourcePlacement: local ? "local" : "synced", intrinsicLocal: row.op === "vault_link" });
168
+ };
169
+ for (const segment of segments) {
170
+ const file = join(segment.source === "generation" ? dir : segment.source === "legacy-main" ? profile.legacyMainHome! : profile.legacyHome!, segment.path);
171
+ const fileStat = lstatSync(file);
172
+ if (!fileStat.isFile() || fileStat.isSymbolicLink() || fileStat.nlink !== 1 || fileStat.uid !== process.getuid?.() || (fileStat.mode & 0o022) !== 0) fail();
173
+ let sourceHash = segment.hash, sourceLength = segment.offset;
174
+ if (segment.tailBytes) {
175
+ const proof = pendingByPath.get(canonicalJSON(identity)+":"+digest(segment.path));
176
+ if (segment.source !== "generation" || !proof || proof.offset !== segment.offset || proof.prefixHash !== segment.hash
177
+ || segment.tailBytes >= Buffer.byteLength(proof.line) || segment.tailBytes > 16384) fail();
178
+ const fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
179
+ try {
180
+ const opened = fstatSync(fd, { bigint: true });
181
+ if (canonicalJSON(stamp(opened)) !== segment.stamp) fail();
182
+ const tail = Buffer.alloc(segment.tailBytes); let at = 0;
183
+ while (at < tail.length) { const n = readSync(fd, tail, at, tail.length-at, segment.offset+at); if (!n) fail(); at+=n; }
184
+ if (!tail.equals(Buffer.from(proof.line).subarray(0,tail.length)) || !same(opened,fstatSync(fd,{bigint:true})) || !same(opened,lstatSync(file,{bigint:true}))) fail();
185
+ sourceHash = createHash("sha256").update(segment.bytes).update(tail).digest("hex"); sourceLength += tail.length;
186
+ } finally { closeSync(fd); }
187
+ }
188
+ facts.push([dir, segment.source, segment.path, sourceLength, sourceHash, segment.stamp]);
189
+ const fileStamp=lstatSync(file,{bigint:true});
190
+ if(canonicalJSON(stamp(fileStamp))!==segment.stamp) fail();
191
+ sourceChecks.push(() => { const now=lstatSync(file,{bigint:true}); if(now.isSymbolicLink()||!now.isFile()||now.nlink!==1n||!same(fileStamp,now)) fail(); });
192
+ if (!segment.stamp || !["generation", "legacy-local", "legacy-main"].includes(segment.source)) fail();
193
+ const physical: unknown = JSON.parse(segment.stamp);
194
+ if (!Array.isArray(physical) || physical.length !== 5 || !physical.every(value => typeof value === "string" && /^[0-9]+$/.test(value))) fail();
195
+ prefixes.push({ realm: identity, source: segment.source as PlacementPublicationSource["source"], pathHash: digest(segment.path),
196
+ dev: physical[0] as string, ino: physical[1] as string, byteLength: sourceLength, hash: sourceHash,
197
+ mtimeNs: physical[3] as string, ctimeNs: physical[4] as string });
198
+ for (const line of completeLines(segment.bytes)) {
199
+ if (!line.length) continue;
200
+ const parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(line));
201
+ if (segment.source === "generation" && parsed?.op === "prune") {
202
+ const row = validatePruneRow(parsed, profile.generation, segment.local ? "local" : "synced");
203
+ if (segment.path !== `${segment.local ? "local" : "log"}/${row.machine}.jsonl`) fail();
204
+ retainSpecial(row, segment.local);
205
+ continue;
206
+ }
207
+ if (segment.source === "generation" && parsed?.op === "vault_link") {
208
+ const row = validateVaultLinkRow(parsed, profile.generation, segment.local ? "local" : "synced");
209
+ if (segment.path !== `local/${row.machine}.jsonl`) fail();
210
+ retainSpecial(row, true);
211
+ continue;
212
+ }
213
+ const entry = checkedEntry(parsed);
214
+ presence.set(entry.id, coalesceGenerationSource(presence.get(entry.id), entry, segment));
215
+ }
216
+ }
217
+ for (const source of special.values()) {
218
+ if (presence.has(source.key.id)) fail();
219
+ specialRows.set(placementKeyId(source.key), source);
220
+ }
221
+ sourceRowsByRealm.set(canonicalJSON(identity), [...presence.values()]);
222
+ for (const source of presence.values()) {
223
+ const key = { realm: identity, id: source.row.id, rowHash: hashJSON(source.row) };
224
+ rows.set(placementKeyId(key), { key, row: source.row, sourcePlacement: source.placement,
225
+ intrinsicLocal: source.row.op === "settle" || source.row.op === "note" && source.row.tier === "short" });
226
+ }
227
+ const bindingHash = hashJSON(facts.slice(beforeFacts));
228
+ realms.push({ ...identity, bindingHash });
229
+ };
230
+ if (!selection || selection.some(item => item.scope === "user")) realm(join(home, "user"), { scope: "user" });
231
+ for (const name of listing(join(home, "projects"), true)) {
232
+ if (!/^[a-f0-9]{16}$/.test(name)) fail();
233
+ if (selection && !selection.some(item => item.scope === "project" && item.keyHash.slice(0,16) === name)) continue;
234
+ const dir = join(home, "projects", name), project = json(join(dir, "project.json"));
235
+ if (typeof project?.key !== "string" || shardKeyHash(project.key) !== name) fail();
236
+ // Capture scaffolds are not curated realms, but their identity still binds this inventory.
237
+ if (!lstatSync(join(dir, "generation.json"), { throwIfNoEntry: false })) continue;
238
+ const keyHash = digest(project.key);
239
+ if (selection && !selection.some(item => item.scope === "project" && item.keyHash === keyHash)) fail();
240
+ realm(dir, { scope: "project", keyHash }, project.key);
241
+ }
242
+ if (selection && realms.length !== selection.length) fail();
243
+ const defaults = memoryPolicySnapshot({ names: DENY_NAMES, segments: DENY_SEGMENTS }); if (!defaults) fail();
244
+ const configuration = loadAmemConfig(home).config;
245
+ const context: SyncPlacementActivationContext = { ...base, homeHash: hashJSON([home, String(root.dev), String(root.ino), String(root.uid)]), sourceHash: hashJSON(facts),
246
+ aliasesHash: digest(aliases ?? Buffer.alloc(0)), policyHash: hashJSON({ config: configuration, defaults, policyVersion }), realms,
247
+ writers: attestation.writers, legacySyncQuiesced: attestation.legacySyncQuiesced };
248
+ // Reuse the closed stage-1 context validator, including writer/version/realm bounds.
249
+ const validated = planSyncPlacementActivation(context, "0".repeat(64));
250
+ context.writers = validated.writers; context.realms = validated.realms;
251
+ // ponytail: fast rechecks rely on held mutation fences plus native inode/size/ctime identity for the already-hashed prefixes.
252
+ // A hostile same-user raw writer is outside that contract; descriptor-bound immutable source blocks are the upgrade path.
253
+ const assertUnchanged = () => {
254
+ try {
255
+ // Host callbacks run first; the final filesystem proof never invokes caller code afterwards.
256
+ const currentAttestation=memoryPolicySnapshot(port.attest()), currentVersion=port.policyVersion?.();
257
+ if(!currentAttestation) fail();
258
+ const checked=planSyncPlacementActivation({...context,writers:currentAttestation.writers,legacySyncQuiesced:currentAttestation.legacySyncQuiesced},"0".repeat(64));
259
+ const currentDefaults=memoryPolicySnapshot({names:DENY_NAMES,segments:DENY_SEGMENTS});
260
+ if(!currentDefaults)fail();
261
+ if(canonicalJSON(checked.writers)!==canonicalJSON(context.writers)||hashJSON({config:configuration,defaults:currentDefaults,policyVersion:currentVersion})!==context.policyHash) fail();
262
+ const now=ownedDirectory(home); if(now.dev!==root.dev||now.ino!==root.ino) fail();
263
+ for(const check of sourceChecks)check();
264
+ } catch { fail(); }
265
+ };
266
+ return { home, context, assertUnchanged, ...(activeHead ? { activeHead } : {}), markerHash, fingerprint: hashJSON({ context, markerHash }), directories: directories.sort(), prefixes, rows, specialRows, sourceRowsByRealm };
267
+ }
268
+ export function captureSyncPlacementContext(home: string, port: PlacementCapturePort, selected?: readonly PlacementRealm[], pendingInput: readonly PlacementPendingSource[] = []): PlacementCapture {
269
+ try {
270
+ const selection = selected === undefined ? undefined : memoryPolicySnapshot(selected);
271
+ if (selected !== undefined && !selection) fail();
272
+ if (!Array.isArray(pendingInput) || types.isProxy(pendingInput) || pendingInput.length > 20000) fail("bound");
273
+ const pending = pendingInput.map(item => {
274
+ if (!item || typeof item !== "object" || types.isProxy(item)) fail();
275
+ const fields = Object.getOwnPropertyDescriptors(item);
276
+ if (Object.keys(fields).sort().join() !== "line,offset,pathHash,prefixHash,realm" || Object.values(fields).some(field => !("value" in field) || !field.enumerable)) fail();
277
+ const realm = validatePlacementKey({realm:fields.realm!.value,id:"0".repeat(32),rowHash:"0".repeat(64)}).realm;
278
+ const line: unknown = fields.line!.value, offset: unknown = fields.offset!.value, pathHash: unknown = fields.pathHash!.value, prefixHash: unknown = fields.prefixHash!.value;
279
+ if (typeof line !== "string" || Buffer.byteLength(line)>16384 || !line.endsWith("\n") || typeof offset!=="number" || !Number.isSafeInteger(offset) || offset<0
280
+ || typeof pathHash!=="string" || pathHash.length!==64 || !/^[a-f0-9]+$/.test(pathHash) || typeof prefixHash!=="string" || prefixHash.length!==64 || !/^[a-f0-9]+$/.test(prefixHash)) fail();
281
+ return {realm,line,offset,pathHash,prefixHash};
282
+ });
283
+ const before = capture(home, port, selection, pending);
284
+ return { ...before, assertCurrent: () => { try { if (capture(home, port, selection, pending).fingerprint !== before.fingerprint) fail(); } catch { fail(); } } };
285
+ } catch (error) {
286
+ if (error && typeof error === "object" && !types.isProxy(error)) {
287
+ if (error instanceof SyncPlacementJournalError) throw error;
288
+ if (error instanceof SegmentReadBudgetError) return fail("bound");
289
+ }
290
+ return fail();
291
+ }
292
+ }