@llblab/pi-kit 0.18.2 → 0.19.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/CHANGELOG.md +6 -0
- package/README.md +3 -1
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +35 -37
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +5 -3
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +19 -1
- package/node_modules/@llblab/pi-state-flow/README.md +102 -48
- package/node_modules/@llblab/pi-state-flow/dist/index.d.ts +6 -9
- package/node_modules/@llblab/pi-state-flow/dist/index.js +6 -9
- package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.d.ts +2 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.js +21 -8
- package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.d.ts +40 -24
- package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.js +93 -63
- package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.d.ts +4 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.js +9 -9
- package/node_modules/@llblab/pi-state-flow/dist/lib/config.d.ts +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/config.js +6 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +8 -7
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +56 -58
- package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +12 -24
- package/node_modules/@llblab/pi-state-flow/dist/lib/durable.d.ts +7 -18
- package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +47 -78
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.d.ts +4 -6
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +241 -437
- package/node_modules/@llblab/pi-state-flow/dist/lib/git.d.ts +2 -72
- package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +120 -499
- package/node_modules/@llblab/pi-state-flow/dist/lib/history.d.ts +2 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/history.js +4 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/json.d.ts +3 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/json.js +43 -22
- package/node_modules/@llblab/pi-state-flow/dist/lib/logging.d.ts +1 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/logging.js +0 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/memory.d.ts +1 -14
- package/node_modules/@llblab/pi-state-flow/dist/lib/memory.js +5 -37
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.d.ts +1 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +13 -32
- package/node_modules/@llblab/pi-state-flow/dist/lib/query.d.ts +6 -6
- package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +25 -21
- package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.d.ts +3 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.js +15 -12
- package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.d.ts +2 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.js +5 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +26 -88
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +159 -255
- package/node_modules/@llblab/pi-state-flow/dist/lib/skills.d.ts +0 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/skills.js +1 -47
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +16 -33
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +48 -137
- package/node_modules/@llblab/pi-state-flow/dist/lib/state.d.ts +16 -11
- package/node_modules/@llblab/pi-state-flow/dist/lib/state.js +13 -14
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +1 -9
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +15 -43
- package/node_modules/@llblab/pi-state-flow/dist/lib/storage.d.ts +5 -9
- package/node_modules/@llblab/pi-state-flow/dist/lib/storage.js +13 -45
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.d.ts +15 -7
- package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.js +104 -24
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.d.ts +0 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +11 -14
- package/node_modules/@llblab/pi-state-flow/dist/package.json +9 -6
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +11 -17
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +8 -8
- package/node_modules/@llblab/pi-state-flow/docs/README.md +2 -2
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +64 -67
- package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +24 -6
- package/node_modules/@llblab/pi-state-flow/docs/filesystem-recovery.md +12 -15
- package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +22 -27
- package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +25 -41
- package/node_modules/@llblab/pi-state-flow/docs/performance.md +38 -421
- package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +33 -46
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +37 -61
- package/node_modules/@llblab/pi-state-flow/index.ts +7 -71
- package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +26 -11
- package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +116 -88
- package/node_modules/@llblab/pi-state-flow/lib/compaction.ts +12 -10
- package/node_modules/@llblab/pi-state-flow/lib/config.ts +6 -6
- package/node_modules/@llblab/pi-state-flow/lib/context.ts +52 -59
- package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +11 -20
- package/node_modules/@llblab/pi-state-flow/lib/durable.ts +44 -86
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +237 -460
- package/node_modules/@llblab/pi-state-flow/lib/git.ts +110 -552
- package/node_modules/@llblab/pi-state-flow/lib/history.ts +4 -3
- package/node_modules/@llblab/pi-state-flow/lib/json.ts +39 -23
- package/node_modules/@llblab/pi-state-flow/lib/logging.ts +1 -7
- package/node_modules/@llblab/pi-state-flow/lib/memory.ts +5 -44
- package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +14 -25
- package/node_modules/@llblab/pi-state-flow/lib/query.ts +26 -22
- package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +17 -11
- package/node_modules/@llblab/pi-state-flow/lib/rehydration.ts +7 -5
- package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +154 -254
- package/node_modules/@llblab/pi-state-flow/lib/skills.ts +1 -49
- package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +58 -142
- package/node_modules/@llblab/pi-state-flow/lib/state.ts +27 -19
- package/node_modules/@llblab/pi-state-flow/lib/status.ts +16 -54
- package/node_modules/@llblab/pi-state-flow/lib/storage.ts +12 -40
- package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +1 -1
- package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +104 -22
- package/node_modules/@llblab/pi-state-flow/lib/transition.ts +16 -26
- package/node_modules/@llblab/pi-state-flow/package.json +9 -6
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +11 -17
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +8 -8
- package/package.json +2 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/discovery.d.ts +0 -21
- package/node_modules/@llblab/pi-state-flow/dist/lib/discovery.js +0 -125
- package/node_modules/@llblab/pi-state-flow/dist/lib/maintenance.d.ts +0 -36
- package/node_modules/@llblab/pi-state-flow/dist/lib/maintenance.js +0 -98
- package/node_modules/@llblab/pi-state-flow/dist/lib/migration.d.ts +0 -13
- package/node_modules/@llblab/pi-state-flow/dist/lib/migration.js +0 -167
- package/node_modules/@llblab/pi-state-flow/dist/lib/publication.d.ts +0 -86
- package/node_modules/@llblab/pi-state-flow/dist/lib/publication.js +0 -437
- package/node_modules/@llblab/pi-state-flow/lib/discovery.ts +0 -133
- package/node_modules/@llblab/pi-state-flow/lib/maintenance.ts +0 -147
- package/node_modules/@llblab/pi-state-flow/lib/migration.ts +0 -171
- package/node_modules/@llblab/pi-state-flow/lib/publication.ts +0 -458
|
@@ -1,25 +1,22 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { lstatSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { classifyScopeStream, parseScopeProvenance, parseScopeStream, sessionRuntimePaths, temporalScopePaths, type SessionAddress } from "./durable.ts";
|
|
4
|
+
import { classifyScopeStream, hasCwdMaterialization, parseScopeProvenance, parseScopeStream, sessionRuntimePaths, temporalScopePaths, type SessionAddress } from "./durable.ts";
|
|
5
5
|
import { parseArtifactProvenanceRegistry, pruneArtifactProvenance, type ArtifactProvenance, type ArtifactProvenanceRegistry } from "./artifact.ts";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { type AcceptedTransition, type RecentTransitionWindow } from "./history.ts";
|
|
6
|
+
import { captureTemporalFileBase, initializeFileStore, publishTemporalStateToFiles, type TemporalFileBase } from "./storage.ts";
|
|
7
|
+
import { DEFAULT_HISTORY_LIMIT, MAX_HISTORY_LIMIT, type AcceptedTransition, type RecentTransitionWindow } from "./history.ts";
|
|
9
8
|
import { hashJson, sameJson } from "./json.ts";
|
|
10
|
-
import {
|
|
11
|
-
import { RevisionUnavailableError, createSessionRuntime, isFileRevision, parseSessionRuntime, resolveFileSessionRuntime, resolveSessionRuntime, type Snapshot } from "./snapshot.ts";
|
|
9
|
+
import { RevisionUnavailableError, createSessionRuntime, parseSessionRuntime, retainedBoundaryCheckpoint, type RetainedBoundaryCheckpoint, type RetainedPiCheckpoint, type Snapshot } from "./snapshot.ts";
|
|
12
10
|
import { emptyState, type MaterializedState, type ScopedStates, type StateScope } from "./state.ts";
|
|
13
|
-
import { adoptTemporalStreams, advanceTemporalState, createTemporalState, readTemporalState,
|
|
11
|
+
import { adoptTemporalStreams, advanceTemporalState, createTemporalState, readTemporalState, selectScopeStreamAtBoundary, validateScopeLineage, type ScopeStream, type TemporalState } from "./temporal.ts";
|
|
14
12
|
|
|
15
13
|
const SCOPES = ["global", "cwd", "session"] as const;
|
|
16
14
|
const SHARED_SCOPES = ["global", "cwd"] as const;
|
|
17
|
-
export type RuntimePublication = ReturnType<typeof
|
|
15
|
+
export type RuntimePublication = ReturnType<typeof publishTemporalStateToFiles>;
|
|
18
16
|
|
|
19
17
|
interface SessionCopy {
|
|
20
18
|
stream: ScopeStream;
|
|
21
19
|
provenance: ArtifactProvenanceRegistry;
|
|
22
|
-
backend: "git" | "files";
|
|
23
20
|
}
|
|
24
21
|
|
|
25
22
|
function emptyProvenance(): Record<StateScope, ArtifactProvenanceRegistry> {
|
|
@@ -57,59 +54,36 @@ function removedTargetScopeConflict(scopes: readonly StateScope[]): Error {
|
|
|
57
54
|
return new SharedScopeRemovalConflictError(scopes);
|
|
58
55
|
}
|
|
59
56
|
|
|
60
|
-
function freshEmptyScopeStream(scope: StateScope, origin: string): ScopeStream {
|
|
61
|
-
return createTemporalState({ global: emptyState(), cwd: emptyState(), session: emptyState() }, origin).scopes[scope];
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export class MissingSessionRuntimeError extends Error {
|
|
65
|
-
constructor() { super("Linked State Flow revision has no session runtime"); }
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** Immutable target validation is independent of acquiring the live publication basis. */
|
|
69
|
-
export function inspectRuntimeRevision(cwd: string, sessionId: string, root: string, revision: string, sessionKey = sessionId) {
|
|
70
|
-
const loaded = loadTemporalRevision(cwd, sessionId, root, revision, sessionKey);
|
|
71
|
-
if (!loaded.runtime) throw new MissingSessionRuntimeError();
|
|
72
|
-
const resolved = resolveSessionRuntime(loaded.runtime.document, loaded.runtime.revision);
|
|
73
|
-
if (!loaded.scopes.global || !loaded.scopes.cwd || !loaded.scopes.session) throw new Error("Incomplete temporal scope cohort");
|
|
74
|
-
const view = { lineage: resolved.lineage, scopes: { global: loaded.scopes.global, cwd: loaded.scopes.cwd, session: loaded.scopes.session } };
|
|
75
|
-
validateTemporalState(view);
|
|
76
|
-
return { runtime: loaded.runtime, resolved, view, provenance: loaded.provenance };
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** Validate one supported immutable target before any live migration/publication. */
|
|
80
|
-
export function inspectSnapshotRevision(cwd: string, sessionId: string, root: string, revision: string, _legacySnapshot?: Snapshot, sessionKey = sessionId) {
|
|
81
|
-
if (isFileRevision(revision)) {
|
|
82
|
-
const file = loadTemporalFileRevision(cwd, sessionId, root, revision, sessionKey);
|
|
83
|
-
return { snapshot: resolveFileSessionRuntime(file.runtime, revision), file };
|
|
84
|
-
}
|
|
85
|
-
if (detectGitCapability() === "files") throw new RevisionUnavailableError("Git is unavailable; cannot restore a Git-linked revision");
|
|
86
|
-
const temporal = inspectRuntimeRevision(cwd, sessionId, root, revision, sessionKey);
|
|
87
|
-
return { snapshot: temporal.resolved.snapshot, temporal };
|
|
57
|
+
function freshEmptyScopeStream(scope: StateScope, origin: string, historyLimit: number): ScopeStream {
|
|
58
|
+
return createTemporalState({ global: emptyState(), cwd: emptyState(), session: emptyState() }, origin, historyLimit).scopes[scope];
|
|
88
59
|
}
|
|
89
60
|
|
|
90
61
|
/** Cached branch-selected temporal state and publication basis; excludes Pi event policy. */
|
|
91
62
|
export class TemporalRuntime {
|
|
92
63
|
view: TemporalState | undefined;
|
|
93
|
-
private base:
|
|
64
|
+
private base: TemporalFileBase | undefined;
|
|
94
65
|
private semanticRevision: string | undefined;
|
|
95
66
|
private savedRuntime: string | undefined;
|
|
96
|
-
private
|
|
67
|
+
private restoredOriginPending = false;
|
|
97
68
|
private provenanceByScope: Record<StateScope, ArtifactProvenanceRegistry> = emptyProvenance();
|
|
98
69
|
/** Shared scopes whose wholly absent live basis was accepted after one stale-target refusal. */
|
|
99
70
|
private readonly absentSharedScopes = new Set<StateScope>();
|
|
100
71
|
readonly cwd: string;
|
|
101
72
|
private readonly session: SessionAddress;
|
|
102
73
|
readonly root: string;
|
|
103
|
-
|
|
74
|
+
readonly historyLimit: number;
|
|
75
|
+
constructor(cwd: string, session: string | SessionAddress, root: string, sessionKey?: string, historyLimit = DEFAULT_HISTORY_LIMIT) {
|
|
104
76
|
this.cwd = cwd;
|
|
105
77
|
this.session = Object.freeze(typeof session === "string" ? { id: session, key: sessionKey ?? session } : { ...session });
|
|
106
78
|
this.root = root;
|
|
79
|
+
if (!Number.isSafeInteger(historyLimit) || historyLimit < 0 || historyLimit > MAX_HISTORY_LIMIT) throw new Error(`State Flow history limit must be an integer from 0 to ${MAX_HISTORY_LIMIT}`);
|
|
80
|
+
this.historyLimit = historyLimit;
|
|
107
81
|
}
|
|
108
82
|
|
|
109
83
|
get sessionId(): string { return this.session.id; }
|
|
110
84
|
get sessionKey(): string { return this.session.key; }
|
|
111
85
|
|
|
112
|
-
/** Runtime-owned artifact
|
|
86
|
+
/** Runtime-owned artifact compilation evidence for one scope; never model-visible state. */
|
|
113
87
|
artifactProvenance(scope: StateScope): ArtifactProvenanceRegistry {
|
|
114
88
|
return structuredClone(this.provenanceByScope[scope]);
|
|
115
89
|
}
|
|
@@ -117,10 +91,7 @@ export class TemporalRuntime {
|
|
|
117
91
|
/** Read canonical shared memory without creating, migrating, or publishing storage. */
|
|
118
92
|
loadPassive(): boolean {
|
|
119
93
|
if (!lstatSync(this.root, { throwIfNoEntry: false })) return false;
|
|
120
|
-
const
|
|
121
|
-
const base = backend === "git"
|
|
122
|
-
? captureTemporalGitBase(this.cwd, this.sessionId, this.root, this.sessionKey)
|
|
123
|
-
: captureTemporalFileBase(this.cwd, this.sessionId, this.root, this.sessionKey);
|
|
94
|
+
const base = captureTemporalFileBase(this.cwd, this.sessionId, this.root, this.sessionKey);
|
|
124
95
|
const files = new Map(base.files.map((file) => [file.path, file.content]));
|
|
125
96
|
const shared = Object.fromEntries(SHARED_SCOPES.map((scope) => {
|
|
126
97
|
const paths = temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey);
|
|
@@ -129,12 +100,10 @@ export class TemporalRuntime {
|
|
|
129
100
|
})) as Record<(typeof SHARED_SCOPES)[number], ScopeStream | undefined>;
|
|
130
101
|
if (!shared.global && !shared.cwd) return false;
|
|
131
102
|
if (!shared.global) throw new Error("Incomplete passive State Flow shared storage: CWD state exists without global state");
|
|
132
|
-
const fresh = createTemporalState({ global: emptyState(), cwd: emptyState(), session: emptyState() }, randomUUID());
|
|
133
|
-
const basis = backend === "git" ? (base as TemporalGitBase).head ?? "unborn" : "files";
|
|
103
|
+
const fresh = createTemporalState({ global: emptyState(), cwd: emptyState(), session: emptyState() }, randomUUID(), this.historyLimit);
|
|
134
104
|
// Global memory is valid before this CWD has ever materialized its own scope.
|
|
135
|
-
this.view = adoptTemporalStreams({ global: shared.global, cwd: shared.cwd ?? fresh.scopes.cwd, session: fresh.scopes.session }, `passive:${
|
|
105
|
+
this.view = adoptTemporalStreams({ global: shared.global, cwd: shared.cwd ?? fresh.scopes.cwd, session: fresh.scopes.session }, `passive:files:${randomUUID()}`, this.historyLimit);
|
|
136
106
|
this.base = base;
|
|
137
|
-
this.backend = backend;
|
|
138
107
|
this.provenanceByScope = {
|
|
139
108
|
global: parseScopeProvenance(files.get(temporalScopePaths(this.cwd, this.sessionId, "global", this.root, this.sessionKey).meta), "State Flow global metadata"),
|
|
140
109
|
cwd: parseScopeProvenance(files.get(temporalScopePaths(this.cwd, this.sessionId, "cwd", this.root, this.sessionKey).meta), "State Flow CWD metadata"),
|
|
@@ -143,68 +112,20 @@ export class TemporalRuntime {
|
|
|
143
112
|
return true;
|
|
144
113
|
}
|
|
145
114
|
|
|
146
|
-
/**
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if (backend === "git") initializeGitRepository(this.root);
|
|
150
|
-
else initializeFileStore(this.root);
|
|
151
|
-
this.backend = backend;
|
|
115
|
+
/** Select canonical file acceptance even when Git is available; backup remains a later concern. */
|
|
116
|
+
prepareCanonical(): void {
|
|
117
|
+
initializeFileStore(this.root);
|
|
152
118
|
}
|
|
153
119
|
|
|
154
|
-
/**
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const publication = this.backend === "git"
|
|
158
|
-
? migrateLegacyStorageToGit(this.cwd, this.sessionId, this.root, this.sessionKey)
|
|
159
|
-
: (migrateLegacyStorageToFiles(this.cwd, this.sessionId, this.root, this.sessionKey), undefined);
|
|
160
|
-
const base = this.backend === "files"
|
|
161
|
-
? captureTemporalFileBase(this.cwd, this.sessionId, this.root, this.sessionKey)
|
|
162
|
-
: captureTemporalGitBase(this.cwd, this.sessionId, this.root, this.sessionKey);
|
|
163
|
-
const files = new Map(base.files.map((file) => [file.path, file.content]));
|
|
164
|
-
const scopes = Object.fromEntries(SCOPES.map((scope) => {
|
|
165
|
-
const paths = temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey);
|
|
166
|
-
const stream = parseScopeStream(files.get(paths.checkpoint), files.get(paths.patches), scope,
|
|
167
|
-
scope === "cwd" ? this.cwd : undefined, files.get(paths.meta));
|
|
168
|
-
if (!stream) throw new Error(`State Flow ${scopeLabel(scope)} scope disappeared during migration`);
|
|
169
|
-
return [scope, stream];
|
|
170
|
-
})) as TemporalState["scopes"];
|
|
171
|
-
const migrated = { lineage: this.view.lineage, scopes };
|
|
172
|
-
validateTemporalState(migrated);
|
|
173
|
-
if (!sameJson(this.states(), {
|
|
174
|
-
global: readTemporalState(migrated, 0, "global"),
|
|
175
|
-
cwd: readTemporalState(migrated, 0, "cwd"),
|
|
176
|
-
session: readTemporalState(migrated, 0, "session"),
|
|
177
|
-
})) throw new Error("State Flow migration changed materialized semantic state");
|
|
178
|
-
this.view = migrated;
|
|
179
|
-
this.base = base;
|
|
180
|
-
this.provenanceByScope = {
|
|
181
|
-
global: parseScopeProvenance(files.get(temporalScopePaths(this.cwd, this.sessionId, "global", this.root, this.sessionKey).meta), "State Flow global metadata"),
|
|
182
|
-
cwd: parseScopeProvenance(files.get(temporalScopePaths(this.cwd, this.sessionId, "cwd", this.root, this.sessionKey).meta), "State Flow CWD metadata"),
|
|
183
|
-
session: this.provenanceByScope.session,
|
|
184
|
-
};
|
|
185
|
-
if (publication?.commit) this.semanticRevision = publication.commit;
|
|
120
|
+
/** Canonical preparation is the default; Git backup is a later independent concern. */
|
|
121
|
+
prepare(): void {
|
|
122
|
+
this.prepareCanonical();
|
|
186
123
|
}
|
|
187
124
|
|
|
188
|
-
/** Explicit start can upgrade a proven file cohort; failed adoption leaves the cache in file mode. */
|
|
189
|
-
promote(snapshot: Snapshot): RuntimePublication | undefined {
|
|
190
|
-
if (this.backend !== "files" || !this.view || detectGitCapability() === "files") return undefined;
|
|
191
|
-
if (!snapshot.meta.durableBase || snapshot.meta.durableBase !== this.semanticRevision) throw new Error("Git adoption requires the selected file revision");
|
|
192
|
-
const pushRemote = (snapshot.meta.remotePublication?.mode ?? "transition") === "transition";
|
|
193
|
-
const result = adoptFileStateToGit(this.cwd, this.sessionId, this.root, snapshot.meta.durableBase, snapshot, this.sessionKey, pushRemote);
|
|
194
|
-
this.provenanceByScope = structuredClone(result.provenance);
|
|
195
|
-
const savedRuntime = hashJson(createSessionRuntime(snapshot, this.cwd, this.sessionId, result.view.lineage, "unconfirmed", this.provenanceByScope.session));
|
|
196
|
-
this.view = result.view;
|
|
197
|
-
this.base = result.base;
|
|
198
|
-
this.backend = "git";
|
|
199
|
-
this.semanticRevision = result.revision;
|
|
200
|
-
this.savedRuntime = savedRuntime;
|
|
201
|
-
this.absentSharedScopes.clear();
|
|
202
|
-
return result;
|
|
203
|
-
}
|
|
204
125
|
|
|
205
126
|
read(offset = 0, scope?: StateScope): MaterializedState {
|
|
206
127
|
if (!this.view) throw new Error("State Flow temporal runtime is unavailable");
|
|
207
|
-
return readTemporalState(this.view, offset, scope);
|
|
128
|
+
return readTemporalState(this.view, offset, scope, this.historyLimit);
|
|
208
129
|
}
|
|
209
130
|
|
|
210
131
|
states(): ScopedStates {
|
|
@@ -216,6 +137,16 @@ export class TemporalRuntime {
|
|
|
216
137
|
return this.view.lineage.at(-1)!.id;
|
|
217
138
|
}
|
|
218
139
|
|
|
140
|
+
/** Encode Pi lifecycle state against the current retained semantic boundary. */
|
|
141
|
+
retainedCheckpoint(snapshot: Snapshot): RetainedPiCheckpoint {
|
|
142
|
+
if (!this.view) return { disabled: true };
|
|
143
|
+
return retainedBoundaryCheckpoint(snapshot, this.causalBasis());
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
usesCanonicalFiles(): boolean {
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
|
|
219
150
|
recent(): RecentTransitionWindow {
|
|
220
151
|
const result: RecentTransitionWindow = [];
|
|
221
152
|
for (const boundary of this.view?.lineage.slice(1) ?? []) {
|
|
@@ -228,103 +159,108 @@ export class TemporalRuntime {
|
|
|
228
159
|
return result;
|
|
229
160
|
}
|
|
230
161
|
|
|
231
|
-
/**
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
const
|
|
162
|
+
/** Prepare a retained session boundary from current canonical files; shared scopes remain live. */
|
|
163
|
+
prepareBoundaryRestore(checkpoint: RetainedBoundaryCheckpoint): { snapshot: Snapshot; restore: () => Snapshot } {
|
|
164
|
+
if (!lstatSync(this.root, { throwIfNoEntry: false })) throw new RevisionUnavailableError("Selected State Flow boundary storage is unavailable");
|
|
165
|
+
const base = captureTemporalFileBase(this.cwd, this.sessionId, this.root, this.sessionKey);
|
|
166
|
+
const files = new Map(base.files.map((file) => [file.path, file.content]));
|
|
167
|
+
const scopes = Object.fromEntries(SCOPES.map((scope) => {
|
|
168
|
+
const paths = temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey);
|
|
169
|
+
const stream = parseScopeStream(files.get(paths.checkpoint), files.get(paths.patches), scope,
|
|
170
|
+
scope === "cwd" ? this.cwd : undefined, files.get(paths.meta));
|
|
171
|
+
if (!stream) throw new RevisionUnavailableError(`State Flow ${scopeLabel(scope)} scope is unavailable for retained-boundary restoration`);
|
|
172
|
+
return [scope, stream];
|
|
173
|
+
})) as Record<StateScope, ScopeStream>;
|
|
174
|
+
const runtimePaths = sessionRuntimePaths(this.cwd, this.sessionId, this.root, this.sessionKey);
|
|
175
|
+
const document = parseSessionRuntime(files.get(runtimePaths.config), files.get(runtimePaths.runtime), this.cwd, this.sessionId);
|
|
176
|
+
if (!document) throw new RevisionUnavailableError("State Flow session runtime is unavailable for retained-boundary restoration");
|
|
177
|
+
// The codecs validate persisted retention against the format maximum, not the new operator limit.
|
|
178
|
+
validateScopeLineage(scopes.session, "session", document.meta.lineage, MAX_HISTORY_LIMIT);
|
|
179
|
+
const boundary = document.meta.lineage.slice(-(this.historyLimit + 1)).find(({ id }) => id === checkpoint.boundary);
|
|
180
|
+
if (!boundary) throw new RevisionUnavailableError("Selected State Flow history boundary is outside the retained temporal window");
|
|
181
|
+
const selectedSession = selectScopeStreamAtBoundary(scopes.session, "session", boundary, MAX_HISTORY_LIMIT);
|
|
182
|
+
const view = adoptTemporalStreams({
|
|
183
|
+
global: scopes.global,
|
|
184
|
+
cwd: scopes.cwd,
|
|
185
|
+
session: selectedSession,
|
|
186
|
+
}, `restore:${checkpoint.boundary}:${randomUUID()}`, this.historyLimit);
|
|
187
|
+
const snapshot: Snapshot = {
|
|
188
|
+
config: { enabled: checkpoint.enabled },
|
|
189
|
+
meta: {
|
|
190
|
+
step: checkpoint.step,
|
|
191
|
+
...(checkpoint.bootstrap === true ? { bootstrap: true } : {}),
|
|
192
|
+
...(checkpoint.specification === undefined ? {} : { specification: checkpoint.specification }),
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
const provenance = Object.fromEntries(SCOPES.map((scope) => {
|
|
196
|
+
const paths = temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey);
|
|
197
|
+
const parsed = parseScopeProvenance(files.get(paths.meta), paths.meta);
|
|
198
|
+
const retained = pruneArtifactProvenance(parsed, readTemporalState(view, 0, scope, this.historyLimit).artifacts);
|
|
199
|
+
if (scope === "session") {
|
|
200
|
+
// Live evidence cannot prove an earlier artifact version, even after a change-away-and-back.
|
|
201
|
+
for (const record of scopes.session.patches) {
|
|
202
|
+
if (record.transition.position <= boundary.position) continue;
|
|
203
|
+
for (const path of Object.keys(record.patch.artifacts ?? {})) delete retained[path];
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return [scope, retained];
|
|
207
|
+
})) as Record<StateScope, ArtifactProvenanceRegistry>;
|
|
235
208
|
let consumed = false;
|
|
236
209
|
return {
|
|
237
|
-
snapshot: structuredClone(
|
|
210
|
+
snapshot: structuredClone(snapshot),
|
|
238
211
|
restore: () => {
|
|
239
212
|
if (consumed) throw new Error("Prepared State Flow restore was already consumed");
|
|
240
213
|
consumed = true;
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
214
|
+
this.view = view;
|
|
215
|
+
this.base = base;
|
|
216
|
+
this.provenanceByScope = provenance;
|
|
217
|
+
this.semanticRevision = undefined;
|
|
218
|
+
this.savedRuntime = hashJson(createSessionRuntime(snapshot, this.cwd, this.sessionId, view.lineage, provenance.session));
|
|
219
|
+
this.restoredOriginPending = true;
|
|
220
|
+
this.absentSharedScopes.clear();
|
|
221
|
+
return structuredClone(snapshot);
|
|
245
222
|
},
|
|
246
223
|
};
|
|
247
224
|
}
|
|
248
225
|
|
|
249
|
-
/**
|
|
250
|
-
|
|
226
|
+
/** Restore and canonically accept one retained boundary as a single lifecycle operation. */
|
|
227
|
+
restoreBoundary(checkpoint: RetainedBoundaryCheckpoint): { snapshot: Snapshot; publication: RuntimePublication } {
|
|
228
|
+
const prepared = this.prepareBoundaryRestore(checkpoint);
|
|
229
|
+
const snapshot = prepared.restore();
|
|
230
|
+
return { snapshot, publication: this.acceptRestoredOrigin(snapshot) };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Copy one retained source-session boundary over the child's current shared scopes. */
|
|
234
|
+
prepareBoundaryFork(source: SessionAddress, checkpoint: RetainedBoundaryCheckpoint): { snapshot: Snapshot; fork: () => { snapshot: Snapshot; publication: RuntimePublication } } {
|
|
251
235
|
const parent = Object.freeze({ ...source });
|
|
252
236
|
if (parent.id === this.sessionId || parent.key === this.sessionKey) throw new Error("State Flow fork requires a distinct session identity and key");
|
|
253
|
-
const
|
|
254
|
-
const
|
|
237
|
+
const sourceRuntime = new TemporalRuntime(this.cwd, parent, this.root, undefined, this.historyLimit);
|
|
238
|
+
const prepared = sourceRuntime.prepareBoundaryRestore(checkpoint);
|
|
239
|
+
prepared.restore();
|
|
255
240
|
const snapshot: Snapshot = {
|
|
256
|
-
config: structuredClone(
|
|
241
|
+
config: structuredClone(prepared.snapshot.config),
|
|
257
242
|
meta: {
|
|
258
243
|
step: 0,
|
|
259
|
-
...(
|
|
260
|
-
...(inspected.snapshot.meta.remotePublication === undefined ? {} : { remotePublication: structuredClone(inspected.snapshot.meta.remotePublication) }),
|
|
244
|
+
...(prepared.snapshot.meta.bootstrap === undefined ? {} : { bootstrap: prepared.snapshot.meta.bootstrap }),
|
|
261
245
|
},
|
|
262
246
|
};
|
|
263
247
|
let consumed = false;
|
|
264
248
|
return {
|
|
265
|
-
snapshot:
|
|
249
|
+
snapshot: structuredClone(snapshot),
|
|
266
250
|
fork: () => {
|
|
267
251
|
if (consumed) throw new Error("Prepared State Flow fork was already consumed");
|
|
268
252
|
consumed = true;
|
|
269
253
|
if (this.view) throw new Error("State Flow fork target already has session storage");
|
|
270
|
-
// Unlike immutable Git input, a file-only source must still match its complete cohort.
|
|
271
|
-
const current = inspected.file ? inspect() : inspected;
|
|
272
|
-
const selected = current.temporal ?? current.file;
|
|
273
|
-
if (!selected) throw new Error("State Flow fork requires a temporal session stream");
|
|
274
254
|
const publication = this.initializeOrigin(snapshot, { allowCreateCwd: false, copy: {
|
|
275
|
-
stream:
|
|
276
|
-
provenance:
|
|
277
|
-
backend: current.file ? "files" : "git",
|
|
255
|
+
stream: sourceRuntime.view!.scopes.session,
|
|
256
|
+
provenance: sourceRuntime.artifactProvenance("session"),
|
|
278
257
|
} });
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
return { snapshot: { ...structuredClone(snapshot), meta: { ...structuredClone(snapshot.meta), durableBase: ownedRevision } }, publication };
|
|
258
|
+
if (!publication?.revision) throw new Error("State Flow fork requires existing shared scope storage");
|
|
259
|
+
return { snapshot: structuredClone(snapshot), publication };
|
|
282
260
|
},
|
|
283
261
|
};
|
|
284
262
|
}
|
|
285
263
|
|
|
286
|
-
restore(revision: string, legacySnapshot?: Snapshot): Snapshot {
|
|
287
|
-
return this.restoreInspected(revision, inspectSnapshotRevision(this.cwd, this.sessionId, this.root, revision, legacySnapshot, this.sessionKey));
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
private restoreInspected(revision: string, inspected: ReturnType<typeof inspectSnapshotRevision>): Snapshot {
|
|
291
|
-
if (inspected.file) {
|
|
292
|
-
const savedRuntime = hashJson(createSessionRuntime(inspected.snapshot, this.cwd, this.sessionId, inspected.file.view.lineage, "files", inspected.file.provenance.session));
|
|
293
|
-
this.view = inspected.file.view;
|
|
294
|
-
this.base = inspected.file.base;
|
|
295
|
-
this.backend = "files";
|
|
296
|
-
this.provenanceByScope = structuredClone(inspected.file.provenance);
|
|
297
|
-
this.semanticRevision = revision;
|
|
298
|
-
this.savedRuntime = savedRuntime;
|
|
299
|
-
this.absentSharedScopes.clear();
|
|
300
|
-
return inspected.snapshot;
|
|
301
|
-
}
|
|
302
|
-
const loaded = inspected.temporal;
|
|
303
|
-
const { resolved, view } = loaded;
|
|
304
|
-
let base = captureTemporalGitBase(this.cwd, this.sessionId, this.root, this.sessionKey);
|
|
305
|
-
const reference = loaded.runtime.document.meta.temporalRevision;
|
|
306
|
-
let semanticRevision = reference === undefined || reference === "self" ? loaded.runtime.revision : reference;
|
|
307
|
-
let publicationTarget = resolved.publicationTarget;
|
|
308
|
-
resolved.snapshot.meta.durableBase = publicationTarget;
|
|
309
|
-
const savedRuntime = hashJson(createSessionRuntime(resolved.snapshot, this.cwd, this.sessionId, view.lineage));
|
|
310
|
-
resolved.snapshot.meta.pendingPublication = { commit: publicationTarget, error: "Durable publication intent is unconfirmed" };
|
|
311
|
-
{
|
|
312
|
-
try {
|
|
313
|
-
if (isLocalGitRepository(this.root)) delete resolved.snapshot.meta.pendingPublication;
|
|
314
|
-
} catch {
|
|
315
|
-
// Invalid remote/branch configuration retains unconfirmed publication for explicit retry.
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
this.view = view;
|
|
319
|
-
this.base = base;
|
|
320
|
-
this.backend = "git";
|
|
321
|
-
this.provenanceByScope = structuredClone(loaded.provenance);
|
|
322
|
-
this.semanticRevision = semanticRevision;
|
|
323
|
-
this.savedRuntime = savedRuntime;
|
|
324
|
-
this.absentSharedScopes.clear();
|
|
325
|
-
return resolved.snapshot;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
264
|
initialize(snapshot: Snapshot, allowCreateCwd: boolean, expectedShared?: Pick<ScopedStates, "global" | "cwd">, newSessionOrigin = false): RuntimePublication | undefined {
|
|
329
265
|
return this.initializeOrigin(snapshot, { allowCreateCwd, expectedShared, newSessionOrigin });
|
|
330
266
|
}
|
|
@@ -338,33 +274,21 @@ export class TemporalRuntime {
|
|
|
338
274
|
const { allowCreateCwd, expectedShared, newSessionOrigin = false, copy } = options;
|
|
339
275
|
const hasCwd = hasCwdMaterialization(this.cwd, this.root);
|
|
340
276
|
if (!allowCreateCwd && !hasCwd) return undefined;
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
if (hasLegacyStateSources(this.cwd, this.sessionId, this.root, this.sessionKey)) {
|
|
344
|
-
migrateLegacyStorageToGit(this.cwd, this.sessionId, this.root, this.sessionKey);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
else if (!copy) {
|
|
348
|
-
if (allowCreateCwd) initializeFileStore(this.root);
|
|
349
|
-
if (hasLegacyStateSources(this.cwd, this.sessionId, this.root, this.sessionKey)) {
|
|
350
|
-
migrateLegacyStorageToFiles(this.cwd, this.sessionId, this.root, this.sessionKey);
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
const base: TemporalGitBase = backend === "git" ? captureTemporalGitBase(this.cwd, this.sessionId, this.root, this.sessionKey) : captureTemporalFileBase(this.cwd, this.sessionId, this.root, this.sessionKey);
|
|
277
|
+
if (!copy && allowCreateCwd) initializeFileStore(this.root);
|
|
278
|
+
const base = captureTemporalFileBase(this.cwd, this.sessionId, this.root, this.sessionKey);
|
|
354
279
|
const files = new Map(base.files.map((file) => [file.path, file.content]));
|
|
355
280
|
if (copy) {
|
|
356
281
|
const session = temporalScopePaths(this.cwd, this.sessionId, "session", this.root, this.sessionKey);
|
|
357
282
|
const owned = [session.checkpoint, session.patches, session.meta, join(session.directory, "config.json"), join(session.directory, "runtime.json"), join(session.directory, "state.json")];
|
|
358
283
|
const occupied = owned.some((path) => files.get(path) !== undefined);
|
|
359
|
-
|
|
360
|
-
if (occupied || historical?.runtime || historical?.scopes.session) {
|
|
284
|
+
if (occupied) {
|
|
361
285
|
throw new Error("State Flow fork target already has session storage");
|
|
362
286
|
}
|
|
363
287
|
}
|
|
364
288
|
if (SCOPES.some((scope) => {
|
|
365
289
|
const paths = temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey);
|
|
366
|
-
return
|
|
367
|
-
})) throw new Error("
|
|
290
|
+
return lstatSync(join(paths.directory, "state.json"), { throwIfNoEntry: false }) !== undefined;
|
|
291
|
+
})) throw new Error("Unsupported State Flow storage exists; preserve or convert it before initialization");
|
|
368
292
|
const streams = Object.fromEntries(SCOPES.map((scope) => {
|
|
369
293
|
const paths = temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey);
|
|
370
294
|
return [scope, parseScopeStream(files.get(paths.checkpoint), files.get(paths.patches), scope,
|
|
@@ -373,16 +297,15 @@ export class TemporalRuntime {
|
|
|
373
297
|
if (!streams.cwd && !allowCreateCwd) return undefined;
|
|
374
298
|
if (copy && !streams.global) throw new Error("State Flow fork requires existing shared scope storage");
|
|
375
299
|
const paths = sessionRuntimePaths(this.cwd, this.sessionId, this.root, this.sessionKey);
|
|
376
|
-
const existingRuntime = parseSessionRuntime(files.get(paths.config), files.get(paths.runtime), this.cwd, this.sessionId
|
|
377
|
-
if (existingRuntime && !newSessionOrigin) throw new Error("Existing session runtime requires
|
|
300
|
+
const existingRuntime = parseSessionRuntime(files.get(paths.config), files.get(paths.runtime), this.cwd, this.sessionId);
|
|
301
|
+
if (existingRuntime && !newSessionOrigin) throw new Error("Existing session runtime requires retained-boundary restoration");
|
|
378
302
|
// Explicit start before any branch runtime is a new origin, never inheritance of a later session layer.
|
|
379
303
|
if (newSessionOrigin) streams.session = undefined;
|
|
380
304
|
if (copy) streams.session = copy.stream;
|
|
381
|
-
const fresh = createTemporalState({ global: emptyState(), cwd: emptyState(), session: emptyState() }, randomUUID());
|
|
382
|
-
const candidate = new TemporalRuntime(this.cwd, this.session, this.root);
|
|
383
|
-
candidate.backend = backend;
|
|
305
|
+
const fresh = createTemporalState({ global: emptyState(), cwd: emptyState(), session: emptyState() }, randomUUID(), this.historyLimit);
|
|
306
|
+
const candidate = new TemporalRuntime(this.cwd, this.session, this.root, undefined, this.historyLimit);
|
|
384
307
|
candidate.base = base;
|
|
385
|
-
candidate.view = adoptTemporalStreams({ global: streams.global ?? fresh.scopes.global, cwd: streams.cwd ?? fresh.scopes.cwd, session: streams.session ?? fresh.scopes.session },
|
|
308
|
+
candidate.view = adoptTemporalStreams({ global: streams.global ?? fresh.scopes.global, cwd: streams.cwd ?? fresh.scopes.cwd, session: streams.session ?? fresh.scopes.session }, `files:${randomUUID()}`, this.historyLimit);
|
|
386
309
|
const globalMeta = temporalScopePaths(this.cwd, this.sessionId, "global", this.root, this.sessionKey).meta;
|
|
387
310
|
const cwdMeta = temporalScopePaths(this.cwd, this.sessionId, "cwd", this.root, this.sessionKey).meta;
|
|
388
311
|
candidate.provenanceByScope = {
|
|
@@ -391,12 +314,11 @@ export class TemporalRuntime {
|
|
|
391
314
|
session: copy ? structuredClone(copy.provenance) : streams.session === undefined ? {} : parseScopeProvenance(files.get(paths.meta), paths.meta),
|
|
392
315
|
};
|
|
393
316
|
if (expectedShared && (["global", "cwd"] as const).some((scope) => !sameJson(candidate.read(0, scope), expectedShared[scope]))) {
|
|
394
|
-
throw new Error("
|
|
317
|
+
throw new Error("Selected branch shared scopes diverged from the live revision");
|
|
395
318
|
}
|
|
396
319
|
const publication = copy ? candidate.publishForkOrigin(snapshot) : candidate.publish(snapshot, true);
|
|
397
320
|
this.view = candidate.view;
|
|
398
321
|
this.base = candidate.base;
|
|
399
|
-
this.backend = backend;
|
|
400
322
|
this.provenanceByScope = structuredClone(candidate.provenanceByScope);
|
|
401
323
|
this.semanticRevision = candidate.semanticRevision;
|
|
402
324
|
this.savedRuntime = candidate.savedRuntime;
|
|
@@ -404,15 +326,12 @@ export class TemporalRuntime {
|
|
|
404
326
|
return publication;
|
|
405
327
|
}
|
|
406
328
|
|
|
407
|
-
/**
|
|
329
|
+
/** Copy the private origin and apply configured retention folding, preserving live shared values/provenance. */
|
|
408
330
|
private publishForkOrigin(snapshot: Snapshot): RuntimePublication {
|
|
409
|
-
const runtime = createSessionRuntime(snapshot, this.cwd, this.sessionId, this.view!.lineage, this.
|
|
410
|
-
const
|
|
411
|
-
? publishTemporalStateToFiles(this.cwd, this.sessionId, this.view!, ["session"], this.base!, this.root, runtime, this.sessionKey, this.provenanceByScope)
|
|
412
|
-
: publishTemporalStateToGit(this.cwd, this.sessionId, this.view!, ["session"], this.base!, this.root, runtime, this.sessionKey, (snapshot.meta.remotePublication?.mode ?? "transition") === "transition", this.provenanceByScope);
|
|
413
|
-
const publication: RuntimePublication = result;
|
|
331
|
+
const runtime = createSessionRuntime(snapshot, this.cwd, this.sessionId, this.view!.lineage, this.provenanceByScope.session);
|
|
332
|
+
const publication = publishTemporalStateToFiles(this.cwd, this.sessionId, this.view!, SCOPES, this.base!, this.root, runtime, this.sessionKey, this.provenanceByScope);
|
|
414
333
|
this.base = publication.base;
|
|
415
|
-
this.semanticRevision = publication.revision
|
|
334
|
+
this.semanticRevision = publication.revision;
|
|
416
335
|
this.savedRuntime = hashJson(runtime);
|
|
417
336
|
return publication;
|
|
418
337
|
}
|
|
@@ -427,13 +346,12 @@ export class TemporalRuntime {
|
|
|
427
346
|
*/
|
|
428
347
|
private reconcileSharedDrift(changedScopes: ReadonlySet<StateScope>): {
|
|
429
348
|
view: TemporalState;
|
|
430
|
-
base:
|
|
349
|
+
base: TemporalFileBase;
|
|
431
350
|
provenance: Record<StateScope, ArtifactProvenanceRegistry>;
|
|
432
351
|
} {
|
|
433
|
-
const captured = this.
|
|
434
|
-
? captureTemporalFileBase(this.cwd, this.sessionId, this.root, this.sessionKey)
|
|
435
|
-
: captureTemporalGitBase(this.cwd, this.sessionId, this.root, this.sessionKey);
|
|
352
|
+
const captured = captureTemporalFileBase(this.cwd, this.sessionId, this.root, this.sessionKey);
|
|
436
353
|
const liveFiles = new Map(captured.files.map((file) => [file.path, file]));
|
|
354
|
+
const priorFiles = new Map(this.base!.files.map((file) => [file.path, file]));
|
|
437
355
|
const adoptable = new Set<string>();
|
|
438
356
|
for (const scope of SHARED_SCOPES) {
|
|
439
357
|
const paths = temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey);
|
|
@@ -453,10 +371,11 @@ export class TemporalRuntime {
|
|
|
453
371
|
const targets: StateScope[] = [];
|
|
454
372
|
const removedTargets: StateScope[] = [];
|
|
455
373
|
const absentScopes: StateScope[] = [];
|
|
456
|
-
const
|
|
457
|
-
const reconciliation = `${head ?? "files"}:reconcile:${randomUUID()}`;
|
|
374
|
+
const reconciliation = `files:reconcile:${randomUUID()}`;
|
|
458
375
|
for (const scope of SHARED_SCOPES) {
|
|
459
376
|
const paths = temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey);
|
|
377
|
+
// A cached reader may fold wider live tails without owning a semantic write.
|
|
378
|
+
if ([paths.checkpoint, paths.patches, paths.meta].every((path) => liveFiles.get(path)?.identity === priorFiles.get(path)?.identity)) continue;
|
|
460
379
|
const presence = classifyScopeStream(liveFiles.get(paths.checkpoint)?.content, liveFiles.get(paths.patches)?.content, scope,
|
|
461
380
|
scope === "cwd" ? this.cwd : undefined, liveFiles.get(paths.meta)?.content);
|
|
462
381
|
if (presence.kind === "absent") {
|
|
@@ -464,7 +383,7 @@ export class TemporalRuntime {
|
|
|
464
383
|
provenance[scope] = {};
|
|
465
384
|
if (changedScopes.has(scope) && !this.absentSharedScopes.has(scope)) removedTargets.push(scope);
|
|
466
385
|
if (!this.absentSharedScopes.has(scope)) {
|
|
467
|
-
adopted.set(scope, freshEmptyScopeStream(scope, `${reconciliation}:${scope}:absent
|
|
386
|
+
adopted.set(scope, freshEmptyScopeStream(scope, `${reconciliation}:${scope}:absent`, this.historyLimit));
|
|
468
387
|
}
|
|
469
388
|
continue;
|
|
470
389
|
}
|
|
@@ -484,7 +403,7 @@ export class TemporalRuntime {
|
|
|
484
403
|
global: adopted.get("global") ?? structuredClone(this.view!.scopes.global),
|
|
485
404
|
cwd: adopted.get("cwd") ?? structuredClone(this.view!.scopes.cwd),
|
|
486
405
|
session: structuredClone(this.view!.scopes.session),
|
|
487
|
-
}, reconciliation);
|
|
406
|
+
}, reconciliation, this.historyLimit);
|
|
488
407
|
if (removedTargets.length > 0) {
|
|
489
408
|
this.view = reconciledView();
|
|
490
409
|
this.base = captured;
|
|
@@ -496,22 +415,30 @@ export class TemporalRuntime {
|
|
|
496
415
|
return { view: reconciledView(), base: captured, provenance };
|
|
497
416
|
}
|
|
498
417
|
|
|
418
|
+
/** Canonically accept a prepared retained-boundary origin before lifecycle-only persistence. */
|
|
419
|
+
acceptRestoredOrigin(snapshot: Snapshot): RuntimePublication {
|
|
420
|
+
if (!this.restoredOriginPending) throw new Error("State Flow has no prepared restored origin to accept");
|
|
421
|
+
const publication = this.publish(snapshot, true);
|
|
422
|
+
if (!publication) throw new Error("State Flow restored origin produced no canonical publication");
|
|
423
|
+
this.restoredOriginPending = false;
|
|
424
|
+
return publication;
|
|
425
|
+
}
|
|
426
|
+
|
|
499
427
|
publish(
|
|
500
428
|
snapshot: Snapshot,
|
|
501
429
|
semantic = false,
|
|
502
430
|
accepted?: AcceptedTransition,
|
|
503
|
-
options: {
|
|
431
|
+
options: { provenance?: Partial<Record<StateScope, Record<string, ArtifactProvenance>>> } = {},
|
|
504
432
|
): RuntimePublication | undefined {
|
|
505
433
|
if (!this.view || !this.base) throw new Error("State Flow temporal publication is unavailable; restore or initialize before accepting a transition");
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
const provenanceScopes = SCOPES.filter((scope) => Object.
|
|
509
|
-
|
|
510
|
-
|| !sameJson(this.provenanceByScope[scope][path], entry)));
|
|
434
|
+
if (this.restoredOriginPending && !semantic) throw new Error("State Flow restored origin must be accepted before runtime-only persistence");
|
|
435
|
+
// Explicit evidence updates still own a scope CAS, even if they equal the stale cache.
|
|
436
|
+
const provenanceScopes = SCOPES.filter((scope) => Object.keys(options.provenance?.[scope] ?? {}).length > 0);
|
|
437
|
+
const runtimeOnly = !semantic && accepted === undefined && provenanceScopes.length === 0;
|
|
511
438
|
let basis = this.view;
|
|
512
|
-
let base:
|
|
439
|
+
let base: TemporalFileBase = this.base;
|
|
513
440
|
let basisProvenance = this.provenanceByScope;
|
|
514
|
-
if (
|
|
441
|
+
if (this.semanticRevision) {
|
|
515
442
|
const changedScopes = new Set<StateScope>([
|
|
516
443
|
...(accepted?.transitions ?? []).map(({ scope }) => scope),
|
|
517
444
|
...provenanceScopes,
|
|
@@ -521,52 +448,25 @@ export class TemporalRuntime {
|
|
|
521
448
|
base = reconciled.base;
|
|
522
449
|
basisProvenance = reconciled.provenance;
|
|
523
450
|
}
|
|
524
|
-
const next = accepted ? advanceTemporalState(basis, accepted.transitions, accepted.id) : basis;
|
|
451
|
+
const next = accepted ? advanceTemporalState(basis, accepted.transitions, accepted.id, this.historyLimit) : basis;
|
|
525
452
|
const nextProvenance = structuredClone(basisProvenance);
|
|
526
453
|
for (const scope of SCOPES) {
|
|
527
454
|
const updates = options.provenance?.[scope];
|
|
528
455
|
if (updates) for (const [path, entry] of Object.entries(updates)) nextProvenance[scope][path] = structuredClone(entry);
|
|
529
456
|
}
|
|
530
|
-
for (const scope of SCOPES) {
|
|
531
|
-
nextProvenance[scope] = pruneArtifactProvenance(nextProvenance[scope], readTemporalState(next, 0, scope).artifacts);
|
|
457
|
+
if (!runtimeOnly) for (const scope of SCOPES) {
|
|
458
|
+
nextProvenance[scope] = pruneArtifactProvenance(nextProvenance[scope], readTemporalState(next, 0, scope, this.historyLimit).artifacts);
|
|
532
459
|
}
|
|
533
460
|
const provenanceChanged = !sameJson(nextProvenance, this.provenanceByScope);
|
|
534
|
-
const
|
|
535
|
-
const runtime = createSessionRuntime(snapshot, this.cwd, this.sessionId, next.lineage, this.backend === "files" ? "files" : "unconfirmed", nextProvenance.session);
|
|
461
|
+
const runtime = createSessionRuntime(snapshot, this.cwd, this.sessionId, next.lineage, nextProvenance.session);
|
|
536
462
|
const fingerprint = hashJson(runtime);
|
|
537
463
|
if (!semantic && fingerprint === this.savedRuntime && !provenanceChanged) return undefined;
|
|
538
|
-
|
|
539
|
-
runtime.meta.temporalRevision = "self";
|
|
540
|
-
const result = publishTemporalStateToFiles(this.cwd, this.sessionId, next, semantic ? SCOPES : [], base, this.root, runtime, this.sessionKey, nextProvenance);
|
|
541
|
-
this.base = result.base;
|
|
542
|
-
this.view = next;
|
|
543
|
-
this.provenanceByScope = nextProvenance;
|
|
544
|
-
this.savedRuntime = fingerprint;
|
|
545
|
-
this.semanticRevision = result.revision;
|
|
546
|
-
if (semantic) this.absentSharedScopes.clear();
|
|
547
|
-
return { base: result.base, revision: result.revision };
|
|
548
|
-
}
|
|
549
|
-
if (!scopedWrite) {
|
|
550
|
-
const current = captureTemporalGitBase(this.cwd, this.sessionId, this.root, this.sessionKey);
|
|
551
|
-
const paths = sessionRuntimePaths(this.cwd, this.sessionId, this.root, this.sessionKey);
|
|
552
|
-
for (const path of [paths.config, paths.runtime]) {
|
|
553
|
-
if (current.files.find((file) => file.path === path)?.identity !== base.files.find((file) => file.path === path)?.identity) {
|
|
554
|
-
throw new Error("Temporal State Flow runtime changed concurrently");
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
base = current;
|
|
558
|
-
}
|
|
559
|
-
runtime.meta.temporalRevision = scopedWrite ? "self" : this.semanticRevision!;
|
|
560
|
-
const result = publishTemporalStateToGit(
|
|
561
|
-
this.cwd, this.sessionId, next, semantic ? SCOPES : provenanceChanged ? provenanceScopes : [], base, this.root, runtime, this.sessionKey,
|
|
562
|
-
pushRemote,
|
|
563
|
-
nextProvenance,
|
|
564
|
-
);
|
|
464
|
+
const result = publishTemporalStateToFiles(this.cwd, this.sessionId, next, runtimeOnly ? [] : SCOPES, base, this.root, runtime, this.sessionKey, runtimeOnly ? undefined : nextProvenance, runtimeOnly);
|
|
565
465
|
this.base = result.base;
|
|
566
466
|
this.view = next;
|
|
567
467
|
this.provenanceByScope = nextProvenance;
|
|
568
468
|
this.savedRuntime = fingerprint;
|
|
569
|
-
|
|
469
|
+
this.semanticRevision = result.revision;
|
|
570
470
|
if (semantic) this.absentSharedScopes.clear();
|
|
571
471
|
return result;
|
|
572
472
|
}
|