@staix/agent-mem 0.13.0 → 0.14.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.
@@ -1,14 +1,20 @@
1
+ import { assertPublisherBoundary, ensurePublisherBoundary, publisherPruneLineages } from "./prune-publisher-lineage.ts";
2
+ import { verifyPublicPruneWitness, type PublicPruneWitnessV1 } from "./public-prune-witness.ts";
3
+ import { readPublicPruneView, type PublicPruneViewV1 } from "./public-prune-view.ts";
4
+ import { advancePublicPruneView } from "./public-prune-advance.ts";
5
+ import { collectPublicPruneGitGraph } from "./shard-sync-graph.ts";
6
+ import { PublicPruneInputError, PUBLIC_PRUNE_REPLAY_MAX_BYTES, PUBLIC_PRUNE_MAX_SETS, preparePublicPruneInputBatch, persistPublicPruneInputBatch, readPublicPruneInputDescriptor, reducePublicPruneInputs, type PublicPruneInputBatch, type PublicPruneInputV1 } from "./public-prune-inputs.ts";
1
7
  import { randomBytes, createHash } from "node:crypto";
2
- import { constants, closeSync, existsSync, fstatSync, fsyncSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, readSync, renameSync, rmSync, writeSync } from "node:fs";
8
+ import { constants, closeSync, existsSync, fstatSync, fsyncSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, readSync, renameSync, rmSync, unlinkSync, writeSync } from "node:fs";
3
9
  import { join } from "node:path";
4
- import { canonicalJSON, fsyncDirectory, hashJSON, readProfileJSON } from "./generation.ts";
10
+ import { canonicalJSON, fsyncDirectory, hashJSON, readProfileJSON, readSelector, rebindPublicPruneProfile } from "./generation.ts";
5
11
  import { generationSyncLock } from "./generation-sync.ts";
6
- import { acquireMutation } from "./mutation.ts";
12
+ import { acquireMutation, assertMutationHandle, type MutationHandle } from "./mutation.ts";
7
13
  import { realPath } from "./platform.ts";
8
14
  import { reducePruneRows, validatePrunePlan, validatePruneRow, type PrunePlan, type PruneRow, type PruneSnapshot, type PruneSourceRow } from "./prune.ts";
9
15
  import { assertCuratedShardPaths, isLocalKey, resolveShardProfile } from "./shard.ts";
10
- import { decodeShardPack, encodeShardPacks, SHARD_PACK_MAX_BYTES, ShardSyncFormatError, shardSyncDescriptor, shardSyncLabel, shardSyncRef, type ShardSyncRealm, type ShardSyncRow } from "./shard-sync-format.ts";
11
- import { generationSyncGit, remoteVisibility, type SyncOptions, type SyncResult } from "./sync.ts";
16
+ import { decodeShardPack, encodeShardPacks, decodeShardDescriptor, decodeShardManifest, encodeShardManifest, isShardManifestPath, SHARD_PACK_MAX_BYTES, ShardSyncFormatError, shardSyncDescriptor, shardSyncLabel, shardSyncRef, type ShardSyncRealm, type ShardSyncRow } from "./shard-sync-format.ts";
17
+ import { generationSyncGit, generationSyncGitBytes, remoteVisibility, shardSyncBinding, shardSyncSourceBinding, type SyncOptions, type SyncResult } from "./sync.ts";
12
18
  import type { Entry, MemoryStore } from "./store.ts";
13
19
 
14
20
  const OID = /^[a-f0-9]{40}$/, PACK = /^rows\/[a-f0-9]{64}\/[a-f0-9]{64}\.jsonl$/;
@@ -16,12 +22,14 @@ const digest = (bytes: string | Uint8Array): string => createHash("sha256").upda
16
22
  const zero = "0".repeat(40);
17
23
  // The descriptor is at most 64 KiB and an original prune at most 16 KiB; keep receipt parsing bounded.
18
24
  const HOLD_MAX_BYTES = 128 * 1024;
19
- type PendingPrune = { plan: PrunePlan; row: PruneRow; line: string; offset: number; prefixHash: string };
25
+ export const PENDING_SELECTOR_LIMIT = 64;
26
+ type PendingPrune = { plan: PrunePlan; row: PruneRow; line: string; offset: number; prefixHash: string; selectorCheck?: () => void; unclaimed?: true };
20
27
  type HoldSource = { ref: string; commit: string; pack: string; blob: string };
21
28
  type HoldReceipt = { schema: "agent-mem.prune-hold/v1"; remote: string; realm: ShardSyncRealm; receivingGeneration: string; row: PruneRow; payloadHash: string; source: HoldSource };
22
29
  type Hold = { file: string; bytes: Buffer };
23
- type SyncPhase = "published" | "hold-written" | "hold-installed" | "imported";
24
- type Target = { store: MemoryStore; realm: ShardSyncRealm; rows: Map<string, ShardSyncRow>; fetched: Set<string>; sources: PruneSourceRow[]; pending: PendingPrune[]; origins: Map<string, HoldSource>; holds: Hold[]; generation: string; local?: string; remote?: string; commit?: string };
30
+ type SyncPhase = "published" | "hold-written" | "hold-installed" | "inputs-installed" | "view-committed" | "imported";
31
+ type SyncManifest = { path: string; bytes: Buffer; value: unknown; row: PruneRow; eligible: boolean; commit: string; blob: string };
32
+ type Target = { store: MemoryStore; realm: ShardSyncRealm; rows: Map<string, ShardSyncRow>; fetched: Set<string>; sources: PruneSourceRow[]; pending: PendingPrune[]; origins: Map<string, HoldSource>; holds: Hold[]; generation: string; manifests: Map<string,SyncManifest>; eligible: Set<string>; mutation?: MutationHandle; batch?: PublicPruneInputBatch; registered: PublicPruneInputV1[]; selectorChecks?: (()=>void)[]; local?: string; remote?: string; commit?: string };
25
33
  class SyncRefusal extends Error {}
26
34
  function refuse(reason: string): never { throw new SyncRefusal(reason); }
27
35
  function directory(path: string): void {
@@ -39,13 +47,41 @@ function mergeRows(into: Map<string, ShardSyncRow>, rows: readonly ShardSyncRow[
39
47
  into.set(row.id, row);
40
48
  }
41
49
  }
42
- function importPrunePlan(store: MemoryStore, realm: ShardSyncRealm, row: PruneRow): PrunePlan {
50
+ function importPrunePlan(store: MemoryStore, realm: ShardSyncRealm, row: PruneRow, selectorRevision = store.prunePort().selectorRevision()): PrunePlan {
43
51
  const body: Omit<PrunePlan, "seal"> = { schema: "agent-mem.prune-plan/v1", generation: store.profile.generation!,
44
- selectorRevision: store.prunePort().selectorRevision(), project: realm.scope === "project" ? realm.key : "user",
52
+ selectorRevision, project: realm.scope === "project" ? realm.key : "user",
45
53
  policyRevision: hashJSON({ schema: "agent-mem.shard-sync/v1", realm }), rows: [{ placement: "synced", payload: row }],
46
54
  counts: { eligible: row.targets.length, selected: row.targets.length, local: 0, synced: row.targets.length }, hasMore: false };
47
55
  return validatePrunePlan({ ...body, seal: hashJSON(body) });
48
56
  }
57
+ /** A retained selector is owned recovery evidence, not authorization or input membership. */
58
+ function selectorEvidence(folder: string, file: string, prefix?: Buffer | true): { value: unknown; bytes: Buffer; check: () => void } {
59
+ privateHoldPath(folder,true); privateHoldPath(file,false);
60
+ const parent=lstatSync(folder), before=lstatSync(file);
61
+ if(parent.uid!==process.getuid?.()||before.uid!==parent.uid||before.size<(prefix?0:1)||before.size>4096)refuse("prior selector unavailable");
62
+ const fd=openSync(file,constants.O_RDONLY|constants.O_NOFOLLOW|constants.O_NONBLOCK);
63
+ try {
64
+ const opened=fstatSync(fd), same=(stat:typeof opened)=>stat.dev===before.dev&&stat.ino===before.ino&&stat.size===before.size&&stat.mtimeMs===before.mtimeMs&&stat.ctimeMs===before.ctimeMs&&stat.uid===before.uid&&stat.nlink===1&&(stat.mode&0o077)===0&&stat.isFile();
65
+ if(!same(opened))refuse("prior selector changed");
66
+ const bytes=Buffer.alloc(before.size+1);let count=0;
67
+ while(count<bytes.length){const n=readSync(fd,bytes,count,bytes.length-count,count);if(!n)break;count+=n;}
68
+ const check=()=>{privateHoldPath(folder,true);privateHoldPath(file,false);const current=lstatSync(folder);if(current.dev!==parent.dev||current.ino!==parent.ino||current.uid!==parent.uid||!same(lstatSync(file)))refuse("prior selector changed");};
69
+ if(count!==before.size||!same(fstatSync(fd)))refuse("prior selector changed");check();
70
+ const data=bytes.subarray(0,count);
71
+ if(prefix){if(prefix!==true&&(data.length>prefix.length||!data.equals(prefix.subarray(0,data.length))))refuse("prior selector staging changed");return{value:undefined,bytes:data,check};}
72
+ const text=new TextDecoder("utf-8",{fatal:true,ignoreBOM:true}).decode(data),value:unknown=JSON.parse(text);
73
+ if(canonicalJSON(value)+"\n"!==text)refuse("prior selector unavailable");return{value,bytes:data,check};
74
+ } finally {closeSync(fd);}
75
+ }
76
+ function pendingSelector(store:MemoryStore,plan:PrunePlan,required=false):(()=>void)|undefined {
77
+ if(!required&&plan.selectorRevision===store.profile.selectorRevision)return undefined;
78
+ if(!store.profile.foreignPruneFormat)refuse("pending selector unavailable");
79
+ const history=join(store.dir,"prune-plans","selector-history"),file=join(history,plan.seal+".json"),evidence=selectorEvidence(history,file),prior=evidence.value as Record<string,unknown>|undefined;
80
+ if(!prior||Object.keys(prior).length!==5||prior.schema!=="agent-mem.profile/v2"||prior.kind!=="generation"||prior.generation!==store.profile.generation
81
+ ||prior.foreignPruneFormat!==store.profile.foreignPruneFormat||typeof prior.pruneInputSetHash!=="string"||!/^[a-f0-9]{64}$/.test(prior.pruneInputSetHash)||hashJSON(prior)!==plan.selectorRevision)refuse("pending selector binding unavailable");
82
+ const bound={...store.profile,selectorRevision:plan.selectorRevision,pruneInputSetHash:prior.pruneInputSetHash};
83
+ const check=()=>{evidence.check();const probe={...bound};rebindPublicPruneProfile(probe);if(probe.selectorRevision!==store.profile.selectorRevision)refuse("pending selector ancestry unavailable");};check();return check;
84
+ }
49
85
  /** Recognize only the exact persisted transport shape; this reads metadata and never resumes an append. */
50
86
  function pendingPrunes(store: MemoryStore, realm: ShardSyncRealm): PendingPrune[] {
51
87
  const dir = join(store.dir, "prune-plans"), pending: PendingPrune[] = [];
@@ -65,13 +101,107 @@ function pendingPrunes(store: MemoryStore, realm: ShardSyncRealm): PendingPrune[
65
101
  const plan = validatePrunePlan(readProfileJSON(planFile));
66
102
  if (plan.rows.length !== 1) throw new Error("plan shape");
67
103
  const row = plan.rows[0]!.payload, line = canonicalJSON(row) + "\n";
68
- if (plan.seal !== claim.seal || canonicalJSON(plan) !== canonicalJSON(importPrunePlan(store, realm, row))
104
+ const selectorCheck=pendingSelector(store,plan);
105
+ if (plan.seal !== claim.seal || canonicalJSON(plan) !== canonicalJSON(importPrunePlan(store, realm, row, plan.selectorRevision))
69
106
  || name !== `pending-log-${row.machine}.json` || claim.rowId !== row.id || claim.lineHash !== digest(line)) throw new Error("plan binding");
70
107
  regular(join(store.dir, "log", `${row.machine}.jsonl`));
71
- pending.push({ plan, row, line, offset: claim.offset, prefixHash: claim.prefixHash });
108
+ pending.push({ plan, row, line, offset: claim.offset, prefixHash: claim.prefixHash, ...(selectorCheck?{selectorCheck}:{}) });
109
+ }
110
+ const history=join(dir,"selector-history");
111
+ if(store.profile.foreignPruneFormat&&existsSync(history)){
112
+ privateHoldPath(history,true);const seals=new Set<string>();
113
+ for(const name of readdirSync(history)){const match=/^([a-f0-9]{64})\.json(\.pending)?$/.exec(name);if(!match||match[0]!==name)throw new Error("selector copy name");seals.add(match[1]!);if(seals.size>PENDING_SELECTOR_LIMIT)throw new PublicPruneInputError("resource");}
114
+ const claimed=new Set(pending.map(item=>item.plan.seal)),durable=new Set(store.prunePort().snapshot().durablePruneIds);
115
+ for(const seal of seals){
116
+ if(claimed.has(seal)||!existsSync(join(history,seal+".json")))continue;
117
+ const plan=validatePrunePlan(readProfileJSON(join(dir,seal+".json"))),row=plan.rows[0]?.payload;
118
+ if(plan.seal!==seal||plan.rows.length!==1||!row||canonicalJSON(plan)!==canonicalJSON(importPrunePlan(store,realm,row,plan.selectorRevision)))throw new Error("selector plan binding");
119
+ if(durable.has(row.id))continue;
120
+ // A durable selector copy also journals the interval before append creates its native claim.
121
+ const selectorCheck=pendingSelector(store,plan,true);
122
+ pending.push({plan,row,line:canonicalJSON(row)+"\n",offset:0,prefixHash:digest(""),unclaimed:true,selectorCheck});
123
+ }
72
124
  }
73
125
  return pending;
74
- } catch { return refuse(`${shardSyncLabel(realm)}: pending prune requires its original plan to resume`); }
126
+ } catch(error) { if(error instanceof PublicPruneInputError)throw error;return refuse(`${shardSyncLabel(realm)}: pending prune requires its original plan to resume`); }
127
+ }
128
+ /** Private copies are addressed by their exact pending transport plan, never used as current selectors. */
129
+ function selectorRetention(target: Target, advancing: boolean, install = false, reserveNative = false): void {
130
+ assertMutationHandle(target.mutation!,target.store.dir);
131
+ if(!target.store.profile.foreignPruneFormat)return;
132
+ const plans=join(target.store.dir,"prune-plans"),folder=join(plans,"selector-history"),exists=lstatSync(folder,{throwIfNoEntry:false});
133
+ const records=new Map<string,{file?:string;stage?:string}>();
134
+ if(exists){
135
+ privateHoldPath(folder,true);if(exists.uid!==process.getuid?.())refuse("selector recovery owner changed");
136
+ for(const name of readdirSync(folder)){
137
+ const match=/^([a-f0-9]{64})\.json(\.pending)?$/.exec(name);if(!match||match[0]!==name)refuse("selector recovery artifact unavailable");
138
+ const entry=records.get(match[1]!)??{};entry[match[2]?"stage":"file"]=join(folder,name);records.set(match[1]!,entry);
139
+ if(records.size>PENDING_SELECTOR_LIMIT)throw new PublicPruneInputError("resource");
140
+ }
141
+ }
142
+ const pending=new Map(target.pending.map(item=>[item.plan.seal,item])),keep=new Map<string,Buffer>(),remove:string[]=[],checks:(()=>void)[]=[];
143
+ let durable:Set<string>|undefined;
144
+ const validate=(plan:PrunePlan,value:unknown)=>{
145
+ const row=plan.rows[0]?.payload,prior=value as Record<string,unknown>|undefined;
146
+ if(plan.rows.length!==1||!row||plan.generation!==target.generation||row.generation!==target.generation
147
+ ||canonicalJSON(plan)!==canonicalJSON(importPrunePlan(target.store,target.realm,row,plan.selectorRevision))
148
+ ||!prior||Object.keys(prior).length!==5||prior.schema!=="agent-mem.profile/v2"||prior.kind!=="generation"
149
+ ||prior.generation!==target.generation||prior.foreignPruneFormat!==target.store.profile.foreignPruneFormat
150
+ ||typeof prior.pruneInputSetHash!=="string"||!/^[a-f0-9]{64}$/.test(prior.pruneInputSetHash)||hashJSON(prior)!==plan.selectorRevision)refuse("selector recovery plan binding unavailable");
151
+ };
152
+ for(const [seal,record] of records){
153
+ const active=pending.get(seal),plan=active?.plan??validatePrunePlan(readProfileJSON(join(plans,seal+".json")));
154
+ if(plan.seal!==seal)refuse("selector recovery plan identity changed");
155
+ if(!record.file){
156
+ if(active){if(plan.selectorRevision!==target.store.profile.selectorRevision)refuse("selector recovery evidence missing");continue;}
157
+ const row=plan.rows[0]?.payload,claim=row?readProfileJSON(join(plans,`pending-log-${row.machine}.json`)) as {seal?:unknown}|undefined:undefined;
158
+ if(!row||plan.rows.length!==1||claim?.seal===seal||canonicalJSON(plan)!==canonicalJSON(importPrunePlan(target.store,target.realm,row,plan.selectorRevision)))refuse("selector staging binding unavailable");
159
+ // An incomplete temp precedes both registration and the native claim; it grants no replay authority.
160
+ checks.push(selectorEvidence(folder,record.stage!,true).check);remove.push(record.stage!);continue;
161
+ }
162
+ const evidence=selectorEvidence(folder,record.file);validate(plan,evidence.value);checks.push(evidence.check);
163
+ if(record.stage)checks.push(selectorEvidence(folder,record.stage,evidence.bytes).check);
164
+ if(active){keep.set(seal,evidence.bytes);continue;}
165
+ // Claim disappearance alone is not completion. Prove the original plan's exact native row is durable.
166
+ durable??=new Set(target.store.prunePort().snapshot().durablePruneIds);
167
+ const row=plan.rows[0]!.payload,claim=readProfileJSON(join(plans,`pending-log-${row.machine}.json`)) as {seal?:unknown}|undefined;
168
+ if(!durable.has(row.id)||canonicalJSON(target.rows.get(row.id))!==canonicalJSON(row)||claim?.seal===seal)refuse("selector recovery completion unavailable");
169
+ remove.push(record.file,...(record.stage?[record.stage]:[]));
170
+ }
171
+ for(const item of target.pending){
172
+ const seal=item.plan.seal;if(keep.has(seal)||!advancing&&!records.has(seal))continue;
173
+ if(item.plan.selectorRevision!==target.store.profile.selectorRevision)refuse("prior pending selector unavailable");
174
+ const selector=readSelector(target.store.profile.home).selector;validate(item.plan,selector);
175
+ const bytes=Buffer.from(canonicalJSON(selector)+"\n"),stage=records.get(seal)?.stage;
176
+ if(stage)checks.push(selectorEvidence(folder,stage,bytes).check);keep.set(seal,bytes);
177
+ }
178
+ if(Math.max(keep.size,reserveNative?1:0)>PENDING_SELECTOR_LIMIT)throw new PublicPruneInputError("resource");
179
+ const inventory=()=>{
180
+ const before=lstatSync(folder,{throwIfNoEntry:false}),names=before?readdirSync(folder).sort().join("\n"):undefined;
181
+ return()=>{const now=lstatSync(folder,{throwIfNoEntry:false});if(!before){if(now)refuse("selector recovery inventory changed");return;}
182
+ privateHoldPath(folder,true);if(!now||now.dev!==before.dev||now.ino!==before.ino||now.uid!==before.uid||readdirSync(folder).sort().join("\n")!==names)refuse("selector recovery inventory changed");};
183
+ };
184
+ for(const check of checks)check();assertMutationHandle(target.mutation!,target.store.dir);
185
+ if(!install){target.selectorChecks=[...checks,inventory()];return;}
186
+ for(const file of remove){unlinkSync(file);fsyncDirectory(folder);}
187
+ if(keep.size&&!exists){mkdirSync(folder,{mode:0o700});fsyncDirectory(plans);}
188
+ const retained:(()=>void)[]=[];
189
+ for(const [seal,bytes] of keep){
190
+ const file=join(folder,seal+".json"),stage=file+".pending";
191
+ if(!lstatSync(file,{throwIfNoEntry:false})){
192
+ const staged=lstatSync(stage,{throwIfNoEntry:false});if(staged)selectorEvidence(folder,stage,bytes).check();
193
+ const fd=openSync(stage,constants.O_RDWR|constants.O_CREAT|constants.O_NOFOLLOW|constants.O_NONBLOCK,0o600);
194
+ try{
195
+ const stat=fstatSync(fd);if(!stat.isFile()||stat.nlink!==1||stat.uid!==process.getuid?.()||(stat.mode&0o077)!==0||stat.size>bytes.length)refuse("selector recovery staging unavailable");
196
+ const prefix=readFileSync(fd);if(!prefix.equals(bytes.subarray(0,prefix.length)))refuse("selector recovery staging changed");
197
+ let offset=prefix.length;while(offset<bytes.length){const count=writeSync(fd,bytes,offset,bytes.length-offset,offset);if(count<1)refuse("selector recovery write unavailable");offset+=count;}fsyncSync(fd);
198
+ const named=lstatSync(stage);if(named.dev!==stat.dev||named.ino!==stat.ino||named.nlink!==1||named.size!==bytes.length)refuse("selector recovery staging changed");
199
+ }finally{closeSync(fd);}
200
+ if(lstatSync(file,{throwIfNoEntry:false}))refuse("selector recovery copy changed");renameSync(stage,file);fsyncDirectory(folder);
201
+ }else if(lstatSync(stage,{throwIfNoEntry:false})){selectorEvidence(folder,stage,bytes).check();unlinkSync(stage);fsyncDirectory(folder);}
202
+ const evidence=selectorEvidence(folder,file);if(!evidence.bytes.equals(bytes))refuse("selector recovery copy changed");retained.push(evidence.check);
203
+ }
204
+ target.selectorChecks=[...retained,inventory()];assertMutationHandle(target.mutation!,target.store.dir);
75
205
  }
76
206
  /** Stream complete authored lines without a permanent file-size/history ceiling. */
77
207
  function* lines(file: string, claim?: PendingPrune): Generator<string> {
@@ -107,7 +237,7 @@ function* lines(file: string, claim?: PendingPrune): Generator<string> {
107
237
  } finally { closeSync(fd); }
108
238
  }
109
239
  function sourceRows(store: MemoryStore, realm: ShardSyncRealm): { rows: ShardSyncRow[]; sources: PruneSourceRow[]; pending: PendingPrune[] } {
110
- const pending = pendingPrunes(store, realm), claims = new Map(pending.map(claim => [`${claim.row.machine}.jsonl`, claim]));
240
+ const pending = pendingPrunes(store, realm), claims = new Map(pending.filter(claim=>!claim.unclaimed).map(claim => [`${claim.row.machine}.jsonl`, claim]));
111
241
  const snapshot = store.prunePort().snapshot();
112
242
  const pruned = new Set(snapshot.pruned ?? []);
113
243
  // Placement decides transport; the verified snapshot retains separate prune provenance.
@@ -181,15 +311,26 @@ function importRows(target: Target): void {
181
311
  port.refresh();
182
312
  const resumed = new Set<string>();
183
313
  // The caller has proved all selected refs and atomically published them. Resume originals before other rows of their author.
184
- for (const pending of target.pending) {
185
- port.persistPrunePlan(pending.plan); port.appendPrune(pending.row, pending.line, pending.plan.seal); resumed.add(pending.row.id);
314
+ for (const pending of [...target.pending]) {
315
+ port.persistPrunePlan(pending.plan);
316
+ if(target.store.profile.foreignPruneFormat)selectorRetention(target,true,true);
317
+ port.appendPrune(pending.row,pending.line,pending.plan.seal);resumed.add(pending.row.id);
318
+ target.pending=target.pending.filter(item=>item.plan.seal!==pending.plan.seal);target.selectorChecks=[];
319
+ if(target.store.profile.foreignPruneFormat)selectorRetention(target,false,true);
186
320
  }
187
321
  for (const row of target.rows.values()) if (row.op === "prune" && row.generation === target.generation) {
188
322
  if (resumed.has(row.id)) continue;
189
323
  // A transport imports an already-authored person row without changing its author or generation.
190
324
  // Native durable-plan append confirms fsync even for an already parsed id after a crash.
191
- const plan = importPrunePlan(target.store, target.realm, row);
192
- port.persistPrunePlan(plan); port.appendPrune(row, canonicalJSON(row) + "\n", plan.seal);
325
+ const plan = importPrunePlan(target.store,target.realm,row),line=canonicalJSON(row)+"\n";
326
+ port.persistPrunePlan(plan);
327
+ if(target.store.profile.foreignPruneFormat&&!before.durablePruneIds?.includes(row.id)){
328
+ target.pending.push({plan,row,line,offset:0,prefixHash:digest(""),unclaimed:true});
329
+ selectorRetention(target,true,true);
330
+ }
331
+ port.appendPrune(row,line,plan.seal);
332
+ target.pending=target.pending.filter(item=>item.plan.seal!==plan.seal);target.selectorChecks=[];
333
+ if(target.store.profile.foreignPruneFormat)selectorRetention(target,false,true);
193
334
  }
194
335
  port.refresh();
195
336
  }
@@ -237,7 +378,7 @@ function installHold(root: string, hold: Hold, phase?: (phase: SyncPhase) => voi
237
378
  function holdWarning(targets: readonly Target[], root: string): string | undefined {
238
379
  const warnings: string[] = [];
239
380
  for (const target of targets) if (target.holds.length) {
240
- const rows = [...target.rows.values()].filter((row): row is PruneRow => row.op === "prune" && row.generation !== target.generation);
381
+ const rows = [...target.rows.values()].filter((row): row is PruneRow => row.op === "prune" && row.generation !== target.generation && !target.eligible.has(row.id));
241
382
  const ids = [...new Set(rows.flatMap(row => row.targets.map(item => item.id)))].sort(), shown = ids.slice(0, 10);
242
383
  const location = target.holds[0]!.file.slice(root.length + 1).split("/").slice(0, -2).join("/");
243
384
  warnings.push(`${shardSyncLabel(target.realm)}: ${rows.length} foreign-generation prune(s) held, not applied; targets ${shown.join(",")}${ids.length > shown.length ? ` (+${ids.length - shown.length} more)` : ""} may remain visible; receipts shard-sync/${location}; bring this machine to the origin generation, or prune the entry here as the person`);
@@ -277,7 +418,8 @@ async function prepareHolds(targets: readonly Target[], root: string, url: strin
277
418
  for (const target of targets) {
278
419
  target.holds = [];
279
420
  for (const row of target.rows.values()) if (row.op === "prune" && row.generation !== target.generation) {
280
- target.holds.push(await prepareHold(root, url, target, row, proveSource));
421
+ const hold = await prepareHold(root, url, target, row, proveSource);
422
+ if (!target.eligible.has(row.id)) target.holds.push(hold);
281
423
  }
282
424
  }
283
425
  }
@@ -294,14 +436,24 @@ function mergePackOrigins(into: Map<string, HoldSource>, byRealm: Map<string, Ma
294
436
  export async function syncShardedMemory(store: MemoryStore, opts: SyncOptions = {}, testOnlyUnexpectedError?: (error: unknown) => void, testOnlyPhase?: (phase: SyncPhase) => void): Promise<SyncResult> {
295
437
  let pushed = false, releaseTransport: (() => void) | undefined, releaseSync: (() => void) | undefined;
296
438
  const releases: (() => void)[] = [], transient: string[] = [], temporary: string[] = [];
297
- const fail = (error: string): SyncResult => ({ ok: false, pushed, entries: 0, error });
298
- let root = "", cache = "", targets: Target[] = [];
439
+ const v2 = store.profile.foreignPruneFormat === "agent-mem.public-prune-witness/v1";
440
+ let expectedRevision = store.profile.selectorRevision;
441
+ let expectedBinding: string | undefined, expectedSources: string | undefined, expectedPolicy: string | undefined;
442
+ const application: NonNullable<SyncResult["pruneApplication"]> = { transport:"unconfirmed", proofComplete:0, projectionActive:null, nativeApplied:null, held:0, phase:"preflight" };
443
+ const fail = (error: string): SyncResult => ({ ok: false, pushed, entries: 0, error, ...(v2 ? {pruneApplication:{...application,transport:pushed?"durable":"unconfirmed"}} : {}) });
444
+ let root = "", cache = "", targets: Target[] = [], homeMutation:MutationHandle|undefined;
445
+ let boundView:PublicPruneViewV1|undefined;
299
446
  const check = () => {
300
447
  if (opts.signal?.aborted) refuse("stopped");
448
+ if (expectedBinding !== undefined && (shardSyncBinding(store) !== expectedBinding || shardSyncSourceBinding(store) !== expectedSources || opts.policyVersion?.() !== expectedPolicy)) refuse("shard sync binding or policy changed");
449
+ if(homeMutation)assertMutationHandle(homeMutation,store.profile.home);
301
450
  for (const target of targets) {
451
+ if (target.mutation) assertMutationHandle(target.mutation,target.store.dir);
452
+ for(const pending of target.pending)pending.selectorCheck?.();
453
+ for(const check of target.selectorChecks??[])check();
302
454
  const profile = target.store.profile;
303
455
  const current = resolveShardProfile(profile.home, target.realm.scope === "user" ? { user: true } : { project: target.realm.key });
304
- if (current.dir !== profile.dir || current.generation !== target.generation || current.selectorRevision !== profile.selectorRevision) refuse(`${shardSyncLabel(target.realm)}: selected profile changed`);
456
+ if (current.dir !== profile.dir || current.generation !== target.generation || current.selectorRevision !== expectedRevision) refuse(`${shardSyncLabel(target.realm)}: selected profile changed`);
305
457
  }
306
458
  };
307
459
  // Git's auto-maintenance may outlive fetch and remove maintenance.lock during the strict cache walk.
@@ -316,11 +468,12 @@ export async function syncShardedMemory(store: MemoryStore, opts: SyncOptions =
316
468
  try {
317
469
  if (opts.signal?.aborted) return fail("stopped");
318
470
  if (!store.profile.sharded || store.profile.kind !== "generation") return fail("shard sync requires a selected curated shard");
471
+ expectedBinding = shardSyncBinding(store); expectedSources = shardSyncSourceBinding(store); expectedPolicy = opts.policyVersion?.();
319
472
  const borrowed = store.openedSyncTargets();
320
473
  targets = borrowed.map(opened => {
321
474
  const profile = opened.profile;
322
475
  if (!profile.sharded || profile.kind !== "generation" || !profile.generation || !profile.shardUser && !profile.shardKey) refuse("shard sync target identity unavailable");
323
- return { store: opened, realm: profile.shardUser ? { scope: "user" as const } : { scope: "project" as const, key: profile.shardKey! }, rows: new Map<string, ShardSyncRow>(), fetched: new Set<string>(), sources: [], pending: [], origins: new Map<string, HoldSource>(), holds: [], generation: profile.generation };
476
+ return { store: opened, realm: profile.shardUser ? { scope: "user" as const } : { scope: "project" as const, key: profile.shardKey! }, rows: new Map<string, ShardSyncRow>(), fetched: new Set<string>(), sources: [], pending: [], origins: new Map<string, HoldSource>(), holds: [], generation: profile.generation, manifests:new Map(),eligible:new Set(),registered:[] };
324
477
  });
325
478
  if (!targets.length || targets.length > 2 || targets.some(target => target.store.profile.home !== store.profile.home)
326
479
  || new Set(targets.map(target => shardSyncRef(target.realm))).size !== targets.length
@@ -368,46 +521,66 @@ export async function syncShardedMemory(store: MemoryStore, opts: SyncOptions =
368
521
  const fetchURLs = (await required(["remote", "get-url", "--all", "origin"], "shard sync fetch destination unavailable")).split("\n");
369
522
  const pushURLs = (await required(["remote", "get-url", "--push", "--all", "origin"], "shard sync push destination unavailable")).split("\n");
370
523
  if (fetchURLs.length !== 1 || fetchURLs[0] !== url || pushURLs.length !== 1 || pushURLs[0] !== url) refuse("shard sync refuses differing or multiple remote destinations");
524
+ homeMutation = v2 ? acquireMutation(store.profile.home) : undefined;
525
+ if (v2 && !homeMutation) refuse("another home selection mutation is running");
526
+ if (homeMutation) releases.push(homeMutation);
371
527
  // Cooperating source writers stay excluded across remote reads and final imports. No nested withMutation calls.
372
528
  for (const target of [...targets].sort((a, b) => a.store.dir < b.store.dir ? -1 : 1)) {
373
- const release = acquireMutation(target.store.dir); if (!release) refuse(`${shardSyncLabel(target.realm)}: another source mutation is running`); releases.push(release);
529
+ const release = acquireMutation(target.store.dir); if (!release) refuse(`${shardSyncLabel(target.realm)}: another source mutation is running`); releases.push(release); target.mutation=release;
530
+ if (v2) ensurePublisherBoundary(target.store.profile,release);
374
531
  }
375
532
  const blobCache = new Map<string, Buffer>(), decoded = new Map<string, ShardSyncRow[]>();
376
533
  const blob = async (object: string, path: string): Promise<Buffer> => {
377
- const maximum = path === "shard.json" ? 64 * 1024 : SHARD_PACK_MAX_BYTES, prior = blobCache.get(object);
534
+ const maximum = path === "shard.json" || isShardManifestPath(path) ? 64 * 1024 : SHARD_PACK_MAX_BYTES, prior = blobCache.get(object);
378
535
  if (prior) { if (prior.length > maximum) refuse("shard sync blob exceeds its bound"); return prior; }
379
536
  const size = Number(await required(["cat-file", "-s", object], "shard sync blob size unavailable"));
380
537
  if (!Number.isSafeInteger(size) || size < 1 || size > maximum) refuse("shard sync blob exceeds its bound");
381
- const value = await required(["cat-file", "blob", object], "shard sync blob unavailable"), bytes = Buffer.from(value + "\n");
382
- if (bytes.length !== size) refuse("shard sync requires complete newline-terminated public blobs");
538
+ check(); const result = await generationSyncGitBytes(root,gitArgs(["cat-file","blob",object]),opts.signal,undefined,maximum); check();
539
+ if(result.code!==0)refuse("shard sync blob unavailable"); const bytes=Buffer.from(result.out);
540
+ if (bytes.length !== size || bytes.at(-1) !== 10) refuse("shard sync requires complete newline-terminated public blobs");
383
541
  const identity = createHash("sha1").update(`blob ${bytes.length}\0`).update(bytes).digest("hex");
384
542
  if (identity !== object) refuse("shard sync public blob identity mismatch");
385
543
  blobCache.set(object, bytes); return bytes;
386
544
  };
387
- type History = { rows: Map<string, ShardSyncRow>; files: Map<string, Buffer>; origins: Map<string, HoldSource> };
545
+ type History = { rows: Map<string, ShardSyncRow>; files: Map<string, Buffer>; origins: Map<string, HoldSource>; version:"v1"|"v2"; manifests:Map<string,SyncManifest> };
388
546
  // Keep only requested tips. Per-commit full row maps would make append-only history quadratic in memory.
389
547
  const histories = new Map<string, History>(), audited = new Set<string>();
390
548
  const firstOrigins = new Map<string, Map<string, HoldSource>>();
549
+ const manifestOrigins = new Map<string,{commit:string;blob:string}>();
391
550
  const treeState = async (commit: string, target: Target): Promise<History> => {
392
- const entries = (await required(["ls-tree", "-r", "-z", commit], "shard sync tree unavailable")).split("\0").filter(Boolean);
393
- const rows = new Map<string, ShardSyncRow>(), files = new Map<string, Buffer>(), origins = new Map<string, HoldSource>();
394
- for (const entry of entries) {
395
- const match = /^100644 blob ([a-f0-9]{40})\t(.+)$/.exec(entry);
396
- if (!match || match[2] !== "shard.json" && !PACK.test(match[2]!)) refuse(`${shardSyncLabel(target.realm)}: forbidden historical tree entry`);
397
- const path = match[2]!, object = match[1]!, bytes = await blob(object, path); files.set(path, bytes);
398
- if (path === "shard.json") {
399
- if (bytes.toString() !== shardSyncDescriptor(target.realm)) refuse(`${shardSyncLabel(target.realm)}: historical descriptor mismatch`);
400
- } else {
401
- const key = `${shardSyncRef(target.realm)}:${object}`;
402
- let pack = decoded.get(key);
403
- if (!pack) { pack = decodeShardPack(path, bytes, target.realm, target.generation); decoded.set(key, pack); }
404
- else if (!path.endsWith(`/${digest(bytes)}.jsonl`) || pack.some(row => path !== `rows/${digest(row.machine)}/${digest(bytes)}.jsonl`)) refuse(`${shardSyncLabel(target.realm)}: immutable pack path changed`);
405
- mergeRows(rows, pack, target.realm);
406
- mergePackOrigins(origins, firstOrigins, pack, { ref: shardSyncRef(target.realm), commit, pack: path, blob: object });
551
+ const entries=(await required(["ls-tree","-r","-z",commit],"shard sync tree unavailable")).split("\0").filter(Boolean);
552
+ const rows=new Map<string,ShardSyncRow>(),files=new Map<string,Buffer>(),origins=new Map<string,HoldSource>(),objects=new Map<string,string>(),manifests=new Map<string,SyncManifest>();
553
+ for(const entry of entries){
554
+ const match=/^100644 blob ([a-f0-9]{40})\t(.+)$/.exec(entry);
555
+ if(!match||match[0]!==entry||match[2]!=="shard.json"&&!PACK.test(match[2]!)&&!isShardManifestPath(match[2]!))refuse(`${shardSyncLabel(target.realm)}: forbidden historical tree entry`);
556
+ const path=match[2]!,oid=match[1]!,bytes=await blob(oid,path);files.set(path,bytes);objects.set(path,oid);
557
+ }
558
+ const descriptor=files.get("shard.json");if(!descriptor)refuse(`${shardSyncLabel(target.realm)}: historical descriptor missing`);
559
+ const version=decodeShardDescriptor(descriptor,target.realm);
560
+ if(version==="v2"&&!v2)refuse("remote shard requires the explicitly activated public prune view");
561
+ for(const [path,bytes] of files)if(PACK.test(path)){
562
+ const oid=objects.get(path)!,key=`${shardSyncRef(target.realm)}:${oid}`;let pack=decoded.get(key);
563
+ if(!pack){pack=decodeShardPack(path,bytes,target.realm,target.generation);decoded.set(key,pack);}
564
+ else if(!path.endsWith(`/${digest(bytes)}.jsonl`)||pack.some(row=>path!==`rows/${digest(row.machine)}/${digest(bytes)}.jsonl`))refuse("immutable pack path changed");
565
+ mergeRows(rows,pack,target.realm);mergePackOrigins(origins,firstOrigins,pack,{ref:shardSyncRef(target.realm),commit,pack:path,blob:oid});
566
+ }
567
+ for(const [path,bytes] of files)if(isShardManifestPath(path)){
568
+ if(version!=="v2")refuse("a v1 tree cannot contain prune manifests");
569
+ const value=decodeShardManifest(path,bytes),candidate=value as {prune?:unknown;pruneOccurrence?:{pack:string;blob:string};witnesses?:{pack:string;blob:string}[]};
570
+ const row=validatePruneRow(candidate.prune,undefined,"synced");
571
+ if(canonicalJSON(rows.get(row.id))!==canonicalJSON(row))refuse("manifest original prune is missing or conflicts");
572
+ if(!candidate.pruneOccurrence||!Array.isArray(candidate.witnesses))refuse("manifest references unavailable");
573
+ const selected=new Map<string,{pack:string;blob:string;bytes:Buffer}>();
574
+ for(const ref of [candidate.pruneOccurrence,...candidate.witnesses]){
575
+ if(!ref||typeof ref.pack!=="string"||objects.get(ref.pack)!==ref.blob||!files.has(ref.pack))refuse("manifest complete pack is unavailable");
576
+ selected.set(ref.pack,{pack:ref.pack,blob:ref.blob,bytes:files.get(ref.pack)!});
407
577
  }
578
+ const proof=verifyPublicPruneWitness(value,{realm:target.realm,packs:[...selected.values()]});
579
+ if(proof.status==="invalid")refuse("manifest lacks complete original public proof");
580
+ const identity=`${shardSyncRef(target.realm)}:${path}`,origin=manifestOrigins.get(identity)??{commit,blob:objects.get(path)!};manifestOrigins.set(identity,origin);
581
+ manifests.set(path,{path,bytes,value,row,eligible:proof.status==="verified",...origin});
408
582
  }
409
- if (!files.has("shard.json")) refuse(`${shardSyncLabel(target.realm)}: historical descriptor missing`);
410
- return { rows, files, origins };
583
+ return{rows,files,origins,version,manifests};
411
584
  };
412
585
  const history = async (tip: string, target: Target): Promise<History> => {
413
586
  const key = `${shardSyncRef(target.realm)}:${tip}`, cached = histories.get(key); if (cached) return cached;
@@ -424,6 +597,8 @@ export async function syncShardedMemory(store: MemoryStore, opts: SyncOptions =
424
597
  for (const parent of parents) {
425
598
  if (!audited.has(`${shardSyncRef(target.realm)}:${parent}`)) refuse(`${shardSyncLabel(target.realm)}: historical parent unavailable`);
426
599
  const previous = histories.get(`${shardSyncRef(target.realm)}:${parent}`) ?? await treeState(parent, target);
600
+ if(previous.version==="v2"&&current.version!=="v2")refuse("shard history cannot downgrade from v2");
601
+ if(previous.version==="v2")for(const [path,bytes] of previous.files)if(path!=="shard.json"&&!current.files.get(path)?.equals(bytes))refuse("immutable v2 history artifact removed or changed");
427
602
  for (const row of previous.rows.values()) if (canonicalJSON(current.rows.get(row.id)) !== canonicalJSON(row)) refuse(`${shardSyncLabel(target.realm)}: historical original row removed or replaced`);
428
603
  }
429
604
  audited.add(identity); if (commit === tip) latest = current;
@@ -451,6 +626,7 @@ export async function syncShardedMemory(store: MemoryStore, opts: SyncOptions =
451
626
  if (target.local) await required(["merge-base", "--is-ancestor", target.local, tip], `${shardSyncLabel(target.realm)}: remote history is not fast-forward`);
452
627
  const remote = await history(tip, target); target.remote = tip;
453
628
  mergeRows(target.rows, Array.from(remote.rows.values()), target.realm);
629
+ target.manifests=new Map(remote.manifests);target.eligible=new Set([...remote.manifests.values()].filter(item=>item.eligible).map(item=>item.row.id));
454
630
  for (const row of remote.rows.values()) { target.fetched.add(row.id); target.origins.set(row.id, remote.origins.get(row.id)!); }
455
631
  }
456
632
  };
@@ -471,12 +647,29 @@ export async function syncShardedMemory(store: MemoryStore, opts: SyncOptions =
471
647
  for (const target of targets) {
472
648
  const remote = target.remote ? await history(target.remote, target) : undefined;
473
649
  const files = new Map(remote?.files);
474
- files.set("shard.json", Buffer.from(shardSyncDescriptor(target.realm)));
650
+ files.set("shard.json", Buffer.from(shardSyncDescriptor(target.realm,v2?"v2":"v1")));
475
651
  // Retain immutable packs and append only new ids; repacking every prefix would duplicate the entire history at each sync.
476
652
  const additions = [...target.rows.values()].filter(row => !remote?.rows.has(row.id));
477
653
  for (const [path, bytes] of encodeShardPacks(additions)) {
478
654
  const old = files.get(path); if (old && !old.equals(bytes)) refuse(`${shardSyncLabel(target.realm)}: immutable pack conflict`); files.set(path, bytes);
479
655
  }
656
+ if(v2){
657
+ assertPublisherBoundary(target.store.profile,target.mutation!);
658
+ const occurrences=new Map<string,{pack:string;blob:string}>();
659
+ for(const [path,bytes] of files)if(PACK.test(path))for(const row of decodeShardPack(path,bytes,target.realm,target.generation))if(!occurrences.has(row.id))occurrences.set(row.id,{pack:path,blob:createHash("sha1").update(`blob ${bytes.length}\0`).update(bytes).digest("hex")});
660
+ const manifested=new Set([...target.manifests.values()].map(item=>item.row.id));
661
+ const candidates=[...target.rows.values()].filter((row):row is PruneRow=>row.op==="prune"&&row.generation===target.generation&&!target.fetched.has(row.id)&&!manifested.has(row.id));
662
+ const lineages=candidates.length?publisherPruneLineages(target.store.profile,target.mutation!,candidates):undefined;
663
+ for(const row of candidates){
664
+ const lineage=lineages?.get(row.id);if(!lineage)continue;
665
+ const own=occurrences.get(row.id);if(!own)refuse("original prune occurrence unavailable");
666
+ const needed=new Map<string,string>();for(const entry of row.targets)for(const ref of [entry,entry.basis])needed.set(ref.id,ref.rowHash);
667
+ const witnesses=[...needed].sort(([a],[b])=>a<b?-1:1).map(([id,rowHash])=>{const location=occurrences.get(id);if(!location)refuse("public witness occurrence unavailable");return{...location,id,rowHash};});
668
+ const manifest:PublicPruneWitnessV1={schema:"agent-mem.public-prune-witness/v1",realm:target.realm,prune:row,lineage,pruneOccurrence:own,witnesses};
669
+ const encoded=encodeShardManifest(manifest);const packs=new Map([own,...witnesses].map(ref=>[ref.pack,{pack:ref.pack,blob:ref.blob,bytes:files.get(ref.pack)!}]));
670
+ if(verifyPublicPruneWitness(manifest,{realm:target.realm,packs:[...packs.values()]}).status!=="verified")refuse("publisher manifest proof unavailable");files.set(encoded.path,encoded.bytes);
671
+ }
672
+ }
480
673
  await required(["read-tree", "--empty"], "shard sync private index unavailable");
481
674
  for (const [path, bytes] of [...files].sort(([a], [b]) => a < b ? -1 : 1)) {
482
675
  const object = await writeBlob(bytes);
@@ -498,11 +691,101 @@ export async function syncShardedMemory(store: MemoryStore, opts: SyncOptions =
498
691
  || !decodeShardPack(value.source.pack, bytes, target.realm, target.generation).some(item => canonicalJSON(item) === canonicalJSON(value.row))
499
692
  || value.source.ref !== shardSyncRef(target.realm)) refuse(`${shardSyncLabel(target.realm)}: prune hold source mismatch`);
500
693
  };
501
- localTargets(); await fetchTargets(); localTargets(); provePrunes(targets); await prepareHolds(targets, root, url, proveHoldSource); check();
694
+ const loadExistingApplications = async () => {
695
+ if(!v2)return;
696
+ const selected=readSelector(store.profile.home).selector;if(selected?.schema!=="agent-mem.profile/v2")refuse("selected v2 profile unavailable");
697
+ boundView=readPublicPruneView(store.profile.home,selected.pruneInputSetHash,store.profile.generation!);
698
+ for(const target of targets){
699
+ target.registered=[];const key=target.realm.scope==="user"?"user":`project:${target.realm.key}`,set=boundView.realms[key];if(!set)continue;
700
+ const projection=reducePublicPruneInputs({dir:target.store.dir,realm:target.realm,receivingGeneration:target.generation,inputSetHash:set});
701
+ for(const hash of projection.inputs){
702
+ const descriptor=readPublicPruneInputDescriptor({dir:target.store.dir,hash});
703
+ if(!target.remote||descriptor.source.remoteHash!==digest(url))refuse("registered prune source history disappeared or its remote binding changed");
704
+ await required(["merge-base","--is-ancestor",descriptor.source.commit,target.remote],"registered prune source is missing from replacement history");
705
+ const original=await history(descriptor.source.commit,target),manifest=original.manifests.get(descriptor.source.manifestPath);
706
+ if(!manifest||!manifest.eligible||manifest.blob!==descriptor.source.manifestBlob||descriptor.source.ref!==shardSyncRef(target.realm)||digest(canonicalJSON(manifest.value))!==descriptor.witnessHash)refuse("registered prune anchor no longer has exact audited evidence");
707
+ target.registered.push(descriptor);
708
+ }
709
+ }
710
+ };
711
+ const objectCache=new Map<string,{type:"commit"|"tree"|"blob";body:Uint8Array}>(), graphObjects=new Map<string,Uint8Array>();
712
+ // ponytail: one sync retains at most the 256 MiB replay budget across selected realms; segmented acquisition is the upgrade.
713
+ let objectBytes=0;
714
+ const readObject=async(oid:string)=>{
715
+ const prior=objectCache.get(oid);if(prior)return prior;
716
+ if(OID.exec(oid)?.[0]!==oid)refuse("Git object identity invalid");
717
+ const type=await required(["cat-file","-t",oid],"Git object type unavailable");
718
+ if(type!=="commit"&&type!=="tree"&&type!=="blob")refuse("Git object type refused");
719
+ const size=Number(await required(["cat-file","-s",oid],"Git object size unavailable")),limit=type==="commit"?65536:SHARD_PACK_MAX_BYTES;
720
+ if(!Number.isSafeInteger(size)||size<0||size>limit)refuse("Git object exceeds complete evidence bound");
721
+ const encodedSize=Buffer.byteLength(`${type} ${size}`)+1+size;
722
+ if(objectBytes+encodedSize>PUBLIC_PRUNE_REPLAY_MAX_BYTES)throw new PublicPruneInputError("resource");
723
+ check();const result=await generationSyncGitBytes(root,gitArgs(["cat-file",type,oid]),opts.signal,undefined,limit);check();
724
+ if(result.code!==0||result.out.length!==size)refuse("Git object bytes unavailable");
725
+ const value:{type:"commit"|"tree"|"blob";body:Uint8Array}={type:type as "commit"|"tree"|"blob",body:result.out};objectBytes+=encodedSize;objectCache.set(oid,value);return value;
726
+ };
727
+ const prepareApplications = async () => {
728
+ if(!v2)return;application.proofComplete=0;application.held=0;
729
+ for(const target of targets){
730
+ const current=await history(target.commit!,target);target.manifests=new Map(current.manifests);
731
+ target.eligible=new Set([...current.manifests.values()].filter(item=>item.eligible).map(item=>item.row.id));
732
+ application.proofComplete+=target.eligible.size;
733
+ const key=target.realm.scope==="user"?"user":`project:${target.realm.key}`,initial=boundView!.realms[key]??null;
734
+ const inputs=[];
735
+ for(const record of [...current.manifests.values()].sort((a,b)=>a.path<b.path?-1:1)){
736
+ if(!record.eligible||record.row.generation===target.generation)continue;
737
+ const witnessHash=digest(canonicalJSON(record.value));
738
+ if(target.registered.some(item=>item.witnessHash===witnessHash))continue;
739
+ if(inputs.length>=PUBLIC_PRUNE_MAX_SETS)throw new PublicPruneInputError("resource");
740
+ const acquired=await collectPublicPruneGitGraph({commit:record.commit,manifestPath:record.path,manifestBlob:record.blob,realm:target.realm,readObject});
741
+ // Share exact raw objects before accumulating inputs; repeated manifests cannot multiply graph memory.
742
+ const objects=acquired.map(item=>{
743
+ const prior=graphObjects.get(item.oid);if(prior&&!Buffer.from(prior).equals(item.bytes))refuse("conflicting Git graph bytes");
744
+ const bytes=prior??item.bytes;graphObjects.set(item.oid,bytes);
745
+ const cached=objectCache.get(item.oid)!;objectCache.set(item.oid,{type:cached.type,body:bytes.subarray(bytes.indexOf(0)+1)});return{oid:item.oid,bytes};
746
+ });
747
+ inputs.push({witnessHash,source:{remoteHash:digest(url),ref:shardSyncRef(target.realm),commit:record.commit,manifestPath:record.path,manifestBlob:record.blob},objects});
748
+ }
749
+ target.batch=undefined;
750
+ if(inputs.length){
751
+ const binding={realm:target.realm,receivingGeneration:target.generation,selectorRevision:expectedRevision,inputSetHash:initial};
752
+ target.batch=preparePublicPruneInputBatch({dir:target.store.dir,binding,current:()=>{check();return{...binding,selectorRevision:expectedRevision};},inputs},target.mutation!);
753
+ }
754
+ }
755
+ await prepareHolds(targets,root,url,proveHoldSource);application.held=targets.reduce((n,item)=>n+item.holds.length,0);application.phase="prepared";
756
+ };
757
+ const installApplications = async () => {
758
+ if(!v2)return;
759
+ const realms={...boundView!.realms};
760
+ for(const target of targets)if(target.batch){
761
+ const installed=persistPublicPruneInputBatch(target.batch,{mutation:target.mutation!,observe:()=>check()});
762
+ realms[target.realm.scope==="user"?"user":`project:${target.realm.key}`]=installed.inputSetHash;testOnlyPhase?.("inputs-installed");
763
+ }
764
+ const view:PublicPruneViewV1={...boundView!,realms};
765
+ const changed=canonicalJSON(view)!==canonicalJSON(boundView);
766
+ if(changed){
767
+ for(const target of targets)selectorRetention(target,true,true);
768
+ const result=await advancePublicPruneView({memoryDir:store.profile.home,view,expectedRevision,mutations:[homeMutation!,...targets.map(item=>item.mutation!)]});
769
+ expectedRevision=result.revision;boundView=view;
770
+ // The same open handles validate/rebind; no close/reopen or authority flag is used.
771
+ for(const target of targets)target.store.refresh();
772
+ if(shardSyncSourceBinding(store)!==expectedSources||opts.policyVersion?.()!==expectedPolicy)refuse("source binding or policy changed during view commit");
773
+ expectedBinding=shardSyncBinding(store);
774
+ testOnlyPhase?.("view-committed");
775
+ }
776
+ application.projectionActive=targets.reduce((n,target)=>n+[...target.eligible].filter(id=>(target.rows.get(id) as PruneRow).generation!==target.generation).length,0);application.phase="active";check();
777
+ };
778
+ localTargets(); await fetchTargets(); localTargets(); provePrunes(targets); await loadExistingApplications(); await prepareHolds(targets, root, url, proveHoldSource); check();
502
779
  // Every selected realm, including its complete remote history/prunes, is preflighted before the first append or publication.
503
780
  for (let attempt = 0; attempt < 2; attempt++) {
504
- if (attempt) { await fetchTargets(); localTargets(); provePrunes(targets); await prepareHolds(targets, root, url, proveHoldSource); check(); }
505
- await buildTargets(); check();
781
+ if (attempt) { await fetchTargets(); localTargets(); provePrunes(targets); await loadExistingApplications(); await prepareHolds(targets, root, url, proveHoldSource); check(); }
782
+ await buildTargets(); await prepareApplications(); check();
783
+ if(v2)for(const target of targets){
784
+ const durable=new Set(target.store.prunePort().snapshot().durablePruneIds),reserve=[...target.rows.values()].some(row=>row.op==="prune"&&row.generation===target.generation&&!durable.has(row.id));
785
+ // Existing pending plans complete before fresh native rows, so only one serial new-plan slot is reserved.
786
+ selectorRetention(target,targets.some(item=>!!item.batch),false,reserve);
787
+ }
788
+ if(v2)for(const target of targets)assertPublisherBoundary(target.store.profile,target.mutation!);
506
789
  const sent = await git(["push", "--atomic", "--no-follow-tags", "-q", "origin", ...targets.map(target => `${target.commit!}:${shardSyncRef(target.realm)}`)]);
507
790
  if (sent.code === 0) pushed = true;
508
791
  check();
@@ -513,20 +796,25 @@ export async function syncShardedMemory(store: MemoryStore, opts: SyncOptions =
513
796
  // No selected ordinary/native imports can precede durable receipts for every foreign prune.
514
797
  for (const target of targets) for (const hold of target.holds) installHold(root, hold, testOnlyPhase);
515
798
  check();
799
+ await installApplications();
516
800
  for (const target of targets) {
517
801
  assertCuratedShardPaths(target.store.profile.home, target.realm.scope === "user" ? { user: true } : { project: target.realm.key });
518
- importRows(target); testOnlyPhase?.("imported");
802
+ importRows(target);
803
+ target.pending=[];target.selectorChecks=[];
804
+ if(v2)selectorRetention(target,false,true);
805
+ testOnlyPhase?.("imported");
519
806
  }
520
807
  for (const target of targets) await required(["update-ref", "--no-deref", shardSyncRef(target.realm), target.commit!, target.local ?? zero], "shard sync cached ref changed concurrently");
521
808
  const warning = holdWarning(targets, root);
809
+ if(v2){application.nativeApplied=targets.reduce((n,target)=>n+[...target.rows.values()].filter(row=>row.op==="prune"&&row.generation===target.generation).length,0);application.phase="complete";application.transport="durable";}
522
810
  return { ok: true, pushed, entries: targets.reduce((n, target) => n + [...target.rows.values()].filter(row => row.op === "note").length, 0),
523
- ...(warning ? { warning } : {}) };
811
+ ...(warning ? { warning } : {}), ...(v2?{pruneApplication:application}:{}) };
524
812
  }
525
813
  if (opts.signal?.aborted) return fail("stopped");
526
814
  }
527
815
  return fail("shard sync selected-ref push failed");
528
816
  } catch (error) {
529
- if (error instanceof ShardSyncFormatError || error instanceof SyncRefusal) return fail(error.message);
817
+ if (error instanceof PublicPruneInputError || error instanceof ShardSyncFormatError || error instanceof SyncRefusal) return fail(error.message);
530
818
  try { testOnlyUnexpectedError?.(error); } catch { /* A test observer cannot change the neutral result. */ }
531
819
  return fail("shard sync unavailable");
532
820
  } finally {