@llblab/pi-kit 0.18.1 → 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.
Files changed (139) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +4 -2
  3. package/node_modules/@llblab/pi-state-flow/AGENTS.md +35 -37
  4. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +5 -3
  5. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +19 -1
  6. package/node_modules/@llblab/pi-state-flow/README.md +102 -48
  7. package/node_modules/@llblab/pi-state-flow/dist/index.d.ts +6 -9
  8. package/node_modules/@llblab/pi-state-flow/dist/index.js +6 -9
  9. package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.d.ts +2 -2
  10. package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.js +21 -8
  11. package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.d.ts +40 -24
  12. package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.js +93 -63
  13. package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.d.ts +4 -3
  14. package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.js +9 -9
  15. package/node_modules/@llblab/pi-state-flow/dist/lib/config.d.ts +1 -1
  16. package/node_modules/@llblab/pi-state-flow/dist/lib/config.js +6 -5
  17. package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +8 -7
  18. package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +56 -58
  19. package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +12 -24
  20. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.d.ts +7 -18
  21. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +47 -78
  22. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.d.ts +4 -6
  23. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +241 -437
  24. package/node_modules/@llblab/pi-state-flow/dist/lib/git.d.ts +2 -72
  25. package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +120 -499
  26. package/node_modules/@llblab/pi-state-flow/dist/lib/history.d.ts +2 -1
  27. package/node_modules/@llblab/pi-state-flow/dist/lib/history.js +4 -3
  28. package/node_modules/@llblab/pi-state-flow/dist/lib/json.d.ts +3 -0
  29. package/node_modules/@llblab/pi-state-flow/dist/lib/json.js +43 -22
  30. package/node_modules/@llblab/pi-state-flow/dist/lib/logging.d.ts +1 -5
  31. package/node_modules/@llblab/pi-state-flow/dist/lib/logging.js +0 -2
  32. package/node_modules/@llblab/pi-state-flow/dist/lib/memory.d.ts +1 -14
  33. package/node_modules/@llblab/pi-state-flow/dist/lib/memory.js +5 -37
  34. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.d.ts +1 -2
  35. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +13 -32
  36. package/node_modules/@llblab/pi-state-flow/dist/lib/query.d.ts +6 -6
  37. package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +25 -21
  38. package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.d.ts +3 -3
  39. package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.js +15 -12
  40. package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.d.ts +2 -4
  41. package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.js +5 -1
  42. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +26 -88
  43. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +159 -255
  44. package/node_modules/@llblab/pi-state-flow/dist/lib/skills.d.ts +0 -3
  45. package/node_modules/@llblab/pi-state-flow/dist/lib/skills.js +1 -47
  46. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +16 -33
  47. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +48 -137
  48. package/node_modules/@llblab/pi-state-flow/dist/lib/state.d.ts +16 -11
  49. package/node_modules/@llblab/pi-state-flow/dist/lib/state.js +13 -14
  50. package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +1 -9
  51. package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +15 -43
  52. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.d.ts +5 -9
  53. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.js +13 -45
  54. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +1 -1
  55. package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.d.ts +15 -7
  56. package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.js +104 -24
  57. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.d.ts +0 -2
  58. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +11 -14
  59. package/node_modules/@llblab/pi-state-flow/dist/package.json +9 -6
  60. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +11 -17
  61. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +8 -8
  62. package/node_modules/@llblab/pi-state-flow/docs/README.md +2 -2
  63. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +64 -67
  64. package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +24 -6
  65. package/node_modules/@llblab/pi-state-flow/docs/filesystem-recovery.md +12 -15
  66. package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +22 -27
  67. package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +25 -41
  68. package/node_modules/@llblab/pi-state-flow/docs/performance.md +38 -421
  69. package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +33 -46
  70. package/node_modules/@llblab/pi-state-flow/docs/usage.md +37 -61
  71. package/node_modules/@llblab/pi-state-flow/index.ts +7 -71
  72. package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +26 -11
  73. package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +116 -88
  74. package/node_modules/@llblab/pi-state-flow/lib/compaction.ts +12 -10
  75. package/node_modules/@llblab/pi-state-flow/lib/config.ts +6 -6
  76. package/node_modules/@llblab/pi-state-flow/lib/context.ts +52 -59
  77. package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +11 -20
  78. package/node_modules/@llblab/pi-state-flow/lib/durable.ts +44 -86
  79. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +237 -460
  80. package/node_modules/@llblab/pi-state-flow/lib/git.ts +110 -552
  81. package/node_modules/@llblab/pi-state-flow/lib/history.ts +4 -3
  82. package/node_modules/@llblab/pi-state-flow/lib/json.ts +39 -23
  83. package/node_modules/@llblab/pi-state-flow/lib/logging.ts +1 -7
  84. package/node_modules/@llblab/pi-state-flow/lib/memory.ts +5 -44
  85. package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +14 -25
  86. package/node_modules/@llblab/pi-state-flow/lib/query.ts +26 -22
  87. package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +17 -11
  88. package/node_modules/@llblab/pi-state-flow/lib/rehydration.ts +7 -5
  89. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +154 -254
  90. package/node_modules/@llblab/pi-state-flow/lib/skills.ts +1 -49
  91. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +58 -142
  92. package/node_modules/@llblab/pi-state-flow/lib/state.ts +27 -19
  93. package/node_modules/@llblab/pi-state-flow/lib/status.ts +16 -54
  94. package/node_modules/@llblab/pi-state-flow/lib/storage.ts +12 -40
  95. package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +1 -1
  96. package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +104 -22
  97. package/node_modules/@llblab/pi-state-flow/lib/transition.ts +16 -26
  98. package/node_modules/@llblab/pi-state-flow/package.json +9 -6
  99. package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +11 -17
  100. package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +8 -8
  101. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +7 -0
  102. package/node_modules/@llblab/pi-telegram/README.md +1 -2
  103. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +1 -3
  104. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +1 -6
  105. package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +1 -4
  106. package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +13 -33
  107. package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +1 -4
  108. package/node_modules/@llblab/pi-telegram/dist/lib/menu-queue.d.ts +1 -0
  109. package/node_modules/@llblab/pi-telegram/dist/lib/menu-queue.js +72 -1
  110. package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +7 -0
  111. package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +27 -0
  112. package/node_modules/@llblab/pi-telegram/dist/lib/replies.d.ts +3 -0
  113. package/node_modules/@llblab/pi-telegram/dist/lib/replies.js +17 -0
  114. package/node_modules/@llblab/pi-telegram/dist/lib/sync.js +8 -1
  115. package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
  116. package/node_modules/@llblab/pi-telegram/docs/architecture.md +2 -0
  117. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +1 -1
  118. package/node_modules/@llblab/pi-telegram/docs/public-api.md +1 -1
  119. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +0 -10
  120. package/node_modules/@llblab/pi-telegram/lib/commands.ts +18 -41
  121. package/node_modules/@llblab/pi-telegram/lib/extension.ts +1 -8
  122. package/node_modules/@llblab/pi-telegram/lib/menu-queue.ts +126 -1
  123. package/node_modules/@llblab/pi-telegram/lib/queue.ts +46 -0
  124. package/node_modules/@llblab/pi-telegram/lib/replies.ts +19 -0
  125. package/node_modules/@llblab/pi-telegram/lib/sync.ts +8 -1
  126. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  127. package/package.json +3 -3
  128. package/node_modules/@llblab/pi-state-flow/dist/lib/discovery.d.ts +0 -21
  129. package/node_modules/@llblab/pi-state-flow/dist/lib/discovery.js +0 -125
  130. package/node_modules/@llblab/pi-state-flow/dist/lib/maintenance.d.ts +0 -36
  131. package/node_modules/@llblab/pi-state-flow/dist/lib/maintenance.js +0 -98
  132. package/node_modules/@llblab/pi-state-flow/dist/lib/migration.d.ts +0 -13
  133. package/node_modules/@llblab/pi-state-flow/dist/lib/migration.js +0 -167
  134. package/node_modules/@llblab/pi-state-flow/dist/lib/publication.d.ts +0 -86
  135. package/node_modules/@llblab/pi-state-flow/dist/lib/publication.js +0 -437
  136. package/node_modules/@llblab/pi-state-flow/lib/discovery.ts +0 -133
  137. package/node_modules/@llblab/pi-state-flow/lib/maintenance.ts +0 -147
  138. package/node_modules/@llblab/pi-state-flow/lib/migration.ts +0 -171
  139. 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 { adoptFileStateToGit, initializeGitRepository, isLocalGitRepository, captureTemporalGitBase, loadTemporalRevision, migrateLegacyStorageToGit, publishTemporalStateToGit, type TemporalGitBase } from "./git.ts";
7
- import { captureTemporalFileBase, detectGitCapability, initializeFileStore, loadTemporalFileRevision, migrateLegacyStorageToFiles, publishTemporalStateToFiles, type TemporalFileBase } from "./storage.ts";
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 { hasCwdMaterialization, hasLegacyStateSources } from "./migration.ts";
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, validateTemporalState, type ScopeStream, type TemporalState } from "./temporal.ts";
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 publishTemporalStateToGit> & { revision?: string };
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: TemporalGitBase | undefined;
64
+ private base: TemporalFileBase | undefined;
94
65
  private semanticRevision: string | undefined;
95
66
  private savedRuntime: string | undefined;
96
- private backend: "git" | "files" | undefined;
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
- constructor(cwd: string, session: string | SessionAddress, root: string, sessionKey?: string) {
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 freshness evidence for one scope; never model-visible state. */
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 backend = detectGitCapability() === "git" && lstatSync(join(this.root, ".git"), { throwIfNoEntry: false }) ? "git" : "files";
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:${basis}:${randomUUID()}`);
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
- /** Explicit start owns directory/repository creation; reads never call this. */
147
- prepare(): void {
148
- const backend = detectGitCapability();
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
- /** Explicit start migrates a predecessor envelope even when passive restore already populated the cache. */
155
- migrateLegacyStorage(): void {
156
- if (!this.view || !this.backend || !hasLegacyStateSources(this.cwd, this.sessionId, this.root, this.sessionKey)) return undefined;
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
- /** Validate selection without installing state; only a matching immutable Git inspection is reusable. */
232
- prepareRestore(revision: string, legacySnapshot?: Snapshot): { snapshot: Snapshot; restore: () => Snapshot } {
233
- const inspected = inspectSnapshotRevision(this.cwd, this.sessionId, this.root, revision, legacySnapshot, this.sessionKey);
234
- const selected = inspected.snapshot.meta.durableBase ?? revision;
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(inspected.snapshot),
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
- // File cohorts can expire; legacy migration and owner redirection keep their fresh-read path.
242
- return inspected.temporal && selected === revision
243
- ? this.restoreInspected(revision, inspected)
244
- : this.restore(selected, inspected.snapshot);
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
- /** Copy only a proven source session stream; shared streams come from the child's fresh live basis. */
250
- prepareFork(source: SessionAddress, revision: string): { snapshot: Snapshot; fork: () => { snapshot: Snapshot; publication: RuntimePublication } } {
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 inspect = () => inspectSnapshotRevision(this.cwd, parent.id, this.root, revision, undefined, parent.key);
254
- const inspected = inspect();
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(inspected.snapshot.config),
241
+ config: structuredClone(prepared.snapshot.config),
257
242
  meta: {
258
243
  step: 0,
259
- ...(inspected.snapshot.meta.bootstrap === undefined ? {} : { bootstrap: inspected.snapshot.meta.bootstrap }),
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: { ...structuredClone(snapshot), meta: { ...structuredClone(snapshot.meta), durableBase: revision } },
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: selected.view.scopes.session,
276
- provenance: selected.provenance.session,
277
- backend: current.file ? "files" : "git",
255
+ stream: sourceRuntime.view!.scopes.session,
256
+ provenance: sourceRuntime.artifactProvenance("session"),
278
257
  } });
279
- const ownedRevision = publication?.revision ?? publication?.commit;
280
- if (!publication || !ownedRevision) throw new Error("State Flow fork requires existing shared scope storage");
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
- const backend = copy?.backend ?? this.backend ?? (detectGitCapability() === "git" && lstatSync(join(this.root, ".git"), { throwIfNoEntry: false }) ? "git" : "files");
342
- if (!copy && backend === "git") {
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
- const historical = !occupied && backend === "git" && base.head ? loadTemporalRevision(this.cwd, this.sessionId, this.root, base.head, this.sessionKey) : undefined;
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 files.get(join(paths.directory, "state.json")) !== undefined;
367
- })) throw new Error("Legacy State Flow storage changed during initialization; retry migration from a fresh basis");
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, files.get(paths.meta));
377
- if (existingRuntime && !newSessionOrigin) throw new Error("Existing session runtime requires a branch revision pointer");
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 }, `${base.head ?? "unborn"}:${randomUUID()}`);
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("Legacy branch shared scopes diverged from the selected revision; migration cannot overwrite them");
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
- /** Initial copy owns only the new session files, without pruning or rewriting shared provenance. */
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.backend === "files" ? "files" : "unconfirmed", this.provenanceByScope.session);
410
- const result = this.backend === "files"
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 ?? publication.commit;
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: TemporalGitBase | TemporalFileBase;
349
+ base: TemporalFileBase;
431
350
  provenance: Record<StateScope, ArtifactProvenanceRegistry>;
432
351
  } {
433
- const captured = this.backend === "files"
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 head = "head" in captured ? captured.head : undefined;
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: { pushRemote?: boolean; provenance?: Partial<Record<StateScope, Record<string, ArtifactProvenance>>> } = {},
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
- // Activation and terminal policy: only the legacy transition mode publishes synchronously.
507
- const pushRemote = options.pushRemote ?? (snapshot.meta.remotePublication?.mode ?? "transition") === "transition";
508
- const provenanceScopes = SCOPES.filter((scope) => Object.entries(options.provenance?.[scope] ?? {})
509
- .some(([path, entry]) => this.provenanceByScope[scope][path] === undefined
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: TemporalGitBase | TemporalFileBase = this.base;
439
+ let base: TemporalFileBase = this.base;
513
440
  let basisProvenance = this.provenanceByScope;
514
- if ((semantic || provenanceScopes.length > 0) && this.semanticRevision) {
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 scopedWrite = semantic || provenanceChanged;
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
- if (this.backend === "files") {
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
- if (scopedWrite && result.commit) this.semanticRevision = result.commit;
469
+ this.semanticRevision = result.revision;
570
470
  if (semantic) this.absentSharedScopes.clear();
571
471
  return result;
572
472
  }