@staix/agent-mem 0.20.3 → 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 +30 -3
- package/package.json +2 -2
- package/src/cli.ts +36 -3
- package/src/commands-table.ts +3 -0
- package/src/hook-spool.ts +45 -3
- package/src/index.ts +31 -2
- package/src/memory/bridge.ts +66 -18
- package/src/memory/candidates.ts +71 -9
- package/src/memory/commands.ts +3 -2
- package/src/memory/generation-index.ts +95 -18
- package/src/memory/generation-sync.ts +3 -0
- package/src/memory/generation.ts +33 -15
- package/src/memory/platform.ts +38 -3
- package/src/memory/policy.ts +28 -15
- package/src/memory/prune-publisher-lineage.ts +50 -12
- package/src/memory/public-prune-activation.ts +4 -4
- package/src/memory/public-prune-advance.ts +11 -5
- package/src/memory/public-prune-inputs.ts +17 -16
- package/src/memory/shard-sync-applications.ts +153 -0
- package/src/memory/shard-sync-format.ts +15 -0
- package/src/memory/shard-sync-graph.ts +6 -4
- package/src/memory/shard-sync-history.ts +238 -0
- package/src/memory/shard-sync.ts +66 -151
- package/src/memory/shard.ts +32 -6
- package/src/memory/store.ts +14 -1
- package/src/memory/sync-binding.ts +63 -0
- 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-active.ts +75 -0
- package/src/memory/sync-placement-cache.ts +32 -0
- package/src/memory/sync-placement-capture.ts +373 -0
- package/src/memory/sync-placement-dependencies.ts +255 -0
- package/src/memory/sync-placement-effective-writers.ts +84 -0
- package/src/memory/sync-placement-format.ts +182 -0
- package/src/memory/sync-placement-git.ts +95 -0
- package/src/memory/sync-placement-guard.ts +104 -0
- package/src/memory/sync-placement-inspection-evidence.ts +257 -0
- package/src/memory/sync-placement-inspection-pending.ts +66 -0
- package/src/memory/sync-placement-inspection.ts +240 -0
- package/src/memory/sync-placement-journal-format.ts +114 -0
- package/src/memory/sync-placement-journal-storage.ts +557 -0
- package/src/memory/sync-placement-journal.ts +160 -0
- package/src/memory/sync-placement-materialize.ts +149 -0
- package/src/memory/sync-placement-plan.ts +171 -0
- package/src/memory/sync-placement-prerequisites.ts +50 -0
- package/src/memory/sync-placement-projection.ts +79 -0
- package/src/memory/sync-placement-publication-plan.ts +189 -0
- package/src/memory/sync-placement-publication-storage.ts +87 -0
- package/src/memory/sync-placement-transport.ts +500 -0
- 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-policy-inspection.ts +126 -0
- package/src/memory/sync-screen.ts +128 -33
- package/src/memory/sync.ts +19 -64
- package/src/setup.ts +101 -14
- package/src/sync-placement-writer-context.ts +136 -0
- package/src/ui/inbox-cache.ts +219 -0
- package/src/ui/inbox-worker-version.ts +14 -0
- package/src/ui/inbox-worker.ts +32 -0
- package/src/ui/inbox.ts +281 -0
- package/src/ui/panel.js +7 -3
- package/src/ui/plugin.ts +26 -53
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/** Complete selected-realm immutable history audit; Git acquisition and binding checks belong to the caller. */
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { types } from "node:util";
|
|
4
|
+
import { canonicalJSON, validGeneration } from "./generation.ts";
|
|
5
|
+
import { validatePruneRow, type PruneRow } from "./prune.ts";
|
|
6
|
+
import { verifyPublicPruneWitness } from "./public-prune-witness.ts";
|
|
7
|
+
import { parsePublicPruneGitTree } from "./shard-sync-graph.ts";
|
|
8
|
+
import { decodeShardDescriptor, decodePlacementTransportDescriptor, decodeShardPack, decodeShardManifest, isShardManifestPath,
|
|
9
|
+
shardSyncRef, SHARD_PACK_MAX_BYTES, type ShardSyncRealm, type ShardSyncRow } from "./shard-sync-format.ts";
|
|
10
|
+
|
|
11
|
+
export interface ShardHistoryOrigin { ref: string; commit: string; pack: string; blob: string }
|
|
12
|
+
export interface ShardHistoryManifest { path: string; bytes: Buffer; value: unknown; row: PruneRow; eligible: boolean; commit: string; blob: string }
|
|
13
|
+
export interface ShardHistory {
|
|
14
|
+
rows: Map<string, ShardSyncRow>; files: Map<string, Buffer>; origins: Map<string, ShardHistoryOrigin>;
|
|
15
|
+
version: "v1" | "v2" | "v3"; manifests: Map<string, ShardHistoryManifest>;
|
|
16
|
+
}
|
|
17
|
+
export interface ShardHistoryAuditorOptions {
|
|
18
|
+
realm: ShardSyncRealm; generation: string; placement: boolean; publicPrune?: boolean;
|
|
19
|
+
/** Complete successful Git output only. The port rechecks receiving bindings around every await. */
|
|
20
|
+
git(args: string[]): Promise<string>;
|
|
21
|
+
/** Caller acquisition bounds and validates objects. Audit also retains identity/newline/bound checks. */
|
|
22
|
+
blob(oid: string, path: string): Promise<Buffer>;
|
|
23
|
+
/** Required by placement publication. Omitting this retains only the legacy trusted-Git abstraction.
|
|
24
|
+
* Body bytes, without the Git object header; the auditor verifies each requested identity itself. */
|
|
25
|
+
object?(oid: string, type: "commit" | "tree"): Promise<Buffer>;
|
|
26
|
+
}
|
|
27
|
+
export type ShardHistoryCause = "unavailable" | "blob" | "forbidden-entry" | "public-prune-floor";
|
|
28
|
+
const refusals: Record<ShardHistoryCause, string> = {
|
|
29
|
+
unavailable: "Shard history is unavailable, inconsistent or unsupported; retain and inspect the complete selected history before continuing.",
|
|
30
|
+
blob: "Shard history blob is unavailable or its identity, bound or complete newline is invalid; retain and inspect the selected history before continuing.",
|
|
31
|
+
"forbidden-entry": "Shard history contains a forbidden historical tree entry; retain and inspect the selected history before continuing.",
|
|
32
|
+
"public-prune-floor": "Shard history requires the v2 public prune floor; activate compatible public-prune support before continuing.",
|
|
33
|
+
};
|
|
34
|
+
export class ShardHistoryError extends Error {
|
|
35
|
+
readonly code: ShardHistoryCause;
|
|
36
|
+
constructor(code: ShardHistoryCause = "unavailable") {
|
|
37
|
+
const safe = typeof code === "string" && Object.hasOwn(refusals, code) ? code : "unavailable";
|
|
38
|
+
super(refusals[safe]); this.code = safe; this.name = "ShardHistoryError";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function cause(error: unknown): ShardHistoryCause {
|
|
42
|
+
try {
|
|
43
|
+
if (!error || typeof error !== "object" || types.isProxy(error) || !(error instanceof ShardHistoryError)) return "unavailable";
|
|
44
|
+
const descriptor = Object.getOwnPropertyDescriptor(error, "code");
|
|
45
|
+
return descriptor && "value" in descriptor && typeof descriptor.value === "string" && Object.hasOwn(refusals, descriptor.value)
|
|
46
|
+
? descriptor.value as ShardHistoryCause : "unavailable";
|
|
47
|
+
} catch { return "unavailable"; }
|
|
48
|
+
}
|
|
49
|
+
/** Never reads an external message/getter; only this module's closed own data-code selects safe static text. */
|
|
50
|
+
export function shardHistoryRefusal(error: unknown): string { return refusals[cause(error)]; }
|
|
51
|
+
function fail(code: ShardHistoryCause = "unavailable"): never { throw new ShardHistoryError(code); }
|
|
52
|
+
const oid = (v: unknown): v is string => typeof v === "string" && v.length === 40 && /^[a-f0-9]+$/.test(v);
|
|
53
|
+
const packPath = (v: string) => /^rows\/[a-f0-9]{64}\/[a-f0-9]{64}\.jsonl$/.exec(v)?.[0] === v;
|
|
54
|
+
const digest = (bytes: Uint8Array | string) => createHash("sha256").update(bytes).digest("hex");
|
|
55
|
+
const rank = { v1: 1, v2: 2, v3: 3 };
|
|
56
|
+
function detached(value: ShardHistory): ShardHistory {
|
|
57
|
+
return { version: value.version, rows: new Map([...value.rows].map(([id, row]) => [id, structuredClone(row)])),
|
|
58
|
+
files: new Map([...value.files].map(([path, bytes]) => [path, Buffer.from(bytes)])),
|
|
59
|
+
origins: new Map([...value.origins].map(([id, origin]) => [id, { ...origin }])),
|
|
60
|
+
manifests: new Map([...value.manifests].map(([path, manifest]) => [path, { ...manifest, bytes: Buffer.from(manifest.bytes), value: structuredClone(manifest.value), row: structuredClone(manifest.row) }])) };
|
|
61
|
+
}
|
|
62
|
+
export class ShardHistoryAuditor {
|
|
63
|
+
readonly #options: ShardHistoryAuditorOptions;
|
|
64
|
+
readonly #ref: string;
|
|
65
|
+
readonly #histories = new Map<string, ShardHistory>();
|
|
66
|
+
readonly #audited = new Set<string>();
|
|
67
|
+
readonly #blobs = new Map<string, Buffer>();
|
|
68
|
+
readonly #decoded = new Map<string, ShardSyncRow[]>();
|
|
69
|
+
readonly #origins = new Map<string, ShardHistoryOrigin>();
|
|
70
|
+
readonly #manifestOrigins = new Map<string, { commit: string; blob: string }>();
|
|
71
|
+
readonly #parents = new Map<string, string[]>();
|
|
72
|
+
#running = false;
|
|
73
|
+
constructor(options: ShardHistoryAuditorOptions) {
|
|
74
|
+
this.#ref = shardSyncRef(options.realm);
|
|
75
|
+
if (!validGeneration(options.generation) || typeof options.placement !== "boolean" || options.publicPrune !== undefined && typeof options.publicPrune !== "boolean") fail();
|
|
76
|
+
this.#options = { ...options, realm: structuredClone(options.realm), publicPrune: options.publicPrune ?? options.placement };
|
|
77
|
+
}
|
|
78
|
+
async #blob(object: string, path: string): Promise<Buffer> {
|
|
79
|
+
try {
|
|
80
|
+
const maximum = path === "shard.json" || isShardManifestPath(path) ? 64 * 1024 : SHARD_PACK_MAX_BYTES;
|
|
81
|
+
let bytes = this.#blobs.get(object);
|
|
82
|
+
if (!bytes) {
|
|
83
|
+
const result = await this.#options.blob(object, path);
|
|
84
|
+
if (!(result instanceof Uint8Array)) fail("blob"); bytes = Buffer.from(result);
|
|
85
|
+
if (bytes.length < 1 || bytes.length > maximum || bytes.at(-1) !== 10
|
|
86
|
+
|| createHash("sha1").update(`blob ${bytes.length}\0`).update(bytes).digest("hex") !== object) fail("blob");
|
|
87
|
+
this.#blobs.set(object, bytes);
|
|
88
|
+
}
|
|
89
|
+
if (bytes.length > maximum) fail("blob"); return bytes;
|
|
90
|
+
} catch { return fail("blob"); }
|
|
91
|
+
}
|
|
92
|
+
async #object(object: string, type: "commit" | "tree"): Promise<Buffer> {
|
|
93
|
+
if (!this.#options.object || !oid(object)) return fail();
|
|
94
|
+
const body = await this.#options.object(object, type);
|
|
95
|
+
if (!(body instanceof Uint8Array) || body.length > (type === "commit" ? 65536 : SHARD_PACK_MAX_BYTES)) return fail();
|
|
96
|
+
const bytes = Buffer.from(body);
|
|
97
|
+
if (createHash("sha1").update(`${type} ${bytes.length}\0`).update(bytes).digest("hex") !== object) return fail();
|
|
98
|
+
return bytes;
|
|
99
|
+
}
|
|
100
|
+
async #commit(commit: string, expected?: readonly string[]): Promise<string> {
|
|
101
|
+
const bytes = await this.#object(commit, "commit"), end = bytes.indexOf(10), headerEnd = bytes.indexOf(Buffer.from("\n\n"));
|
|
102
|
+
if (end !== 45 || headerEnd < end || !bytes.subarray(0, 5).equals(Buffer.from("tree "))) return fail();
|
|
103
|
+
const rootBytes = bytes.subarray(5, end), tree = rootBytes.toString("ascii");
|
|
104
|
+
if (!oid(tree) || !rootBytes.equals(Buffer.from(tree))) return fail();
|
|
105
|
+
const parents: string[] = [];
|
|
106
|
+
let at = end + 1;
|
|
107
|
+
while (at < headerEnd) {
|
|
108
|
+
const next = bytes.indexOf(10, at); if (next < at || next > headerEnd) return fail();
|
|
109
|
+
const line = bytes.subarray(at, next); at = next + 1;
|
|
110
|
+
if (line.subarray(0, 7).equals(Buffer.from("parent "))) {
|
|
111
|
+
const parentBytes = line.subarray(7), parent = parentBytes.toString("ascii");
|
|
112
|
+
if (!oid(parent) || !parentBytes.equals(Buffer.from(parent))) return fail(); parents.push(parent);
|
|
113
|
+
} else if (line.subarray(0, 5).equals(Buffer.from("tree "))) return fail();
|
|
114
|
+
}
|
|
115
|
+
const known = expected ?? this.#parents.get(commit);
|
|
116
|
+
if (known && canonicalJSON(parents) !== canonicalJSON(known)) return fail();
|
|
117
|
+
if (expected) this.#parents.set(commit, parents);
|
|
118
|
+
return tree;
|
|
119
|
+
}
|
|
120
|
+
async #treeEntries(root: string): Promise<string[]> {
|
|
121
|
+
// Raw trees are scoped to this one snapshot; retaining a full tree map for every historical commit is quadratic.
|
|
122
|
+
const parsed = new Map<string, ReturnType<typeof parsePublicPruneGitTree>>(), entries: string[] = [];
|
|
123
|
+
const walk = async (object: string, prefix: string): Promise<void> => {
|
|
124
|
+
let tree = parsed.get(object);
|
|
125
|
+
if (!tree) {
|
|
126
|
+
const bytes = await this.#object(object, "tree");
|
|
127
|
+
try { tree = parsePublicPruneGitTree(bytes); } catch { return fail("forbidden-entry"); }
|
|
128
|
+
parsed.set(object, tree);
|
|
129
|
+
}
|
|
130
|
+
for (const [name, entry] of tree) {
|
|
131
|
+
const path = prefix ? `${prefix}/${name}` : name;
|
|
132
|
+
if (entry.mode === "40000") {
|
|
133
|
+
const allowed = !prefix ? name === "rows" || name === "prune-witnesses"
|
|
134
|
+
: prefix === "rows" && name.length === 64 && /^[a-f0-9]+$/.test(name);
|
|
135
|
+
if (!allowed) return fail("forbidden-entry"); await walk(entry.oid, path);
|
|
136
|
+
} else {
|
|
137
|
+
if (path !== "shard.json" && !packPath(path) && !isShardManifestPath(path)) return fail("forbidden-entry");
|
|
138
|
+
entries.push(`100644 blob ${entry.oid}\t${path}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
await walk(root, ""); return entries;
|
|
143
|
+
}
|
|
144
|
+
async #tree(commit: string, verifiedRoot?: string): Promise<ShardHistory> {
|
|
145
|
+
const entries = this.#options.object ? await this.#treeEntries(verifiedRoot ?? await this.#commit(commit))
|
|
146
|
+
: (await this.#options.git(["ls-tree", "-r", "-z", commit])).split("\0").filter(Boolean);
|
|
147
|
+
const rows = new Map<string, ShardSyncRow>(), files = new Map<string, Buffer>(), origins = new Map<string, ShardHistoryOrigin>();
|
|
148
|
+
const objects = new Map<string, string>(), manifests = new Map<string, ShardHistoryManifest>();
|
|
149
|
+
for (const entry of entries) {
|
|
150
|
+
const match = /^100644 blob ([a-f0-9]{40})\t(.+)$/.exec(entry);
|
|
151
|
+
if (!match || match[0] !== entry) fail("forbidden-entry");
|
|
152
|
+
const path = match[2]!, object = match[1]!;
|
|
153
|
+
if (files.has(path) || path !== "shard.json" && !packPath(path) && !isShardManifestPath(path)) fail("forbidden-entry");
|
|
154
|
+
files.set(path, await this.#blob(object, path)); objects.set(path, object);
|
|
155
|
+
}
|
|
156
|
+
const descriptor = files.get("shard.json"); if (!descriptor) return fail();
|
|
157
|
+
const version = this.#options.placement ? decodePlacementTransportDescriptor(descriptor, this.#options.realm) : decodeShardDescriptor(descriptor, this.#options.realm);
|
|
158
|
+
if (version !== "v1" && !this.#options.publicPrune) fail("public-prune-floor");
|
|
159
|
+
for (const [path, bytes] of files) if (packPath(path)) {
|
|
160
|
+
const object = objects.get(path)!; let pack = this.#decoded.get(object);
|
|
161
|
+
if (!pack) { pack = decodeShardPack(path, bytes, this.#options.realm, this.#options.generation); this.#decoded.set(object, pack); }
|
|
162
|
+
else if (!path.endsWith(`/${digest(bytes)}.jsonl`) || pack.some(row => path !== `rows/${digest(row.machine)}/${digest(bytes)}.jsonl`)) fail();
|
|
163
|
+
for (const row of pack) {
|
|
164
|
+
const old = rows.get(row.id); if (old && canonicalJSON(old) !== canonicalJSON(row)) fail(); rows.set(row.id, row);
|
|
165
|
+
if (!origins.has(row.id)) origins.set(row.id, this.#origins.get(row.id) ?? { ref: this.#ref, commit, pack: path, blob: object });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
for (const [path, bytes] of files) if (isShardManifestPath(path)) {
|
|
169
|
+
if (version === "v1") fail();
|
|
170
|
+
const value = decodeShardManifest(path, bytes), candidate = value as { prune?: unknown; pruneOccurrence?: { pack: string; blob: string }; witnesses?: { pack: string; blob: string }[] };
|
|
171
|
+
const row = validatePruneRow(candidate.prune, undefined, "synced");
|
|
172
|
+
if (canonicalJSON(rows.get(row.id)) !== canonicalJSON(row) || !candidate.pruneOccurrence || !Array.isArray(candidate.witnesses)) fail();
|
|
173
|
+
const selected = new Map<string, { pack: string; blob: string; bytes: Buffer }>();
|
|
174
|
+
for (const ref of [candidate.pruneOccurrence, ...candidate.witnesses]) {
|
|
175
|
+
if (!ref || typeof ref.pack !== "string" || objects.get(ref.pack) !== ref.blob || !files.has(ref.pack)) fail();
|
|
176
|
+
selected.set(ref.pack, { pack: ref.pack, blob: ref.blob, bytes: files.get(ref.pack)! });
|
|
177
|
+
}
|
|
178
|
+
const proof = verifyPublicPruneWitness(value, { realm: this.#options.realm, packs: [...selected.values()] });
|
|
179
|
+
if (proof.status === "invalid") fail();
|
|
180
|
+
const origin = this.#manifestOrigins.get(path) ?? { commit, blob: objects.get(path)! };
|
|
181
|
+
manifests.set(path, { path, bytes, value, row, eligible: proof.status === "verified", ...origin });
|
|
182
|
+
}
|
|
183
|
+
return { rows, files, origins, version, manifests };
|
|
184
|
+
}
|
|
185
|
+
#accept(current: ShardHistory): void {
|
|
186
|
+
// Commit origins only after the complete tree and every parent retention rule passed.
|
|
187
|
+
for (const [id, origin] of current.origins) { if (!this.#origins.has(id)) this.#origins.set(id, origin); current.origins.set(id, this.#origins.get(id)!); }
|
|
188
|
+
for (const [path, manifest] of current.manifests) {
|
|
189
|
+
const first = this.#manifestOrigins.get(path) ?? { commit: manifest.commit, blob: manifest.blob };
|
|
190
|
+
this.#manifestOrigins.set(path, first); current.manifests.set(path, { ...manifest, ...first });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/** Ancestry of the immutable hash-verified commit graph, never a second Git interpretation of mutable cache bytes.
|
|
194
|
+
* The caller still proves this tip is the selected actual remote head. Legacy trusted-Git ports are insufficient. */
|
|
195
|
+
async verifiedAncestor(ancestor: string, tip: string): Promise<boolean> {
|
|
196
|
+
if (!this.#options.object || !oid(ancestor) || !oid(tip)) return fail();
|
|
197
|
+
await this.history(tip);
|
|
198
|
+
const pending = [tip], seen = new Set<string>();
|
|
199
|
+
while (pending.length) {
|
|
200
|
+
const commit = pending.pop()!; if (seen.has(commit)) continue; seen.add(commit);
|
|
201
|
+
const parents = this.#parents.get(commit);
|
|
202
|
+
if (!this.#audited.has(commit) || !parents) return fail();
|
|
203
|
+
if (commit === ancestor) return true;
|
|
204
|
+
for (const parent of parents) pending.push(parent);
|
|
205
|
+
}
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
/** Only requested tips retain full row maps. Parent trees are reconstructed with decoded immutable blobs. */
|
|
209
|
+
async history(tip: string): Promise<ShardHistory> {
|
|
210
|
+
if (!oid(tip) || this.#running) return fail(); this.#running = true;
|
|
211
|
+
try {
|
|
212
|
+
const cached = this.#histories.get(tip); if (cached) return detached(cached);
|
|
213
|
+
const lines = (await this.#options.git(["rev-list", "--topo-order", "--reverse", "--parents", tip])).split("\n").filter(Boolean);
|
|
214
|
+
let latest: ShardHistory | undefined; const seen = new Set<string>();
|
|
215
|
+
// ponytail: complete selected history still costs time and immutable selected-blob memory per invocation.
|
|
216
|
+
// Upgrade with a validated immutable commit/blob digest cache, never an arbitrary history lifetime cliff.
|
|
217
|
+
for (const line of lines) {
|
|
218
|
+
const [commit, ...parents] = line.split(" ");
|
|
219
|
+
if (!oid(commit) || parents.some(parent => !oid(parent)) || seen.has(commit)) return fail(); seen.add(commit);
|
|
220
|
+
const verifiedRoot = this.#options.object ? await this.#commit(commit, parents) : undefined;
|
|
221
|
+
if (this.#audited.has(commit)) { if (commit === tip) latest = await this.#tree(commit, verifiedRoot); continue; }
|
|
222
|
+
const current = await this.#tree(commit, verifiedRoot);
|
|
223
|
+
for (const parent of parents) {
|
|
224
|
+
if (!this.#audited.has(parent) || !seen.has(parent)) return fail();
|
|
225
|
+
const previous = this.#histories.get(parent) ?? await this.#tree(parent);
|
|
226
|
+
if (rank[current.version] < rank[previous.version]) fail();
|
|
227
|
+
if (previous.version !== "v1") for (const [path, bytes] of previous.files)
|
|
228
|
+
if (path !== "shard.json" && !current.files.get(path)?.equals(bytes)) fail();
|
|
229
|
+
for (const row of previous.rows.values()) if (canonicalJSON(current.rows.get(row.id)) !== canonicalJSON(row)) fail();
|
|
230
|
+
}
|
|
231
|
+
this.#accept(current); this.#audited.add(commit); if (commit === tip) latest = current;
|
|
232
|
+
}
|
|
233
|
+
if (!latest || lines.at(-1)?.split(" ")[0] !== tip) return fail();
|
|
234
|
+
this.#histories.set(tip, latest); return detached(latest);
|
|
235
|
+
} catch (error) { return fail(cause(error)); }
|
|
236
|
+
finally { this.#running = false; }
|
|
237
|
+
}
|
|
238
|
+
}
|
package/src/memory/shard-sync.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { ShardSyncApplications, ShardSyncApplicationError } from "./shard-sync-applications.ts";
|
|
2
|
+
import { ShardHistoryAuditor, ShardHistoryError, shardHistoryRefusal } from "./shard-sync-history.ts";
|
|
3
|
+
import { validatePlacementProfile, type PlacementProfile } from "./sync-placement-format.ts";
|
|
4
|
+
import { placementRuntimeRefusal } from "./sync-placement-guard.ts";
|
|
1
5
|
import { assertPublisherBoundary, ensurePublisherBoundary, publisherPruneLineages } from "./prune-publisher-lineage.ts";
|
|
2
6
|
import { verifyPublicPruneWitness, type PublicPruneWitnessV1 } from "./public-prune-witness.ts";
|
|
3
7
|
import { readPublicPruneView, type PublicPruneViewV1 } from "./public-prune-view.ts";
|
|
@@ -7,7 +11,7 @@ import { PublicPruneInputError, PUBLIC_PRUNE_REPLAY_MAX_BYTES, PUBLIC_PRUNE_MAX_
|
|
|
7
11
|
import { randomBytes, createHash } from "node:crypto";
|
|
8
12
|
import { constants, closeSync, existsSync, fstatSync, fsyncSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, readSync, renameSync, rmSync, unlinkSync, writeSync } from "node:fs";
|
|
9
13
|
import { join } from "node:path";
|
|
10
|
-
import { canonicalJSON, fsyncDirectory, hashJSON, readProfileJSON, readSelector, rebindPublicPruneProfile } from "./generation.ts";
|
|
14
|
+
import { canonicalJSON, fsyncDirectory, hashJSON, readProfileJSON, readSelector, rebindPublicPruneProfile, legacyProfileMarkerShape, type ProfileSelector } from "./generation.ts";
|
|
11
15
|
import { generationSyncLock } from "./generation-sync.ts";
|
|
12
16
|
import { acquireMutation, assertMutationHandle, type MutationHandle } from "./mutation.ts";
|
|
13
17
|
import { realPath } from "./platform.ts";
|
|
@@ -73,13 +77,30 @@ function selectorEvidence(folder: string, file: string, prefix?: Buffer | true):
|
|
|
73
77
|
if(canonicalJSON(value)+"\n"!==text)refuse("prior selector unavailable");return{value,bytes:data,check};
|
|
74
78
|
} finally {closeSync(fd);}
|
|
75
79
|
}
|
|
80
|
+
/** Closed prior selectors never authorize profile activation or change a pending prune payload. */
|
|
81
|
+
function retainedSelector(store: MemoryStore, value: unknown, revision: string): { pruneInputSetHash: string } {
|
|
82
|
+
const candidate=value as {schema?:unknown}|undefined;
|
|
83
|
+
let prior: PlacementProfile | Extract<ProfileSelector, {schema:"agent-mem.profile/v2"}>;
|
|
84
|
+
if(candidate?.schema==="agent-mem.profile/v3") {
|
|
85
|
+
prior=validatePlacementProfile(value);
|
|
86
|
+
if(!store.profile.publicationFormat||prior.publicationFormat!==store.profile.publicationFormat||prior.placementHead!==store.profile.placementHead)
|
|
87
|
+
refuse("pending selector activation binding unavailable");
|
|
88
|
+
} else {
|
|
89
|
+
if(!legacyProfileMarkerShape(value)||value?.schema!=="agent-mem.profile/v2")refuse("pending selector binding unavailable");
|
|
90
|
+
prior=value;
|
|
91
|
+
}
|
|
92
|
+
if(prior.generation!==store.profile.generation||prior.foreignPruneFormat!==store.profile.foreignPruneFormat||hashJSON(prior)!==revision)
|
|
93
|
+
refuse("pending selector binding unavailable");
|
|
94
|
+
return prior;
|
|
95
|
+
}
|
|
76
96
|
function pendingSelector(store:MemoryStore,plan:PrunePlan,required=false):(()=>void)|undefined {
|
|
77
97
|
if(!required&&plan.selectorRevision===store.profile.selectorRevision)return undefined;
|
|
78
98
|
if(!store.profile.foreignPruneFormat)refuse("pending selector unavailable");
|
|
79
99
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
100
|
+
const checked=retainedSelector(store,prior,plan.selectorRevision);
|
|
101
|
+
// A historical v2 selector remains evidence only. Retain the current v3 activation binding while proving
|
|
102
|
+
// its public-prune view can advance monotonically to the current selector, never enabling a v2 writer.
|
|
103
|
+
const bound={...store.profile,selectorRevision:plan.selectorRevision,pruneInputSetHash:checked.pruneInputSetHash};
|
|
83
104
|
const check=()=>{evidence.check();const probe={...bound};rebindPublicPruneProfile(probe);if(probe.selectorRevision!==store.profile.selectorRevision)refuse("pending selector ancestry unavailable");};check();return check;
|
|
84
105
|
}
|
|
85
106
|
/** Recognize only the exact persisted transport shape; this reads metadata and never resumes an append. */
|
|
@@ -144,10 +165,8 @@ function selectorRetention(target: Target, advancing: boolean, install = false,
|
|
|
144
165
|
const validate=(plan:PrunePlan,value:unknown)=>{
|
|
145
166
|
const row=plan.rows[0]?.payload,prior=value as Record<string,unknown>|undefined;
|
|
146
167
|
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
|
-
|
|
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");
|
|
168
|
+
||canonicalJSON(plan)!==canonicalJSON(importPrunePlan(target.store,target.realm,row,plan.selectorRevision)))refuse("selector recovery plan binding unavailable");
|
|
169
|
+
retainedSelector(target.store,prior,plan.selectorRevision);
|
|
151
170
|
};
|
|
152
171
|
for(const [seal,record] of records){
|
|
153
172
|
const active=pending.get(seal),plan=active?.plan??validatePrunePlan(readProfileJSON(join(plans,seal+".json")));
|
|
@@ -423,23 +442,16 @@ async function prepareHolds(targets: readonly Target[], root: string, url: strin
|
|
|
423
442
|
}
|
|
424
443
|
}
|
|
425
444
|
}
|
|
426
|
-
function mergePackOrigins(into: Map<string, HoldSource>, byRealm: Map<string, Map<string, HoldSource>>, rows: readonly ShardSyncRow[], source: HoldSource): void {
|
|
427
|
-
const first = byRealm.get(source.ref) ?? new Map<string, HoldSource>(); byRealm.set(source.ref, first);
|
|
428
|
-
// Oldest-first history keeps a receipt's source stable across tip advancement and partial-write retry.
|
|
429
|
-
for (const row of rows) {
|
|
430
|
-
if (!first.has(row.id)) first.set(row.id, source);
|
|
431
|
-
if (!into.has(row.id)) into.set(row.id, first.get(row.id)!);
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
/** Sync only borrowed canonical project/user targets. Internal error/phase observers are test-only, never CLI/SyncOptions ports. */
|
|
436
445
|
export async function syncShardedMemory(store: MemoryStore, opts: SyncOptions = {}, testOnlyUnexpectedError?: (error: unknown) => void, testOnlyPhase?: (phase: SyncPhase) => void): Promise<SyncResult> {
|
|
446
|
+
const placementRefusal = placementRuntimeRefusal(store.profile.home, store.profile);
|
|
447
|
+
if (placementRefusal) return { ok: false, pushed: false, entries: 0, error: placementRefusal };
|
|
448
|
+
if (store.profile.publicationFormat === "agent-mem.sync-placement/v1") return (await import("./sync-placement-transport.ts")).syncPlacementMemory(store, opts);
|
|
437
449
|
let pushed = false, releaseTransport: (() => void) | undefined, releaseSync: (() => void) | undefined;
|
|
438
450
|
const releases: (() => void)[] = [], transient: string[] = [], temporary: string[] = [];
|
|
439
451
|
const v2 = store.profile.foreignPruneFormat === "agent-mem.public-prune-witness/v1";
|
|
440
452
|
let expectedRevision = store.profile.selectorRevision;
|
|
441
453
|
let expectedBinding: string | undefined, expectedSources: string | undefined, expectedPolicy: string | undefined;
|
|
442
|
-
|
|
454
|
+
let application: NonNullable<SyncResult["pruneApplication"]> = { transport:"unconfirmed", proofComplete:0, projectionActive:null, nativeApplied:null, held:0, phase:"preflight" };
|
|
443
455
|
const fail = (error: string): SyncResult => ({ ok: false, pushed, entries: 0, error, ...(v2 ? {pruneApplication:{...application,transport:pushed?"durable":"unconfirmed"}} : {}) });
|
|
444
456
|
let root = "", cache = "", targets: Target[] = [], homeMutation:MutationHandle|undefined;
|
|
445
457
|
let boundView:PublicPruneViewV1|undefined;
|
|
@@ -529,7 +541,7 @@ export async function syncShardedMemory(store: MemoryStore, opts: SyncOptions =
|
|
|
529
541
|
const release = acquireMutation(target.store.dir); if (!release) refuse(`${shardSyncLabel(target.realm)}: another source mutation is running`); releases.push(release); target.mutation=release;
|
|
530
542
|
if (v2) ensurePublisherBoundary(target.store.profile,release);
|
|
531
543
|
}
|
|
532
|
-
const blobCache = new Map<string, Buffer>()
|
|
544
|
+
const blobCache = new Map<string, Buffer>();
|
|
533
545
|
const blob = async (object: string, path: string): Promise<Buffer> => {
|
|
534
546
|
const maximum = path === "shard.json" || isShardManifestPath(path) ? 64 * 1024 : SHARD_PACK_MAX_BYTES, prior = blobCache.get(object);
|
|
535
547
|
if (prior) { if (prior.length > maximum) refuse("shard sync blob exceeds its bound"); return prior; }
|
|
@@ -542,69 +554,16 @@ export async function syncShardedMemory(store: MemoryStore, opts: SyncOptions =
|
|
|
542
554
|
if (identity !== object) refuse("shard sync public blob identity mismatch");
|
|
543
555
|
blobCache.set(object, bytes); return bytes;
|
|
544
556
|
};
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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)!});
|
|
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});
|
|
582
|
-
}
|
|
583
|
-
return{rows,files,origins,version,manifests};
|
|
584
|
-
};
|
|
585
|
-
const history = async (tip: string, target: Target): Promise<History> => {
|
|
586
|
-
const key = `${shardSyncRef(target.realm)}:${tip}`, cached = histories.get(key); if (cached) return cached;
|
|
587
|
-
const commits = (await required(["rev-list", "--topo-order", "--reverse", "--parents", tip], "shard sync history unavailable")).split("\n").filter(Boolean);
|
|
588
|
-
let latest: History | undefined;
|
|
589
|
-
// ponytail: complete selected history still costs time and selected pack memory per invocation.
|
|
590
|
-
// Upgrade: a validated immutable commit/blob digest cache, never an arbitrary lifetime refusal cliff.
|
|
591
|
-
for (const line of commits) {
|
|
592
|
-
const [commit, ...parents] = line.split(" ");
|
|
593
|
-
if (!commit || !OID.test(commit) || parents.some(parent => !OID.test(parent))) refuse("shard sync history identity invalid");
|
|
594
|
-
const identity = `${shardSyncRef(target.realm)}:${commit}`;
|
|
595
|
-
if (audited.has(identity)) { if (commit === tip) latest = await treeState(commit, target); continue; }
|
|
596
|
-
const current = await treeState(commit, target);
|
|
597
|
-
for (const parent of parents) {
|
|
598
|
-
if (!audited.has(`${shardSyncRef(target.realm)}:${parent}`)) refuse(`${shardSyncLabel(target.realm)}: historical parent unavailable`);
|
|
599
|
-
const previous = histories.get(`${shardSyncRef(target.realm)}:${parent}`) ?? await treeState(parent, target);
|
|
600
|
-
if(previous.version==="v2"&¤t.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");
|
|
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`);
|
|
603
|
-
}
|
|
604
|
-
audited.add(identity); if (commit === tip) latest = current;
|
|
557
|
+
const auditors = new Map<string, ShardHistoryAuditor>();
|
|
558
|
+
const history = (tip: string, target: Target) => {
|
|
559
|
+
const ref = shardSyncRef(target.realm);
|
|
560
|
+
let auditor = auditors.get(ref);
|
|
561
|
+
if (!auditor) {
|
|
562
|
+
auditor = new ShardHistoryAuditor({ realm: target.realm, generation: target.generation, placement: false, publicPrune: v2,
|
|
563
|
+
git: args => required(args, "shard sync history unavailable"), blob });
|
|
564
|
+
auditors.set(ref, auditor);
|
|
605
565
|
}
|
|
606
|
-
|
|
607
|
-
histories.set(key, latest); return latest;
|
|
566
|
+
return auditor.history(tip);
|
|
608
567
|
};
|
|
609
568
|
const fetchTargets = async (): Promise<void> => {
|
|
610
569
|
for (const target of targets) {
|
|
@@ -691,23 +650,6 @@ export async function syncShardedMemory(store: MemoryStore, opts: SyncOptions =
|
|
|
691
650
|
|| !decodeShardPack(value.source.pack, bytes, target.realm, target.generation).some(item => canonicalJSON(item) === canonicalJSON(value.row))
|
|
692
651
|
|| value.source.ref !== shardSyncRef(target.realm)) refuse(`${shardSyncLabel(target.realm)}: prune hold source mismatch`);
|
|
693
652
|
};
|
|
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
653
|
const objectCache=new Map<string,{type:"commit"|"tree"|"blob";body:Uint8Array}>(), graphObjects=new Map<string,Uint8Array>();
|
|
712
654
|
// ponytail: one sync retains at most the 256 MiB replay budget across selected realms; segmented acquisition is the upgrade.
|
|
713
655
|
let objectBytes=0;
|
|
@@ -724,57 +666,20 @@ export async function syncShardedMemory(store: MemoryStore, opts: SyncOptions =
|
|
|
724
666
|
if(result.code!==0||result.out.length!==size)refuse("Git object bytes unavailable");
|
|
725
667
|
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
668
|
};
|
|
727
|
-
const
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
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
|
-
};
|
|
669
|
+
const applications = v2 ? new ShardSyncApplications({ home: store.profile.home, root, url, targets, homeMutation: homeMutation!,
|
|
670
|
+
getRevision: () => expectedRevision, setRevision: revision => { expectedRevision = revision; }, check, history,
|
|
671
|
+
candidateHistory: target => history(target.commit!, target), readObject,
|
|
672
|
+
retainGraphObject: (oid, bytes) => { const cached = objectCache.get(oid)!; objectCache.set(oid, {type:cached.type,body:bytes.subarray(bytes.indexOf(0)+1)}); },
|
|
673
|
+
assertAncestor: (ancestor, descendant) => required(["merge-base","--is-ancestor",ancestor,descendant], "registered prune source is missing from selected history").then(() => {}),
|
|
674
|
+
afterAdvance: () => {
|
|
675
|
+
if (shardSyncSourceBinding(store) !== expectedSources || opts.policyVersion?.() !== expectedPolicy) refuse("source binding or policy changed during view commit");
|
|
676
|
+
expectedBinding = shardSyncBinding(store);
|
|
677
|
+
}, phase: testOnlyPhase,
|
|
678
|
+
}) : undefined;
|
|
679
|
+
if (applications) application = applications.application;
|
|
680
|
+
const loadExistingApplications = async () => { await applications?.loadExisting(); };
|
|
681
|
+
const prepareApplications = async () => { await applications?.prepare(); };
|
|
682
|
+
const installApplications = async () => { await applications?.install(); };
|
|
778
683
|
localTargets(); await fetchTargets(); localTargets(); provePrunes(targets); await loadExistingApplications(); await prepareHolds(targets, root, url, proveHoldSource); check();
|
|
779
684
|
// Every selected realm, including its complete remote history/prunes, is preflighted before the first append or publication.
|
|
780
685
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
@@ -814,7 +719,8 @@ export async function syncShardedMemory(store: MemoryStore, opts: SyncOptions =
|
|
|
814
719
|
}
|
|
815
720
|
return fail("shard sync selected-ref push failed");
|
|
816
721
|
} catch (error) {
|
|
817
|
-
if (error instanceof
|
|
722
|
+
if (error instanceof ShardHistoryError) return fail(shardHistoryRefusal(error));
|
|
723
|
+
if (error instanceof PublicPruneInputError || error instanceof ShardSyncFormatError || error instanceof ShardSyncApplicationError || error instanceof SyncRefusal) return fail(error.message);
|
|
818
724
|
try { testOnlyUnexpectedError?.(error); } catch { /* A test observer cannot change the neutral result. */ }
|
|
819
725
|
return fail("shard sync unavailable");
|
|
820
726
|
} finally {
|
|
@@ -825,3 +731,12 @@ export async function syncShardedMemory(store: MemoryStore, opts: SyncOptions =
|
|
|
825
731
|
try { releaseTransport?.(); } catch { /* private lock cleanup */ }
|
|
826
732
|
}
|
|
827
733
|
}
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
// Internal application seams shared by the independently admitted placement coordinator.
|
|
737
|
+
// These are deliberately absent from the package barrel: validation and complete fetched-proof preflight stay mandatory.
|
|
738
|
+
export type { Target as ShardSyncApplicationTarget, PendingPrune as ShardSyncPendingPrune, HoldSource as ShardSyncHoldSource,
|
|
739
|
+
HoldReceipt as ShardSyncHoldReceipt, Hold as ShardSyncHold, SyncPhase as ShardSyncApplicationPhase,
|
|
740
|
+
SyncManifest as ShardSyncApplicationManifest, ProveHoldSource as ShardSyncProveHoldSource };
|
|
741
|
+
export { sourceRows as shardSourceRows, provePrunes as shardProvePrunes, importRows as shardImportRows,
|
|
742
|
+
selectorRetention as shardSelectorRetention, prepareHolds as shardPrepareHolds, installHold as shardInstallHold, holdWarning as shardHoldWarning };
|