@llblab/pi-kit 0.13.0 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +1 -1
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +7 -7
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +8 -9
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +30 -0
- package/node_modules/@llblab/pi-state-flow/README.md +1 -3
- package/node_modules/@llblab/pi-state-flow/dist/index.d.ts +21 -0
- package/node_modules/@llblab/pi-state-flow/dist/index.js +20 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.d.ts +39 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.js +78 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.d.ts +110 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.js +334 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.d.ts +49 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.js +67 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/config.d.ts +11 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/config.js +53 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +23 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +109 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.d.ts +111 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +189 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/discovery.d.ts +21 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/discovery.js +125 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/durable.d.ts +102 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +507 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/episode.d.ts +8 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/episode.js +27 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.d.ts +22 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +1263 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/git.d.ts +72 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +565 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/history.d.ts +22 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/history.js +79 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/json.d.ts +12 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/json.js +109 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/logging.d.ts +25 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/logging.js +24 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/maintenance.d.ts +36 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/maintenance.js +98 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/memory.d.ts +15 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/memory.js +42 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/migration.d.ts +13 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/migration.js +133 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.d.ts +7 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +59 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/publication.d.ts +69 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/publication.js +335 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/query.d.ts +27 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +35 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.d.ts +8 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.js +27 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.d.ts +36 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.js +38 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +142 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +529 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/session.d.ts +21 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/session.js +44 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/skills.d.ts +25 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/skills.js +131 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +88 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +255 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/state.d.ts +55 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/state.js +31 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +38 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +79 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/storage.d.ts +46 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/storage.js +217 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +100 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +234 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.d.ts +39 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.js +203 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.d.ts +25 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +204 -0
- package/node_modules/@llblab/pi-state-flow/dist/package.json +79 -0
- package/node_modules/@llblab/pi-state-flow/dist/pi-state-flow/index.js +1 -0
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +138 -0
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +11 -5
- package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +5 -1
- package/node_modules/@llblab/pi-state-flow/docs/filesystem-recovery.md +3 -3
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +6 -4
- package/node_modules/@llblab/pi-state-flow/index.ts +1 -0
- package/node_modules/@llblab/pi-state-flow/lib/compaction.ts +17 -1
- package/node_modules/@llblab/pi-state-flow/lib/config.ts +6 -1
- package/node_modules/@llblab/pi-state-flow/lib/durable.ts +88 -96
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +64 -12
- package/node_modules/@llblab/pi-state-flow/lib/git.ts +32 -188
- package/node_modules/@llblab/pi-state-flow/lib/migration.ts +84 -48
- package/node_modules/@llblab/pi-state-flow/lib/query.ts +40 -0
- package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +7 -30
- package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +48 -50
- package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +41 -97
- package/node_modules/@llblab/pi-state-flow/lib/storage.ts +17 -12
- package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +13 -9
- package/node_modules/@llblab/pi-state-flow/package.json +23 -6
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +3 -1
- package/package.json +4 -4
|
@@ -7,17 +7,11 @@ import { spawn, spawnSync } from "node:child_process";
|
|
|
7
7
|
import {
|
|
8
8
|
assertOwnedFileUpdates,
|
|
9
9
|
captureOwnedFileBases,
|
|
10
|
-
captureLegacyTemporalFileBases,
|
|
11
10
|
captureTemporalFileBases,
|
|
12
11
|
cwdScopePaths,
|
|
13
|
-
legacySessionRuntimePaths,
|
|
14
|
-
legacyTemporalScopePaths,
|
|
15
|
-
durablePaths,
|
|
16
12
|
isStateFlowOwnedPath,
|
|
17
13
|
parseScopeProvenance,
|
|
18
14
|
parseScopeStream,
|
|
19
|
-
parseStateSource,
|
|
20
|
-
serializeScopeStream,
|
|
21
15
|
restoreDurableFileBases,
|
|
22
16
|
temporalScopePaths,
|
|
23
17
|
sessionScopePaths,
|
|
@@ -28,7 +22,7 @@ import {
|
|
|
28
22
|
} from "./durable.ts";
|
|
29
23
|
import { parseArtifactProvenanceRegistry, type ArtifactProvenanceRegistry } from "./artifact.ts";
|
|
30
24
|
import { planLegacyStorageMigration } from "./migration.ts";
|
|
31
|
-
import { sameJson } from "./json.ts";
|
|
25
|
+
import { canonicalJson, sameJson } from "./json.ts";
|
|
32
26
|
import { validateTemporalState, type ScopeStream, type TemporalState } from "./temporal.ts";
|
|
33
27
|
import { createSessionRuntime, parseSessionRuntime, serializeSessionRuntime, type SessionRuntime, type Snapshot } from "./snapshot.ts";
|
|
34
28
|
import { assertTemporalFileBase, loadTemporalFileRevision, planTemporalPublication, temporalFileReceipts, withStoragePublicationLock } from "./storage.ts";
|
|
@@ -206,8 +200,6 @@ export interface TemporalRevisionLoad {
|
|
|
206
200
|
runtime?: { document: SessionRuntime; revision: string };
|
|
207
201
|
/** Runtime-owned artifact provenance retained at this revision. */
|
|
208
202
|
provenance: Record<StateScope, ArtifactProvenanceRegistry>;
|
|
209
|
-
/** True only when this revision directly selected pre-0.4 hashed paths. */
|
|
210
|
-
legacyLayout?: true;
|
|
211
203
|
}
|
|
212
204
|
|
|
213
205
|
export function captureTemporalGitBase(cwd: string, sessionId: string, repositoryRoot: string, sessionKey = sessionId): TemporalGitBase {
|
|
@@ -223,64 +215,57 @@ function revisionScopeFiles(read: (path: string) => DurableFileBase, paths: Retu
|
|
|
223
215
|
paths,
|
|
224
216
|
checkpoint: read(paths.checkpoint),
|
|
225
217
|
patches: read(paths.patches),
|
|
226
|
-
legacy: read(resolve(paths.directory, "state.json")),
|
|
227
218
|
meta: read(paths.meta),
|
|
228
219
|
};
|
|
229
220
|
}
|
|
230
221
|
|
|
231
|
-
/** Cold scope-stream reconstruction
|
|
222
|
+
/** Cold scope-stream reconstruction from canonical revision paths only. */
|
|
232
223
|
export function loadTemporalRevision(cwd: string, sessionId: string, repositoryRoot: string, revision: string, sessionKey = sessionId): TemporalRevisionLoad {
|
|
233
224
|
const root = assertRepositoryRoot(repositoryRoot);
|
|
234
225
|
assertReadableRevision(root, revision);
|
|
235
226
|
const scopePaths = (["global", "cwd", "session"] as const).map((scope) => ({
|
|
236
|
-
scope,
|
|
237
|
-
canonical: temporalScopePaths(cwd, sessionId, scope, root, sessionKey),
|
|
238
|
-
legacy: scope === "global" ? undefined : legacyTemporalScopePaths(cwd, sessionId, scope, root),
|
|
227
|
+
scope, paths: temporalScopePaths(cwd, sessionId, scope, root, sessionKey),
|
|
239
228
|
}));
|
|
240
229
|
const canonicalRuntime = sessionRuntimePaths(cwd, sessionId, root, sessionKey);
|
|
241
|
-
const legacyRuntime = legacySessionRuntimePaths(cwd, sessionId, root);
|
|
242
230
|
const readFile = revisionFileReader(root, revision, [
|
|
243
|
-
...scopePaths.flatMap(({
|
|
244
|
-
|
|
245
|
-
canonicalRuntime.config, canonicalRuntime.meta, legacyRuntime.config, legacyRuntime.meta,
|
|
231
|
+
...scopePaths.flatMap(({ paths }) => [paths.checkpoint, paths.patches, paths.meta]),
|
|
232
|
+
canonicalRuntime.config, canonicalRuntime.meta,
|
|
246
233
|
]);
|
|
247
234
|
const files: DurableFileBase[] = [];
|
|
248
235
|
const scopes = {} as Record<StateScope, ScopeStream | undefined>;
|
|
249
236
|
const provenance: Record<StateScope, ArtifactProvenanceRegistry> = { global: {}, cwd: {}, session: {} };
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
if (legacy && [selected.checkpoint, selected.patches, selected.legacy].every(({ identity }) => identity === "missing")) {
|
|
254
|
-
selected = { ...revisionScopeFiles(readFile, legacy), legacyLayout: true };
|
|
255
|
-
}
|
|
256
|
-
if (selected.legacy.identity !== "missing") throw new Error("Historical legacy storage requires explicit migration interpretation");
|
|
257
|
-
legacyLayout ||= selected.legacyLayout;
|
|
258
|
-
files.push(selected.checkpoint, selected.patches, selected.legacy, ...(scope === "session" ? [] : [selected.meta]));
|
|
237
|
+
for (const { scope, paths } of scopePaths) {
|
|
238
|
+
const selected = revisionScopeFiles(readFile, paths);
|
|
239
|
+
files.push(selected.checkpoint, selected.patches, ...(scope === "session" ? [] : [selected.meta]));
|
|
259
240
|
if (scope !== "session") provenance[scope] = parseScopeProvenance(selected.meta.content, selected.meta.path);
|
|
260
|
-
|
|
261
|
-
scope
|
|
241
|
+
try {
|
|
242
|
+
scopes[scope] = parseScopeStream(selected.checkpoint.content, selected.patches.content, scope,
|
|
243
|
+
scope === "cwd" ? cwd : undefined, selected.meta.content);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
throw new Error(`Invalid historical State Flow ${scope} scope`, { cause: error });
|
|
246
|
+
}
|
|
262
247
|
}
|
|
263
248
|
const readRuntime = (paths: ReturnType<typeof sessionRuntimePaths>) => ({
|
|
264
249
|
paths, config: readFile(paths.config), meta: readFile(paths.meta),
|
|
265
250
|
});
|
|
266
|
-
|
|
267
|
-
if (selectedRuntime.config.identity === "missing" && selectedRuntime.meta.identity === "missing") {
|
|
268
|
-
selectedRuntime = { ...readRuntime(legacyRuntime), legacyLayout: true };
|
|
269
|
-
}
|
|
270
|
-
legacyLayout ||= selectedRuntime.legacyLayout;
|
|
271
|
-
const { paths: runtimePaths, config, meta } = selectedRuntime;
|
|
251
|
+
const { paths: runtimePaths, config, meta } = readRuntime(canonicalRuntime);
|
|
272
252
|
files.push(config, meta);
|
|
273
253
|
const document = parseSessionRuntime(config.content, meta.content, cwd, sessionId);
|
|
274
|
-
if (document === undefined) return { base: { head: revision, files }, scopes, provenance
|
|
254
|
+
if (document === undefined) return { base: { head: revision, files }, scopes, provenance };
|
|
275
255
|
provenance.session = parseArtifactProvenanceRegistry(document.meta.artifacts, "State Flow session artifact provenance");
|
|
276
256
|
const owner = git(root, ["log", "-1", "--format=%H", revision, "--",
|
|
277
257
|
relativeOwnedPath(runtimePaths.config, root), relativeOwnedPath(runtimePaths.meta, root),
|
|
278
258
|
]).stdout.trim();
|
|
279
259
|
assertReadableRevision(root, owner);
|
|
280
|
-
|
|
260
|
+
let temporalRevision = document.meta.temporalRevision === undefined || document.meta.temporalRevision === "self"
|
|
281
261
|
? owner : document.meta.temporalRevision;
|
|
282
262
|
if (temporalRevision !== revision) {
|
|
283
|
-
|
|
263
|
+
const ancestor = git(root, ["merge-base", "--is-ancestor", temporalRevision, revision], { allowFailure: true }).status === 0;
|
|
264
|
+
const parents = git(root, ["rev-list", "--parents", "-n", "1", revision]).stdout.trim().split(/\s+/);
|
|
265
|
+
// A store-wide semantic migration intentionally replaces all prior history with one root.
|
|
266
|
+
// Its complete tree owns every scope, so predecessor temporal pointers collapse to self.
|
|
267
|
+
if (!ancestor && parents.length === 1) temporalRevision = owner;
|
|
268
|
+
else if (!ancestor) {
|
|
284
269
|
throw new Error("Temporal revision must be an ancestor of its runtime owner");
|
|
285
270
|
}
|
|
286
271
|
const selected = loadTemporalRevision(cwd, sessionId, root, temporalRevision, sessionKey);
|
|
@@ -291,151 +276,7 @@ export function loadTemporalRevision(cwd: string, sessionId: string, repositoryR
|
|
|
291
276
|
throw new Error("Session runtime has incomplete temporal scope storage");
|
|
292
277
|
}
|
|
293
278
|
validateTemporalState({ lineage: document.meta.lineage, scopes: { global: scopes.global, cwd: scopes.cwd, session: scopes.session } });
|
|
294
|
-
return { base: { head: revision, files }, scopes, runtime: { document, revision: owner }, provenance
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/** Legacy state.json is already current; explanatory journals are irrelevant to semantic recovery. */
|
|
298
|
-
export function loadLegacyStatesAtRevision(cwd: string, sessionId: string, repositoryRoot: string, revision: string, sessionKey = sessionId): Record<StateScope, MaterializedState | undefined> {
|
|
299
|
-
const root = assertRepositoryRoot(repositoryRoot);
|
|
300
|
-
assertReadableRevision(root, revision);
|
|
301
|
-
const canonical = {
|
|
302
|
-
global: durablePaths(root).globalState,
|
|
303
|
-
cwd: cwdScopePaths(cwd, root).state,
|
|
304
|
-
session: sessionScopePaths(cwd, sessionId, root, sessionKey).state,
|
|
305
|
-
};
|
|
306
|
-
const states = {} as Record<StateScope, MaterializedState | undefined>;
|
|
307
|
-
for (const scope of ["global", "cwd", "session"] as const) {
|
|
308
|
-
let path = canonical[scope];
|
|
309
|
-
let source = revisionFile(root, revision, path).content;
|
|
310
|
-
if (source === undefined && scope !== "global") {
|
|
311
|
-
path = resolve(legacyTemporalScopePaths(cwd, sessionId, scope, root).directory, "state.json");
|
|
312
|
-
source = revisionFile(root, revision, path).content;
|
|
313
|
-
}
|
|
314
|
-
states[scope] = parseStateSource(source, path);
|
|
315
|
-
}
|
|
316
|
-
return states;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/** Move a current-head draft CWD pair before a new native-named session is initialized. */
|
|
320
|
-
export function migrateHashedCwdAtHead(cwd: string, repositoryRoot: string): void {
|
|
321
|
-
withPublicationLock(repositoryRoot, (root) => {
|
|
322
|
-
const head = currentHead(root);
|
|
323
|
-
if (!head) return;
|
|
324
|
-
const old = legacyTemporalScopePaths(cwd, "unused", "cwd", root);
|
|
325
|
-
const target = temporalScopePaths(cwd, "unused", "cwd", root);
|
|
326
|
-
const paths = [old.checkpoint, old.patches, resolve(old.directory, "state.json"), target.checkpoint, target.patches, resolve(target.directory, "state.json")];
|
|
327
|
-
const captured = captureOwnedFileBases(paths, root);
|
|
328
|
-
const byPath = new Map(captured.map((file) => [file.path, file]));
|
|
329
|
-
const oldCheckpoint = byPath.get(old.checkpoint)!;
|
|
330
|
-
const oldPatches = byPath.get(old.patches)!;
|
|
331
|
-
const oldState = byPath.get(resolve(old.directory, "state.json"))!;
|
|
332
|
-
const targetFiles = [byPath.get(target.checkpoint)!, byPath.get(target.patches)!, byPath.get(resolve(target.directory, "state.json"))!];
|
|
333
|
-
if (targetFiles[2]!.identity !== "missing") return; // Canonical current-state format migrates in the next owner.
|
|
334
|
-
if (targetFiles[0]!.identity !== "missing" || targetFiles[1]!.identity !== "missing") {
|
|
335
|
-
parseScopeStream(targetFiles[0]!.content, targetFiles[1]!.content, "cwd", cwd);
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
if (oldCheckpoint.identity === "missing" && oldPatches.identity === "missing" && oldState.identity === "missing") return;
|
|
339
|
-
if (oldState.identity !== "missing") throw new Error("Hashed current-state storage requires explicit format migration before path migration");
|
|
340
|
-
const oldStream = parseScopeStream(oldCheckpoint.content, oldPatches.content, "cwd")!;
|
|
341
|
-
for (const file of [oldCheckpoint, oldPatches]) {
|
|
342
|
-
if (revisionFile(root, head, file.path).identity !== file.identity) throw new Error(`Hashed CWD source is not anchored at current HEAD: ${file.path}`);
|
|
343
|
-
}
|
|
344
|
-
const source = serializeScopeStream(oldStream, "cwd", cwd);
|
|
345
|
-
const updates = [
|
|
346
|
-
{ path: target.checkpoint, content: source.checkpoint }, { path: target.patches, content: source.patches },
|
|
347
|
-
{ path: old.checkpoint }, { path: old.patches },
|
|
348
|
-
];
|
|
349
|
-
try {
|
|
350
|
-
publishOwnedCohort(root, updates, captured, head, [], "migrate");
|
|
351
|
-
} catch (error) {
|
|
352
|
-
try { rmdirSync(target.directory); } catch { /* Preserve nonempty or concurrently used directories. */ }
|
|
353
|
-
throw error;
|
|
354
|
-
}
|
|
355
|
-
try { rmdirSync(old.directory); } catch { /* Draft sessions may remain under this directory. */ }
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
/** Move only the selected current-head draft layout; older branch layouts remain cold read input. */
|
|
360
|
-
export function migrateHashedLayoutAtHead(
|
|
361
|
-
cwd: string,
|
|
362
|
-
sessionId: string,
|
|
363
|
-
repositoryRoot: string,
|
|
364
|
-
revision: string,
|
|
365
|
-
sessionKey: string,
|
|
366
|
-
): (ReturnType<typeof publishOwnedCohort> & { base: TemporalGitBase; view: TemporalState }) | undefined {
|
|
367
|
-
return withPublicationLock(repositoryRoot, (root) => {
|
|
368
|
-
if (currentHead(root) !== revision) return undefined;
|
|
369
|
-
const selected = loadTemporalRevision(cwd, sessionId, root, revision, sessionKey);
|
|
370
|
-
if (!selected.legacyLayout || !selected.runtime || !selected.scopes.global || !selected.scopes.cwd || !selected.scopes.session) return undefined;
|
|
371
|
-
const view = { lineage: selected.runtime.document.meta.lineage, scopes: {
|
|
372
|
-
global: selected.scopes.global, cwd: selected.scopes.cwd, session: selected.scopes.session,
|
|
373
|
-
} };
|
|
374
|
-
validateTemporalState(view);
|
|
375
|
-
const canonical = captureTemporalBaseUnderLock(cwd, sessionId, root, sessionKey);
|
|
376
|
-
const legacyLive = captureLegacyTemporalFileBases(cwd, sessionId, root);
|
|
377
|
-
const bases = new Map([...canonical.files, ...legacyLive].map((file) => [file.path, file]));
|
|
378
|
-
const selectedFiles = new Map(selected.base.files.map((file) => [file.path, file]));
|
|
379
|
-
const updates: OwnedFileUpdate[] = [];
|
|
380
|
-
const cleanupDirectories = new Set<string>();
|
|
381
|
-
const targetDirectories = new Set<string>();
|
|
382
|
-
const removeEmptyDirectories = (directories: ReadonlySet<string>): void => {
|
|
383
|
-
for (const directory of [...directories].sort((left, right) => right.length - left.length)) {
|
|
384
|
-
try { rmdirSync(directory); } catch { /* Preserve nonempty or concurrently used directories. */ }
|
|
385
|
-
}
|
|
386
|
-
};
|
|
387
|
-
const move = (sourcePath: string, targetPath: string, content?: string): void => {
|
|
388
|
-
const source = selectedFiles.get(sourcePath);
|
|
389
|
-
if (!source || source.identity === "missing" || source.content === undefined) throw new Error(`Hashed-layout migration source is unavailable: ${sourcePath}`);
|
|
390
|
-
if (bases.get(sourcePath)?.identity !== source.identity) throw new Error(`Hashed-layout source changed after selected revision: ${sourcePath}`);
|
|
391
|
-
if (bases.get(targetPath)?.identity !== "missing") throw new Error(`Canonical State Flow path already exists during hashed-layout migration: ${targetPath}`);
|
|
392
|
-
updates.push({ path: targetPath, content: content ?? source.content }, { path: sourcePath });
|
|
393
|
-
cleanupDirectories.add(dirname(sourcePath));
|
|
394
|
-
targetDirectories.add(dirname(targetPath));
|
|
395
|
-
};
|
|
396
|
-
for (const scope of ["cwd", "session"] as const) {
|
|
397
|
-
const old = legacyTemporalScopePaths(cwd, sessionId, scope, root);
|
|
398
|
-
const target = temporalScopePaths(cwd, sessionId, scope, root, sessionKey);
|
|
399
|
-
if (selectedFiles.get(old.checkpoint)?.identity !== "missing") {
|
|
400
|
-
if (scope === "cwd") {
|
|
401
|
-
const stream = parseScopeStream(selectedFiles.get(old.checkpoint)!.content, selectedFiles.get(old.patches)!.content, "cwd")!;
|
|
402
|
-
const source = serializeScopeStream(stream, "cwd", cwd);
|
|
403
|
-
move(old.checkpoint, target.checkpoint, source.checkpoint);
|
|
404
|
-
move(old.patches, target.patches, source.patches);
|
|
405
|
-
} else {
|
|
406
|
-
move(old.checkpoint, target.checkpoint);
|
|
407
|
-
move(old.patches, target.patches);
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
const oldRuntime = legacySessionRuntimePaths(cwd, sessionId, root);
|
|
412
|
-
const targetRuntime = sessionRuntimePaths(cwd, sessionId, root, sessionKey);
|
|
413
|
-
const migratedRuntime = structuredClone(selected.runtime.document);
|
|
414
|
-
migratedRuntime.meta.temporalRevision = "self";
|
|
415
|
-
const runtimeSource = serializeSessionRuntime(migratedRuntime, cwd, sessionId);
|
|
416
|
-
if (selectedFiles.get(oldRuntime.config)?.identity !== "missing") {
|
|
417
|
-
move(oldRuntime.config, targetRuntime.config, runtimeSource.config);
|
|
418
|
-
move(oldRuntime.meta, targetRuntime.meta, runtimeSource.meta);
|
|
419
|
-
} else {
|
|
420
|
-
for (const [path, content] of [[targetRuntime.config, runtimeSource.config], [targetRuntime.meta, runtimeSource.meta]] as const) {
|
|
421
|
-
const source = selectedFiles.get(path);
|
|
422
|
-
if (!source || source.identity === "missing" || source.content === undefined) throw new Error(`Layout migration runtime is unavailable: ${path}`);
|
|
423
|
-
if (bases.get(path)?.identity !== source.identity) throw new Error(`Layout migration runtime changed after selected revision: ${path}`);
|
|
424
|
-
if (source.content !== content) updates.push({ path, content });
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
if (updates.length === 0) return undefined;
|
|
428
|
-
let publication: ReturnType<typeof publishOwnedCohort>;
|
|
429
|
-
try {
|
|
430
|
-
publication = publishOwnedCohort(root, updates, [...bases.values()], revision, [], "migrate");
|
|
431
|
-
} catch (error) {
|
|
432
|
-
removeEmptyDirectories(targetDirectories);
|
|
433
|
-
throw error;
|
|
434
|
-
}
|
|
435
|
-
const nextFiles = temporalFileReceipts(canonical, updates);
|
|
436
|
-
removeEmptyDirectories(cleanupDirectories);
|
|
437
|
-
return { ...publication, base: { head: publication.commit ?? revision, files: nextFiles }, view };
|
|
438
|
-
});
|
|
279
|
+
return { base: { head: revision, files }, scopes, runtime: { document, revision: owner }, provenance };
|
|
439
280
|
}
|
|
440
281
|
|
|
441
282
|
function relativeOwnedPath(path: string, repositoryRoot: string): string {
|
|
@@ -452,6 +293,7 @@ function commitOwnedFiles(
|
|
|
452
293
|
expectedHead: string | undefined,
|
|
453
294
|
scopes: readonly StateScope[],
|
|
454
295
|
operation: "persist" | "migrate" = "persist",
|
|
296
|
+
rootCommit = false,
|
|
455
297
|
): string | undefined {
|
|
456
298
|
assertOwnedFileUpdates(updates, repositoryRoot);
|
|
457
299
|
const branchRef = currentBranchRef(repositoryRoot);
|
|
@@ -482,13 +324,13 @@ function commitOwnedFiles(
|
|
|
482
324
|
// NUL framing preserves literal path characters while the blobs retain exact prepared bytes.
|
|
483
325
|
if (entries.length > 0) git(repositoryRoot, ["update-index", "-z", "--index-info"], { env, input: entries.join("") });
|
|
484
326
|
const tree = git(repositoryRoot, ["write-tree"], { env }).stdout.trim();
|
|
485
|
-
if (expectedHead !== undefined) {
|
|
327
|
+
if (expectedHead !== undefined && !rootCommit) {
|
|
486
328
|
const previousTree = git(repositoryRoot, ["rev-parse", `${expectedHead}^{tree}`]).stdout.trim();
|
|
487
329
|
if (tree === previousTree) return undefined;
|
|
488
330
|
}
|
|
489
331
|
const message = `state-flow: ${operation} ${scopes.join("+") || "runtime"} transition\n\n${STATE_FLOW_COMMIT_TRAILER}\n`;
|
|
490
332
|
const commitArgs = ["commit-tree", tree];
|
|
491
|
-
if (expectedHead !== undefined) commitArgs.push("-p", expectedHead);
|
|
333
|
+
if (expectedHead !== undefined && !rootCommit) commitArgs.push("-p", expectedHead);
|
|
492
334
|
const commit = git(repositoryRoot, commitArgs, { input: message }).stdout.trim();
|
|
493
335
|
const zero = "0".repeat(40);
|
|
494
336
|
assertOwnedFileUpdates(updates, repositoryRoot);
|
|
@@ -530,7 +372,7 @@ function migrateLegacyStorageUnderLock(cwd: string, sessionId: string, root: str
|
|
|
530
372
|
if (current.some((base, index) => base.identity !== plan.bases[index]!.identity)) {
|
|
531
373
|
throw new Error("State Flow storage changed concurrently while planning migration");
|
|
532
374
|
}
|
|
533
|
-
return { scopes: plan.scopes, ...publishOwnedCohort(root, plan.updates, plan.bases, currentHead(root), plan.scopes, "migrate") };
|
|
375
|
+
return { scopes: plan.scopes, ...publishOwnedCohort(root, plan.updates, plan.bases, currentHead(root), plan.scopes, "migrate", false, true) };
|
|
534
376
|
}
|
|
535
377
|
|
|
536
378
|
function publishOwnedCohort(
|
|
@@ -541,12 +383,13 @@ function publishOwnedCohort(
|
|
|
541
383
|
scopes: readonly StateScope[],
|
|
542
384
|
operation: "persist" | "migrate",
|
|
543
385
|
push = true,
|
|
386
|
+
rootCommit = false,
|
|
544
387
|
): { commit?: string; push?: GitPushResult } {
|
|
545
388
|
let published = false;
|
|
546
389
|
try {
|
|
547
390
|
writeOwnedFileUpdates(updates, bases, root);
|
|
548
391
|
published = true;
|
|
549
|
-
const commit = commitOwnedFiles(root, updates, head, scopes, operation);
|
|
392
|
+
const commit = commitOwnedFiles(root, updates, head, scopes, operation, rootCommit);
|
|
550
393
|
return commit === undefined ? {} : push ? { commit, push: pushGitCommit(root, commit) } : { commit };
|
|
551
394
|
} catch (error) {
|
|
552
395
|
if (published) {
|
|
@@ -567,7 +410,8 @@ export function adoptFileStateToGit(cwd: string, sessionId: string, repositoryRo
|
|
|
567
410
|
if (snapshot.meta.step !== selected.runtime.meta.step) throw new Error("Git adoption must preserve the semantic step");
|
|
568
411
|
const runtime = createSessionRuntime(snapshot, cwd, sessionId, selected.view.lineage, "unconfirmed", selected.provenance.session);
|
|
569
412
|
runtime.meta.temporalRevision = "self";
|
|
570
|
-
const sources = serializeSessionRuntime(runtime, cwd, sessionId
|
|
413
|
+
const sources = serializeSessionRuntime(runtime, cwd, sessionId, selected.view.scopes.session,
|
|
414
|
+
selected.base.files.find(({ path }) => path === sessionRuntimePaths(cwd, sessionId, repositoryRoot, sessionKey).meta)?.content);
|
|
571
415
|
initializeGitRepository(repositoryRoot);
|
|
572
416
|
return withPublicationLock(repositoryRoot, (root) => {
|
|
573
417
|
const current = captureTemporalBaseUnderLock(cwd, sessionId, root, sessionKey);
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
// Domain:
|
|
2
|
-
import {
|
|
3
|
-
import { lstatSync } from "node:fs";
|
|
1
|
+
// Domain: predecessor temporal-envelope migration planning; Git/files backends own publication and rollback.
|
|
2
|
+
import { lstatSync, readFileSync, readdirSync } from "node:fs";
|
|
4
3
|
import { join, resolve } from "node:path";
|
|
5
4
|
import {
|
|
6
5
|
captureOwnedFileBases,
|
|
7
6
|
cwdScopePaths,
|
|
8
7
|
parseScopeStream,
|
|
9
|
-
|
|
8
|
+
serializeScopeMetadata,
|
|
10
9
|
serializeScopeStream,
|
|
10
|
+
sessionScopeKey,
|
|
11
11
|
sessionScopePaths,
|
|
12
12
|
type DurableFileBase,
|
|
13
13
|
type OwnedFileUpdate,
|
|
14
14
|
} from "./durable.ts";
|
|
15
|
+
import { canonicalJson } from "./json.ts";
|
|
15
16
|
import type { StateScope } from "./state.ts";
|
|
16
|
-
import type { ScopeStream } from "./temporal.ts";
|
|
17
17
|
|
|
18
18
|
export interface LegacyStorageMigration {
|
|
19
19
|
bases: DurableFileBase[];
|
|
@@ -21,23 +21,66 @@ export interface LegacyStorageMigration {
|
|
|
21
21
|
scopes: StateScope[];
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
interface MigrationDirectory {
|
|
25
|
+
directory: string;
|
|
26
|
+
scope: StateScope;
|
|
27
|
+
cwdIdentity?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Discover every owner-proven CWD and session cohort beneath the configured store. */
|
|
31
|
+
function migrationDirectories(cwd: string, sessionId: string, root: string, sessionKey: string): MigrationDirectory[] {
|
|
32
|
+
const selectedCwd = cwdScopePaths(cwd, root).directory;
|
|
33
|
+
const selectedSession = sessionScopePaths(cwd, sessionId, root, sessionKey).directory;
|
|
34
|
+
const directories: MigrationDirectory[] = [{ directory: root, scope: "global" }];
|
|
35
|
+
const cwdOwners = new Map<string, string>([[selectedCwd, resolve(cwd)]]);
|
|
36
|
+
if (lstatSync(root, { throwIfNoEntry: false }) !== undefined) {
|
|
37
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
38
|
+
if (!entry.isDirectory()) continue;
|
|
39
|
+
const directory = join(root, entry.name);
|
|
40
|
+
let checkpoint: unknown;
|
|
41
|
+
let meta: unknown;
|
|
42
|
+
try { checkpoint = JSON.parse(readFileSync(join(directory, "checkpoint.json"), "utf8")); }
|
|
43
|
+
catch { continue; }
|
|
44
|
+
try { meta = JSON.parse(readFileSync(join(directory, "meta.json"), "utf8")); } catch { /* predecessor metadata may be absent */ }
|
|
45
|
+
const owner = checkpoint && typeof checkpoint === "object" && !Array.isArray(checkpoint) && (checkpoint as { owner?: unknown }).owner
|
|
46
|
+
? (checkpoint as { owner: { cwd?: unknown } }).owner
|
|
47
|
+
: meta && typeof meta === "object" && !Array.isArray(meta) ? (meta as { owner?: { cwd?: unknown } }).owner : undefined;
|
|
48
|
+
if (owner && typeof owner.cwd === "string" && resolve(owner.cwd) === owner.cwd) cwdOwners.set(directory, owner.cwd);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
for (const [cwdDirectory, cwdOwner] of cwdOwners) {
|
|
52
|
+
directories.push({ directory: cwdDirectory, scope: "cwd", cwdIdentity: cwdOwner });
|
|
53
|
+
if (lstatSync(cwdDirectory, { throwIfNoEntry: false }) === undefined) continue;
|
|
54
|
+
for (const entry of readdirSync(cwdDirectory, { withFileTypes: true })) {
|
|
55
|
+
if (!entry.isDirectory()) continue;
|
|
56
|
+
try { sessionScopeKey(entry.name); } catch { continue; }
|
|
57
|
+
const directory = join(cwdDirectory, entry.name);
|
|
58
|
+
let meta: unknown;
|
|
59
|
+
try { meta = JSON.parse(readFileSync(join(directory, "meta.json"), "utf8")); }
|
|
60
|
+
catch { continue; }
|
|
61
|
+
const identity = meta && typeof meta === "object" && !Array.isArray(meta) ? (meta as { identity?: unknown }).identity : undefined;
|
|
62
|
+
if (!identity || typeof identity !== "object" || Array.isArray(identity)) continue;
|
|
63
|
+
const owner = identity as { cwd?: unknown; sessionId?: unknown };
|
|
64
|
+
if (owner.cwd === cwdOwner && typeof owner.sessionId === "string" && owner.sessionId.length > 0) directories.push({ directory, scope: "session" });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (!directories.some(({ directory }) => directory === selectedSession)) directories.push({ directory: selectedSession, scope: "session" });
|
|
68
|
+
return directories;
|
|
69
|
+
}
|
|
70
|
+
|
|
24
71
|
/** Read-only activation eligibility, before global migration or any repository publication. */
|
|
25
72
|
export function hasCwdMaterialization(cwd: string, repositoryRoot: string): boolean {
|
|
26
73
|
const root = resolve(repositoryRoot);
|
|
27
74
|
const directory = cwdScopePaths(cwd, root).directory;
|
|
28
|
-
const [
|
|
29
|
-
join(directory, "
|
|
75
|
+
const [checkpoint, patches, meta] = captureOwnedFileBases([
|
|
76
|
+
join(directory, "checkpoint.json"), join(directory, "patches.jsonl"), join(directory, "meta.json"),
|
|
30
77
|
], root);
|
|
31
|
-
if (checkpoint!.content !== undefined)
|
|
32
|
-
|
|
33
|
-
return parseScopeStream(checkpoint!.content, patches!.content, "cwd", cwd) !== undefined;
|
|
34
|
-
}
|
|
35
|
-
if (legacy!.content !== undefined) return parseStateSource(legacy!.content, legacy!.path) !== undefined;
|
|
36
|
-
if (patches!.content !== undefined) throw new Error(`State Flow tail has no provable snapshot: ${directory}`);
|
|
78
|
+
if (checkpoint!.content !== undefined) return parseScopeStream(checkpoint!.content, patches!.content, "cwd", cwd, meta!.content) !== undefined;
|
|
79
|
+
if (patches!.content !== undefined) throw new Error(`State Flow tail has no provable checkpoint: ${directory}`);
|
|
37
80
|
return false;
|
|
38
81
|
}
|
|
39
82
|
|
|
40
|
-
/**
|
|
83
|
+
/** Detect predecessor snapshots or temporal envelopes without mutating the store. */
|
|
41
84
|
export function hasLegacyStateSources(
|
|
42
85
|
cwd: string,
|
|
43
86
|
sessionId: string,
|
|
@@ -45,11 +88,13 @@ export function hasLegacyStateSources(
|
|
|
45
88
|
sessionKey = sessionId,
|
|
46
89
|
): boolean {
|
|
47
90
|
const root = resolve(repositoryRoot);
|
|
48
|
-
return
|
|
49
|
-
join(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
91
|
+
return migrationDirectories(cwd, sessionId, root, sessionKey).some(({ directory }) => {
|
|
92
|
+
if (lstatSync(join(directory, "checkpoint.json"), { throwIfNoEntry: false }) === undefined) return false;
|
|
93
|
+
try {
|
|
94
|
+
const meta = JSON.parse(readFileSync(join(directory, "meta.json"), "utf8"));
|
|
95
|
+
return meta === null || typeof meta !== "object" || !("temporal" in meta);
|
|
96
|
+
} catch { return true; }
|
|
97
|
+
});
|
|
53
98
|
}
|
|
54
99
|
|
|
55
100
|
/** Plan from current scope snapshots only; old explanatory journals are never replay input. */
|
|
@@ -57,48 +102,39 @@ export function planLegacyStorageMigration(
|
|
|
57
102
|
cwd: string,
|
|
58
103
|
sessionId: string,
|
|
59
104
|
repositoryRoot: string,
|
|
60
|
-
|
|
105
|
+
_origin?: string,
|
|
61
106
|
sessionKey = sessionId,
|
|
62
107
|
): LegacyStorageMigration {
|
|
63
108
|
const root = resolve(repositoryRoot);
|
|
64
|
-
const directories
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
session: sessionScopePaths(cwd, sessionId, root, sessionKey).directory,
|
|
68
|
-
};
|
|
69
|
-
const paths = Object.values(directories).flatMap((directory) => [
|
|
70
|
-
join(directory, "state.json"), join(directory, "checkpoint.json"), join(directory, "patches.jsonl"),
|
|
109
|
+
const directories = migrationDirectories(cwd, sessionId, root, sessionKey);
|
|
110
|
+
const paths = directories.flatMap(({ directory }) => [
|
|
111
|
+
join(directory, "checkpoint.json"), join(directory, "patches.jsonl"), join(directory, "meta.json"),
|
|
71
112
|
]);
|
|
72
113
|
const bases = captureOwnedFileBases(paths, root);
|
|
73
114
|
const byPath = new Map(bases.map((base) => [base.path, base]));
|
|
74
115
|
const updates: OwnedFileUpdate[] = [];
|
|
75
116
|
const scopes: StateScope[] = [];
|
|
76
|
-
for (const scope
|
|
77
|
-
const directory = directories[scope];
|
|
78
|
-
const legacy = byPath.get(join(directory, "state.json"))!;
|
|
117
|
+
for (const { scope, directory, cwdIdentity } of directories) {
|
|
79
118
|
const checkpoint = byPath.get(join(directory, "checkpoint.json"))!;
|
|
80
119
|
const patches = byPath.get(join(directory, "patches.jsonl"))!;
|
|
120
|
+
const meta = byPath.get(join(directory, "meta.json"))!;
|
|
81
121
|
if (checkpoint.content !== undefined) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
122
|
+
const stream = parseScopeStream(checkpoint.content, patches.content, scope, cwdIdentity, meta.content)!;
|
|
123
|
+
const source = serializeScopeStream(stream, scope, cwdIdentity);
|
|
124
|
+
let metadata = serializeScopeMetadata(undefined, stream, scope, cwdIdentity, meta.content);
|
|
125
|
+
if (scope === "session") {
|
|
126
|
+
const runtime = JSON.parse(metadata) as Record<string, unknown>;
|
|
127
|
+
runtime.revision = "self";
|
|
128
|
+
runtime.temporalRevision = "self";
|
|
129
|
+
metadata = `${canonicalJson(runtime)}\n`;
|
|
130
|
+
}
|
|
131
|
+
if (checkpoint.content !== source.checkpoint || patches.content !== source.patches || meta.content !== metadata) {
|
|
132
|
+
if (!scopes.includes(scope)) scopes.push(scope);
|
|
133
|
+
updates.push({ path: checkpoint.path, content: source.checkpoint }, { path: patches.path, content: source.patches }, { path: meta.path, content: metadata });
|
|
134
|
+
}
|
|
88
135
|
continue;
|
|
89
136
|
}
|
|
90
|
-
|
|
91
|
-
const stream: ScopeStream = {
|
|
92
|
-
checkpoint: { through: { id: origin, position: 0, parent: null }, state },
|
|
93
|
-
patches: [],
|
|
94
|
-
};
|
|
95
|
-
const source = serializeScopeStream(stream, scope, scope === "cwd" ? cwd : undefined);
|
|
96
|
-
scopes.push(scope);
|
|
97
|
-
updates.push(
|
|
98
|
-
{ path: checkpoint.path, content: source.checkpoint },
|
|
99
|
-
{ path: patches.path, content: source.patches },
|
|
100
|
-
{ path: legacy.path },
|
|
101
|
-
);
|
|
137
|
+
if (patches.content !== undefined) throw new Error(`State Flow tail has no provable checkpoint: ${directory}`);
|
|
102
138
|
}
|
|
103
139
|
return { bases, updates, scopes };
|
|
104
140
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { projectStateForModel, type MaterializedState, type ScopePatch, type StateScope } from "./state.ts";
|
|
2
|
+
import { readTemporalState, type TemporalState, type TransitionBoundary } from "./temporal.ts";
|
|
3
|
+
|
|
4
|
+
export type StateReadQuery =
|
|
5
|
+
| { kind: "state"; path: string; offset: number; scope?: StateScope }
|
|
6
|
+
| { kind: "patch"; path: string; offset: number; scope: StateScope };
|
|
7
|
+
|
|
8
|
+
export type StateReadResult =
|
|
9
|
+
| { path: string; boundary: TransitionBoundary; state: MaterializedState }
|
|
10
|
+
| { path: string; boundary: TransitionBoundary; patch: ScopePatch & { response?: string } };
|
|
11
|
+
|
|
12
|
+
const PATH_PATTERN = /^state(?:\[(\d+)\])?(?:\.(global|cwd|session)(?:\[(\d+)\])?(?:\.patches(?:\[(\d+)\])?)?)?$/;
|
|
13
|
+
|
|
14
|
+
/** Resolve the compact model-facing path grammar without treating aliases as literal JSON containers. */
|
|
15
|
+
export function parseStateReadPath(path: string): StateReadQuery {
|
|
16
|
+
const match = PATH_PATTERN.exec(path);
|
|
17
|
+
if (!match) throw new Error("Invalid State Flow read path");
|
|
18
|
+
const [, effectiveOffset, scope, scopeOffset, patchOffset] = match;
|
|
19
|
+
if (effectiveOffset !== undefined && scope !== undefined) throw new Error("State Flow read path cannot index both state and a scope");
|
|
20
|
+
if (path.includes(".patches") && scopeOffset !== undefined) throw new Error("Index patches after .patches, not after the scope");
|
|
21
|
+
const rawOffset = patchOffset ?? scopeOffset ?? effectiveOffset ?? "0";
|
|
22
|
+
const offset = Number(rawOffset);
|
|
23
|
+
if (!Number.isSafeInteger(offset) || offset < 0 || offset > 7) throw new Error("State Flow read path index must be an integer from 0 to 7");
|
|
24
|
+
if (patchOffset !== undefined || path.endsWith(".patches")) {
|
|
25
|
+
return { kind: "patch", path, offset, scope: scope as StateScope };
|
|
26
|
+
}
|
|
27
|
+
return { kind: "state", path, offset, ...(scope === undefined ? {} : { scope: scope as StateScope }) };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function readStatePath(view: TemporalState, path: string): StateReadResult {
|
|
31
|
+
const query = parseStateReadPath(path);
|
|
32
|
+
if (query.kind === "state") {
|
|
33
|
+
const boundary = view.lineage[view.lineage.length - 1 - query.offset];
|
|
34
|
+
if (!boundary) throw new Error("Requested history predates the proven temporal origin");
|
|
35
|
+
return { path, boundary: structuredClone(boundary), state: projectStateForModel(readTemporalState(view, query.offset, query.scope)) };
|
|
36
|
+
}
|
|
37
|
+
const record = view.scopes[query.scope].patches.at(-1 - query.offset);
|
|
38
|
+
if (!record) throw new Error(`Requested ${query.scope} patch predates retained hot history`);
|
|
39
|
+
return { path, boundary: structuredClone(record.transition), patch: structuredClone(record.patch) };
|
|
40
|
+
}
|
|
@@ -6,49 +6,26 @@ export interface SnapshotRecovery {
|
|
|
6
6
|
disabledMarker?: true;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
? snapshot.meta.validation.error
|
|
12
|
-
: undefined;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** Recover the newest valid snapshot, falling back through the active branch. */
|
|
16
|
-
export function recoverSnapshot(candidates: readonly unknown[], resolveRevision?: (revision: string, legacy?: Snapshot) => Snapshot): SnapshotRecovery {
|
|
9
|
+
/** Recover the newest supported pointer or disabled marker from the active branch. */
|
|
10
|
+
export function recoverSnapshot(candidates: readonly unknown[], resolveRevision?: (revision: string) => Snapshot): SnapshotRecovery {
|
|
17
11
|
const skipped: string[] = [];
|
|
18
|
-
let newestFailure: Snapshot | undefined;
|
|
19
12
|
for (const candidate of candidates) {
|
|
20
|
-
let migrated: Snapshot;
|
|
21
13
|
let selectedRevision: string | undefined;
|
|
22
14
|
try {
|
|
23
15
|
const parsed = parsePiCheckpoint(candidate);
|
|
24
|
-
if ("revision" in parsed) {
|
|
25
|
-
selectedRevision = parsed.revision;
|
|
26
|
-
if (!resolveRevision) throw new Error("Checkpoint pointer requires immutable runtime resolution");
|
|
27
|
-
return { snapshot: resolveRevision(parsed.revision), skipped };
|
|
28
|
-
}
|
|
29
16
|
if ("disabled" in parsed) return { snapshot: emptySnapshot(), skipped, disabledMarker: true };
|
|
30
|
-
|
|
31
|
-
if (
|
|
32
|
-
|
|
33
|
-
if (!resolveRevision) throw new Error("Legacy checkpoint requires immutable runtime resolution");
|
|
34
|
-
return { snapshot: resolveRevision(migrated.meta.durableBase, migrated), skipped };
|
|
35
|
-
}
|
|
17
|
+
selectedRevision = parsed.revision;
|
|
18
|
+
if (!resolveRevision) throw new Error("Checkpoint pointer requires immutable runtime resolution");
|
|
19
|
+
return { snapshot: resolveRevision(parsed.revision), skipped };
|
|
36
20
|
} catch (error) {
|
|
37
21
|
if (selectedRevision && error instanceof RevisionUnavailableError) {
|
|
38
22
|
return { snapshot: migrationFailure({ meta: { durableBase: selectedRevision } }, `Snapshot restoration failed: ${error.message}`), skipped };
|
|
39
23
|
}
|
|
40
|
-
|
|
41
|
-
{},
|
|
42
|
-
`Snapshot restoration failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
43
|
-
);
|
|
24
|
+
skipped.push(`Snapshot restoration failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
44
25
|
}
|
|
45
|
-
const failure = failureMessage(migrated);
|
|
46
|
-
if (failure === undefined) return { snapshot: migrated, skipped };
|
|
47
|
-
newestFailure ??= migrated;
|
|
48
|
-
skipped.push(failure);
|
|
49
26
|
}
|
|
50
27
|
return {
|
|
51
|
-
snapshot:
|
|
28
|
+
snapshot: migrationFailure({}, skipped[0] ?? "Snapshot restoration failed: no supported checkpoint"),
|
|
52
29
|
skipped,
|
|
53
30
|
};
|
|
54
31
|
}
|