@staix/agent-mem 0.20.3 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +4 -3
- package/package.json +2 -2
- package/src/cli.ts +3 -0
- package/src/hook-spool.ts +45 -3
- package/src/index.ts +25 -2
- package/src/memory/bridge.ts +66 -18
- package/src/memory/candidates.ts +71 -9
- 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-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 +67 -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-placement-active.ts +75 -0
- package/src/memory/sync-placement-cache.ts +32 -0
- package/src/memory/sync-placement-capture.ts +292 -0
- package/src/memory/sync-placement-dependencies.ts +255 -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 +98 -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 +236 -0
- package/src/memory/sync-placement-journal-format.ts +114 -0
- package/src/memory/sync-placement-journal-storage.ts +513 -0
- package/src/memory/sync-placement-journal.ts +153 -0
- package/src/memory/sync-placement-materialize.ts +149 -0
- package/src/memory/sync-placement-plan.ts +171 -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 +489 -0
- package/src/memory/sync-policy-inspection.ts +126 -0
- package/src/memory/sync-screen.ts +128 -33
- package/src/memory/sync.ts +20 -64
- 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,489 @@
|
|
|
1
|
+
/** Activated placement transport. The public sync entrypoint owns dispatch; this module never activates a profile. */
|
|
2
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
+
import { constants, closeSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readdirSync, rmSync, writeSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { canonicalJSON, hashJSON, readSelector } from "./generation.ts";
|
|
6
|
+
import { generationSyncLock } from "./generation-sync.ts";
|
|
7
|
+
import { generationSyncGit, generationSyncGitBytes, remoteVisibility, type SyncOptions, type SyncResult } from "./sync.ts";
|
|
8
|
+
import { acquireMutation, assertMutationHandle, type MutationHandle } from "./mutation.ts";
|
|
9
|
+
import { isLocalKey, resolveShardProfile } from "./shard.ts";
|
|
10
|
+
import { memoryPolicySnapshot } from "./policy.ts";
|
|
11
|
+
import { outboundPolicyEvaluator } from "./sync-screen.ts";
|
|
12
|
+
import { readActivePlacementBinding } from "./sync-placement-active.ts";
|
|
13
|
+
import { readPlacementRuntimeMarkers } from "./sync-placement-guard.ts";
|
|
14
|
+
import { captureSyncPlacementContext, type PlacementCapture, type PlacementPendingSource } from "./sync-placement-capture.ts";
|
|
15
|
+
import type { PlannedGitObject } from "./sync-placement-git.ts";
|
|
16
|
+
import { PLACEMENT_FORMAT } from "./sync-placement-format.ts";
|
|
17
|
+
import { createPlacementRecord, placementKeyId, type PlacementEvent, type PlacementRealm, type PlacementHoldReason, type PlacementRecord } from "./sync-placement-journal-format.ts";
|
|
18
|
+
import { rebuildPlacementProjection, type PlacementProjection } from "./sync-placement-projection.ts";
|
|
19
|
+
import { readPlacementJournal, appendPlacementEvent, ensurePlacementPublicationDurability, type PlacementJournalRead, type PlacementStorageOptions } from "./sync-placement-journal-storage.ts";
|
|
20
|
+
import { preparePlacementPublication, readPlacementPublication, recordPlacementPublicationPhase, type PlacementPublicationRead } from "./sync-placement-publication-storage.ts";
|
|
21
|
+
import { planPlacementPublication, publicationBytes, type PlacementPublicationBundle, type PlacementPublicationDecision, type PlacementPublicationObject } from "./sync-placement-publication-plan.ts";
|
|
22
|
+
import { ShardHistoryAuditor, type ShardHistory } from "./shard-sync-history.ts";
|
|
23
|
+
import { ShardSyncApplications } from "./shard-sync-applications.ts";
|
|
24
|
+
import { shardSourceRows, shardProvePrunes, shardImportRows, shardSelectorRetention, shardInstallHold, shardHoldWarning, type ShardSyncApplicationTarget } from "./shard-sync.ts";
|
|
25
|
+
import { shardSyncRef, SHARD_PACK_MAX_BYTES } from "./shard-sync-format.ts";
|
|
26
|
+
import { assertPublisherBoundary, ensurePublisherBoundary, publisherPruneLineages } from "./prune-publisher-lineage.ts";
|
|
27
|
+
import type { MemoryStore } from "./store.ts";
|
|
28
|
+
import type { PruneRow } from "./prune.ts";
|
|
29
|
+
import { materializePlacementRows, verifiedPlacementPrunes } from "./sync-placement-materialize.ts";
|
|
30
|
+
import { placementCacheConfigStamp, placementCacheRootStamp, validatePlacementCacheConfig } from "./sync-placement-cache.ts";
|
|
31
|
+
|
|
32
|
+
export type PlacementSyncPhase = "before-plan-fsync" | "plan-fsynced" | "artifact-built" | "before-cas" | "cas-returned" | "before-ack" | "acknowledged" | "imported" | "before-receipt" | "receipt-written";
|
|
33
|
+
type Phase = (phase: PlacementSyncPhase) => void | Promise<void>;
|
|
34
|
+
type Target = ShardSyncApplicationTarget & { auditor: ShardHistoryAuditor; remoteHistory?: ShardHistory; candidate?: ShardHistory; objects: PlannedGitObject[]; localPrunes: Map<string, PruneRow> };
|
|
35
|
+
interface Materialized { bundle: PlacementPublicationBundle; decisions: PlacementPublicationDecision[]; projection: PlacementProjection; old: (ShardHistory | undefined)[]; events: PlacementEvent[]; preCount: number; holdCount: number; exported: number; withheld: number }
|
|
36
|
+
const digest = (v: Uint8Array | string) => createHash("sha256").update(v).digest("hex");
|
|
37
|
+
const oid = (v: unknown): v is string => typeof v === "string" && v.length === 40 && /^[a-f0-9]+$/.test(v);
|
|
38
|
+
const sameRealm = (a: PlacementRealm, b: PlacementRealm) => canonicalJSON(a) === canonicalJSON(b);
|
|
39
|
+
const physical = (target: Target): PlacementRealm => target.realm.scope === "user" ? { scope: "user" } : { scope: "project", keyHash: digest(target.realm.key) };
|
|
40
|
+
function refuse(): never { throw new Error("placement sync unavailable"); }
|
|
41
|
+
function directory(file: string): void { const s = lstatSync(file); if (!s.isDirectory() || s.isSymbolicLink() || s.uid !== process.getuid?.() || (s.mode & 0o077) !== 0) refuse(); }
|
|
42
|
+
function emptyHistory(): ShardHistory { return { rows: new Map(), files: new Map(), origins: new Map(), manifests: new Map(), version: "v3" }; }
|
|
43
|
+
/** Internal phase observer is a fault seam, not part of SyncOptions or the package barrel. */
|
|
44
|
+
export async function syncPlacementMemory(store: MemoryStore, opts: SyncOptions = {}, phase?: Phase, testOnlyError?: (error:unknown)=>void): Promise<SyncResult> {
|
|
45
|
+
return new PlacementTransport(store, opts, phase, testOnlyError).run();
|
|
46
|
+
}
|
|
47
|
+
class PlacementTransport {
|
|
48
|
+
readonly store: MemoryStore; readonly opts: SyncOptions; readonly phase?: Phase; readonly testOnlyError?: (error:unknown)=>void; readonly home: string; readonly root: string; readonly cache: string;
|
|
49
|
+
readonly releases: (() => void)[] = []; readonly temporary: string[] = []; readonly refs: string[] = [];
|
|
50
|
+
targets: Target[] = []; homeMutation?: MutationHandle; rootMutation?: MutationHandle; cacheIdentity?: {dev:number;ino:number}; cacheConfigIdentity?: string; epochHash = ""; journal!: PlacementJournalRead; capture?: PlacementCapture;
|
|
51
|
+
machineId = ""; url = ""; revision = ""; accepted = false; factual = false; attempted = false; lastPolicyCheckUnixMs?: number; lastPolicyHash?: string; policyCheckSource?: "live"|"durable-intent"; attemptHash?: string;
|
|
52
|
+
evaluator?: ReturnType<typeof outboundPolicyEvaluator>; applications?: ShardSyncApplications; current?: Materialized; recovered = false;
|
|
53
|
+
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
|
+
attest(): void {
|
|
55
|
+
if (!this.opts.placementAttestation || this.opts.screen && !this.opts.policyVersion) refuse();
|
|
56
|
+
const value = memoryPolicySnapshot(this.opts.placementAttestation());
|
|
57
|
+
if (!value || value.legacySyncQuiesced !== true || !Array.isArray(value.writers)) refuse();
|
|
58
|
+
const writers = [...value.writers].sort((a,b) => a.writerId.localeCompare(b.writerId));
|
|
59
|
+
if (canonicalJSON(writers) !== canonicalJSON(this.journal.activation.writers)) refuse();
|
|
60
|
+
}
|
|
61
|
+
metadata(): void {
|
|
62
|
+
if(this.rootMutation)assertMutationHandle(this.rootMutation,this.root);
|
|
63
|
+
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
|
+
if(this.cacheIdentity){
|
|
65
|
+
for(const name of ["commondir","shallow","info/grafts","objects/info/alternates","objects/info/http-alternates","config.worktree"])if(existsSync(join(this.cache,name)))refuse();
|
|
66
|
+
const stamp=this.configStamp();if(this.cacheConfigIdentity!==undefined&&stamp!==this.cacheConfigIdentity)refuse();
|
|
67
|
+
}
|
|
68
|
+
if (this.homeMutation) assertMutationHandle(this.homeMutation, this.home);
|
|
69
|
+
for (const target of this.targets) if (target.mutation) assertMutationHandle(target.mutation, target.store.dir);
|
|
70
|
+
const markers = readPlacementRuntimeMarkers(this.home), active = readActivePlacementBinding(this.home, markers.profile, markers.shards);
|
|
71
|
+
if (!active || active.profile.placementHead !== this.epochHash || active.epoch.homeHash !== this.journal.epoch.homeHash) refuse();
|
|
72
|
+
}
|
|
73
|
+
check = (): void => {
|
|
74
|
+
this.metadata();
|
|
75
|
+
if (this.factual) return;
|
|
76
|
+
if (this.opts.signal?.aborted) refuse(); this.attest(); this.capture?.assertUnchanged();
|
|
77
|
+
if(this.capture){this.lastPolicyHash=this.capture.context.policyHash;this.lastPolicyCheckUnixMs=Date.now();this.policyCheckSource="live";}
|
|
78
|
+
for (const target of this.targets) for (const check of target.selectorChecks ?? []) check();
|
|
79
|
+
this.metadata();
|
|
80
|
+
};
|
|
81
|
+
async call<T>(work: () => T | Promise<T>): Promise<T> { this.check(); const result = await work(); this.check(); return result; }
|
|
82
|
+
args(args: string[]): string[] { return ["--no-replace-objects", "--git-dir", this.cache, "-c", "maintenance.auto=false", "-c", "gc.auto=0", "-c", "core.commitGraph=false", ...args]; }
|
|
83
|
+
async git(args: string[]) { return this.call(() => generationSyncGit(this.root, this.args(args), this.opts.signal)); }
|
|
84
|
+
async required(args: string[]): Promise<string> { const result = await this.git(args); if (result.code !== 0) refuse(); return result.out; }
|
|
85
|
+
async observe(phase: PlacementSyncPhase): Promise<void> { await this.phase?.(phase); }
|
|
86
|
+
storage(factual = false): PlacementStorageOptions { if (!this.homeMutation) refuse(); return { mutation: this.homeMutation, assertCurrent: factual ? () => this.metadata() : this.check, ...(this.attemptHash ? { publicationAttemptHash: this.attemptHash } : {}) }; }
|
|
87
|
+
configStamp():string {
|
|
88
|
+
return placementCacheConfigStamp(this.cache);
|
|
89
|
+
}
|
|
90
|
+
async bindCacheConfig():Promise<void>{
|
|
91
|
+
const before=this.configStamp(),output=await this.required(["config","--local","--no-includes","--null","--list"]);
|
|
92
|
+
validatePlacementCacheConfig(output);
|
|
93
|
+
if(this.configStamp()!==before)refuse();this.cacheConfigIdentity=before;
|
|
94
|
+
}
|
|
95
|
+
async independent(): Promise<boolean> {
|
|
96
|
+
if (!existsSync(this.cache)) return false; directory(this.cache); const initial=lstatSync(this.cache);
|
|
97
|
+
const walk = (dir: string): void => { for (const name of readdirSync(dir)) { const file = join(dir, name), s = lstatSync(file); if (s.isSymbolicLink() || s.uid !== process.getuid?.() || !s.isFile() && !s.isDirectory() || s.isFile() && s.nlink !== 1 || name.endsWith(".promisor")) refuse(); if (s.isDirectory()) {directory(file);walk(file);} } }; walk(this.cache);
|
|
98
|
+
if (existsSync(join(this.cache, "commondir")) || existsSync(join(this.cache, "objects", "info", "http-alternates")) || existsSync(join(this.cache, "objects", "info", "alternates")) || existsSync(join(this.cache, "shallow")) || existsSync(join(this.cache, "info", "grafts"))) refuse();
|
|
99
|
+
if (await this.required(["rev-parse", "--is-bare-repository"]) !== "true" || await this.required(["rev-parse", "--show-object-format"]) !== "sha1" || (await this.git(["config", "--get", "core.worktree"])).code !== 1) refuse();
|
|
100
|
+
if((await this.git(["config","--get","extensions.partialClone"])).code !== 1
|
|
101
|
+
|| (await this.git(["config","--get-regexp","^remote[.].*[.](promisor|partialclonefilter)$"])).code !== 1)refuse();
|
|
102
|
+
const final=lstatSync(this.cache); if(final.isSymbolicLink()||!final.isDirectory()||final.dev!==initial.dev||final.ino!==initial.ino)refuse();
|
|
103
|
+
this.cacheIdentity={dev:final.dev,ino:final.ino};await this.bindCacheConfig();return true;
|
|
104
|
+
}
|
|
105
|
+
async initialize(): Promise<void> {
|
|
106
|
+
const markers = readPlacementRuntimeMarkers(this.home), active = readActivePlacementBinding(this.home, markers.profile, markers.shards);
|
|
107
|
+
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();
|
|
109
|
+
const opened = this.store.openedSyncTargets();
|
|
110
|
+
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
|
+
|| !item.profile.shardUser && item.profile.shardKey !== this.store.profile.shardKey)) refuse();
|
|
112
|
+
for (const item of opened) {
|
|
113
|
+
const realm = item.profile.shardUser ? { scope: "user" as const } : { scope: "project" as const, key: item.profile.shardKey! };
|
|
114
|
+
const target = { store: item, realm, generation: item.profile.generation!, rows: new Map(), fetched: new Set(), sources: [], pending: [], origins: new Map(), holds: [], manifests: new Map(), eligible: new Set(), registered: [], objects: [], localPrunes: new Map() } as unknown as Target;
|
|
115
|
+
target.auditor = new ShardHistoryAuditor({ realm, generation: target.generation, placement: true, publicPrune: true,
|
|
116
|
+
git: args => this.required(args), blob: (object, path) => this.blob(object, path), object: (oid,type) => this.rawHistoryObject(oid,type) }); this.targets.push(target);
|
|
117
|
+
}
|
|
118
|
+
if (new Set(this.targets.map(target => shardSyncRef(target.realm))).size !== this.targets.length) refuse();
|
|
119
|
+
if (this.targets.some(target => target.realm.scope === "project" && isLocalKey(target.realm.key)) && this.opts.remote === undefined) refuse();
|
|
120
|
+
if (existsSync(this.root)) placementCacheRootStamp(this.home);
|
|
121
|
+
const existing = existsSync(this.root) ? await this.independent() : false;
|
|
122
|
+
if (!existing && !this.opts.remote) refuse();
|
|
123
|
+
this.url = this.opts.remote ?? await this.required(["remote", "get-url", "origin"]);
|
|
124
|
+
if (!this.url || this.url.length > 4096 || this.url.startsWith("-") || /[\r\n\0]/.test(this.url)) refuse();
|
|
125
|
+
if (await this.call(() => (this.opts.visibility ?? remoteVisibility)(this.url)) !== "private") refuse();
|
|
126
|
+
if (!existsSync(this.root)) mkdirSync(this.root, { mode: 0o700 });
|
|
127
|
+
const transport = acquireMutation(this.root); if (!transport) refuse(); this.rootMutation=transport; this.releases.push(transport);
|
|
128
|
+
const sync = generationSyncLock(this.root); if (!sync) refuse(); this.releases.push(sync);
|
|
129
|
+
for (const dir of [...new Set([this.home, ...this.targets.map(target => target.store.dir)])].sort()) {
|
|
130
|
+
const handle = acquireMutation(dir); if (!handle) refuse(); this.releases.push(handle); if (dir === this.home) this.homeMutation = handle;
|
|
131
|
+
for (const target of this.targets) if (target.store.dir === dir) target.mutation = handle;
|
|
132
|
+
}
|
|
133
|
+
this.journal = readPlacementJournal(this.home, this.epochHash); this.revision = readSelector(this.home).revision; this.attest();
|
|
134
|
+
if (!existing) await this.required(["init", "--bare", "--template=", "--object-format=sha1", "-q", this.cache]);
|
|
135
|
+
if (!await this.independent()) refuse();
|
|
136
|
+
const previous = await this.git(["remote", "get-url", "origin"]);
|
|
137
|
+
if (this.opts.remote) { this.cacheConfigIdentity=undefined; await this.required(previous.code === 0 ? ["remote", "set-url", "origin", this.url] : ["remote", "add", "origin", this.url]); }
|
|
138
|
+
await this.bindCacheConfig();
|
|
139
|
+
for (const args of [["remote", "get-url", "--all", "origin"], ["remote", "get-url", "--push", "--all", "origin"]])
|
|
140
|
+
if (await this.required(args) !== this.url) refuse();
|
|
141
|
+
const reservation = this.journal.publicationReservation;
|
|
142
|
+
const pending = reservation ? readPlacementPublication(this.home, this.epochHash, reservation.attemptHash) : undefined;
|
|
143
|
+
if (!pending || pending.bundle.root.remoteHash === digest(this.url)) await this.fetch();
|
|
144
|
+
}
|
|
145
|
+
async rawHistoryObject(object:string,type:"commit"|"tree"):Promise<Buffer>{
|
|
146
|
+
if(!oid(object)||await this.required(["cat-file","-t",object])!==type)refuse();
|
|
147
|
+
const limit=type==="commit"?65536:SHARD_PACK_MAX_BYTES,size=Number(await this.required(["cat-file","-s",object]));
|
|
148
|
+
if(!Number.isSafeInteger(size)||size<0||size>limit)refuse();
|
|
149
|
+
const result=await this.call(()=>generationSyncGitBytes(this.root,this.args(["cat-file",type,object]),this.opts.signal,undefined,limit));
|
|
150
|
+
if(result.code!==0||result.out.length!==size||createHash("sha1").update(`${type} ${size}\0`).update(result.out).digest("hex")!==object)refuse();
|
|
151
|
+
return Buffer.from(result.out);
|
|
152
|
+
}
|
|
153
|
+
async blob(object: string, path: string): Promise<Buffer> {
|
|
154
|
+
const limit = path === "shard.json" || path.startsWith("prune-witnesses/") ? 65536 : SHARD_PACK_MAX_BYTES;
|
|
155
|
+
const size = Number(await this.required(["cat-file", "-s", object])); if (!Number.isSafeInteger(size) || size < 1 || size > limit) refuse();
|
|
156
|
+
const result = await this.call(() => generationSyncGitBytes(this.root, this.args(["cat-file", "blob", object]), this.opts.signal, undefined, limit));
|
|
157
|
+
if (result.code !== 0 || result.out.length !== size) refuse(); return Buffer.from(result.out);
|
|
158
|
+
}
|
|
159
|
+
async fetch(): Promise<void> {
|
|
160
|
+
for (const target of this.targets) {
|
|
161
|
+
const ref = shardSyncRef(target.realm); if ((await this.git(["symbolic-ref", "--quiet", ref])).code !== 1) refuse();
|
|
162
|
+
const cached = await this.git(["rev-parse", "--verify", "--quiet", ref]);
|
|
163
|
+
if (cached.code !== 0 && cached.code !== 1 || cached.code === 0 && !oid(cached.out)) refuse(); target.local = cached.code === 0 ? cached.out : undefined;
|
|
164
|
+
const advertised = await this.git(["ls-remote", "--exit-code", "origin", ref]);
|
|
165
|
+
if (advertised.code === 2) { if (target.local && !this.journal.publicationReservation) refuse(); target.remote = undefined; target.remoteHistory = undefined; continue; }
|
|
166
|
+
const fields = advertised.out.split("\t"); if (advertised.code !== 0 || fields.length !== 2 || !oid(fields[0]) || fields[1] !== ref) refuse();
|
|
167
|
+
const incoming = `refs/amem/placement-incoming/${randomBytes(16).toString("hex")}`; this.refs.push(incoming);
|
|
168
|
+
await this.required(["fetch", "-q", "--no-tags", "--no-write-fetch-head", "--no-recurse-submodules", "origin", `${ref}:${incoming}`]);
|
|
169
|
+
const tip = await this.required(["rev-parse", "--verify", incoming]); if (!oid(tip) || tip !== fields[0]) refuse();
|
|
170
|
+
if (target.local && !await target.auditor.verifiedAncestor(target.local, tip) && !this.journal.publicationReservation) refuse();
|
|
171
|
+
target.remote = tip; target.remoteHistory = await target.auditor.history(tip);
|
|
172
|
+
target.rows = new Map(target.remoteHistory.rows); target.fetched = new Set(target.remoteHistory.rows.keys()); target.origins = new Map(target.remoteHistory.origins);
|
|
173
|
+
target.manifests = new Map(target.remoteHistory.manifests); target.eligible = new Set([...target.manifests.values()].filter(item => item.eligible).map(item => item.row.id));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
preflight(mergeLocal = true): void {
|
|
177
|
+
for (const target of this.targets) {
|
|
178
|
+
const source = shardSourceRows(target.store, target.realm); target.sources = source.sources; target.pending = source.pending;
|
|
179
|
+
target.localPrunes = new Map([...source.rows.filter((row): row is PruneRow => row.op === "prune"), ...source.pending.map(item => item.row)].map(row => [row.id,row]));
|
|
180
|
+
if (mergeLocal) for (const row of source.rows) { const old = target.rows.get(row.id); if (old && hashJSON(old) !== hashJSON(row)) refuse(); target.rows.set(row.id, row); }
|
|
181
|
+
}
|
|
182
|
+
shardProvePrunes(this.targets);
|
|
183
|
+
}
|
|
184
|
+
startCapture(): void {
|
|
185
|
+
const pending: PlacementPendingSource[] = this.targets.flatMap(target => target.pending.filter(item => !item.unclaimed).map(item => ({ realm: physical(target),
|
|
186
|
+
pathHash: digest(`log/${item.row.machine}.jsonl`), offset: item.offset, prefixHash: item.prefixHash, line: item.line })));
|
|
187
|
+
this.capture = captureSyncPlacementContext(this.home, { attest: this.opts.placementAttestation!, ...(this.opts.policyVersion ? { policyVersion: this.opts.policyVersion } : {}) }, this.targets.map(physical), pending);
|
|
188
|
+
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();
|
|
190
|
+
this.check();
|
|
191
|
+
}
|
|
192
|
+
async applicationCoordinator(): Promise<ShardSyncApplications> {
|
|
193
|
+
if (!this.homeMutation) refuse();
|
|
194
|
+
const applications = new ShardSyncApplications({ home: this.home, root: this.root, url: this.url, targets: this.targets, homeMutation: this.homeMutation,
|
|
195
|
+
getRevision: () => this.revision, setRevision: revision => { this.revision = revision; }, check: this.check,
|
|
196
|
+
history: (tip, target) => (target as Target).auditor.history(tip), candidateHistory: async target => (target as Target).candidate ?? (target as Target).remoteHistory ?? emptyHistory(),
|
|
197
|
+
assertAncestor: async (a,b,target) => { if (!await (target as Target).auditor.verifiedAncestor(a,b)) refuse(); },
|
|
198
|
+
readObject: async object => {
|
|
199
|
+
const type = await this.required(["cat-file", "-t", object]); if (type !== "commit" && type !== "tree" && type !== "blob") refuse();
|
|
200
|
+
const limit = type === "commit" ? 65536 : SHARD_PACK_MAX_BYTES, size = Number(await this.required(["cat-file", "-s", object]));
|
|
201
|
+
if (!Number.isSafeInteger(size) || size < 0 || size > limit) refuse();
|
|
202
|
+
const result = await this.call(() => generationSyncGitBytes(this.root, this.args(["cat-file", type, object]), this.opts.signal, undefined, limit));
|
|
203
|
+
if (result.code !== 0 || result.out.length !== size) refuse(); return { type, body: result.out };
|
|
204
|
+
}, afterAdvance: () => { this.capture = undefined; this.metadata(); } });
|
|
205
|
+
await applications.loadExisting(); this.applications = applications; return applications;
|
|
206
|
+
}
|
|
207
|
+
evaluate(value: unknown) {
|
|
208
|
+
this.check(); const evaluate = this.evaluator ??= outboundPolicyEvaluator(this.home, this.opts.screen), verdict = evaluate(structuredClone(value) as Record<string, unknown>); this.check(); return verdict;
|
|
209
|
+
}
|
|
210
|
+
nativeProofs(): Set<string> {
|
|
211
|
+
const verified=new Set<string>();
|
|
212
|
+
for(const target of this.targets){
|
|
213
|
+
const candidates=[...target.rows.values()].filter((row):row is PruneRow=>row.op==="prune"&&row.generation===target.generation);
|
|
214
|
+
const lineages=candidates.length&&existsSync(join(target.store.dir,"local",`publisher-boundary-${this.machineId}.json`))?publisherPruneLineages(target.store.profile,target.mutation!,candidates):undefined;
|
|
215
|
+
for(const key of verifiedPlacementPrunes([{realm:target.realm,generation:target.generation,rows:target.rows,fetched:target.fetched,...(lineages?{lineageIds:new Set(lineages.keys())}:{})}]))verified.add(key);
|
|
216
|
+
}
|
|
217
|
+
return verified;
|
|
218
|
+
}
|
|
219
|
+
async materialize(createdUnixSeconds: number, frontier: PlacementJournalRead, frozen?: PlacementPublicationBundle): Promise<Materialized> {
|
|
220
|
+
if (!this.capture) refuse(); const capture = this.capture;
|
|
221
|
+
const projection = rebuildPlacementProjection(frontier.epoch, frontier.records.slice(0, frozen?.root.journal.sequence ?? frontier.sequence));
|
|
222
|
+
const old = await Promise.all(this.targets.map(async (target, index) => {
|
|
223
|
+
const head = frozen ? frozen.root.targets[index]?.expectedRemoteHead : target.remote;
|
|
224
|
+
return head ? target.auditor.history(head) : undefined;
|
|
225
|
+
}));
|
|
226
|
+
const verified = this.nativeProofs();
|
|
227
|
+
const materialized = await materializePlacementRows({capture,projection,generation:this.journal.epoch.generation,
|
|
228
|
+
targets:this.targets.map(target=>({realm:target.realm,generation:target.generation,remote:target.remote,local:target.local,fetched:target.fetched,
|
|
229
|
+
localPrunes:target.localPrunes,pruned:new Set(target.store.prunePort().snapshot().pruned??[])})),
|
|
230
|
+
old,verifiedPrunes:verified,evaluate:value=>this.evaluate(value),createdUnixSeconds,...(frozen?{frozen}:{}),
|
|
231
|
+
readTree:(_index,head)=>this.required(["rev-parse",`${head}^{tree}`]),
|
|
232
|
+
publisherLineages:(index,rows)=>{const target=this.targets[index]!;assertPublisherBoundary(target.store.profile,target.mutation!);return publisherPruneLineages(target.store.profile,target.mutation!,rows);}});
|
|
233
|
+
for(const [index,target]of this.targets.entries())Object.assign(target,materialized.targets[index]!);
|
|
234
|
+
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,
|
|
236
|
+
sourceHash:capture.context.sourceHash,aliasesHash:capture.context.aliasesHash,policyHash:capture.context.policyHash,remoteHash:digest(this.url),
|
|
237
|
+
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
|
+
let bundle=planPlacementPublication(body,{sources:capture.prefixes,decisions,objects});
|
|
239
|
+
let scheduled=this.events(bundle,projection,old);
|
|
240
|
+
let head=body.journal.headHash;
|
|
241
|
+
const lengths=scheduled.events.map((event,index)=>{const record=createPlacementRecord(this.epochHash,body.journal.sequence+index+1,head,event);head=record.seal;return publicationBytes(record).length;});
|
|
242
|
+
bundle=planPlacementPublication({...body,reservedRecords:lengths.length,reservedRecordBytes:lengths},{sources:capture.prefixes,decisions,objects});
|
|
243
|
+
scheduled=this.events(bundle,projection,old);
|
|
244
|
+
if(frozen&&canonicalJSON(bundle)!==canonicalJSON(frozen))refuse();
|
|
245
|
+
return{bundle,decisions,projection,old,...scheduled,exported:scheduled.events.filter(event=>event.kind==="publication-ambiguous").length,withheld:decisions.filter(decision=>!decision.eligible).length};
|
|
246
|
+
}
|
|
247
|
+
events(bundle:PlacementPublicationBundle,projection:PlacementProjection,old:(ShardHistory|undefined)[]):{events:PlacementEvent[];preCount:number;holdCount:number}{
|
|
248
|
+
const decisions=bundle.chunks.filter(chunk=>chunk.kind==="decisions").flatMap(chunk=>chunk.items as PlacementPublicationDecision[]).sort((a,b)=>placementKeyId(a.key).localeCompare(placementKeyId(b.key)));
|
|
249
|
+
const holds:PlacementEvent[]=[],known:PlacementEvent[]=[],ambiguous:PlacementEvent[]=[],committed:PlacementEvent[]=[];
|
|
250
|
+
const root=bundle.root;
|
|
251
|
+
const make=(decision:PlacementPublicationDecision,kind:PlacementEvent["kind"],commit?:string):PlacementEvent=>{
|
|
252
|
+
const base={requestId:hashJSON({attempt:root.seal,key:decision.key,kind,commit:commit??null}),key:decision.key,sourcePlacement:decision.sourcePlacement,
|
|
253
|
+
effectHash:hashJSON({attempt:root.seal,context:root.contextHash,key:decision.key,kind,commit:commit??null})};
|
|
254
|
+
if(kind==="hold"){
|
|
255
|
+
const reason=decision.reason;
|
|
256
|
+
const hold:PlacementHoldReason=reason==="secret"||reason==="personal-data"||reason==="deny-path"||reason==="private-or-invalid"||reason==="host"||reason==="person-local"?reason:"related-local";
|
|
257
|
+
return{...base,kind,reason:hold};
|
|
258
|
+
}
|
|
259
|
+
const index=root.targets.findIndex(target=>sameRealm(target.realm,decision.key.realm));if(index<0)refuse();
|
|
260
|
+
const outcome={attemptHash:root.seal,remoteHash:root.remoteHash,refHash:root.targets[index]!.refHash};
|
|
261
|
+
if(kind==="publication-ambiguous")return{...base,kind,outcome};
|
|
262
|
+
if(kind!=="publication-committed"||!commit)refuse();
|
|
263
|
+
return{...base,kind,outcome:{...outcome,commit,proofHash:hashJSON({attempt:root.seal,key:decision.key,commit,remote:root.remoteHash,ref:root.targets[index]!.refHash})}};
|
|
264
|
+
};
|
|
265
|
+
for(const decision of decisions){
|
|
266
|
+
const state=projection.get(decision.key);
|
|
267
|
+
const index=root.targets.findIndex(target=>sameRealm(target.realm,decision.key.realm)),target=root.targets[index];if(!target)refuse();
|
|
268
|
+
const original=old[index]?.rows.get(decision.key.id),published=original&&hashJSON(original)===decision.key.rowHash;
|
|
269
|
+
if(decision.hold)holds.push(make(decision,"hold"));
|
|
270
|
+
if(published&&state?.exposure!=="published"&&target.expectedRemoteHead)known.push(make(decision,"publication-committed",target.expectedRemoteHead));
|
|
271
|
+
if(decision.eligible&&target.publish&&!published){
|
|
272
|
+
ambiguous.push(make(decision,"publication-ambiguous"));committed.push(make(decision,"publication-committed",target.commit!));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return{events:[...holds,...known,...ambiguous,...committed],preCount:holds.length+known.length+ambiguous.length,holdCount:holds.length};
|
|
276
|
+
}
|
|
277
|
+
async frozenOld(bundle:PlacementPublicationBundle):(Promise<(ShardHistory|undefined)[]>){
|
|
278
|
+
const values:(ShardHistory|undefined)[]=[];
|
|
279
|
+
for(const [index,target]of this.targets.entries()){
|
|
280
|
+
const planned=bundle.root.targets[index];if(!planned||!sameRealm(planned.realm,physical(target))||planned.refHash!==digest(shardSyncRef(target.realm)))refuse();
|
|
281
|
+
values.push(planned.expectedRemoteHead?await target.auditor.history(planned.expectedRemoteHead):undefined);
|
|
282
|
+
}
|
|
283
|
+
return values;
|
|
284
|
+
}
|
|
285
|
+
appendThrough(bundle:PlacementPublicationBundle,events:PlacementEvent[],count:number,factual=false):void{
|
|
286
|
+
const root=bundle.root;let read=readPlacementJournal(this.home,this.epochHash),head=root.journal.headHash;
|
|
287
|
+
const records:PlacementRecord[]=[];
|
|
288
|
+
for(const [index,event]of events.entries()){
|
|
289
|
+
const record=createPlacementRecord(this.epochHash,root.journal.sequence+index+1,head,event);head=record.seal;records.push(record);
|
|
290
|
+
}
|
|
291
|
+
const consumed=read.sequence-root.journal.sequence;
|
|
292
|
+
if(consumed<0||consumed>root.reservedRecords)refuse();
|
|
293
|
+
for(let index=0;index<Math.min(consumed,records.length);index++)
|
|
294
|
+
if(canonicalJSON(read.records[root.journal.sequence+index])!==canonicalJSON(records[index]))refuse();
|
|
295
|
+
if(read.pending){
|
|
296
|
+
const sequence=read.pendingRecordSequence??read.sequence+1, index=sequence-root.journal.sequence-1;
|
|
297
|
+
const record=records[index];
|
|
298
|
+
if(!record||index>=count||read.pendingRecord&&canonicalJSON(read.pendingRecord)!==canonicalJSON(record))refuse();
|
|
299
|
+
appendPlacementEvent(this.home,this.epochHash,events[index]!,record.previousHash,this.storage(factual));read=readPlacementJournal(this.home,this.epochHash);
|
|
300
|
+
}
|
|
301
|
+
for(let index=read.sequence-root.journal.sequence;index<count;index++){
|
|
302
|
+
const record=records[index];if(!record)refuse();appendPlacementEvent(this.home,this.epochHash,events[index]!,record.previousHash,this.storage(factual));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
cacheStamp():string {
|
|
306
|
+
const hash=createHash("sha256");
|
|
307
|
+
const walk=(dir:string)=>{for(const name of readdirSync(dir).sort()){
|
|
308
|
+
const file=join(dir,name),stat=lstatSync(file,{bigint:true});
|
|
309
|
+
if(stat.isSymbolicLink()||stat.uid!==BigInt(process.getuid?.()??-1)||(!stat.isDirectory()&&(!stat.isFile()||stat.nlink!==1n)))refuse();
|
|
310
|
+
hash.update(canonicalJSON([file.slice(this.cache.length),String(stat.dev),String(stat.ino),String(stat.size),String(stat.mtimeNs),String(stat.ctimeNs),String(stat.mode)]));
|
|
311
|
+
if(stat.isDirectory())walk(file);
|
|
312
|
+
}};
|
|
313
|
+
walk(this.cache);return hash.digest("hex");
|
|
314
|
+
}
|
|
315
|
+
/** Bind installed bytes after all host callbacks, not merely the pure pre-install object plan.
|
|
316
|
+
* ponytail: the final private-cache/source check and Git's accepted push are separate transactions.
|
|
317
|
+
* Compatible writers honor the held fences; stronger same-user raw-writer isolation needs descriptor-bound immutable object access.
|
|
318
|
+
* Policy tightening cannot revoke an already in-flight accepted push; the result reports that ceiling. */
|
|
319
|
+
async installedObjectGuard(bundle:PlacementPublicationBundle):Promise<()=>void>{
|
|
320
|
+
const before=this.cacheStamp();
|
|
321
|
+
for(const chunk of bundle.chunks)if(chunk.kind==="objects")for(const item of chunk.items as PlacementPublicationObject[]){
|
|
322
|
+
const type=await this.required(["cat-file","-t",item.oid]);
|
|
323
|
+
const size=Number(await this.required(["cat-file","-s",item.oid]));
|
|
324
|
+
if(type!==item.type||size!==item.size)refuse();
|
|
325
|
+
const result=await this.call(()=>generationSyncGitBytes(this.root,this.args(["cat-file",item.type,item.oid]),this.opts.signal,undefined,item.size));
|
|
326
|
+
if(result.code!==0||result.out.length!==item.size||digest(result.out)!==item.sha256
|
|
327
|
+
||createHash("sha1").update(`${item.type} ${item.size}\0`).update(result.out).digest("hex")!==item.oid)refuse();
|
|
328
|
+
}
|
|
329
|
+
return()=>{if(this.cacheStamp()!==before)refuse();};
|
|
330
|
+
}
|
|
331
|
+
async allPublic(bundle:PlacementPublicationBundle):Promise<boolean>{
|
|
332
|
+
for(const [index,planned]of bundle.root.targets.entries())if(planned.publish){
|
|
333
|
+
const target=this.targets[index];if(!target||!target.remote||!planned.commit)return false;
|
|
334
|
+
if(!await target.auditor.verifiedAncestor(planned.commit,target.remote))return false;
|
|
335
|
+
}
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
338
|
+
async terminalUnknown(read:PlacementPublicationRead):Promise<SyncResult>{
|
|
339
|
+
this.factual=true;this.recovered=true;const root=read.bundle.root;this.attemptHash=root.seal;
|
|
340
|
+
const projection=rebuildPlacementProjection(this.journal.epoch,this.journal.records.slice(0,root.journal.sequence));
|
|
341
|
+
const holds=this.events(read.bundle,projection,[]).events.filter(event=>event.kind==="hold");
|
|
342
|
+
const before=readPlacementJournal(this.home,this.epochHash);
|
|
343
|
+
if(before.pending){
|
|
344
|
+
const old=await this.frozenOld(read.bundle),schedule=this.events(read.bundle,projection,old);
|
|
345
|
+
const sequence=before.pendingRecordSequence??before.sequence+1;
|
|
346
|
+
if(!before.pendingRecord&&schedule.events[sequence-root.journal.sequence-1]?.kind!=="hold")refuse();
|
|
347
|
+
this.appendThrough(read.bundle,schedule.events,Math.max(holds.length,sequence-root.journal.sequence),true);
|
|
348
|
+
}else this.appendThrough(read.bundle,holds,holds.length,true);
|
|
349
|
+
if(read.pendingPhase==="artifact-built"){
|
|
350
|
+
const guard=await this.installedObjectGuard(read.bundle);guard();
|
|
351
|
+
recordPlacementPublicationPhase(this.home,this.epochHash,root.seal,"artifact-built",{exportHash:hashJSON(root.targets.map(target=>target.exportSetHash))},this.storage(true));
|
|
352
|
+
}
|
|
353
|
+
if(read.pendingPhase==="publication-intent"){
|
|
354
|
+
const artifact=read.facts.find(fact=>fact.phase==="artifact-built");if(!artifact)refuse();
|
|
355
|
+
recordPlacementPublicationPhase(this.home,this.epochHash,root.seal,"publication-intent",{artifactFactHash:artifact.seal,policyHash:root.policyHash,policyCheckUnixMs:root.policySnapshotUnixMs},this.storage(true));
|
|
356
|
+
}
|
|
357
|
+
read=readPlacementPublication(this.home,this.epochHash,root.seal);
|
|
358
|
+
const journal=readPlacementJournal(this.home,this.epochHash),intent=read.facts.find(fact=>fact.phase==="publication-intent");
|
|
359
|
+
await this.observe("before-receipt");
|
|
360
|
+
recordPlacementPublicationPhase(this.home,this.epochHash,root.seal,"completed",{intentFactHash:intent?.seal??null,journal:{sequence:journal.sequence,headHash:journal.headHash},
|
|
361
|
+
outcomeHash:hashJSON({attempt:root.seal,outcome:"unknown"}),outcome:"unknown"},this.storage(true));
|
|
362
|
+
await this.observe("receipt-written");
|
|
363
|
+
return this.result(false,"unknown","The retained placement attempt may have published; its frozen holds and unknown outcome were preserved. Start a fresh sync to recheck current sources.");
|
|
364
|
+
}
|
|
365
|
+
async materializeObjects(bundle:PlacementPublicationBundle):Promise<void>{
|
|
366
|
+
for(const target of this.targets)for(const object of target.objects){
|
|
367
|
+
this.check();const file=join(this.root,`.placement-object-${randomBytes(16).toString("hex")}`);this.temporary.push(file);
|
|
368
|
+
const fd=openSync(file,constants.O_WRONLY|constants.O_CREAT|constants.O_EXCL|constants.O_NOFOLLOW,0o600);
|
|
369
|
+
try{let at=0;while(at<object.bytes.length){const n=writeSync(fd,object.bytes,at,object.bytes.length-at,at);if(n<=0)refuse();at+=n;}fsyncSync(fd);}finally{closeSync(fd);}
|
|
370
|
+
const written=await this.required(["hash-object","-w","-t",object.type,"--no-filters","--",file]);if(written!==object.oid)refuse();
|
|
371
|
+
}
|
|
372
|
+
const saved=readPlacementPublication(this.home,this.epochHash,bundle.root.seal);
|
|
373
|
+
if(!saved.facts.some(fact=>fact.phase==="artifact-built"))recordPlacementPublicationPhase(this.home,this.epochHash,bundle.root.seal,"artifact-built",{exportHash:hashJSON(bundle.root.targets.map(target=>target.exportSetHash))},this.storage());
|
|
374
|
+
await this.observe("artifact-built");this.check();
|
|
375
|
+
}
|
|
376
|
+
async finish(material:Materialized,recovery=false):Promise<SyncResult>{
|
|
377
|
+
this.factual=true;this.recovered=recovery;const root=material.bundle.root;
|
|
378
|
+
await this.observe("before-ack");this.appendThrough(material.bundle,material.events,material.events.length,true);await this.observe("acknowledged");this.metadata();
|
|
379
|
+
for (const [index,target] of this.targets.entries()) {
|
|
380
|
+
const planned = root.targets[index]!;
|
|
381
|
+
const head = planned.publish ? planned.commit : target.remote;
|
|
382
|
+
if (head) {
|
|
383
|
+
const actual = await target.auditor.history(head); target.remoteHistory = actual; target.candidate = actual;
|
|
384
|
+
target.rows = new Map(actual.rows); target.fetched = new Set(actual.rows.keys()); target.origins = new Map(actual.origins);
|
|
385
|
+
} else { target.rows.clear(); target.fetched.clear(); target.candidate = emptyHistory(); }
|
|
386
|
+
}
|
|
387
|
+
this.preflight(false);
|
|
388
|
+
const applications=await this.applicationCoordinator();await applications.prepare();
|
|
389
|
+
for(const target of this.targets)for(const hold of target.holds)shardInstallHold(this.root,hold);
|
|
390
|
+
await applications.install();
|
|
391
|
+
for(const target of this.targets){
|
|
392
|
+
shardImportRows(target);target.pending=[];target.selectorChecks=[];shardSelectorRetention(target,false,true);
|
|
393
|
+
}
|
|
394
|
+
applications.application.nativeApplied=this.targets.reduce((sum,target)=>sum+[...target.rows.values()].filter(row=>row.op==="prune"&&row.generation===target.generation).length,0);
|
|
395
|
+
applications.application.transport="durable";applications.application.phase="complete";
|
|
396
|
+
await this.observe("imported");this.metadata();
|
|
397
|
+
for(const target of this.targets)ensurePublisherBoundary(target.store.profile,target.mutation!);
|
|
398
|
+
for(const [index,target]of this.targets.entries())if(root.targets[index]!.publish){
|
|
399
|
+
const planned=root.targets[index]!, ref=shardSyncRef(target.realm), existing=await this.git(["rev-parse","--verify","--quiet",ref]);
|
|
400
|
+
if (existing.code === 0 && existing.out === planned.commit) continue;
|
|
401
|
+
if (existing.code !== 1 && (existing.code !== 0 || existing.out !== planned.expectedCacheHead)) refuse();
|
|
402
|
+
if (existing.code === 1 && planned.expectedCacheHead !== null) refuse();
|
|
403
|
+
await this.required(["update-ref","--no-deref",ref,planned.commit!,planned.expectedCacheHead??"0".repeat(40)]);
|
|
404
|
+
}
|
|
405
|
+
await this.observe("before-receipt");
|
|
406
|
+
const read=readPlacementPublication(this.home,this.epochHash,root.seal),intent=read.facts.find(fact=>fact.phase==="publication-intent"),journal=readPlacementJournal(this.home,this.epochHash);
|
|
407
|
+
if(!intent)refuse();
|
|
408
|
+
recordPlacementPublicationPhase(this.home,this.epochHash,root.seal,"completed",{intentFactHash:intent.seal,journal:{sequence:journal.sequence,headHash:journal.headHash},
|
|
409
|
+
outcomeHash:hashJSON({attempt:root.seal,commits:root.targets.map(target=>target.commit)}),outcome:"committed"},this.storage(true));
|
|
410
|
+
await this.observe("receipt-written");return this.result(true,"committed");
|
|
411
|
+
}
|
|
412
|
+
importsNeeded():boolean{
|
|
413
|
+
return this.targets.some(target=>{
|
|
414
|
+
if(target.pending.length||target.batch||target.holds.some(hold=>!existsSync(hold.file)||existsSync(hold.file+".pending")))return true;
|
|
415
|
+
const durable=new Set(target.store.prunePort().snapshot().durablePruneIds??[]);
|
|
416
|
+
return [...target.fetched].some(id=>{
|
|
417
|
+
const row=target.rows.get(id);if(!row)refuse();
|
|
418
|
+
return row.op==="prune" ? row.generation===target.generation&&!durable.has(id)
|
|
419
|
+
: !target.sources.some(source=>source.row.id===id&&source.publicWitness);
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
result(ok:boolean,outcome:"committed"|"unknown"|"not-needed",error?:string):SyncResult{
|
|
424
|
+
return{ok,pushed:this.accepted&&(this.attempted||this.recovered&&!!this.current?.bundle.root.targets.some(target=>target.publish)),entries:this.current?.exported??0,...(error?{error}:{}),...(this.applications?{pruneApplication:{...this.applications.application}}:{}),
|
|
425
|
+
placementPublication:{...(this.attemptHash?{attemptHash:this.attemptHash}:{}),outcome,withheld:this.current?.withheld??0,exported:this.current?.exported??0,
|
|
426
|
+
...(this.lastPolicyHash?{lastPolicyHash:this.lastPolicyHash}:{}),...(this.lastPolicyCheckUnixMs!==undefined?{lastPolicyCheckUnixMs:this.lastPolicyCheckUnixMs,policyCheckSource:this.policyCheckSource??(this.recovered?"durable-intent" as const:"live" as const)}:{}),
|
|
427
|
+
policyCeiling:"last-positive-check-before-in-flight-push",...(this.recovered?{recovered:true}:{})}};
|
|
428
|
+
}
|
|
429
|
+
async run():Promise<SyncResult>{
|
|
430
|
+
try{
|
|
431
|
+
await this.initialize();
|
|
432
|
+
const reservation=this.journal.publicationReservation,retained=reservation?readPlacementPublication(this.home,this.epochHash,reservation.attemptHash):undefined;
|
|
433
|
+
if(retained){
|
|
434
|
+
this.attemptHash=retained.bundle.root.seal;if(retained.pending&&!retained.pendingPhase)refuse();
|
|
435
|
+
ensurePlacementPublicationDurability(this.home,this.epochHash,this.storage(true));
|
|
436
|
+
const lastIntent=retained.facts.find(fact=>fact.phase==="publication-intent");if(lastIntent?.phase==="publication-intent"){this.lastPolicyHash=lastIntent.policyHash;this.lastPolicyCheckUnixMs=lastIntent.policyCheckUnixMs;this.policyCheckSource="durable-intent";}
|
|
437
|
+
if(retained.bundle.root.remoteHash!==digest(this.url))return await this.terminalUnknown(retained);
|
|
438
|
+
if(retained.facts.some(fact=>fact.phase==="publication-intent")&&await this.allPublic(retained.bundle)){
|
|
439
|
+
this.factual=true;const old=await this.frozenOld(retained.bundle),projection=rebuildPlacementProjection(this.journal.epoch,this.journal.records.slice(0,retained.bundle.root.journal.sequence));
|
|
440
|
+
const schedule=this.events(retained.bundle,projection,old),decisions=retained.bundle.chunks.filter(chunk=>chunk.kind==="decisions").flatMap(chunk=>chunk.items as PlacementPublicationDecision[]);
|
|
441
|
+
this.current={bundle:retained.bundle,decisions,projection,old,...schedule,exported:schedule.events.filter(event=>event.kind==="publication-ambiguous").length,withheld:decisions.filter(decision=>!decision.eligible).length};
|
|
442
|
+
this.accepted=true;return await this.finish(this.current,true);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
try { this.preflight();for(const target of this.targets)if(existsSync(join(target.store.dir,"local",`publisher-boundary-${this.machineId}.json`)))assertPublisherBoundary(target.store.profile,target.mutation!);this.startCapture(); }
|
|
446
|
+
catch { if(retained)return await this.terminalUnknown(retained);throw new Error("placement source preflight unavailable"); }
|
|
447
|
+
if(retained){
|
|
448
|
+
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
|
|
450
|
+
&&root.policyHash===this.capture!.context.policyHash&&root.targets.every((target,index)=>target.expectedRemoteHead===(this.targets[index]?.remote??null));
|
|
451
|
+
if(!stable)return await this.terminalUnknown(retained);
|
|
452
|
+
}
|
|
453
|
+
const frontier=retained?{...this.journal,sequence:retained.bundle.root.journal.sequence,headHash:retained.bundle.root.journal.headHash}:this.journal;
|
|
454
|
+
const material=await this.materialize(retained?.bundle.root.createdUnixSeconds??Math.floor(Date.now()/1000),frontier,retained?.bundle);this.current=material;this.attemptHash=material.bundle.root.seal;
|
|
455
|
+
const applications=await this.applicationCoordinator();await applications.prepare();
|
|
456
|
+
const changed=material.bundle.root.targets.some((target,index)=>target.publish&&target.commit!==(this.targets[index]!.remote??null));
|
|
457
|
+
if(!retained&&!material.events.length&&!changed&&!this.importsNeeded()){this.attemptHash=undefined;return this.result(true,"not-needed");}
|
|
458
|
+
if(!retained){await this.observe("before-plan-fsync");this.check();preparePlacementPublication(this.home,material.bundle,this.storage());await this.observe("plan-fsynced");this.check();}
|
|
459
|
+
this.appendThrough(material.bundle,material.events,material.preCount);
|
|
460
|
+
await this.materializeObjects(material.bundle);
|
|
461
|
+
let read=readPlacementPublication(this.home,this.epochHash,material.bundle.root.seal);
|
|
462
|
+
const artifact=read.facts.find(fact=>fact.phase==="artifact-built");if(!artifact)refuse();
|
|
463
|
+
this.check();this.lastPolicyCheckUnixMs=Date.now();this.lastPolicyHash=material.bundle.root.policyHash;this.policyCheckSource="live";
|
|
464
|
+
if(!read.facts.some(fact=>fact.phase==="publication-intent"))recordPlacementPublicationPhase(this.home,this.epochHash,material.bundle.root.seal,"publication-intent",{artifactFactHash:artifact.seal,policyHash:material.bundle.root.policyHash,policyCheckUnixMs:material.bundle.root.policySnapshotUnixMs},this.storage());
|
|
465
|
+
ensurePlacementPublicationDurability(this.home,this.epochHash,this.storage());
|
|
466
|
+
await this.observe("before-cas");this.check();
|
|
467
|
+
const installedGuard=await this.installedObjectGuard(material.bundle);
|
|
468
|
+
this.check();for(const target of this.targets)for(const check of target.selectorChecks??[])check();
|
|
469
|
+
installedGuard();
|
|
470
|
+
this.lastPolicyCheckUnixMs=Date.now(); this.lastPolicyHash=material.bundle.root.policyHash;this.policyCheckSource="live";
|
|
471
|
+
const participating=material.bundle.root.targets.map((target,index)=>({target,index})).filter(item=>item.target.publish);
|
|
472
|
+
this.attempted=participating.length>0;
|
|
473
|
+
const sent=participating.length?await generationSyncGit(this.root,this.args(["push","--atomic","--no-follow-tags","-q",...participating.map(({target,index})=>`--force-with-lease=${shardSyncRef(this.targets[index]!.realm)}:${target.expectedRemoteHead??""}`),"origin",...participating.map(({target,index})=>`${target.commit}:${shardSyncRef(this.targets[index]!.realm)}`)]),this.opts.signal):{code:0};
|
|
474
|
+
this.accepted=sent.code===0;await this.observe("cas-returned");
|
|
475
|
+
if(!this.accepted)return this.result(false,"unknown","The placement publication outcome is unknown; retained receipts may have published. Recheck this exact attempt before retrying.");
|
|
476
|
+
this.factual=true;return await this.finish(material);
|
|
477
|
+
}catch(error){
|
|
478
|
+
try { this.testOnlyError?.(error); } catch { /* Test observers never alter production results. */ }
|
|
479
|
+
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
|
+
}finally{
|
|
481
|
+
let ownedCache=true;try{this.metadata();}catch{ownedCache=false;}
|
|
482
|
+
if(ownedCache){
|
|
483
|
+
for(const ref of this.refs)try{this.metadata();await generationSyncGit(this.root,this.args(["update-ref","--no-deref","-d",ref]),undefined);}catch{}
|
|
484
|
+
for(const file of this.temporary)try{this.metadata();rmSync(file,{force:true});}catch{}
|
|
485
|
+
}
|
|
486
|
+
for(const release of this.releases.reverse())try{release();}catch{}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|