@llblab/pi-kit 0.13.0 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +1 -1
  3. package/node_modules/@llblab/pi-state-flow/AGENTS.md +7 -7
  4. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +8 -9
  5. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +30 -0
  6. package/node_modules/@llblab/pi-state-flow/README.md +1 -3
  7. package/node_modules/@llblab/pi-state-flow/dist/index.d.ts +21 -0
  8. package/node_modules/@llblab/pi-state-flow/dist/index.js +20 -0
  9. package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.d.ts +39 -0
  10. package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.js +78 -0
  11. package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.d.ts +110 -0
  12. package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.js +334 -0
  13. package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.d.ts +49 -0
  14. package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.js +67 -0
  15. package/node_modules/@llblab/pi-state-flow/dist/lib/config.d.ts +11 -0
  16. package/node_modules/@llblab/pi-state-flow/dist/lib/config.js +53 -0
  17. package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +23 -0
  18. package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +109 -0
  19. package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.d.ts +111 -0
  20. package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +189 -0
  21. package/node_modules/@llblab/pi-state-flow/dist/lib/discovery.d.ts +21 -0
  22. package/node_modules/@llblab/pi-state-flow/dist/lib/discovery.js +125 -0
  23. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.d.ts +102 -0
  24. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +507 -0
  25. package/node_modules/@llblab/pi-state-flow/dist/lib/episode.d.ts +8 -0
  26. package/node_modules/@llblab/pi-state-flow/dist/lib/episode.js +27 -0
  27. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.d.ts +22 -0
  28. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +1263 -0
  29. package/node_modules/@llblab/pi-state-flow/dist/lib/git.d.ts +72 -0
  30. package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +565 -0
  31. package/node_modules/@llblab/pi-state-flow/dist/lib/history.d.ts +22 -0
  32. package/node_modules/@llblab/pi-state-flow/dist/lib/history.js +79 -0
  33. package/node_modules/@llblab/pi-state-flow/dist/lib/json.d.ts +12 -0
  34. package/node_modules/@llblab/pi-state-flow/dist/lib/json.js +109 -0
  35. package/node_modules/@llblab/pi-state-flow/dist/lib/logging.d.ts +25 -0
  36. package/node_modules/@llblab/pi-state-flow/dist/lib/logging.js +24 -0
  37. package/node_modules/@llblab/pi-state-flow/dist/lib/maintenance.d.ts +36 -0
  38. package/node_modules/@llblab/pi-state-flow/dist/lib/maintenance.js +98 -0
  39. package/node_modules/@llblab/pi-state-flow/dist/lib/memory.d.ts +15 -0
  40. package/node_modules/@llblab/pi-state-flow/dist/lib/memory.js +42 -0
  41. package/node_modules/@llblab/pi-state-flow/dist/lib/migration.d.ts +13 -0
  42. package/node_modules/@llblab/pi-state-flow/dist/lib/migration.js +133 -0
  43. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.d.ts +7 -0
  44. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +59 -0
  45. package/node_modules/@llblab/pi-state-flow/dist/lib/publication.d.ts +69 -0
  46. package/node_modules/@llblab/pi-state-flow/dist/lib/publication.js +335 -0
  47. package/node_modules/@llblab/pi-state-flow/dist/lib/query.d.ts +27 -0
  48. package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +35 -0
  49. package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.d.ts +8 -0
  50. package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.js +27 -0
  51. package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.d.ts +36 -0
  52. package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.js +38 -0
  53. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +142 -0
  54. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +529 -0
  55. package/node_modules/@llblab/pi-state-flow/dist/lib/session.d.ts +21 -0
  56. package/node_modules/@llblab/pi-state-flow/dist/lib/session.js +44 -0
  57. package/node_modules/@llblab/pi-state-flow/dist/lib/skills.d.ts +25 -0
  58. package/node_modules/@llblab/pi-state-flow/dist/lib/skills.js +131 -0
  59. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +88 -0
  60. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +255 -0
  61. package/node_modules/@llblab/pi-state-flow/dist/lib/state.d.ts +55 -0
  62. package/node_modules/@llblab/pi-state-flow/dist/lib/state.js +31 -0
  63. package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +38 -0
  64. package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +79 -0
  65. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.d.ts +46 -0
  66. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.js +217 -0
  67. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +100 -0
  68. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +234 -0
  69. package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.d.ts +39 -0
  70. package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.js +203 -0
  71. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.d.ts +25 -0
  72. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +204 -0
  73. package/node_modules/@llblab/pi-state-flow/dist/package.json +79 -0
  74. package/node_modules/@llblab/pi-state-flow/dist/pi-state-flow/index.js +1 -0
  75. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +138 -0
  76. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +11 -5
  77. package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +5 -1
  78. package/node_modules/@llblab/pi-state-flow/docs/filesystem-recovery.md +3 -3
  79. package/node_modules/@llblab/pi-state-flow/docs/usage.md +6 -4
  80. package/node_modules/@llblab/pi-state-flow/index.ts +1 -0
  81. package/node_modules/@llblab/pi-state-flow/lib/compaction.ts +17 -1
  82. package/node_modules/@llblab/pi-state-flow/lib/config.ts +6 -1
  83. package/node_modules/@llblab/pi-state-flow/lib/durable.ts +88 -96
  84. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +64 -12
  85. package/node_modules/@llblab/pi-state-flow/lib/git.ts +32 -188
  86. package/node_modules/@llblab/pi-state-flow/lib/migration.ts +84 -48
  87. package/node_modules/@llblab/pi-state-flow/lib/query.ts +40 -0
  88. package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +7 -30
  89. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +48 -50
  90. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +41 -97
  91. package/node_modules/@llblab/pi-state-flow/lib/storage.ts +17 -12
  92. package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +13 -9
  93. package/node_modules/@llblab/pi-state-flow/package.json +23 -6
  94. package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +3 -1
  95. package/package.json +4 -4
@@ -0,0 +1,142 @@
1
+ import { type SessionAddress } from "./durable.ts";
2
+ import { type ArtifactProvenance, type ArtifactProvenanceRegistry } from "./artifact.ts";
3
+ import { publishTemporalStateToGit } from "./git.ts";
4
+ import { type AcceptedTransition, type RecentTransitionWindow } from "./history.ts";
5
+ import { type Snapshot } from "./snapshot.ts";
6
+ import { type MaterializedState, type ScopedStates, type StateScope } from "./state.ts";
7
+ import { type ScopeStream, type TemporalState } from "./temporal.ts";
8
+ export type RuntimePublication = ReturnType<typeof publishTemporalStateToGit> & {
9
+ revision?: string;
10
+ };
11
+ /** A targeted removed scope was deliberately adopted as empty before refusing the stale semantic patch. */
12
+ export declare class SharedScopeRemovalConflictError extends Error {
13
+ readonly scopes: readonly StateScope[];
14
+ constructor(scopes: readonly StateScope[]);
15
+ }
16
+ export declare class MissingSessionRuntimeError extends Error {
17
+ constructor();
18
+ }
19
+ /** Immutable target validation is independent of acquiring the live publication basis. */
20
+ export declare function inspectRuntimeRevision(cwd: string, sessionId: string, root: string, revision: string, sessionKey?: string): {
21
+ runtime: {
22
+ document: import("./snapshot.ts").SessionRuntime;
23
+ revision: string;
24
+ };
25
+ resolved: {
26
+ snapshot: Snapshot;
27
+ lineage: import("./temporal.ts").TransitionBoundary[];
28
+ publicationTarget: string;
29
+ artifacts: ArtifactProvenanceRegistry;
30
+ };
31
+ view: {
32
+ lineage: import("./temporal.ts").TransitionBoundary[];
33
+ scopes: {
34
+ global: ScopeStream;
35
+ cwd: ScopeStream;
36
+ session: ScopeStream;
37
+ };
38
+ };
39
+ provenance: Record<StateScope, ArtifactProvenanceRegistry>;
40
+ };
41
+ /** Validate one supported immutable target before any live migration/publication. */
42
+ export declare function inspectSnapshotRevision(cwd: string, sessionId: string, root: string, revision: string, _legacySnapshot?: Snapshot, sessionKey?: string): {
43
+ snapshot: import("./snapshot.ts").StateFlowSnapshot;
44
+ file: {
45
+ runtime: import("./snapshot.ts").SessionRuntime;
46
+ view: {
47
+ scopes: Record<StateScope, ScopeStream>;
48
+ lineage: import("./temporal.ts").TransitionBoundary[];
49
+ };
50
+ provenance: Record<StateScope, ArtifactProvenanceRegistry>;
51
+ base: {
52
+ files: import("./durable.ts").DurableFileBase[];
53
+ };
54
+ revision: `file:${string}`;
55
+ };
56
+ temporal?: undefined;
57
+ } | {
58
+ file?: undefined;
59
+ snapshot: import("./snapshot.ts").StateFlowSnapshot;
60
+ temporal: {
61
+ runtime: {
62
+ document: import("./snapshot.ts").SessionRuntime;
63
+ revision: string;
64
+ };
65
+ resolved: {
66
+ snapshot: Snapshot;
67
+ lineage: import("./temporal.ts").TransitionBoundary[];
68
+ publicationTarget: string;
69
+ artifacts: ArtifactProvenanceRegistry;
70
+ };
71
+ view: {
72
+ lineage: import("./temporal.ts").TransitionBoundary[];
73
+ scopes: {
74
+ global: ScopeStream;
75
+ cwd: ScopeStream;
76
+ session: ScopeStream;
77
+ };
78
+ };
79
+ provenance: Record<StateScope, ArtifactProvenanceRegistry>;
80
+ };
81
+ };
82
+ /** Cached branch-selected temporal state and publication basis; excludes Pi event policy. */
83
+ export declare class TemporalRuntime {
84
+ view: TemporalState | undefined;
85
+ private base;
86
+ private semanticRevision;
87
+ private savedRuntime;
88
+ private backend;
89
+ private provenanceByScope;
90
+ /** Shared scopes whose wholly absent live basis was accepted after one stale-target refusal. */
91
+ private readonly absentSharedScopes;
92
+ readonly cwd: string;
93
+ private readonly session;
94
+ readonly root: string;
95
+ constructor(cwd: string, session: string | SessionAddress, root: string, sessionKey?: string);
96
+ get sessionId(): string;
97
+ get sessionKey(): string;
98
+ /** Runtime-owned artifact freshness evidence for one scope; never model-visible state. */
99
+ artifactProvenance(scope: StateScope): ArtifactProvenanceRegistry;
100
+ /** Explicit start owns directory/repository creation; reads never call this. */
101
+ prepare(): void;
102
+ /** Explicit start migrates a predecessor envelope even when passive restore already populated the cache. */
103
+ migrateLegacyStorage(): void;
104
+ /** Explicit start can upgrade a proven file cohort; failed adoption leaves the cache in file mode. */
105
+ promote(snapshot: Snapshot): RuntimePublication | undefined;
106
+ read(offset?: number, scope?: StateScope): MaterializedState;
107
+ states(): ScopedStates;
108
+ causalBasis(): string;
109
+ recent(): RecentTransitionWindow;
110
+ /** Validate selection without installing state; only a matching immutable Git inspection is reusable. */
111
+ prepareRestore(revision: string, legacySnapshot?: Snapshot): {
112
+ snapshot: Snapshot;
113
+ restore: () => Snapshot;
114
+ };
115
+ /** Copy only a proven source session stream; shared streams come from the child's fresh live basis. */
116
+ prepareFork(source: SessionAddress, revision: string): {
117
+ snapshot: Snapshot;
118
+ fork: () => {
119
+ snapshot: Snapshot;
120
+ publication: RuntimePublication;
121
+ };
122
+ };
123
+ restore(revision: string, legacySnapshot?: Snapshot): Snapshot;
124
+ private restoreInspected;
125
+ initialize(snapshot: Snapshot, allowCreateCwd: boolean, expectedShared?: Pick<ScopedStates, "global" | "cwd">, newSessionOrigin?: boolean): RuntimePublication | undefined;
126
+ private initializeOrigin;
127
+ /** Initial copy owns only the new session files, without pruning or rewriting shared provenance. */
128
+ private publishForkOrigin;
129
+ /**
130
+ * Reconcile untouched shared-scope drift against the current proven live basis.
131
+ *
132
+ * A restored branch can lag behind live global/CWD state. Untouched shared scopes adopt
133
+ * the current live streams at a fresh origin; a shared scope the accepted transition
134
+ * actually changes remains a fail-closed write conflict. Divergence in non-adoptable
135
+ * session or runtime files also fails closed under the existing race rule.
136
+ */
137
+ private reconcileSharedDrift;
138
+ publish(snapshot: Snapshot, semantic?: boolean, accepted?: AcceptedTransition, options?: {
139
+ pushRemote?: boolean;
140
+ provenance?: Partial<Record<StateScope, Record<string, ArtifactProvenance>>>;
141
+ }): RuntimePublication | undefined;
142
+ }
@@ -0,0 +1,529 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { lstatSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { classifyScopeStream, parseScopeProvenance, parseScopeStream, sessionRuntimePaths, temporalScopePaths } from "./durable.js";
5
+ import { parseArtifactProvenanceRegistry, pruneArtifactProvenance } from "./artifact.js";
6
+ import { adoptFileStateToGit, initializeGitRepository, isLocalGitRepository, captureTemporalGitBase, loadTemporalRevision, migrateLegacyStorageToGit, publishTemporalStateToGit } from "./git.js";
7
+ import { captureTemporalFileBase, detectGitCapability, initializeFileStore, loadTemporalFileRevision, migrateLegacyStorageToFiles, publishTemporalStateToFiles } from "./storage.js";
8
+ import { hashJson, sameJson } from "./json.js";
9
+ import { hasCwdMaterialization, hasLegacyStateSources } from "./migration.js";
10
+ import { RevisionUnavailableError, createSessionRuntime, isFileRevision, parseSessionRuntime, resolveFileSessionRuntime, resolveSessionRuntime } from "./snapshot.js";
11
+ import { emptyState } from "./state.js";
12
+ import { adoptTemporalStreams, advanceTemporalState, createTemporalState, readTemporalState, validateTemporalState } from "./temporal.js";
13
+ const SCOPES = ["global", "cwd", "session"];
14
+ const SHARED_SCOPES = ["global", "cwd"];
15
+ function emptyProvenance() {
16
+ return { global: {}, cwd: {}, session: {} };
17
+ }
18
+ function scopeLabel(scope) {
19
+ return scope === "cwd" ? "CWD" : scope;
20
+ }
21
+ /** Precise fail-closed conflict for a shared scope this transition actually overwrites. */
22
+ function targetScopeConflict(scopes) {
23
+ const labels = scopes.map(scopeLabel);
24
+ if (labels.length === 1) {
25
+ return new Error(`State Flow cannot publish the ${labels[0]} patch because the live ${labels[0]} state advanced after this transition's selected basis. Refresh or reconcile the target scope before retrying.`);
26
+ }
27
+ return new Error(`State Flow cannot publish the ${labels.join(" and ")} patches because the live ${labels.join(" and ")} states advanced after this transition's selected basis. Refresh or reconcile the target scopes before retrying.`);
28
+ }
29
+ /** A targeted removed scope was deliberately adopted as empty before refusing the stale semantic patch. */
30
+ export class SharedScopeRemovalConflictError extends Error {
31
+ scopes;
32
+ constructor(scopes) {
33
+ const labels = scopes.map(scopeLabel);
34
+ super(labels.length === 1
35
+ ? `State Flow cannot publish the ${labels[0]} patch because the live ${labels[0]} scope was removed after this transition's selected basis. Refresh or reconcile the target scope before retrying.`
36
+ : `State Flow cannot publish the ${labels.join(" and ")} patches because the live ${labels.join(" and ")} scopes were removed after this transition's selected basis. Refresh or reconcile the target scopes before retrying.`);
37
+ this.name = "SharedScopeRemovalConflictError";
38
+ this.scopes = Object.freeze([...scopes]);
39
+ }
40
+ }
41
+ /** Disappearance invalidates a selected write target even though untouched scopes can adopt empty reality. */
42
+ function removedTargetScopeConflict(scopes) {
43
+ return new SharedScopeRemovalConflictError(scopes);
44
+ }
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 };
73
+ }
74
+ /** Cached branch-selected temporal state and publication basis; excludes Pi event policy. */
75
+ export class TemporalRuntime {
76
+ view;
77
+ base;
78
+ semanticRevision;
79
+ savedRuntime;
80
+ backend;
81
+ provenanceByScope = emptyProvenance();
82
+ /** Shared scopes whose wholly absent live basis was accepted after one stale-target refusal. */
83
+ absentSharedScopes = new Set();
84
+ cwd;
85
+ session;
86
+ root;
87
+ constructor(cwd, session, root, sessionKey) {
88
+ this.cwd = cwd;
89
+ this.session = Object.freeze(typeof session === "string" ? { id: session, key: sessionKey ?? session } : { ...session });
90
+ this.root = root;
91
+ }
92
+ get sessionId() { return this.session.id; }
93
+ get sessionKey() { return this.session.key; }
94
+ /** Runtime-owned artifact freshness evidence for one scope; never model-visible state. */
95
+ artifactProvenance(scope) {
96
+ return structuredClone(this.provenanceByScope[scope]);
97
+ }
98
+ /** Explicit start owns directory/repository creation; reads never call this. */
99
+ prepare() {
100
+ const backend = detectGitCapability();
101
+ if (backend === "git")
102
+ initializeGitRepository(this.root);
103
+ else
104
+ initializeFileStore(this.root);
105
+ this.backend = backend;
106
+ }
107
+ /** Explicit start migrates a predecessor envelope even when passive restore already populated the cache. */
108
+ migrateLegacyStorage() {
109
+ if (!this.view || !this.backend || !hasLegacyStateSources(this.cwd, this.sessionId, this.root, this.sessionKey))
110
+ return undefined;
111
+ const publication = this.backend === "git"
112
+ ? migrateLegacyStorageToGit(this.cwd, this.sessionId, this.root, this.sessionKey)
113
+ : (migrateLegacyStorageToFiles(this.cwd, this.sessionId, this.root, this.sessionKey), undefined);
114
+ const base = this.backend === "files"
115
+ ? captureTemporalFileBase(this.cwd, this.sessionId, this.root, this.sessionKey)
116
+ : captureTemporalGitBase(this.cwd, this.sessionId, this.root, this.sessionKey);
117
+ const files = new Map(base.files.map((file) => [file.path, file.content]));
118
+ const scopes = Object.fromEntries(SCOPES.map((scope) => {
119
+ const paths = temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey);
120
+ const stream = parseScopeStream(files.get(paths.checkpoint), files.get(paths.patches), scope, scope === "cwd" ? this.cwd : undefined, files.get(paths.meta));
121
+ if (!stream)
122
+ throw new Error(`State Flow ${scopeLabel(scope)} scope disappeared during migration`);
123
+ return [scope, stream];
124
+ }));
125
+ const migrated = { lineage: this.view.lineage, scopes };
126
+ validateTemporalState(migrated);
127
+ if (!sameJson(this.states(), {
128
+ global: readTemporalState(migrated, 0, "global"),
129
+ cwd: readTemporalState(migrated, 0, "cwd"),
130
+ session: readTemporalState(migrated, 0, "session"),
131
+ }))
132
+ throw new Error("State Flow migration changed materialized semantic state");
133
+ this.view = migrated;
134
+ this.base = base;
135
+ this.provenanceByScope = {
136
+ global: parseScopeProvenance(files.get(temporalScopePaths(this.cwd, this.sessionId, "global", this.root, this.sessionKey).meta), "State Flow global metadata"),
137
+ cwd: parseScopeProvenance(files.get(temporalScopePaths(this.cwd, this.sessionId, "cwd", this.root, this.sessionKey).meta), "State Flow CWD metadata"),
138
+ session: this.provenanceByScope.session,
139
+ };
140
+ if (publication?.commit)
141
+ this.semanticRevision = publication.commit;
142
+ }
143
+ /** Explicit start can upgrade a proven file cohort; failed adoption leaves the cache in file mode. */
144
+ promote(snapshot) {
145
+ if (this.backend !== "files" || !this.view || detectGitCapability() === "files")
146
+ return undefined;
147
+ if (!snapshot.meta.durableBase || snapshot.meta.durableBase !== this.semanticRevision)
148
+ throw new Error("Git adoption requires the selected file revision");
149
+ const pushRemote = (snapshot.meta.remotePublication?.mode ?? "transition") === "transition";
150
+ const result = adoptFileStateToGit(this.cwd, this.sessionId, this.root, snapshot.meta.durableBase, snapshot, this.sessionKey, pushRemote);
151
+ this.provenanceByScope = structuredClone(result.provenance);
152
+ const savedRuntime = hashJson(createSessionRuntime(snapshot, this.cwd, this.sessionId, result.view.lineage, "unconfirmed", this.provenanceByScope.session));
153
+ this.view = result.view;
154
+ this.base = result.base;
155
+ this.backend = "git";
156
+ this.semanticRevision = result.revision;
157
+ this.savedRuntime = savedRuntime;
158
+ this.absentSharedScopes.clear();
159
+ return result;
160
+ }
161
+ read(offset = 0, scope) {
162
+ if (!this.view)
163
+ throw new Error("State Flow temporal runtime is unavailable");
164
+ return readTemporalState(this.view, offset, scope);
165
+ }
166
+ states() {
167
+ return { global: this.read(0, "global"), cwd: this.read(0, "cwd"), session: this.read(0, "session") };
168
+ }
169
+ causalBasis() {
170
+ if (!this.view)
171
+ throw new Error("State Flow temporal runtime is unavailable");
172
+ return this.view.lineage.at(-1).id;
173
+ }
174
+ recent() {
175
+ const result = [];
176
+ for (const boundary of this.view?.lineage.slice(1) ?? []) {
177
+ const transitions = SCOPES.flatMap((scope) => {
178
+ const record = this.view.scopes[scope].patches.find(({ transition }) => transition.id === boundary.id);
179
+ return record ? [{ scope, patch: structuredClone(record.patch) }] : [];
180
+ });
181
+ if (transitions.length)
182
+ result.push({ id: boundary.id, at: boundary.position, transitions });
183
+ }
184
+ return result;
185
+ }
186
+ /** Validate selection without installing state; only a matching immutable Git inspection is reusable. */
187
+ prepareRestore(revision, legacySnapshot) {
188
+ const inspected = inspectSnapshotRevision(this.cwd, this.sessionId, this.root, revision, legacySnapshot, this.sessionKey);
189
+ const selected = inspected.snapshot.meta.durableBase ?? revision;
190
+ let consumed = false;
191
+ return {
192
+ snapshot: structuredClone(inspected.snapshot),
193
+ restore: () => {
194
+ if (consumed)
195
+ throw new Error("Prepared State Flow restore was already consumed");
196
+ consumed = true;
197
+ // File cohorts can expire; legacy migration and owner redirection keep their fresh-read path.
198
+ return inspected.temporal && selected === revision
199
+ ? this.restoreInspected(revision, inspected)
200
+ : this.restore(selected, inspected.snapshot);
201
+ },
202
+ };
203
+ }
204
+ /** Copy only a proven source session stream; shared streams come from the child's fresh live basis. */
205
+ prepareFork(source, revision) {
206
+ const parent = Object.freeze({ ...source });
207
+ if (parent.id === this.sessionId || parent.key === this.sessionKey)
208
+ throw new Error("State Flow fork requires a distinct session identity and key");
209
+ const inspect = () => inspectSnapshotRevision(this.cwd, parent.id, this.root, revision, undefined, parent.key);
210
+ const inspected = inspect();
211
+ const snapshot = {
212
+ config: structuredClone(inspected.snapshot.config),
213
+ meta: {
214
+ step: 0,
215
+ ...(inspected.snapshot.meta.bootstrap === undefined ? {} : { bootstrap: inspected.snapshot.meta.bootstrap }),
216
+ ...(inspected.snapshot.meta.remotePublication === undefined ? {} : { remotePublication: structuredClone(inspected.snapshot.meta.remotePublication) }),
217
+ },
218
+ };
219
+ let consumed = false;
220
+ return {
221
+ snapshot: { ...structuredClone(snapshot), meta: { ...structuredClone(snapshot.meta), durableBase: revision } },
222
+ fork: () => {
223
+ if (consumed)
224
+ throw new Error("Prepared State Flow fork was already consumed");
225
+ consumed = true;
226
+ if (this.view)
227
+ throw new Error("State Flow fork target already has session storage");
228
+ // Unlike immutable Git input, a file-only source must still match its complete cohort.
229
+ const current = inspected.file ? inspect() : inspected;
230
+ const selected = current.temporal ?? current.file;
231
+ if (!selected)
232
+ throw new Error("State Flow fork requires a temporal session stream");
233
+ const publication = this.initializeOrigin(snapshot, { allowCreateCwd: false, copy: {
234
+ stream: selected.view.scopes.session,
235
+ provenance: selected.provenance.session,
236
+ backend: current.file ? "files" : "git",
237
+ } });
238
+ const ownedRevision = publication?.revision ?? publication?.commit;
239
+ if (!publication || !ownedRevision)
240
+ throw new Error("State Flow fork requires existing shared scope storage");
241
+ return { snapshot: { ...structuredClone(snapshot), meta: { ...structuredClone(snapshot.meta), durableBase: ownedRevision } }, publication };
242
+ },
243
+ };
244
+ }
245
+ restore(revision, legacySnapshot) {
246
+ return this.restoreInspected(revision, inspectSnapshotRevision(this.cwd, this.sessionId, this.root, revision, legacySnapshot, this.sessionKey));
247
+ }
248
+ restoreInspected(revision, inspected) {
249
+ if (inspected.file) {
250
+ const savedRuntime = hashJson(createSessionRuntime(inspected.snapshot, this.cwd, this.sessionId, inspected.file.view.lineage, "files", inspected.file.provenance.session));
251
+ this.view = inspected.file.view;
252
+ this.base = inspected.file.base;
253
+ this.backend = "files";
254
+ this.provenanceByScope = structuredClone(inspected.file.provenance);
255
+ this.semanticRevision = revision;
256
+ this.savedRuntime = savedRuntime;
257
+ this.absentSharedScopes.clear();
258
+ return inspected.snapshot;
259
+ }
260
+ const loaded = inspected.temporal;
261
+ const { resolved, view } = loaded;
262
+ let base = captureTemporalGitBase(this.cwd, this.sessionId, this.root, this.sessionKey);
263
+ const reference = loaded.runtime.document.meta.temporalRevision;
264
+ let semanticRevision = reference === undefined || reference === "self" ? loaded.runtime.revision : reference;
265
+ let publicationTarget = resolved.publicationTarget;
266
+ resolved.snapshot.meta.durableBase = publicationTarget;
267
+ const savedRuntime = hashJson(createSessionRuntime(resolved.snapshot, this.cwd, this.sessionId, view.lineage));
268
+ resolved.snapshot.meta.pendingPublication = { commit: publicationTarget, error: "Durable publication intent is unconfirmed" };
269
+ {
270
+ try {
271
+ if (isLocalGitRepository(this.root))
272
+ delete resolved.snapshot.meta.pendingPublication;
273
+ }
274
+ catch {
275
+ // Invalid remote/branch configuration retains unconfirmed publication for explicit retry.
276
+ }
277
+ }
278
+ this.view = view;
279
+ this.base = base;
280
+ this.backend = "git";
281
+ this.provenanceByScope = structuredClone(loaded.provenance);
282
+ this.semanticRevision = semanticRevision;
283
+ this.savedRuntime = savedRuntime;
284
+ this.absentSharedScopes.clear();
285
+ return resolved.snapshot;
286
+ }
287
+ initialize(snapshot, allowCreateCwd, expectedShared, newSessionOrigin = false) {
288
+ return this.initializeOrigin(snapshot, { allowCreateCwd, expectedShared, newSessionOrigin });
289
+ }
290
+ initializeOrigin(snapshot, options) {
291
+ const { allowCreateCwd, expectedShared, newSessionOrigin = false, copy } = options;
292
+ const hasCwd = hasCwdMaterialization(this.cwd, this.root);
293
+ if (!allowCreateCwd && !hasCwd)
294
+ return undefined;
295
+ const backend = copy?.backend ?? this.backend ?? (detectGitCapability() === "git" && lstatSync(join(this.root, ".git"), { throwIfNoEntry: false }) ? "git" : "files");
296
+ if (!copy && backend === "git") {
297
+ if (hasLegacyStateSources(this.cwd, this.sessionId, this.root, this.sessionKey)) {
298
+ migrateLegacyStorageToGit(this.cwd, this.sessionId, this.root, this.sessionKey);
299
+ }
300
+ }
301
+ else if (!copy) {
302
+ if (allowCreateCwd)
303
+ initializeFileStore(this.root);
304
+ if (hasLegacyStateSources(this.cwd, this.sessionId, this.root, this.sessionKey)) {
305
+ migrateLegacyStorageToFiles(this.cwd, this.sessionId, this.root, this.sessionKey);
306
+ }
307
+ }
308
+ const base = backend === "git" ? captureTemporalGitBase(this.cwd, this.sessionId, this.root, this.sessionKey) : captureTemporalFileBase(this.cwd, this.sessionId, this.root, this.sessionKey);
309
+ const files = new Map(base.files.map((file) => [file.path, file.content]));
310
+ if (copy) {
311
+ const session = temporalScopePaths(this.cwd, this.sessionId, "session", this.root, this.sessionKey);
312
+ const owned = [session.checkpoint, session.patches, session.meta, join(session.directory, "config.json"), join(session.directory, "state.json")];
313
+ const occupied = owned.some((path) => files.get(path) !== undefined);
314
+ const historical = !occupied && backend === "git" && base.head ? loadTemporalRevision(this.cwd, this.sessionId, this.root, base.head, this.sessionKey) : undefined;
315
+ if (occupied || historical?.runtime || historical?.scopes.session) {
316
+ throw new Error("State Flow fork target already has session storage");
317
+ }
318
+ }
319
+ if (SCOPES.some((scope) => {
320
+ const paths = temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey);
321
+ return files.get(join(paths.directory, "state.json")) !== undefined;
322
+ }))
323
+ throw new Error("Legacy State Flow storage changed during initialization; retry migration from a fresh basis");
324
+ const streams = Object.fromEntries(SCOPES.map((scope) => {
325
+ const paths = temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey);
326
+ return [scope, parseScopeStream(files.get(paths.checkpoint), files.get(paths.patches), scope, scope === "cwd" ? this.cwd : undefined, files.get(paths.meta))];
327
+ }));
328
+ if (!streams.cwd && !allowCreateCwd)
329
+ return undefined;
330
+ if (copy && !streams.global)
331
+ throw new Error("State Flow fork requires existing shared scope storage");
332
+ const paths = sessionRuntimePaths(this.cwd, this.sessionId, this.root, this.sessionKey);
333
+ const existingRuntime = parseSessionRuntime(files.get(paths.config), files.get(paths.meta), this.cwd, this.sessionId);
334
+ if (existingRuntime && !newSessionOrigin)
335
+ throw new Error("Existing session runtime requires a branch revision pointer");
336
+ // Explicit start before any branch runtime is a new origin, never inheritance of a later session layer.
337
+ if (newSessionOrigin)
338
+ streams.session = undefined;
339
+ if (copy)
340
+ streams.session = copy.stream;
341
+ const fresh = createTemporalState({ global: emptyState(), cwd: emptyState(), session: emptyState() }, randomUUID());
342
+ const candidate = new TemporalRuntime(this.cwd, this.session, this.root);
343
+ candidate.backend = backend;
344
+ candidate.base = base;
345
+ candidate.view = adoptTemporalStreams({ global: streams.global ?? fresh.scopes.global, cwd: streams.cwd ?? fresh.scopes.cwd, session: streams.session ?? fresh.scopes.session }, `${base.head ?? "unborn"}:${randomUUID()}`);
346
+ const globalMeta = temporalScopePaths(this.cwd, this.sessionId, "global", this.root, this.sessionKey).meta;
347
+ const cwdMeta = temporalScopePaths(this.cwd, this.sessionId, "cwd", this.root, this.sessionKey).meta;
348
+ candidate.provenanceByScope = {
349
+ global: parseScopeProvenance(files.get(globalMeta), globalMeta),
350
+ cwd: parseScopeProvenance(files.get(cwdMeta), cwdMeta),
351
+ session: copy ? structuredClone(copy.provenance) : streams.session === undefined ? {} : parseArtifactProvenanceRegistry(existingRuntime?.meta.artifacts, "State Flow session artifact provenance"),
352
+ };
353
+ if (expectedShared && ["global", "cwd"].some((scope) => !sameJson(candidate.read(0, scope), expectedShared[scope]))) {
354
+ throw new Error("Legacy branch shared scopes diverged from the selected revision; migration cannot overwrite them");
355
+ }
356
+ const publication = copy ? candidate.publishForkOrigin(snapshot) : candidate.publish(snapshot, true);
357
+ this.view = candidate.view;
358
+ this.base = candidate.base;
359
+ this.backend = backend;
360
+ this.provenanceByScope = structuredClone(candidate.provenanceByScope);
361
+ this.semanticRevision = candidate.semanticRevision;
362
+ this.savedRuntime = candidate.savedRuntime;
363
+ this.absentSharedScopes.clear();
364
+ return publication;
365
+ }
366
+ /** Initial copy owns only the new session files, without pruning or rewriting shared provenance. */
367
+ publishForkOrigin(snapshot) {
368
+ const runtime = createSessionRuntime(snapshot, this.cwd, this.sessionId, this.view.lineage, this.backend === "files" ? "files" : "unconfirmed", this.provenanceByScope.session);
369
+ const result = this.backend === "files"
370
+ ? publishTemporalStateToFiles(this.cwd, this.sessionId, this.view, ["session"], this.base, this.root, runtime, this.sessionKey, this.provenanceByScope)
371
+ : publishTemporalStateToGit(this.cwd, this.sessionId, this.view, ["session"], this.base, this.root, runtime, this.sessionKey, (snapshot.meta.remotePublication?.mode ?? "transition") === "transition", this.provenanceByScope);
372
+ const publication = result;
373
+ this.base = publication.base;
374
+ this.semanticRevision = publication.revision ?? publication.commit;
375
+ this.savedRuntime = hashJson(runtime);
376
+ return publication;
377
+ }
378
+ /**
379
+ * Reconcile untouched shared-scope drift against the current proven live basis.
380
+ *
381
+ * A restored branch can lag behind live global/CWD state. Untouched shared scopes adopt
382
+ * the current live streams at a fresh origin; a shared scope the accepted transition
383
+ * actually changes remains a fail-closed write conflict. Divergence in non-adoptable
384
+ * session or runtime files also fails closed under the existing race rule.
385
+ */
386
+ reconcileSharedDrift(changedScopes) {
387
+ const captured = this.backend === "files"
388
+ ? captureTemporalFileBase(this.cwd, this.sessionId, this.root, this.sessionKey)
389
+ : captureTemporalGitBase(this.cwd, this.sessionId, this.root, this.sessionKey);
390
+ const liveFiles = new Map(captured.files.map((file) => [file.path, file]));
391
+ const adoptable = new Set();
392
+ for (const scope of SHARED_SCOPES) {
393
+ const paths = temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey);
394
+ adoptable.add(paths.checkpoint);
395
+ adoptable.add(paths.patches);
396
+ adoptable.add(paths.meta);
397
+ }
398
+ for (const file of this.base.files) {
399
+ if (adoptable.has(file.path))
400
+ continue;
401
+ const current = liveFiles.get(file.path);
402
+ if (current === undefined || current.identity !== file.identity) {
403
+ throw new Error("Temporal State Flow base or scope identity changed concurrently");
404
+ }
405
+ }
406
+ const provenance = structuredClone(this.provenanceByScope);
407
+ const adopted = new Map();
408
+ const targets = [];
409
+ const removedTargets = [];
410
+ const absentScopes = [];
411
+ const head = "head" in captured ? captured.head : undefined;
412
+ const reconciliation = `${head ?? "files"}:reconcile:${randomUUID()}`;
413
+ for (const scope of SHARED_SCOPES) {
414
+ const paths = temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey);
415
+ const presence = classifyScopeStream(liveFiles.get(paths.checkpoint)?.content, liveFiles.get(paths.patches)?.content, scope, scope === "cwd" ? this.cwd : undefined, liveFiles.get(paths.meta)?.content);
416
+ if (presence.kind === "absent") {
417
+ absentScopes.push(scope);
418
+ provenance[scope] = {};
419
+ if (changedScopes.has(scope) && !this.absentSharedScopes.has(scope))
420
+ removedTargets.push(scope);
421
+ if (!this.absentSharedScopes.has(scope)) {
422
+ adopted.set(scope, freshEmptyScopeStream(scope, `${reconciliation}:${scope}:absent`));
423
+ }
424
+ continue;
425
+ }
426
+ const stream = presence.stream;
427
+ const liveProvenance = parseScopeProvenance(liveFiles.get(paths.meta)?.content, paths.meta);
428
+ const streamDrifted = !sameJson(stream, this.view.scopes[scope]);
429
+ const provenanceDrifted = !sameJson(liveProvenance, this.provenanceByScope[scope]);
430
+ if (!streamDrifted && !provenanceDrifted)
431
+ continue;
432
+ if (changedScopes.has(scope)) {
433
+ targets.push(scope);
434
+ continue;
435
+ }
436
+ provenance[scope] = liveProvenance;
437
+ if (streamDrifted)
438
+ adopted.set(scope, stream);
439
+ }
440
+ const reconciledView = () => adopted.size === 0 ? this.view : adoptTemporalStreams({
441
+ global: adopted.get("global") ?? structuredClone(this.view.scopes.global),
442
+ cwd: adopted.get("cwd") ?? structuredClone(this.view.scopes.cwd),
443
+ session: structuredClone(this.view.scopes.session),
444
+ }, reconciliation);
445
+ if (removedTargets.length > 0) {
446
+ this.view = reconciledView();
447
+ this.base = captured;
448
+ this.provenanceByScope = provenance;
449
+ for (const scope of absentScopes)
450
+ this.absentSharedScopes.add(scope);
451
+ throw removedTargetScopeConflict(removedTargets);
452
+ }
453
+ if (targets.length > 0)
454
+ throw targetScopeConflict(targets);
455
+ return { view: reconciledView(), base: captured, provenance };
456
+ }
457
+ publish(snapshot, semantic = false, accepted, options = {}) {
458
+ if (!this.view || !this.base)
459
+ throw new Error("State Flow temporal publication is unavailable; restore or initialize before accepting a transition");
460
+ // Activation and terminal policy: only the legacy transition mode publishes synchronously.
461
+ const pushRemote = options.pushRemote ?? (snapshot.meta.remotePublication?.mode ?? "transition") === "transition";
462
+ const provenanceScopes = SCOPES.filter((scope) => Object.entries(options.provenance?.[scope] ?? {})
463
+ .some(([path, entry]) => this.provenanceByScope[scope][path] === undefined
464
+ || !sameJson(this.provenanceByScope[scope][path], entry)));
465
+ let basis = this.view;
466
+ let base = this.base;
467
+ let basisProvenance = this.provenanceByScope;
468
+ if ((semantic || provenanceScopes.length > 0) && this.semanticRevision) {
469
+ const changedScopes = new Set([
470
+ ...(accepted?.transitions ?? []).map(({ scope }) => scope),
471
+ ...provenanceScopes,
472
+ ]);
473
+ const reconciled = this.reconcileSharedDrift(changedScopes);
474
+ basis = reconciled.view;
475
+ base = reconciled.base;
476
+ basisProvenance = reconciled.provenance;
477
+ }
478
+ const next = accepted ? advanceTemporalState(basis, accepted.transitions, accepted.id) : basis;
479
+ const nextProvenance = structuredClone(basisProvenance);
480
+ for (const scope of SCOPES) {
481
+ const updates = options.provenance?.[scope];
482
+ if (updates)
483
+ for (const [path, entry] of Object.entries(updates))
484
+ nextProvenance[scope][path] = structuredClone(entry);
485
+ }
486
+ for (const scope of SCOPES) {
487
+ nextProvenance[scope] = pruneArtifactProvenance(nextProvenance[scope], readTemporalState(next, 0, scope).artifacts);
488
+ }
489
+ const provenanceChanged = !sameJson(nextProvenance, this.provenanceByScope);
490
+ const scopedWrite = semantic || provenanceChanged;
491
+ const runtime = createSessionRuntime(snapshot, this.cwd, this.sessionId, next.lineage, this.backend === "files" ? "files" : "unconfirmed", nextProvenance.session);
492
+ const fingerprint = hashJson(runtime);
493
+ if (!semantic && fingerprint === this.savedRuntime && !provenanceChanged)
494
+ return undefined;
495
+ if (this.backend === "files") {
496
+ runtime.meta.temporalRevision = "self";
497
+ const result = publishTemporalStateToFiles(this.cwd, this.sessionId, next, semantic ? SCOPES : [], base, this.root, runtime, this.sessionKey, nextProvenance);
498
+ this.base = result.base;
499
+ this.view = next;
500
+ this.provenanceByScope = nextProvenance;
501
+ this.savedRuntime = fingerprint;
502
+ this.semanticRevision = result.revision;
503
+ if (semantic)
504
+ this.absentSharedScopes.clear();
505
+ return { base: result.base, revision: result.revision };
506
+ }
507
+ if (!scopedWrite) {
508
+ const current = captureTemporalGitBase(this.cwd, this.sessionId, this.root, this.sessionKey);
509
+ const paths = sessionRuntimePaths(this.cwd, this.sessionId, this.root, this.sessionKey);
510
+ for (const path of [paths.config, paths.meta]) {
511
+ if (current.files.find((file) => file.path === path)?.identity !== base.files.find((file) => file.path === path)?.identity) {
512
+ throw new Error("Temporal State Flow runtime changed concurrently");
513
+ }
514
+ }
515
+ base = current;
516
+ }
517
+ runtime.meta.temporalRevision = scopedWrite ? "self" : this.semanticRevision;
518
+ const result = publishTemporalStateToGit(this.cwd, this.sessionId, next, semantic ? SCOPES : provenanceChanged ? provenanceScopes : [], base, this.root, runtime, this.sessionKey, pushRemote, nextProvenance);
519
+ this.base = result.base;
520
+ this.view = next;
521
+ this.provenanceByScope = nextProvenance;
522
+ this.savedRuntime = fingerprint;
523
+ if (scopedWrite && result.commit)
524
+ this.semanticRevision = result.commit;
525
+ if (semantic)
526
+ this.absentSharedScopes.clear();
527
+ return result;
528
+ }
529
+ }