@staix/agent-mem 0.21.0 → 0.22.1

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 (38) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +27 -1
  3. package/package.json +5 -2
  4. package/src/cli-agent-reads.ts +3 -1
  5. package/src/cli.ts +36 -4
  6. package/src/commands-table.ts +3 -0
  7. package/src/index.ts +6 -0
  8. package/src/memory/commands.ts +3 -2
  9. package/src/memory/generation.ts +6 -3
  10. package/src/memory/profile-lock-diagnostic.ts +69 -0
  11. package/src/memory/public-prune-activation.ts +4 -4
  12. package/src/memory/reader-lease.ts +3 -2
  13. package/src/memory/shard-sync.ts +1 -2
  14. package/src/memory/shard.ts +4 -0
  15. package/src/memory/sync-cadence.ts +2 -1
  16. package/src/memory/sync-placement-abandon.ts +142 -0
  17. package/src/memory/sync-placement-activation-locks.ts +28 -0
  18. package/src/memory/sync-placement-activation-path.ts +2 -0
  19. package/src/memory/sync-placement-activation-proof.ts +68 -0
  20. package/src/memory/sync-placement-activation-storage.ts +167 -0
  21. package/src/memory/sync-placement-activation.ts +185 -0
  22. package/src/memory/sync-placement-capture.ts +104 -23
  23. package/src/memory/sync-placement-effective-writers.ts +84 -0
  24. package/src/memory/sync-placement-guard.ts +18 -8
  25. package/src/memory/sync-placement-inspection.ts +5 -1
  26. package/src/memory/sync-placement-journal-storage.ts +51 -7
  27. package/src/memory/sync-placement-journal.ts +11 -4
  28. package/src/memory/sync-placement-prerequisites.ts +50 -0
  29. package/src/memory/sync-placement-transport.ts +17 -6
  30. package/src/memory/sync-placement-writer-history.ts +278 -0
  31. package/src/memory/sync-placement-writer-renewal.ts +179 -0
  32. package/src/memory/sync-placement-writer-revision-format.ts +122 -0
  33. package/src/memory/sync-placement-writer-roots.ts +167 -0
  34. package/src/memory/sync-placement-writers.ts +135 -0
  35. package/src/memory/sync.ts +1 -2
  36. package/src/setup-toml.ts +3 -2
  37. package/src/setup.ts +101 -14
  38. package/src/sync-placement-writer-context.ts +136 -0
@@ -11,7 +11,10 @@ import { memoryPolicySnapshot } from "./policy.ts";
11
11
  import { outboundPolicyEvaluator } from "./sync-screen.ts";
12
12
  import { readActivePlacementBinding } from "./sync-placement-active.ts";
13
13
  import { readPlacementRuntimeMarkers } from "./sync-placement-guard.ts";
14
- import { captureSyncPlacementContext, type PlacementCapture, type PlacementPendingSource } from "./sync-placement-capture.ts";
14
+ import { captureSyncPlacementContext, placementCaptureContextHash, type PlacementCapture, type PlacementPendingSource } from "./sync-placement-capture.ts";
15
+ import { readEffectiveSyncPlacementWriters, type EffectiveSyncPlacementWriters } from "./sync-placement-effective-writers.ts";
16
+ import { SharedWriterRevisionError } from "./sync-placement-writer-revision-format.ts";
17
+ import { types } from "node:util";
15
18
  import type { PlannedGitObject } from "./sync-placement-git.ts";
16
19
  import { PLACEMENT_FORMAT } from "./sync-placement-format.ts";
17
20
  import { createPlacementRecord, placementKeyId, type PlacementEvent, type PlacementRealm, type PlacementHoldReason, type PlacementRecord } from "./sync-placement-journal-format.ts";
@@ -50,15 +53,18 @@ class PlacementTransport {
50
53
  targets: Target[] = []; homeMutation?: MutationHandle; rootMutation?: MutationHandle; cacheIdentity?: {dev:number;ino:number}; cacheConfigIdentity?: string; epochHash = ""; journal!: PlacementJournalRead; capture?: PlacementCapture;
51
54
  machineId = ""; url = ""; revision = ""; accepted = false; factual = false; attempted = false; lastPolicyCheckUnixMs?: number; lastPolicyHash?: string; policyCheckSource?: "live"|"durable-intent"; attemptHash?: string;
52
55
  evaluator?: ReturnType<typeof outboundPolicyEvaluator>; applications?: ShardSyncApplications; current?: Materialized; recovered = false;
56
+ writerInventory?:EffectiveSyncPlacementWriters;
53
57
  constructor(store: MemoryStore, opts: SyncOptions, phase?: Phase, testOnlyError?: (error:unknown)=>void) { this.testOnlyError=testOnlyError; this.store = store; this.opts = opts; this.phase = phase; this.home = store.profile.home; this.root = join(this.home, "shard-sync-placement"); this.cache = join(this.root, "objects.git"); }
54
58
  attest(): void {
55
59
  if (!this.opts.placementAttestation || this.opts.screen && !this.opts.policyVersion) refuse();
56
60
  const value = memoryPolicySnapshot(this.opts.placementAttestation());
57
61
  if (!value || value.legacySyncQuiesced !== true || !Array.isArray(value.writers)) refuse();
58
62
  const writers = [...value.writers].sort((a,b) => a.writerId.localeCompare(b.writerId));
59
- if (canonicalJSON(writers) !== canonicalJSON(this.journal.activation.writers)) refuse();
63
+ if(!this.writerInventory||canonicalJSON(writers)!==canonicalJSON(this.writerInventory.writers))refuse();
64
+ this.writerInventory.assertCurrent();
60
65
  }
61
66
  metadata(): void {
67
+ this.writerInventory?.assertCurrent();
62
68
  if(this.rootMutation)assertMutationHandle(this.rootMutation,this.root);
63
69
  if(this.cacheIdentity){const stat=lstatSync(this.cache);if(stat.isSymbolicLink()||!stat.isDirectory()||stat.dev!==this.cacheIdentity.dev||stat.ino!==this.cacheIdentity.ino)refuse();}
64
70
  if(this.cacheIdentity){
@@ -105,7 +111,7 @@ class PlacementTransport {
105
111
  async initialize(): Promise<void> {
106
112
  const markers = readPlacementRuntimeMarkers(this.home), active = readActivePlacementBinding(this.home, markers.profile, markers.shards);
107
113
  if (!active || this.store.profile.publicationFormat !== PLACEMENT_FORMAT || this.store.profile.placementHead !== active.profile.placementHead) refuse();
108
- this.epochHash = active.profile.placementHead; this.machineId = active.shards.machine.id; this.revision = readSelector(this.home).revision; this.journal = readPlacementJournal(this.home, this.epochHash); this.attest();
114
+ this.epochHash = active.profile.placementHead; this.machineId = active.shards.machine.id; this.revision = readSelector(this.home).revision; this.journal = readPlacementJournal(this.home, this.epochHash); this.writerInventory=readEffectiveSyncPlacementWriters(this.home);this.attest();
109
115
  const opened = this.store.openedSyncTargets();
110
116
  if (!opened.length || opened.length > 2 || !opened.includes(this.store) || opened.some(item => item.profile.home !== this.home || !item.profile.sharded || !item.profile.generation
111
117
  || !item.profile.shardUser && item.profile.shardKey !== this.store.profile.shardKey)) refuse();
@@ -186,7 +192,8 @@ class PlacementTransport {
186
192
  pathHash: digest(`log/${item.row.machine}.jsonl`), offset: item.offset, prefixHash: item.prefixHash, line: item.line })));
187
193
  this.capture = captureSyncPlacementContext(this.home, { attest: this.opts.placementAttestation!, ...(this.opts.policyVersion ? { policyVersion: this.opts.policyVersion } : {}) }, this.targets.map(physical), pending);
188
194
  if (this.capture.activeHead !== this.epochHash || this.capture.context.homeHash !== this.journal.epoch.homeHash
189
- || canonicalJSON(this.capture.context.writers) !== canonicalJSON(this.journal.activation.writers)) refuse();
195
+ || canonicalJSON(this.capture.context.writers) !== canonicalJSON(this.writerInventory?.writers)
196
+ || !!this.writerInventory?.revisionSequence&&this.capture.writerInventory?.revisionHash!==this.writerInventory.revisionHash) refuse();
190
197
  this.check();
191
198
  }
192
199
  async applicationCoordinator(): Promise<ShardSyncApplications> {
@@ -232,7 +239,7 @@ class PlacementTransport {
232
239
  publisherLineages:(index,rows)=>{const target=this.targets[index]!;assertPublisherBoundary(target.store.profile,target.mutation!);return publisherPruneLineages(target.store.profile,target.mutation!,rows);}});
233
240
  for(const [index,target]of this.targets.entries())Object.assign(target,materialized.targets[index]!);
234
241
  const {decisions,plannedTargets:targets,objects}=materialized;
235
- const body={epochHash:this.epochHash,generation:this.journal.epoch.generation,homeHash:capture.context.homeHash,contextHash:hashJSON(capture.context),markerHash:capture.markerHash,
242
+ const body={epochHash:this.epochHash,generation:this.journal.epoch.generation,homeHash:capture.context.homeHash,contextHash:placementCaptureContextHash(capture),markerHash:capture.markerHash,
236
243
  sourceHash:capture.context.sourceHash,aliasesHash:capture.context.aliasesHash,policyHash:capture.context.policyHash,remoteHash:digest(this.url),
237
244
  journal:frozen?.root.journal??{sequence:frontier.sequence,headHash:frontier.headHash},createdUnixSeconds,policySnapshotUnixMs:frozen?.root.policySnapshotUnixMs??this.lastPolicyCheckUnixMs??Date.now(),targets,reservedRecords:0,reservedRecordBytes:[] as number[]};
238
245
  let bundle=planPlacementPublication(body,{sources:capture.prefixes,decisions,objects});
@@ -446,7 +453,7 @@ class PlacementTransport {
446
453
  catch { if(retained)return await this.terminalUnknown(retained);throw new Error("placement source preflight unavailable"); }
447
454
  if(retained){
448
455
  const root=retained.bundle.root;
449
- const stable=root.contextHash===hashJSON(this.capture!.context)&&root.markerHash===this.capture!.markerHash&&root.sourceHash===this.capture!.context.sourceHash
456
+ const stable=root.contextHash===placementCaptureContextHash(this.capture!)&&root.markerHash===this.capture!.markerHash&&root.sourceHash===this.capture!.context.sourceHash
450
457
  &&root.policyHash===this.capture!.context.policyHash&&root.targets.every((target,index)=>target.expectedRemoteHead===(this.targets[index]?.remote??null));
451
458
  if(!stable)return await this.terminalUnknown(retained);
452
459
  }
@@ -476,6 +483,10 @@ class PlacementTransport {
476
483
  this.factual=true;return await this.finish(material);
477
484
  }catch(error){
478
485
  try { this.testOnlyError?.(error); } catch { /* Test observers never alter production results. */ }
486
+ if(error&&typeof error==="object"&&!types.isProxy(error)&&error instanceof SharedWriterRevisionError){
487
+ const code=Object.getOwnPropertyDescriptor(error,"code")?.value;
488
+ if(code==="staging"||code==="pending")return this.result(false,this.attemptHash?"unknown":"not-needed",new SharedWriterRevisionError(code).message);
489
+ }
479
490
  return this.result(false,this.attemptHash?"unknown":"not-needed",this.attemptHash?"Placement sync stopped with a retained attempt that may have published; recheck its exact receipts before another publication.":"Placement sync is unavailable; verify the active epoch, selected sources, policy, private remote and writer pins before retrying.");
480
491
  }finally{
481
492
  let ownedCache=true;try{this.metadata();}catch{ownedCache=false;}
@@ -0,0 +1,278 @@
1
+ /** Private append-only writer inventory history. It never measures installs or grants activation authority. */
2
+ import { createHash } from "node:crypto";
3
+ import { closeSync, constants, fstatSync, fsyncSync, linkSync, lstatSync, openSync, opendirSync, readSync, unlinkSync, writeSync, type BigIntStats } from "node:fs";
4
+ import { dirname, join } from "node:path";
5
+ import { types } from "node:util";
6
+ import { canonicalJSON, hashJSON } from "./generation.ts";
7
+ import { realPath } from "./platform.ts";
8
+ import { assertMutationHandle, type MutationHandle } from "./mutation.ts";
9
+ import {
10
+ WRITER_REVISION_MAX_BYTES, WRITER_COMMIT_MAX_BYTES, WRITER_RENEWAL_INTENT_MAX_BYTES, WRITER_REVISION_LIMIT,
11
+ WRITER_COMMIT_LOG, WRITER_RENEWAL_INTENT_FILE, writerRevisionFile, writerInventoryGenesis,
12
+ validateWriterRevision, validateWriterCommit, validateWriterRenewalIntent, createWriterCommit,
13
+ SharedWriterRevisionError, type WriterRevision, type WriterCommit, type WriterRenewalIntent,
14
+ } from "./sync-placement-writer-revision-format.ts";
15
+
16
+ export interface WriterHistoryBinding { epochHash: string; activationHash: string; homeHash: string; generation: string; initialLocatorHash?: string }
17
+ export type WriterHistoryPhase = "intent-started" | "intent-written" | "intent-fsync" | "intent-published" | "revision-written" | "revision-fsync" | "commit-started" | "commit-written" | "commit-fsync" | "intent-cleared";
18
+ export interface WriterHistoryOptions { mutation: MutationHandle; assertCurrent(): void; phase?(phase: WriterHistoryPhase): void }
19
+ export interface WriterHistoryRead {
20
+ current?: WriterRevision; commits: WriterCommit[]; anchors: { sequence: number; headHash: string }[];
21
+ sequence: number; headHash: string; logBytes: number; logHash: string; pending?: WriterRenewalIntent; appendStarted: boolean; assertCurrent(): void;
22
+ }
23
+ const encode = (value: unknown) => Buffer.from(canonicalJSON(value) + "\n");
24
+ const sha = (value: Uint8Array) => createHash("sha256").update(value).digest("hex");
25
+ const stamp = (s: BigIntStats) => [s.dev,s.ino,s.uid,s.mode,s.nlink,s.size,s.mtimeNs,s.ctimeNs].map(String).join(":");
26
+ const identity = (s: BigIntStats) => [s.dev,s.ino,s.uid,s.mode].map(String).join(":");
27
+ function fail(code: "invalid" | "bound" | "changed" | "pending" | "staging" = "invalid"): never { throw new SharedWriterRevisionError(code); }
28
+ function safe<T>(work: () => T): T {
29
+ try { return work(); } catch (error) {
30
+ if (error && typeof error === "object" && !types.isProxy(error) && error instanceof SharedWriterRevisionError) {
31
+ const code=Object.getOwnPropertyDescriptor(error,"code");
32
+ if(code&&"value"in code&&["invalid","bound","changed","pending","staging","cap"].includes(code.value))throw new SharedWriterRevisionError(code.value);
33
+ }
34
+ return fail();
35
+ }
36
+ }
37
+ function owned(s: BigIntStats, dir = false, links: readonly bigint[] = [1n]): void {
38
+ if (s.isSymbolicLink() || s.uid !== BigInt(process.getuid?.() ?? -1)
39
+ || (dir ? !s.isDirectory() || (s.mode & 0o077n) !== 0n : !s.isFile() || !links.includes(s.nlink) || (s.mode & 0o777n) !== 0o600n)) fail();
40
+ }
41
+ function checkedDir(file: string): BigIntStats { const s = lstatSync(file,{bigint:true}); owned(s,true); return s; }
42
+ interface FileRead { bytes: Buffer; stat: BigIntStats }
43
+ function read(file: string, max: number, checks: (() => void)[] = [], links: readonly bigint[] = [1n]): FileRead | undefined {
44
+ const before = lstatSync(file,{bigint:true,throwIfNoEntry:false});
45
+ if (!before) { checks.push(() => { if (lstatSync(file,{throwIfNoEntry:false})) fail("changed"); }); return undefined; }
46
+ owned(before,false,links); if (before.size > BigInt(max)) fail("bound");
47
+ const fd = openSync(file,constants.O_RDONLY|constants.O_NOFOLLOW|constants.O_NONBLOCK);
48
+ try {
49
+ const opened = fstatSync(fd,{bigint:true}); owned(opened,false,links); if (stamp(opened)!==stamp(before)) fail("changed");
50
+ const buffer = Buffer.alloc(Number(opened.size)+1); let at=0;
51
+ while (at<buffer.length) { const n=readSync(fd,buffer,at,buffer.length-at,at); if (!n) break; at+=n; }
52
+ if (at!==Number(opened.size) || stamp(fstatSync(fd,{bigint:true}))!==stamp(opened) || stamp(lstatSync(file,{bigint:true}))!==stamp(opened)) fail("changed");
53
+ checks.push(() => { const now=lstatSync(file,{bigint:true}); owned(now,false,links); if(stamp(now)!==stamp(opened))fail("changed"); });
54
+ return {bytes:buffer.subarray(0,at),stat:opened};
55
+ } finally { closeSync(fd); }
56
+ }
57
+ function decode<T>(bytes: Buffer, validate: (value: unknown) => T): T {
58
+ const text = new TextDecoder("utf-8",{fatal:true}).decode(bytes), result=validate(JSON.parse(text));
59
+ if (!bytes.equals(encode(result))) fail(); return result;
60
+ }
61
+ function binding(value: WriterHistoryBinding): WriterHistoryBinding {
62
+ if (!value || typeof value!=="object" || types.isProxy(value) || Object.getOwnPropertySymbols(value).length) fail();
63
+ const descriptors=Object.getOwnPropertyDescriptors(value), keys=["epochHash","activationHash","homeHash","generation"];
64
+ if(Object.keys(descriptors).some(key=>![...keys,"initialLocatorHash"].includes(key)))fail();
65
+ const result: Record<string,string>={};
66
+ for(const key of keys){const d=descriptors[key];if(!d||!("value"in d)||!d.enumerable||typeof d.value!=="string"||d.value.length!==(key==="generation"?32:64)||!/^[a-f0-9]+$/.test(d.value))fail();result[key]=d.value;}
67
+ const locator=descriptors.initialLocatorHash;if(locator){if(!("value"in locator)||!locator.enumerable||typeof locator.value!=="string"||locator.value.length!==64||!/^[a-f0-9]+$/.test(locator.value))fail();result.initialLocatorHash=locator.value;}
68
+ return result as unknown as WriterHistoryBinding;
69
+ }
70
+ interface Paths { home: string; epoch: string; intent: string; log: string; dirs: [string,BigIntStats][] }
71
+ function paths(homeInput: string, input: WriterHistoryBinding): Paths {
72
+ const home=realPath(homeInput), original=checkedDir(homeInput), root=checkedDir(home);
73
+ if(identity(original)!==identity(root)||hashJSON([home,String(root.dev),String(root.ino),String(root.uid)])!==input.homeHash)fail("changed");
74
+ const dirs:[string,BigIntStats][]=[[home,root]];
75
+ let current=home;for(const part of ["sync-placement","epochs",input.epochHash]){current=join(current,part);dirs.push([current,checkedDir(current)]);}
76
+ return {home,epoch:current,intent:join(home,WRITER_RENEWAL_INTENT_FILE),log:join(current,WRITER_COMMIT_LOG),dirs};
77
+ }
78
+ function manifest(p: Paths): string[] {
79
+ const iterator=opendirSync(p.epoch), names:string[]=[]; let count=0;
80
+ try {for(;;){const entry=iterator.readSync();if(!entry)break;if(++count>255)fail("bound");
81
+ if(entry.name.startsWith("writer-")){if(entry.name!==WRITER_COMMIT_LOG&&!/^writer-revision-\d{5}\.json$/.test(entry.name))fail();names.push(entry.name);}
82
+ }} finally {iterator.closeSync();} return names.sort();
83
+ }
84
+ function boundRevision(revision: WriterRevision,b:WriterHistoryBinding):void {
85
+ if(revision.epochHash!==b.epochHash||revision.activationHash!==b.activationHash||revision.homeHash!==b.homeHash||revision.generation!==b.generation||b.initialLocatorHash!==undefined&&revision.initialLocatorHash!==b.initialLocatorHash)fail();
86
+ }
87
+ function intentBinding(intent:WriterRenewalIntent):WriterHistoryBinding {const r=intent.revision;return {epochHash:r.epochHash,activationHash:r.activationHash,homeHash:r.homeHash,generation:r.generation};}
88
+ /** Exact basename reserved alongside the final outside intent during atomic publication. */
89
+ export function writerRenewalIntentTempName(value:WriterRenewalIntent):string {return WRITER_RENEWAL_INTENT_FILE+".pending-"+sha(encode(validateWriterRenewalIntent(value)));}
90
+ export function isWriterRenewalIntentTempName(name:string):boolean {const match=new RegExp("^"+WRITER_RENEWAL_INTENT_FILE.replaceAll(".","\\.")+"\\.pending-[a-f0-9]{64}$").exec(name);return !!match&&match[0]===name;}
91
+ function intentTemps(p:Paths):string[]{
92
+ const iterator=opendirSync(p.home),out:string[]=[];let count=0,bytes=0;
93
+ try{for(;;){const item=iterator.readSync();if(!item)break;if(++count>20000||(bytes+=Buffer.byteLength(item.name))>8*1024*1024)fail("bound");
94
+ if(item.name.startsWith(WRITER_RENEWAL_INTENT_FILE+".pending-")){if(!isWriterRenewalIntentTempName(item.name))fail();if(out.length>=256)fail("bound");out.push(item.name);}
95
+ }}finally{iterator.closeSync();}return out.sort();
96
+ }
97
+ function readIntent(p:Paths,checks:(()=>void)[],expected?:WriterRenewalIntent,ignoreUnpublishedStaging=false):{file?:FileRead;intent?:WriterRenewalIntent;temp?:string}{
98
+ const names=intentTemps(p);if(!ignoreUnpublishedStaging)checks.push(()=>{if(canonicalJSON(intentTemps(p))!==canonicalJSON(names))fail("changed");});
99
+ const final=read(p.intent,WRITER_RENEWAL_INTENT_MAX_BYTES,checks,[1n,2n]);
100
+ if(!final&&names.length){if(ignoreUnpublishedStaging)return{};fail("staging");}
101
+ if(names.length>1)fail();const temp=names[0]?join(p.home,names[0]):undefined;
102
+ const staging=temp?read(temp,WRITER_RENEWAL_INTENT_MAX_BYTES,checks,[1n,2n]):undefined;
103
+ if(!final&&!staging)return{};
104
+ const file=final??staging!;
105
+ let intent:WriterRenewalIntent;
106
+ if(expected){const full=encode(expected);if(file.bytes.length>full.length||!file.bytes.equals(full.subarray(0,file.bytes.length)))fail();intent=expected;}
107
+ else {try{intent=decode(file.bytes,validateWriterRenewalIntent);}catch{fail("pending");}}
108
+ const full=encode(intent),known=join(p.home,writerRenewalIntentTempName(intent));if(temp&&temp!==known)fail();
109
+ if(final){if(!final.bytes.equals(full))fail();if(staging){if(final.stat.nlink!==2n||staging.stat.nlink!==2n||stamp(final.stat)!==stamp(staging.stat)||!staging.bytes.equals(full))fail();}else if(final.stat.nlink!==1n)fail();}
110
+ else if(staging!.stat.nlink!==1n)fail();
111
+ return{file,intent,temp};
112
+ }
113
+ function publishIntent(p:Paths,intent:WriterRenewalIntent,opts:WriterHistoryOptions):void{
114
+ const before=readIntent(p,[],intent),full=encode(intent),temp=join(p.home,writerRenewalIntentTempName(intent));
115
+ if(!before.file||!lstatSync(p.intent,{throwIfNoEntry:false})){
116
+ complete(p,temp,full,WRITER_RENEWAL_INTENT_MAX_BYTES,opts,"intent-written","intent-fsync",{at:0,phase:"intent-started"});proof(p,opts);
117
+ const staged=readFileExpected(temp,full,WRITER_RENEWAL_INTENT_MAX_BYTES);proof(p,opts);
118
+ if(stamp(lstatSync(temp,{bigint:true}))!==stamp(staged.stat))fail("changed");linkSync(temp,p.intent);syncDir(p,p.home,opts);
119
+ }
120
+ const current=readIntent(p,[],intent);if(!current.file||!current.file.bytes.equals(full))fail();
121
+ if(current.temp){proof(p,opts);const pair=readIntent(p,[],intent);if(pair.temp!==temp||!pair.file||stamp(pair.file.stat)!==stamp(current.file.stat))fail("changed");unlinkSync(temp);syncDir(p,p.home,opts);}
122
+ const final=readFileExpected(p.intent,full,WRITER_RENEWAL_INTENT_MAX_BYTES),fd=openSync(p.intent,constants.O_RDONLY|constants.O_NOFOLLOW|constants.O_NONBLOCK);
123
+ try{if(stamp(fstatSync(fd,{bigint:true}))!==stamp(final.stat))fail("changed");proof(p,opts);fsyncSync(fd);if(stamp(lstatSync(p.intent,{bigint:true}))!==stamp(final.stat))fail("changed");}finally{closeSync(fd);}syncDir(p,p.home,opts);
124
+ if(stamp(lstatSync(p.intent,{bigint:true}))!==stamp(final.stat))fail("changed");
125
+ }
126
+ interface Snapshot { result:WriterHistoryRead; paths:Paths; log:Buffer; intentBytes?:Buffer }
127
+ function snapshot(home:string,b:WriterHistoryBinding,allowPending:boolean,expected?:WriterRenewalIntent,ignoreUnpublishedStaging=false):Snapshot {
128
+ const p=paths(home,b), checks:(()=>void)[]=[], names=manifest(p);
129
+ for(const [dir,stat]of p.dirs)checks.push(()=>{if(identity(checkedDir(dir))!==identity(stat))fail("changed");});
130
+ checks.push(()=>{if(canonicalJSON(manifest(p))!==canonicalJSON(names))fail("changed");});
131
+ const intentRead=readIntent(p,checks,expected,ignoreUnpublishedStaging),intentFile=intentRead.file;let pending=intentRead.intent;
132
+ if(pending){if(!allowPending)fail("pending");if(pending.home!==p.home)fail();boundRevision(pending.revision,b);}
133
+ else if(expected)fail("pending");
134
+ const logFile=read(p.log,WRITER_COMMIT_MAX_BYTES,checks), log=logFile?.bytes??Buffer.alloc(0);
135
+ const commits:WriterCommit[]=[],anchors:{sequence:number;headHash:string}[]=[];let current:WriterRevision|undefined;
136
+ let head=writerInventoryGenesis(b.epochHash,b.activationHash),previous=0,completeBytes=log.length;
137
+ const lastLF=log.lastIndexOf(10);if(lastLF!==log.length-1){if(!pending)fail("pending");completeBytes=lastLF+1;}
138
+ let at=0;
139
+ while(at<completeBytes){const end=log.indexOf(10,at);if(end<at)fail();const commit=decode(log.subarray(at,end+1),validateWriterCommit);at=end+1;
140
+ if(commits.length>=WRITER_REVISION_LIMIT)fail("bound");if(commit.epochHash!==b.epochHash||commit.sequence!==commits.length+1||commit.previousHash!==head)fail();
141
+ const file=join(p.epoch,writerRevisionFile(commit.sequence)),raw=read(file,WRITER_REVISION_MAX_BYTES,checks);if(!raw)fail();
142
+ const revision=decode(raw.bytes,validateWriterRevision);boundRevision(revision,b);
143
+ if(revision.sequence!==commit.sequence||revision.previousHash!==head||canonicalJSON(createWriterCommit(revision))!==canonicalJSON(commit))fail();
144
+ if(current&&revision.initialLocatorHash!==current.initialLocatorHash)fail();
145
+ if(revision.journal.sequence<previous||revision.journal.sequence===previous&&anchors.length&&revision.journal.headHash!==anchors.at(-1)!.headHash)fail();
146
+ anchors.push({...revision.journal});previous=revision.journal.sequence;commits.push(commit);current=revision;head=commit.seal;
147
+ }
148
+ const expectedNames=new Set(commits.map(commit=>writerRevisionFile(commit.sequence)));if(logFile)expectedNames.add(WRITER_COMMIT_LOG);
149
+ if(pending){
150
+ const start=pending.previousLogBytes, next=pending.revision.sequence;
151
+ if(start>log.length||sha(log.subarray(0,start))!==pending.previousLogHash||start>0&&log[start-1]!==10)fail();
152
+ const beforeCount=commits.filter(commit=>commit.sequence<next).length;
153
+ if(beforeCount!==next-1||commits.length<next-1||commits.length>next)fail();
154
+ const beforeHead=next===1?writerInventoryGenesis(b.epochHash,b.activationHash):commits[next-2]!.seal;
155
+ if(pending.revision.previousHash!==beforeHead||start!==committedPrefixLength(log,next-1))fail();
156
+ const tail=log.subarray(start),line=encode(pending.commit);if(tail.length>line.length||!tail.equals(line.subarray(0,tail.length)))fail();
157
+ const candidateFile=join(p.epoch,writerRevisionFile(next)),candidate=read(candidateFile,WRITER_REVISION_MAX_BYTES,checks),full=encode(pending.revision);
158
+ if(candidate){if(candidate.bytes.length>full.length||!candidate.bytes.equals(full.subarray(0,candidate.bytes.length)))fail();expectedNames.add(writerRevisionFile(next));}
159
+ if(tail.length&&(!candidate||!candidate.bytes.equals(full)))fail();
160
+ if(current&&current.initialLocatorHash!==pending.revision.initialLocatorHash)fail();
161
+ if(next>1){const anchor=anchors[next-2]!;if(pending.revision.journal.sequence<anchor.sequence||pending.revision.journal.sequence===anchor.sequence&&pending.revision.journal.headHash!==anchor.headHash)fail();}
162
+ }else if(completeBytes!==log.length||logFile&&commits.length===0)fail("pending");
163
+ if(canonicalJSON([...expectedNames].sort())!==canonicalJSON(names))fail();
164
+ const assertCurrent=()=>safe(()=>{for(const check of checks)check();});assertCurrent();
165
+ return{paths:p,log,intentBytes:intentFile?.bytes,result:{current,commits,anchors,sequence:commits.length,headHash:head,logBytes:log.length,logHash:sha(log),pending,appendStarted:!!pending&&log.length>pending.previousLogBytes,assertCurrent}};
166
+ }
167
+ function committedPrefixLength(log:Buffer,count:number):number {let at=0;for(let n=0;n<count;n++){const lf=log.indexOf(10,at);if(lf<0)fail();at=lf+1;}return at;}
168
+ export function readWriterHistory(home:string,input:WriterHistoryBinding,options:{allowPending?:boolean}={}):WriterHistoryRead {
169
+ return safe(()=>snapshot(home,binding(input),options.allowPending===true).result);
170
+ }
171
+ function proof(p:Paths,opts:WriterHistoryOptions):void {
172
+ assertMutationHandle(opts.mutation,p.home);opts.assertCurrent();assertMutationHandle(opts.mutation,p.home);
173
+ for(const [dir,stat]of p.dirs)if(identity(checkedDir(dir))!==identity(stat))fail("changed");
174
+ }
175
+ function syncDir(p:Paths,dir:string,opts:WriterHistoryOptions):void {
176
+ proof(p,opts);const stat=checkedDir(dir),fd=openSync(dir,constants.O_RDONLY|constants.O_DIRECTORY|constants.O_NOFOLLOW);
177
+ try{if(identity(fstatSync(fd,{bigint:true}))!==identity(stat))fail("changed");proof(p,opts);fsyncSync(fd);}finally{closeSync(fd);}proof(p,opts);
178
+ }
179
+ function complete(p:Paths,file:string,full:Buffer,max:number,opts:WriterHistoryOptions,written:WriterHistoryPhase,synced:WriterHistoryPhase,start?:{at:number;phase:WriterHistoryPhase}):void {
180
+ if(full.length>max)fail("bound");proof(p,opts);const prior=read(file,max);if(prior&&(prior.bytes.length>full.length||!prior.bytes.equals(full.subarray(0,prior.bytes.length))))fail();
181
+ let installed:BigIntStats;
182
+ const fd=openSync(file,constants.O_WRONLY|constants.O_NOFOLLOW|constants.O_NONBLOCK|(prior?0:constants.O_CREAT|constants.O_EXCL),0o600);
183
+ try{
184
+ const opened=fstatSync(fd,{bigint:true});owned(opened);if(prior?stamp(opened)!==stamp(prior.stat):opened.size!==0n)fail("changed");
185
+ let observed=opened;proof(p,opts);let at=prior?.bytes.length??0;
186
+ while(at<full.length){proof(p,opts);if(stamp(fstatSync(fd,{bigint:true}))!==stamp(observed)||stamp(lstatSync(file,{bigint:true}))!==stamp(observed))fail("changed");const firstByte=start!==undefined&&at===start.at;const n=writeSync(fd,full,at,firstByte?1:full.length-at,at);if(n<=0)fail();at+=n;observed=fstatSync(fd,{bigint:true});if(firstByte)opts.phase?.(start!.phase);}
187
+ opts.phase?.(written);proof(p,opts);const after=fstatSync(fd,{bigint:true});if(stamp(after)!==stamp(observed)||after.size!==BigInt(full.length)||stamp(lstatSync(file,{bigint:true}))!==stamp(after))fail("changed");fsyncSync(fd);installed=fstatSync(fd,{bigint:true});opts.phase?.(synced);
188
+ if(stamp(lstatSync(file,{bigint:true}))!==stamp(installed))fail("changed");
189
+ }finally{closeSync(fd);}syncDir(p,dirname(file),opts);proof(p,opts);if(stamp(lstatSync(file,{bigint:true}))!==stamp(installed!))fail("changed");
190
+ }
191
+ function checkedIntent(home:string,value:WriterRenewalIntent,opts:WriterHistoryOptions):{intent:WriterRenewalIntent;snapshot:Snapshot}{
192
+ const intent=validateWriterRenewalIntent(value),b=intentBinding(intent),p=paths(home,b);proof(p,opts);
193
+ const read=snapshot(home,b,true,intent);if(!read.intentBytes)fail("pending");return{intent,snapshot:read};
194
+ }
195
+ export function writeWriterRenewalIntent(home:string,value:WriterRenewalIntent,opts:WriterHistoryOptions):void {
196
+ safe(()=>{
197
+ const intent=validateWriterRenewalIntent(value),b=intentBinding(intent),p=paths(home,b);if(intent.home!==p.home)fail();
198
+ if(intent.previousLogBytes+encode(intent.commit).length>WRITER_COMMIT_MAX_BYTES)fail("bound");proof(p,opts);
199
+ const prior=readIntent(p,[],intent).file;
200
+ const old=prior?snapshot(home,b,true,intent):snapshot(home,b,false);
201
+ if(!prior&&(intent.revision.sequence!==old.result.sequence+1||intent.revision.previousHash!==old.result.headHash||intent.previousLogBytes!==old.result.logBytes||intent.previousLogHash!==old.result.logHash))fail("changed");
202
+ old.result.assertCurrent();publishIntent(p,intent,opts);snapshot(home,b,true).result.assertCurrent();opts.phase?.("intent-published");
203
+ });
204
+ }
205
+ export function writeWriterRevision(home:string,value:WriterRenewalIntent,opts:WriterHistoryOptions):void {
206
+ safe(()=>{const {intent,snapshot:read}=checkedIntent(home,value,opts);read.result.assertCurrent();
207
+ publishIntent(read.paths,intent,opts);
208
+ complete(read.paths,join(read.paths.epoch,writerRevisionFile(intent.revision.sequence)),encode(intent.revision),WRITER_REVISION_MAX_BYTES,opts,"revision-written","revision-fsync");
209
+ snapshot(home,intentBinding(intent),true).result.assertCurrent();});
210
+ }
211
+ export function appendWriterCommit(home:string,value:WriterRenewalIntent,opts:WriterHistoryOptions):void {
212
+ safe(()=>{const {intent,snapshot:read}=checkedIntent(home,value,opts);read.result.assertCurrent();
213
+ publishIntent(read.paths,intent,opts);
214
+ complete(read.paths,join(read.paths.epoch,writerRevisionFile(intent.revision.sequence)),encode(intent.revision),WRITER_REVISION_MAX_BYTES,opts,"revision-written","revision-fsync");
215
+ const prefix=read.log.subarray(0,intent.previousLogBytes),full=Buffer.concat([prefix,encode(intent.commit)]);
216
+ complete(read.paths,read.paths.log,full,WRITER_COMMIT_MAX_BYTES,opts,"commit-written","commit-fsync",{at:intent.previousLogBytes,phase:"commit-started"});snapshot(home,intentBinding(intent),true).result.assertCurrent();});
217
+ }
218
+ export function clearWriterRenewalIntent(home:string,value:WriterRenewalIntent,opts:WriterHistoryOptions):void {
219
+ safe(()=>{const intent=validateWriterRenewalIntent(value),p=paths(home,intentBinding(intent));proof(p,opts);
220
+ const prior=readIntent(p,[],intent).file;
221
+ if(prior){const read=snapshot(home,intentBinding(intent),true,intent);if(!prior.bytes.equals(encode(intent))||read.result.headHash!==intent.commit.seal||read.log.length!==intent.previousLogBytes+encode(intent.commit).length)fail("pending");
222
+ appendWriterCommit(home,intent,opts);proof(p,opts);const now=readFileExpected(p.intent,encode(intent),WRITER_RENEWAL_INTENT_MAX_BYTES);proof(p,opts);if(stamp(lstatSync(p.intent,{bigint:true}))!==stamp(now.stat))fail("changed");unlinkSync(p.intent);opts.phase?.("intent-cleared");}
223
+ else {const read=snapshot(home,intentBinding(intent),false);if(read.result.headHash!==intent.commit.seal)fail("changed");}
224
+ syncDir(p,p.epoch,opts);syncDir(p,p.home,opts);proof(p,opts);snapshot(home,intentBinding(intent),false).result.assertCurrent();});
225
+ }
226
+ function readFileExpected(file:string,full:Buffer,max:number):FileRead {const value=read(file,max);if(!value||!value.bytes.equals(full))fail();return value;}
227
+ export function abandonWriterRenewal(home:string,value:WriterRenewalIntent,opts:WriterHistoryOptions):void {
228
+ safe(()=>{const intent=validateWriterRenewalIntent(value),p=paths(home,intentBinding(intent));proof(p,opts);
229
+ if(!readIntent(p,[],intent).file){
230
+ const cleared=snapshot(home,intentBinding(intent),false);
231
+ if(cleared.result.sequence!==intent.revision.sequence-1||cleared.result.headHash!==intent.revision.previousHash||cleared.result.logBytes!==intent.previousLogBytes||cleared.result.logHash!==intent.previousLogHash)fail("changed");
232
+ cleared.result.assertCurrent();syncDir(p,p.epoch,opts);syncDir(p,p.home,opts);proof(p,opts);return;
233
+ }
234
+ const before=snapshot(home,intentBinding(intent),true,intent);
235
+ if(before.result.appendStarted)fail("pending");
236
+ const check=()=>{proof(p,opts);const current=snapshot(home,intentBinding(intent),true,intent);if(current.result.appendStarted||current.log.length!==intent.previousLogBytes||sha(current.log)!==intent.previousLogHash)fail("pending");};
237
+ check();const candidate=join(p.epoch,writerRevisionFile(intent.revision.sequence)),prior=read(candidate,WRITER_REVISION_MAX_BYTES);
238
+ if(prior){check();if(stamp(lstatSync(candidate,{bigint:true}))!==stamp(prior.stat))fail("changed");unlinkSync(candidate);syncDir(p,p.epoch,opts);}
239
+ const log=read(p.log,WRITER_COMMIT_MAX_BYTES);if(intent.revision.sequence===1&&log){if(log.bytes.length)fail("pending");check();if(stamp(lstatSync(p.log,{bigint:true}))!==stamp(log.stat))fail("changed");unlinkSync(p.log);syncDir(p,p.epoch,opts);}
240
+ // Reassert parent durability even when the prior owner removed a member before its fsync.
241
+ syncDir(p,p.epoch,opts);check();const intentRead=readIntent(p,[],intent);if(!intentRead.file)fail("changed");
242
+ if(intentRead.temp){check();unlinkSync(intentRead.temp);syncDir(p,p.home,opts);}
243
+ const pending=read(p.intent,WRITER_RENEWAL_INTENT_MAX_BYTES);
244
+ if(pending){proof(p,opts);if(!pending.bytes.equals(encode(intent))||stamp(lstatSync(p.intent,{bigint:true}))!==stamp(pending.stat))fail("changed");unlinkSync(p.intent);opts.phase?.("intent-cleared");}
245
+ syncDir(p,p.home,opts);proof(p,opts);snapshot(home,intentBinding(intent),false).result.assertCurrent();
246
+ });
247
+ }
248
+
249
+
250
+ /** Person coordinator only: discard unpublished atomic-intent staging, never a registered or committed renewal.
251
+ * This purpose-specific reader exception is deliberately unavailable to ordinary history/resolver callers. */
252
+ export function abandonUnpublishedWriterStaging(home:string,input:WriterHistoryBinding,opts:WriterHistoryOptions&{assertCommittedHistory?(history:WriterHistoryRead):void}):{removed:number}{
253
+ return safe(()=>{
254
+ const b=binding(input),p=paths(home,b);proof(p,opts);
255
+ if(lstatSync(p.intent,{throwIfNoEntry:false}))fail("pending");
256
+ const before=snapshot(home,b,false,undefined,true);before.result.assertCurrent();
257
+ const guarded:WriterHistoryOptions={...opts,assertCurrent(){opts.assertCurrent();opts.assertCommittedHistory?.(before.result);}};
258
+ proof(p,guarded);
259
+ const members=new Map<string,{stat:BigIntStats;hash:string}>();
260
+ for(const name of intentTemps(p)){
261
+ proof(p,guarded);const file=join(p.home,name),value=read(file,WRITER_RENEWAL_INTENT_MAX_BYTES);if(!value)fail("changed");
262
+ members.set(name,{stat:value.stat,hash:sha(value.bytes)});
263
+ }
264
+ const check=()=>{
265
+ proof(p,guarded);before.result.assertCurrent();
266
+ if(lstatSync(p.intent,{throwIfNoEntry:false})||canonicalJSON(intentTemps(p))!==canonicalJSON([...members.keys()].sort()))fail("changed");
267
+ };
268
+ check();let removed=0;
269
+ for(const [name,expected]of members){
270
+ check();const file=join(p.home,name),value=read(file,WRITER_RENEWAL_INTENT_MAX_BYTES);
271
+ if(!value||stamp(value.stat)!==stamp(expected.stat)||sha(value.bytes)!==expected.hash)fail("changed");
272
+ check();if(stamp(lstatSync(file,{bigint:true}))!==stamp(expected.stat))fail("changed");
273
+ unlinkSync(file);members.delete(name);removed++;syncDir(p,p.home,guarded);check();
274
+ }
275
+ // A previous owner may have unlinked the last stage before the directory fsync/acknowledgement.
276
+ syncDir(p,p.epoch,guarded);syncDir(p,p.home,guarded);check();return{removed};
277
+ });
278
+ }
@@ -0,0 +1,179 @@
1
+ /** Person-only same-epoch writer renewal. No source/journal recovery, publication or floor writer. */
2
+ import { randomBytes, createHash } from "node:crypto";
3
+ import { lstatSync, opendirSync } from "node:fs";
4
+ import { join, basename } from "node:path";
5
+ import { types } from "node:util";
6
+ import { canonicalJSON, readProfileJSON, hashJSON, legacyProfileMarkerShape } from "./generation.ts";
7
+ import { validatePlacementProfile } from "./sync-placement-format.ts";
8
+ import { realPath } from "./platform.ts";
9
+ import { readPlacementRuntimeMarkers } from "./sync-placement-guard.ts";
10
+ import { readActivePlacementBinding, type ActivePlacementBinding } from "./sync-placement-active.ts";
11
+ import { readInitialSyncPlacementWriterLocator } from "./sync-placement-writer-roots.ts";
12
+ import { readEffectiveSyncPlacementWriters } from "./sync-placement-effective-writers.ts";
13
+ import { readPlacementJournal, readPlacementWriterJournalAnchors, writerRenewalCapacity } from "./sync-placement-journal-storage.ts";
14
+ import { readWriterHistory, writeWriterRenewalIntent, writeWriterRevision, appendWriterCommit, clearWriterRenewalIntent, abandonWriterRenewal, abandonUnpublishedWriterStaging, type WriterHistoryPhase, type WriterHistoryRead } from "./sync-placement-writer-history.ts";
15
+ import { createWriterRevision, createWriterCommit, createWriterRenewalIntent, writerRevisionWriters, WRITER_REVISION_SCHEMA, WRITER_RENEWAL_INTENT_SCHEMA, WRITER_REVISION_LIMIT, WRITER_COMMIT_LOG, SharedWriterRevisionError, type WriterRenewalIntent } from "./sync-placement-writer-revision-format.ts";
16
+ import { requireWriterRenewalHomeFloor } from "./sync-placement-prerequisites.ts";
17
+ import { withPlacementActivationFences } from "./sync-placement-activation-locks.ts";
18
+ import { assertPlacementActivationOwner, type PlacementActivationOwner } from "./sync-placement-activation-proof.ts";
19
+ import { previewSyncPlacementSources, previewOwnedSyncPlacementSources } from "./sync-placement-capture.ts";
20
+ import { captureInspectionPendingPrunes } from "./sync-placement-inspection-pending.ts";
21
+ import { validatePrunePlan } from "./prune.ts";
22
+ import { initialPlacementWriterContext, previewActivePlacementWriters } from "../sync-placement-writer-context.ts";
23
+ import type { PlacementActivationOptions } from "./sync-placement-activation.ts";
24
+
25
+ export class WriterRenewalError extends Error{
26
+ constructor(readonly causeClass:"pending"|"candidate"|"changed"|"capacity"|"authorization",details=""){
27
+ super(causeClass==="pending"
28
+ ? "Writer renewal refuses an outstanding append, publication, reservation or source-recovery intent; if a precommit renewal is retained, run amem migrate --sync-placement --abandon first. Then restore the inventoried install (for example dot ai rollback amem or the host's previous pin), run amem sync to finish the exact attempt, then preview and apply renewal."
29
+ :causeClass==="candidate"
30
+ ? "Writer renewal requires an explicit complete --writer-root inventory of 0.22.0-or-later installs; upgrade the CLI/host pins, refresh managed setup receipts and run amem migrate --sync-placement --dry-run before applying."
31
+ :causeClass==="capacity"
32
+ ? `Writer renewal metadata capacity is exhausted ${details}; retain this epoch and request a separately reviewed archival/checkpoint protocol before another renewal.`
33
+ :causeClass==="authorization"
34
+ ? "Writer renewal requires explicit person authorization of this exact inventory; have the person preview it with amem migrate --sync-placement --dry-run and then apply."
35
+ : "Writer renewal frozen inventory or binding changed; restore it and run amem migrate --sync-placement --resume, or use --abandon before commit append starts and review a new dry-run.");
36
+ this.name="WriterRenewalError";
37
+ }
38
+ }
39
+ function active(home:string):ActivePlacementBinding{
40
+ const markers=readPlacementRuntimeMarkers(home),value=readActivePlacementBinding(home,markers.profile,markers.shards);if(!value)throw new WriterRenewalError("changed");return value;
41
+ }
42
+ const binding=(a:ActivePlacementBinding)=>({epochHash:a.profile.placementHead,activationHash:a.activation.seal,homeHash:a.epoch.homeHash,generation:a.epoch.generation});
43
+ function list(dir:string,maximum:number):string[]{
44
+ const out:string[]=[],reader=opendirSync(dir);try{for(;;){const e=reader.readSync();if(!e)break;if(out.length>=maximum)throw new SharedWriterRevisionError("bound");out.push(e.name);}}finally{reader.closeSync();}return out.sort();
45
+ }
46
+ function directories(home:string,a:ActivePlacementBinding):string[]{
47
+ const out=[home];for(const r of a.activation.realms)out.push(r.scope==="user"?join(home,"user"):join(home,"projects",r.keyHash.slice(0,16)));
48
+ const checked=(dir:string)=>{const s=lstatSync(dir);if(!s.isDirectory()||s.isSymbolicLink()||s.uid!==process.getuid?.()||(s.mode&0o022)!==0)throw new WriterRenewalError("changed");};
49
+ const projects=join(home,"projects");if(lstatSync(projects,{throwIfNoEntry:false})){
50
+ checked(projects);for(const name of list(projects,256))if(/^[a-f0-9]{16}$/.test(name)){
51
+ const dir=join(projects,name);checked(dir);if(lstatSync(join(dir,"generation.json"),{throwIfNoEntry:false}))out.push(dir);
52
+ }
53
+ }
54
+ for(const dir of out)checked(dir);
55
+ return[...new Set(out)].sort();
56
+ }
57
+ function pendingSources(home:string,owner?:PlacementActivationOwner):()=>void{
58
+ try{
59
+ const source=owner?previewOwnedSyncPlacementSources(home,owner):previewSyncPlacementSources(home),checks=source.directories.map(captureInspectionPendingPrunes);
60
+ for(const dir of source.directories){
61
+ const history=join(dir,"prune-plans","selector-history");if(!lstatSync(history,{throwIfNoEntry:false}))continue;
62
+ const seen=new Set<string>();
63
+ for(const name of list(history,128)){
64
+ const m=/^([a-f0-9]{64})\.json(\.pending)?$/.exec(name);if(!m||m[2])throw new WriterRenewalError("pending");seen.add(m[1]!);
65
+ if(seen.size>64)throw new SharedWriterRevisionError("bound");
66
+ const stat=lstatSync(join(history,name));if(!stat.isFile()||stat.isSymbolicLink()||stat.nlink!==1||stat.uid!==process.getuid?.()||(stat.mode&0o077)!==0||stat.size>262144)throw new WriterRenewalError("pending");
67
+ const plan=validatePrunePlan(readProfileJSON(join(dir,"prune-plans",m[1]+".json")));
68
+ if(plan.seal!==m[1]||plan.rows.length!==1)throw new WriterRenewalError("pending");
69
+ const prior=readProfileJSON(join(history,name));if(!prior||typeof prior!=="object"||hashJSON(prior)!==plan.selectorRevision)throw new WriterRenewalError("pending");
70
+ if((prior as {schema?:unknown}).schema==="agent-mem.profile/v3")validatePlacementProfile(prior);else if(!legacyProfileMarkerShape(prior))throw new WriterRenewalError("pending");
71
+ const row=plan.rows[0]!.payload;
72
+ if(![...source.specialRows.values()].some(item=>item.row.id===row.id&&hashJSON(item.row)===hashJSON(row)
73
+ &&(dir===home||basename(dir)==="user"&&item.key.realm.scope==="user"||item.key.realm.scope==="project"&&item.key.realm.keyHash.slice(0,16)===basename(dir))))throw new WriterRenewalError("pending");
74
+ }
75
+ }
76
+ return()=>{source.assertSourcesUnchanged();for(const check of checks)check();};
77
+ }catch(error){if(error instanceof SharedWriterRevisionError&&error.code==="bound")throw error;throw new WriterRenewalError("pending");}
78
+ }
79
+ function journalClear(home:string,epochHash:string):void{
80
+ try{const j=readPlacementJournal(home,epochHash);if(j.pending||j.publicationReservation)throw Error();}catch{throw new WriterRenewalError("pending");}
81
+ }
82
+ function supported(version:string):boolean{const p=version.split(".").map(Number);return p[0]!>0||p[0]===0&&p[1]!>=22;}
83
+ function person(options:PlacementActivationOptions,intent:WriterRenewalIntent):void{
84
+ let okay=false;try{okay=options.authorize(Object.freeze({seal:intent.revision.seal,generation:intent.revision.generation,homeHash:intent.revision.homeHash}))===true;}catch{}
85
+ if(!okay)throw new WriterRenewalError("authorization");
86
+ }
87
+ export async function renewSyncPlacementWriters(options:PlacementActivationOptions):Promise<Record<string,unknown>|undefined>{
88
+ const home=realPath(options.memoryDir),initial=active(home),b=binding(initial);
89
+ let history:WriterHistoryRead;
90
+ try{history=readWriterHistory(home,b,{allowPending:true});}catch(error){
91
+ if(options.mode!=="abandon"||!error||typeof error!=="object"||types.isProxy(error)||!(error instanceof SharedWriterRevisionError)||Object.getOwnPropertyDescriptor(error,"code")?.value!=="staging")throw error;
92
+ requireWriterRenewalHomeFloor(home);
93
+ return withPlacementActivationFences(home,directories(home,initial),async(mutation,owner)=>{
94
+ const locator=readInitialSyncPlacementWriterLocator(home),seal=hashJSON({action:"abandon-unpublished-writer-staging",...b,initialLocatorHash:locator.seal});
95
+ let authorized=false;try{authorized=options.authorize(Object.freeze({seal,generation:b.generation,homeHash:b.homeHash}))===true;}catch{}
96
+ if(!authorized)throw new WriterRenewalError("authorization");
97
+ const assertCurrent=()=>{assertPlacementActivationOwner(owner);requireWriterRenewalHomeFloor(home);if(canonicalJSON(binding(active(home)))!==canonicalJSON(b)||readInitialSyncPlacementWriterLocator(home).seal!==locator.seal)throw new WriterRenewalError("changed");};
98
+ const assertCommittedHistory=(history:WriterHistoryRead)=>{
99
+ assertCurrent();const journal=readPlacementWriterJournalAnchors(home,b.epochHash);
100
+ for(const anchor of history.anchors)if(anchor.sequence>journal.sequence||(anchor.sequence===0?b.epochHash:journal.records[anchor.sequence-1]?.seal)!==anchor.headHash)throw new WriterRenewalError("changed");
101
+ };
102
+ const cleared=abandonUnpublishedWriterStaging(home,{...b,initialLocatorHash:locator.seal},{mutation,assertCurrent,assertCommittedHistory});
103
+ return{mode:options.mode,active:true,abandoned:true,unpublishedStagingRemoved:cleared.removed,nextStep:"amem migrate --sync-placement --dry-run"};
104
+ });
105
+ }
106
+ if(!history.pending&&(options.mode==="resume"||options.mode==="abandon")&&history.sequence===0)return undefined;
107
+ if(options.mode==="dry-run"){
108
+ if(history.pending)return{mode:options.mode,active:true,renewalSupported:true,pending:true,commitStarted:history.appendStarted,abandonable:!history.appendStarted,sequence:history.sequence,remainingRevisions:WRITER_REVISION_LIMIT-history.sequence};
109
+ const expected=readEffectiveSyncPlacementWriters(home);let floorReady=true;try{requireWriterRenewalHomeFloor(home);}catch{floorReady=false;}
110
+ let recoveryReady=true,objectSlotsRemaining:number|undefined;
111
+ try{journalClear(home,b.epochHash);pendingSources(home);objectSlotsRemaining=256-readPlacementJournal(home,b.epochHash).objects;}catch{recoveryReady=false;}
112
+ const writers=previewActivePlacementWriters(home,options.ownRoot,options.writerRoots),remaining=WRITER_REVISION_LIMIT-history.sequence;
113
+ return{mode:options.mode,active:true,renewalSupported:true,forwardOnly:true,sequence:history.sequence,revisionHash:history.headHash,remainingRevisions:WRITER_REVISION_LIMIT-history.sequence,
114
+ quiescence:"deferred-to-apply",floorReady,requiredHomeFloor:"0.22.0",recoveryReady,...(recoveryReady?{}:{nextStep:new WriterRenewalError("pending").message}),
115
+ ...(objectSlotsRemaining===undefined?{}:{objectSlotsRemaining}),...(remaining<=4?{capacityNotice:`${remaining} renewals remain before the 64-revision ceiling; the shared object budget may refuse earlier. Archival/checkpointing requires a separate reviewed protocol.`}:{}),
116
+ writers,candidateMinimumVersion:"0.22.0",candidatesSupported:writers.complete&&writers.roots.every(r=>supported(r.version)),expectedWriterCount:expected.writers.length};
117
+ }
118
+ requireWriterRenewalHomeFloor(home);
119
+ if(history.pending&&options.mode==="apply")throw new SharedWriterRevisionError("pending");
120
+ if(!history.pending&&options.mode==="abandon")throw new WriterRenewalError("changed");
121
+ if(!history.pending&&options.mode==="apply"&&!options.writerRoots.length)throw new WriterRenewalError("candidate");
122
+ // No new intent is written before every pre-existing recovery obligation has been rejected.
123
+ // An unlocked refusal stays entirely passive; crashed owners must first be reclaimed by the real fences below.
124
+ const passivePreflight=options.mode==="apply"||options.mode==="resume"&&history.pending&&!history.appendStarted&&!lstatSync(join(home,"profile.lock"),{throwIfNoEntry:false});
125
+ let sourceCheck=passivePreflight?(journalClear(home,b.epochHash),pendingSources(home)):undefined;
126
+ return withPlacementActivationFences(home,directories(home,initial),async(mutation,owner)=>{
127
+ if(options.mode==="resume"&&history.pending&&!history.appendStarted){journalClear(home,b.epochHash);sourceCheck=pendingSources(home,owner);}
128
+ let intent=history.pending;
129
+ const cleanupOnly=!intent&&options.mode==="resume"&&!!history.current;
130
+ if(cleanupOnly){
131
+ const prefix=Buffer.from(history.commits.slice(0,-1).map(row=>canonicalJSON(row)+"\n").join(""));
132
+ intent=createWriterRenewalIntent({schema:WRITER_RENEWAL_INTENT_SCHEMA,home,revision:history.current!,commit:history.commits.at(-1)!,previousLogBytes:prefix.length,previousLogHash:createHash("sha256").update(prefix).digest("hex")});
133
+ }
134
+ const locator=readInitialSyncPlacementWriterLocator(home);
135
+ const metadata=()=>{
136
+ assertPlacementActivationOwner(owner);requireWriterRenewalHomeFloor(home);
137
+ const now=active(home);if(canonicalJSON(binding(now))!==canonicalJSON(b)||readInitialSyncPlacementWriterLocator(home).seal!==locator.seal)throw new WriterRenewalError("changed");
138
+ if(intent){const journal=readPlacementWriterJournalAnchors(home,b.epochHash),anchor=intent.revision.journal;
139
+ if(anchor.sequence>journal.sequence||(anchor.sequence===0?b.epochHash:journal.records[anchor.sequence-1]?.seal)!==anchor.headHash)throw new WriterRenewalError("changed");}
140
+ };
141
+ metadata();
142
+ if(cleanupOnly){
143
+ person(options,intent!);clearWriterRenewalIntent(home,intent!,{mutation,assertCurrent:metadata});
144
+ return{mode:options.mode,active:true,renewed:false,alreadyCommitted:true,sequence:history.sequence,remainingRevisions:WRITER_REVISION_LIMIT-history.sequence};
145
+ }
146
+ if(!intent){
147
+ journalClear(home,b.epochHash);sourceCheck?.();
148
+ const current=readWriterHistory(home,{...b,initialLocatorHash:locator.seal});
149
+ if(current.sequence>=WRITER_REVISION_LIMIT)throw new SharedWriterRevisionError("cap");
150
+ const candidate=initialPlacementWriterContext(home,options.writerRoots,options.ownRoot);if(candidate.measurement.roots.some(r=>!supported(r.version)))throw new WriterRenewalError("candidate");
151
+ candidate.attest();const previous=current.current?writerRevisionWriters(current.current):initial.activation.writers;
152
+ if(canonicalJSON(previous)===canonicalJSON(candidate.measurement.writers))return{mode:options.mode,active:true,renewed:false,sequence:current.sequence,remainingRevisions:WRITER_REVISION_LIMIT-current.sequence};
153
+ const journal=readPlacementJournal(home,b.epochHash),revision=createWriterRevision({schema:WRITER_REVISION_SCHEMA,...b,initialLocatorHash:locator.seal,sequence:current.sequence+1,previousHash:current.headHash,
154
+ journal:{sequence:journal.sequence,headHash:journal.headHash},roots:candidate.measurement.roots,requestId:randomBytes(32).toString("hex")});
155
+ intent=createWriterRenewalIntent({schema:WRITER_RENEWAL_INTENT_SCHEMA,home,revision,commit:createWriterCommit(revision),previousLogBytes:current.logBytes,previousLogHash:current.logHash});
156
+ }
157
+ const fixed=intent;person(options,fixed);
158
+ const log=join(home,"sync-placement","epochs",b.epochHash,WRITER_COMMIT_LOG);
159
+ const started=()=>{const s=lstatSync(log,{throwIfNoEntry:false});return!!s&&s.isFile()&&!s.isSymbolicLink()&&s.size>fixed.previousLogBytes;};
160
+ const check=()=>{
161
+ metadata();
162
+ try{writerRenewalCapacity(home,b.epochHash,fixed.revision.sequence);}catch{throw new WriterRenewalError("capacity",`(remaining ${WRITER_REVISION_LIMIT-history.sequence} revision slots before the independent 64-revision ceiling)`);}
163
+ if(!started()&&options.mode!=="abandon"){
164
+ journalClear(home,b.epochHash);sourceCheck?.();
165
+ const candidate=initialPlacementWriterContext(home,fixed.revision.roots.map(r=>r.root),options.ownRoot);candidate.attest();
166
+ if(canonicalJSON(candidate.measurement.roots)!==canonicalJSON(fixed.revision.roots))throw new WriterRenewalError("changed");
167
+ const j=readPlacementJournal(home,b.epochHash);if(j.sequence!==fixed.revision.journal.sequence||j.headHash!==fixed.revision.journal.headHash)throw new WriterRenewalError("changed");
168
+ }
169
+ };
170
+ const storage={mutation,assertCurrent:check,phase:(phase:WriterHistoryPhase)=>options.renewalCheckpoint?.(phase)};
171
+ if(options.mode==="abandon"){
172
+ abandonWriterRenewal(home,fixed,{...storage,assertCurrent:metadata});
173
+ return{mode:options.mode,active:true,abandoned:true,sequence:history.sequence,remainingRevisions:WRITER_REVISION_LIMIT-history.sequence};
174
+ }
175
+ check();writeWriterRenewalIntent(home,fixed,storage);writeWriterRevision(home,fixed,storage);appendWriterCommit(home,fixed,storage);
176
+ clearWriterRenewalIntent(home,fixed,{...storage,assertCurrent:metadata});
177
+ return{mode:options.mode,active:true,renewed:true,epochHash:b.epochHash,sequence:fixed.revision.sequence,revisionHash:fixed.commit.seal,remainingRevisions:WRITER_REVISION_LIMIT-fixed.revision.sequence};
178
+ });
179
+ }