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