@staix/agent-mem 0.21.0 → 0.22.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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +27 -1
- package/package.json +1 -1
- package/src/cli.ts +34 -4
- package/src/commands-table.ts +3 -0
- package/src/index.ts +6 -0
- package/src/memory/commands.ts +3 -2
- package/src/memory/public-prune-activation.ts +4 -4
- package/src/memory/shard-sync.ts +1 -2
- package/src/memory/sync-cadence.ts +2 -1
- package/src/memory/sync-placement-abandon.ts +142 -0
- package/src/memory/sync-placement-activation-locks.ts +28 -0
- package/src/memory/sync-placement-activation-path.ts +2 -0
- package/src/memory/sync-placement-activation-proof.ts +68 -0
- package/src/memory/sync-placement-activation-storage.ts +167 -0
- package/src/memory/sync-placement-activation.ts +185 -0
- package/src/memory/sync-placement-capture.ts +104 -23
- package/src/memory/sync-placement-effective-writers.ts +84 -0
- package/src/memory/sync-placement-guard.ts +12 -6
- package/src/memory/sync-placement-inspection.ts +5 -1
- package/src/memory/sync-placement-journal-storage.ts +51 -7
- package/src/memory/sync-placement-journal.ts +11 -4
- package/src/memory/sync-placement-prerequisites.ts +50 -0
- package/src/memory/sync-placement-transport.ts +17 -6
- package/src/memory/sync-placement-writer-history.ts +278 -0
- package/src/memory/sync-placement-writer-renewal.ts +179 -0
- package/src/memory/sync-placement-writer-revision-format.ts +122 -0
- package/src/memory/sync-placement-writer-roots.ts +167 -0
- package/src/memory/sync-placement-writers.ts +135 -0
- package/src/memory/sync.ts +1 -2
- package/src/setup.ts +101 -14
- package/src/sync-placement-writer-context.ts +136 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/** CLI adapter only. Expected locators are never returned as fresh writer facts. */
|
|
2
|
+
import { types } from "node:util";
|
|
3
|
+
import { realPath } from "./memory/platform.ts";
|
|
4
|
+
import { canonicalJSON } from "./memory/generation.ts";
|
|
5
|
+
import { readSetupWriterRoots } from "./setup.ts";
|
|
6
|
+
import { measureSyncPlacementWriters, extractSyncPlacementWriterFailure, type SyncPlacementWriterFailureDetail, type SyncPlacementWriterMeasurement } from "./memory/sync-placement-writers.ts";
|
|
7
|
+
import { readEffectiveSyncPlacementWriters } from "./memory/sync-placement-effective-writers.ts";
|
|
8
|
+
import { requirePlacementHomeFloor } from "./memory/sync-placement-prerequisites.ts";
|
|
9
|
+
import type { PlacementCapturePort } from "./memory/sync-placement-capture.ts";
|
|
10
|
+
import { SharedWriterRevisionError } from "./memory/sync-placement-writer-revision-format.ts";
|
|
11
|
+
|
|
12
|
+
export type PlacementWriterRootCategory = "running CLI" | "setup receipt" | "declared writer" | "active locator" | "home floor" | "writer renewal";
|
|
13
|
+
export type PlacementWriterContextPhase = "initial" | "active";
|
|
14
|
+
const failureDetails:Record<SyncPlacementWriterFailureDetail,string>={
|
|
15
|
+
"below-minimum":"release is below 0.21.0", "wrong-package":"package is not @staix/agent-mem", "writable-runtime":"runtime code is group- or world-writable", unreadable:"runtime files are unreadable or missing",
|
|
16
|
+
"invalid-version":"package version is not a supported plain release", "invalid-inventory":"root inventory is invalid or duplicated", "unsafe-runtime":"runtime files have unsafe types or symlinks",
|
|
17
|
+
"missing-runtime":"required bin/amem.js or src/index.ts is missing",bound:"inventory exceeds a supported bound",changed:"install changed during measurement",
|
|
18
|
+
};
|
|
19
|
+
function writerContextMessage(category:PlacementWriterRootCategory,causeClass:"missing"|"changed"|"unavailable",phase:PlacementWriterContextPhase,detail?:SyncPlacementWriterFailureDetail,renewalState?:"pending"|"staging"):string {
|
|
20
|
+
if(category==="writer renewal")return new SharedWriterRevisionError(renewalState==="staging"?"staging":"pending").message;
|
|
21
|
+
if(category==="active locator") return `Sync placement initial root locator is ${causeClass} (active locator category); have the person inspect sync-placement-writer-roots.json and the matching activation metadata with a compatible recovery release, then run amem migrate --sync-placement --dry-run.`;
|
|
22
|
+
if(category==="home floor") return `Sync placement reader-floor proof is ${causeClass} (home floor category); have the person inspect formats.json and use a reader compatible with its recorded floor, then run amem migrate --sync-placement --dry-run.`;
|
|
23
|
+
const cause=detail?`; ${failureDetails[detail]}`:"";
|
|
24
|
+
if(phase==="initial")return `Sync placement initial writer proof is ${causeClass} (${category} category${cause}); ${category==="setup receipt"?"upgrade or repair the managed amem install and its host exact pin, then rerun amem setup <target> --apply":"upgrade or repair the amem install and its host exact pin"}. Declare the complete writer roots and rerun amem migrate --sync-placement --dry-run.`;
|
|
25
|
+
return `Sync placement writer proof is ${causeClass} (${category} category${cause}); restore the inventoried install, or have the person run amem migrate --sync-placement --dry-run and --apply with the complete --writer-root inventory after installing the required 0.22.0 home floor.`;
|
|
26
|
+
}
|
|
27
|
+
export class PlacementWriterContextError extends Error {
|
|
28
|
+
constructor(readonly category: PlacementWriterRootCategory, readonly causeClass: "missing" | "changed" | "unavailable",readonly phase:PlacementWriterContextPhase="active",readonly detail?:SyncPlacementWriterFailureDetail,readonly renewalState?:"pending"|"staging") {
|
|
29
|
+
super(writerContextMessage(category,causeClass,phase,detail,renewalState)); this.name = "PlacementWriterContextError";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function safeRefusal(error: unknown, category: PlacementWriterRootCategory,phase:PlacementWriterContextPhase="active"): PlacementWriterContextError {
|
|
33
|
+
if(error&&typeof error==="object"&&!types.isProxy(error)&&error instanceof SharedWriterRevisionError){const code=Object.getOwnPropertyDescriptor(error,"code")?.value;if(code==="pending"||code==="staging")return new PlacementWriterContextError("writer renewal","unavailable",phase,undefined,code);}
|
|
34
|
+
if (error && typeof error === "object" && !types.isProxy(error) && error instanceof PlacementWriterContextError) {
|
|
35
|
+
const fields=Object.getOwnPropertyDescriptors(error),root=fields.category,cause=fields.causeClass,detail=fields.detail;
|
|
36
|
+
const category:unknown=root&&"value"in root?root.value:undefined,causeClass:unknown=cause&&"value"in cause?cause.value:undefined;
|
|
37
|
+
if (typeof category==="string" && ["running CLI","setup receipt","declared writer","active locator","home floor","writer renewal"].includes(category)
|
|
38
|
+
&& typeof causeClass==="string" && ["missing","changed","unavailable"].includes(causeClass)) {
|
|
39
|
+
const ownDetail=detail&&"value"in detail&&typeof detail.value==="string"&&Object.hasOwn(failureDetails,detail.value)?detail.value as SyncPlacementWriterFailureDetail:undefined;
|
|
40
|
+
const state=fields.renewalState?.value;
|
|
41
|
+
return new PlacementWriterContextError(category as PlacementWriterRootCategory,causeClass as PlacementWriterContextError["causeClass"],phase,ownDetail,state==="staging"||state==="pending"?state:undefined);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return new PlacementWriterContextError(category,"unavailable",phase);
|
|
45
|
+
}
|
|
46
|
+
const fail = (category: PlacementWriterRootCategory, cause: PlacementWriterContextError["causeClass"],phase:PlacementWriterContextPhase="active",detail?:SyncPlacementWriterFailureDetail): never => { throw new PlacementWriterContextError(category,cause,phase,detail); };
|
|
47
|
+
function required(home: string, ownRoot: string,phase:PlacementWriterContextPhase="active") {
|
|
48
|
+
let own: string, receipt: string[];
|
|
49
|
+
try { own = realPath(ownRoot); } catch { return fail("running CLI","unavailable",phase,"unreadable"); }
|
|
50
|
+
try { receipt = readSetupWriterRoots(home); } catch { return fail("setup receipt","unavailable",phase,"unreadable"); }
|
|
51
|
+
return { own, receipt, roots: [...new Set([own,...receipt])].sort() };
|
|
52
|
+
}
|
|
53
|
+
function rootCategory(root:string|undefined,needed:ReturnType<typeof required>):PlacementWriterRootCategory {
|
|
54
|
+
if(root===needed.own)return "running CLI";return root&&needed.receipt.includes(root)?"setup receipt":"declared writer";
|
|
55
|
+
}
|
|
56
|
+
function measurementRefusal(error:unknown,roots:readonly string[],needed:ReturnType<typeof required>,phase:PlacementWriterContextPhase):PlacementWriterContextError {
|
|
57
|
+
const detail=extractSyncPlacementWriterFailure(error);let root:string|undefined;
|
|
58
|
+
if(detail?.rootIndex!==undefined){try{root=realPath(roots[detail.rootIndex]!);}catch{/* Missing noncanonical declared path is still unavailable, never fresh evidence. */}}
|
|
59
|
+
return new PlacementWriterContextError(rootCategory(root,needed),detail?.code==="changed"?"changed":"unavailable",phase,detail?.detail??"unreadable");
|
|
60
|
+
}
|
|
61
|
+
function measureComplete(home: string, roots: readonly string[], ownRoot: string,phase:PlacementWriterContextPhase="active"): SyncPlacementWriterMeasurement {
|
|
62
|
+
const before = required(home,ownRoot,phase);let result: SyncPlacementWriterMeasurement;
|
|
63
|
+
try { result = measureSyncPlacementWriters(roots); } catch(error) { throw measurementRefusal(error,roots,before,phase); }
|
|
64
|
+
if (!result.roots.some(row => row.root === before.own)) fail("running CLI","missing",phase);
|
|
65
|
+
if (before.receipt.some(root => !result.roots.some(row => row.root === root))) fail("setup receipt","missing",phase);
|
|
66
|
+
const after = required(home,ownRoot,phase);
|
|
67
|
+
if (after.own !== before.own) fail("running CLI","changed",phase,"changed");
|
|
68
|
+
if (canonicalJSON(after.receipt) !== canonicalJSON(before.receipt)) fail("setup receipt","changed",phase,"changed");
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
export interface PlacementWriterContext {
|
|
72
|
+
measurement: SyncPlacementWriterMeasurement;
|
|
73
|
+
attest: PlacementCapturePort["attest"];
|
|
74
|
+
lastRefusal?: PlacementWriterContextError;
|
|
75
|
+
}
|
|
76
|
+
/** Preview creates no quiescence claim. The coordinator invokes attest only while its live apply fences are held. */
|
|
77
|
+
export function initialPlacementWriterContext(home: string, roots: readonly string[], ownRoot: string): PlacementWriterContext {
|
|
78
|
+
const selected = roots.length ? [...roots] : required(home,ownRoot,"initial").roots;
|
|
79
|
+
const measurement = measureComplete(home,selected,ownRoot,"initial"), canonical = measurement.roots.map(row=>row.root);
|
|
80
|
+
const floor = requirePlacementHomeFloor(home);
|
|
81
|
+
const context: PlacementWriterContext = { measurement, attest: () => {
|
|
82
|
+
try {
|
|
83
|
+
if (requirePlacementHomeFloor(home) !== floor) return fail("home floor","changed","initial");
|
|
84
|
+
const current = measureComplete(home,canonical,ownRoot,"initial");
|
|
85
|
+
if (canonicalJSON(current.writers) !== canonicalJSON(measurement.writers)) {
|
|
86
|
+
const changed=current.roots.find(row=>!measurement.writers.some(old=>old.writerId===row.writerId&&old.version===row.version&&old.pin===row.pin));
|
|
87
|
+
return fail(rootCategory(changed?.root,required(home,ownRoot,"initial")),"changed","initial","changed");
|
|
88
|
+
}
|
|
89
|
+
return { writers: current.writers, legacySyncQuiesced: true };
|
|
90
|
+
} catch (error) { context.lastRefusal = safeRefusal(error,"declared writer","initial"); throw context.lastRefusal; }
|
|
91
|
+
} }; return context;
|
|
92
|
+
}
|
|
93
|
+
/** Active callbacks re-read the bound locator AND measure every package anew. No activation or renewal occurs. */
|
|
94
|
+
export function activePlacementWriterContext(home: string, ownRoot: string): PlacementWriterContext {
|
|
95
|
+
const read = () => {
|
|
96
|
+
requirePlacementHomeFloor(home);
|
|
97
|
+
const effective=readEffectiveSyncPlacementWriters(home);
|
|
98
|
+
const roots=effective.roots;if(!roots)return fail("active locator","unavailable");
|
|
99
|
+
const needed = required(home,ownRoot);
|
|
100
|
+
if(!roots.includes(needed.own))return fail("running CLI","missing");
|
|
101
|
+
if(needed.receipt.some(root=>!roots.includes(root)))return fail("setup receipt","missing");
|
|
102
|
+
const measurement = measureComplete(home,roots,ownRoot);
|
|
103
|
+
if (canonicalJSON(measurement.writers) !== canonicalJSON(effective.writers)) {
|
|
104
|
+
const changed=measurement.roots.find(row=>!effective.writers.some(old=>old.writerId===row.writerId&&old.version===row.version&&old.pin===row.pin));
|
|
105
|
+
return fail(rootCategory(changed?.root,needed),"changed","active","changed");
|
|
106
|
+
}
|
|
107
|
+
effective.assertCurrent();
|
|
108
|
+
return measurement;
|
|
109
|
+
};
|
|
110
|
+
const measurement = read(), context: PlacementWriterContext = { measurement, attest: () => {
|
|
111
|
+
try { return { writers: read().writers, legacySyncQuiesced: true }; }
|
|
112
|
+
catch (error) { context.lastRefusal = safeRefusal(error,"active locator"); throw context.lastRefusal; }
|
|
113
|
+
} }; return context;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Person preview only: incomplete measurements remain explicit and never become an attestation. */
|
|
117
|
+
export function previewActivePlacementWriters(home: string, ownRoot: string, requested: readonly string[] = []) {
|
|
118
|
+
const effective=readEffectiveSyncPlacementWriters(home),mapped=effective.roots;if(!mapped)return fail("active locator","unavailable");
|
|
119
|
+
const selected = requested.length ? [...requested] : mapped;
|
|
120
|
+
const expected = effective.writers;
|
|
121
|
+
let measurement: SyncPlacementWriterMeasurement;
|
|
122
|
+
try { measurement = measureSyncPlacementWriters(selected); }
|
|
123
|
+
catch { return { complete:false, matches:false, roots:[] as SyncPlacementWriterMeasurement["roots"], expected, missingCategories:["declared writer"] as PlacementWriterRootCategory[], differences:expected.map(row=>({writerId:row.writerId,state:"unverified" as const})),refusalCategory:"declared writer" as PlacementWriterRootCategory }; }
|
|
124
|
+
const needed = required(home,ownRoot), missingCategories: PlacementWriterRootCategory[] = [];
|
|
125
|
+
if (!measurement.roots.some(row=>row.root===needed.own)) missingCategories.push("running CLI");
|
|
126
|
+
if (needed.receipt.some(root=>!measurement.roots.some(row=>row.root===root))) missingCategories.push("setup receipt");
|
|
127
|
+
const differences = expected.map(row=>{
|
|
128
|
+
const actual = measurement.writers.find(value=>value.writerId===row.writerId);
|
|
129
|
+
return {writerId:row.writerId,state:!actual?"missing":canonicalJSON(row)===canonicalJSON(actual)?"unchanged":"changed"};
|
|
130
|
+
});
|
|
131
|
+
for (const row of measurement.writers) if (!expected.some(value=>value.writerId===row.writerId)) differences.push({writerId:row.writerId,state:"added"});
|
|
132
|
+
const matches=missingCategories.length===0&&differences.every(row=>row.state==="unchanged");
|
|
133
|
+
const changed=measurement.roots.find(row=>{const old=expected.find(value=>value.writerId===row.writerId);return !old||old.pin!==row.pin||old.version!==row.version;});
|
|
134
|
+
const refusalCategory:PlacementWriterRootCategory=missingCategories[0]??(changed?.root===needed.own?"running CLI":changed&&needed.receipt.includes(changed.root)?"setup receipt":"declared writer");
|
|
135
|
+
return {complete:true,matches,roots:measurement.roots,expected,missingCategories,differences,refusalCategory};
|
|
136
|
+
}
|