@llblab/pi-kit 0.22.2 → 0.23.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 (100) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +5 -5
  3. package/node_modules/@llblab/pi-actors/AGENTS.md +4 -1
  4. package/node_modules/@llblab/pi-actors/CHANGELOG.md +10 -0
  5. package/node_modules/@llblab/pi-actors/README.md +2 -2
  6. package/node_modules/@llblab/pi-actors/dist/index.js +4 -1
  7. package/node_modules/@llblab/pi-actors/dist/lib/inspector-overlay.js +2 -1
  8. package/node_modules/@llblab/pi-actors/dist/lib/paths.d.ts +6 -0
  9. package/node_modules/@llblab/pi-actors/dist/lib/paths.js +19 -1
  10. package/node_modules/@llblab/pi-actors/dist/lib/trace-projection.d.ts +2 -0
  11. package/node_modules/@llblab/pi-actors/dist/lib/trace-projection.js +11 -7
  12. package/node_modules/@llblab/pi-actors/dist/scripts/build-dist.mjs +94 -30
  13. package/node_modules/@llblab/pi-actors/docs/actor-inspector.md +1 -1
  14. package/node_modules/@llblab/pi-actors/index.ts +6 -3
  15. package/node_modules/@llblab/pi-actors/lib/inspector-overlay.ts +2 -1
  16. package/node_modules/@llblab/pi-actors/lib/paths.ts +27 -1
  17. package/node_modules/@llblab/pi-actors/lib/trace-projection.ts +13 -6
  18. package/node_modules/@llblab/pi-actors/package.json +3 -8
  19. package/node_modules/@llblab/pi-actors/scripts/build-dist.mjs +94 -30
  20. package/node_modules/@llblab/pi-grow-loop/AGENTS.md +10 -6
  21. package/node_modules/@llblab/pi-grow-loop/CHANGELOG.md +7 -0
  22. package/node_modules/@llblab/pi-grow-loop/README.md +2 -0
  23. package/node_modules/@llblab/pi-grow-loop/dist/index.d.ts +33 -0
  24. package/node_modules/@llblab/pi-grow-loop/dist/index.js +286 -0
  25. package/node_modules/@llblab/pi-grow-loop/dist/pi-grow-loop/index.d.ts +1 -0
  26. package/node_modules/@llblab/pi-grow-loop/dist/pi-grow-loop/index.js +1 -0
  27. package/node_modules/@llblab/pi-grow-loop/dist/skills/grow-loop/SKILL.md +117 -0
  28. package/node_modules/@llblab/pi-grow-loop/dist/skills/while-true/SKILL.md +233 -0
  29. package/node_modules/@llblab/pi-grow-loop/index.ts +67 -12
  30. package/node_modules/@llblab/pi-grow-loop/package.json +9 -8
  31. package/node_modules/@llblab/pi-state-flow/AGENTS.md +23 -17
  32. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +5 -3
  33. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +11 -0
  34. package/node_modules/@llblab/pi-state-flow/README.md +18 -6
  35. package/node_modules/@llblab/pi-state-flow/dist/index.d.ts +2 -1
  36. package/node_modules/@llblab/pi-state-flow/dist/index.js +1 -0
  37. package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.d.ts +2 -2
  38. package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.js +5 -5
  39. package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +3 -2
  40. package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +7 -2
  41. package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.d.ts +5 -5
  42. package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +54 -40
  43. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +20 -20
  44. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +755 -325
  45. package/node_modules/@llblab/pi-state-flow/dist/lib/git.d.ts +2 -2
  46. package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +14 -17
  47. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.d.ts +4 -0
  48. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +65 -23
  49. package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.d.ts +16 -5
  50. package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.js +32 -15
  51. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +28 -2
  52. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +276 -26
  53. package/node_modules/@llblab/pi-state-flow/dist/lib/session.d.ts +5 -0
  54. package/node_modules/@llblab/pi-state-flow/dist/lib/session.js +36 -2
  55. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +3 -0
  56. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +3 -0
  57. package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +1 -0
  58. package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +3 -1
  59. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.d.ts +11 -0
  60. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.js +150 -24
  61. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +14 -1
  62. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +52 -18
  63. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +8 -8
  64. package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
  65. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +3 -1
  66. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +53 -7
  67. package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +84 -44
  68. package/node_modules/@llblab/pi-state-flow/docs/filesystem-recovery.md +12 -2
  69. package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +6 -4
  70. package/node_modules/@llblab/pi-state-flow/docs/performance.md +2 -2
  71. package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +28 -8
  72. package/node_modules/@llblab/pi-state-flow/docs/usage.md +41 -14
  73. package/node_modules/@llblab/pi-state-flow/index.ts +3 -0
  74. package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +5 -5
  75. package/node_modules/@llblab/pi-state-flow/lib/context.ts +8 -3
  76. package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +57 -40
  77. package/node_modules/@llblab/pi-state-flow/lib/durable.ts +20 -20
  78. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +719 -316
  79. package/node_modules/@llblab/pi-state-flow/lib/git.ts +16 -18
  80. package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +60 -24
  81. package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +34 -21
  82. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +290 -25
  83. package/node_modules/@llblab/pi-state-flow/lib/session.ts +37 -2
  84. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +3 -0
  85. package/node_modules/@llblab/pi-state-flow/lib/status.ts +4 -1
  86. package/node_modules/@llblab/pi-state-flow/lib/storage.ts +141 -22
  87. package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +60 -19
  88. package/node_modules/@llblab/pi-state-flow/lib/transition.ts +8 -8
  89. package/node_modules/@llblab/pi-state-flow/package.json +1 -1
  90. package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +3 -1
  91. package/node_modules/@llblab/pi-telegram/AGENTS.md +1 -1
  92. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +5 -0
  93. package/node_modules/@llblab/pi-telegram/README.md +2 -2
  94. package/node_modules/@llblab/pi-telegram/dist/lib/skills.d.ts +7 -1
  95. package/node_modules/@llblab/pi-telegram/dist/lib/skills.js +32 -7
  96. package/node_modules/@llblab/pi-telegram/dist/package.json +3 -8
  97. package/node_modules/@llblab/pi-telegram/lib/skills.ts +43 -7
  98. package/node_modules/@llblab/pi-telegram/package.json +3 -8
  99. package/node_modules/@llblab/pi-telegram/scripts/build-dist.mjs +103 -32
  100. package/package.json +7 -7
@@ -3,17 +3,30 @@ import { lstatSync } from "node:fs";
3
3
  import { join } from "node:path";
4
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 { captureTemporalFileBase, initializeFileStore, publishTemporalStateToFiles, type TemporalFileBase } from "./storage.ts";
6
+ import { assertTemporalFileBase, captureTemporalFileBase, initializeFileStore, publishTemporalStateToFiles, withStorageTransaction, type StorageTransaction, type TemporalFileBase } from "./storage.ts";
7
7
  import { DEFAULT_HISTORY_LIMIT, MAX_HISTORY_LIMIT, type AcceptedTransition, type RecentTransitionWindow } from "./history.ts";
8
8
  import { hashJson, sameJson } from "./json.ts";
9
- import { RevisionUnavailableError, createSessionRuntime, parseSessionRuntime, retainedBoundaryCheckpoint, type RetainedBoundaryCheckpoint, type RetainedPiCheckpoint, type Snapshot } from "./snapshot.ts";
9
+ import { HistoryBoundaryExpiredError, RevisionUnavailableError, createSessionRuntime, parseSessionRuntime, retainedBoundaryCheckpoint, type RetainedBoundaryCheckpoint, type RetainedPiCheckpoint, type Snapshot } from "./snapshot.ts";
10
10
  import { emptyState, type MaterializedState, type ScopedStates, type StateScope } from "./state.ts";
11
- import { adoptTemporalStreams, advanceTemporalState, createTemporalState, readTemporalState, selectScopeStreamAtBoundary, validateScopeLineage, type ScopeStream, type TemporalState } from "./temporal.ts";
11
+ import { adoptTemporalStreams, advanceTemporalState, constrainTemporalState, createTemporalState, readTemporalState, selectScopeStreamAtBoundary, validateScopeLineage, type ScopeStream, type TemporalState } from "./temporal.ts";
12
12
 
13
13
  const SCOPES = ["global", "cwd", "session"] as const;
14
14
  const SHARED_SCOPES = ["global", "cwd"] as const;
15
15
  export type RuntimePublication = ReturnType<typeof publishTemporalStateToFiles>;
16
16
 
17
+ export interface RuntimePatchTransaction {
18
+ readonly states: ScopedStates;
19
+ readonly causalBasis: string;
20
+ readonly provenance: Record<StateScope, ArtifactProvenanceRegistry>;
21
+ publish(snapshot: Snapshot, accepted?: AcceptedTransition, provenance?: Partial<Record<StateScope, Record<string, ArtifactProvenance>>>): RuntimePublication;
22
+ }
23
+
24
+ type PublicationSelection =
25
+ | { kind: "patch" | "lifecycle" }
26
+ | { kind: "start"; allowCreateOrigin: boolean }
27
+ | { kind: "restore"; checkpoint: RetainedBoundaryCheckpoint }
28
+ | { kind: "fork"; source: SessionAddress; checkpoint: RetainedBoundaryCheckpoint };
29
+
17
30
  interface SessionCopy {
18
31
  stream: ScopeStream;
19
32
  provenance: ArtifactProvenanceRegistry;
@@ -64,6 +77,7 @@ export class TemporalRuntime {
64
77
  private base: TemporalFileBase | undefined;
65
78
  private semanticRevision: string | undefined;
66
79
  private savedRuntime: string | undefined;
80
+ private transaction: StorageTransaction | undefined;
67
81
  private restoredOriginPending = false;
68
82
  private provenanceByScope: Record<StateScope, ArtifactProvenanceRegistry> = emptyProvenance();
69
83
  /** Shared scopes whose wholly absent live basis was accepted after one stale-target refusal. */
@@ -91,7 +105,10 @@ export class TemporalRuntime {
91
105
  /** Read canonical shared memory without creating, migrating, or publishing storage. */
92
106
  loadPassive(): boolean {
93
107
  if (!lstatSync(this.root, { throwIfNoEntry: false })) return false;
94
- const base = captureTemporalFileBase(this.cwd, this.sessionId, this.root, this.sessionKey);
108
+ return this.loadPassiveBase(captureTemporalFileBase(this.cwd, this.sessionId, this.root, this.sessionKey));
109
+ }
110
+
111
+ private loadPassiveBase(base: TemporalFileBase): boolean {
95
112
  const files = new Map(base.files.map((file) => [file.path, file.content]));
96
113
  const shared = Object.fromEntries(SHARED_SCOPES.map((scope) => {
97
114
  const paths = temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey);
@@ -102,13 +119,19 @@ export class TemporalRuntime {
102
119
  if (!shared.global) throw new Error("Incomplete passive State Flow shared storage: CWD state exists without global state");
103
120
  const fresh = createTemporalState({ global: emptyState(), cwd: emptyState(), session: emptyState() }, randomUUID(), this.historyLimit);
104
121
  // Global memory is valid before this CWD has ever materialized its own scope.
105
- this.view = adoptTemporalStreams({ global: shared.global, cwd: shared.cwd ?? fresh.scopes.cwd, session: fresh.scopes.session }, `passive:files:${randomUUID()}`, this.historyLimit);
106
- this.base = base;
107
- this.provenanceByScope = {
122
+ const view = adoptTemporalStreams({ global: shared.global, cwd: shared.cwd ?? fresh.scopes.cwd, session: fresh.scopes.session }, `passive:files:${randomUUID()}`, this.historyLimit);
123
+ const provenance = {
108
124
  global: parseScopeProvenance(files.get(temporalScopePaths(this.cwd, this.sessionId, "global", this.root, this.sessionKey).meta), "State Flow global metadata"),
109
125
  cwd: parseScopeProvenance(files.get(temporalScopePaths(this.cwd, this.sessionId, "cwd", this.root, this.sessionKey).meta), "State Flow CWD metadata"),
110
126
  session: {},
111
127
  };
128
+ this.view = view;
129
+ this.base = base;
130
+ this.provenanceByScope = provenance;
131
+ this.semanticRevision = undefined;
132
+ this.savedRuntime = undefined;
133
+ this.restoredOriginPending = false;
134
+ this.absentSharedScopes.clear();
112
135
  return true;
113
136
  }
114
137
 
@@ -162,7 +185,10 @@ export class TemporalRuntime {
162
185
  /** Prepare a retained session boundary from current canonical files; shared scopes remain live. */
163
186
  prepareBoundaryRestore(checkpoint: RetainedBoundaryCheckpoint): { snapshot: Snapshot; restore: () => Snapshot } {
164
187
  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);
188
+ return this.prepareBoundaryRestoreBase(checkpoint, captureTemporalFileBase(this.cwd, this.sessionId, this.root, this.sessionKey));
189
+ }
190
+
191
+ private prepareBoundaryRestoreBase(checkpoint: RetainedBoundaryCheckpoint, base: TemporalFileBase): { snapshot: Snapshot; restore: () => Snapshot } {
166
192
  const files = new Map(base.files.map((file) => [file.path, file.content]));
167
193
  const scopes = Object.fromEntries(SCOPES.map((scope) => {
168
194
  const paths = temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey);
@@ -177,7 +203,7 @@ export class TemporalRuntime {
177
203
  // The codecs validate persisted retention against the format maximum, not the new operator limit.
178
204
  validateScopeLineage(scopes.session, "session", document.meta.lineage, MAX_HISTORY_LIMIT);
179
205
  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");
206
+ if (!boundary) throw new HistoryBoundaryExpiredError("Selected State Flow history boundary is outside the retained temporal window");
181
207
  const selectedSession = selectScopeStreamAtBoundary(scopes.session, "session", boundary, MAX_HISTORY_LIMIT);
182
208
  const view = adoptTemporalStreams({
183
209
  global: scopes.global,
@@ -230,6 +256,58 @@ export class TemporalRuntime {
230
256
  return { snapshot, publication: this.acceptRestoredOrigin(snapshot) };
231
257
  }
232
258
 
259
+ /** Await a coherent read-only recovery view; this neither activates policy nor accepts publication authority. */
260
+ async refreshCurrentMemory(signal?: AbortSignal): Promise<Snapshot | undefined> {
261
+ signal?.throwIfAborted();
262
+ if (!lstatSync(this.root, { throwIfNoEntry: false })) return undefined;
263
+ return withStorageTransaction(this.root, (storage) => {
264
+ const base = storage.capture(this.cwd, this.sessionId, this.root, this.sessionKey);
265
+ signal?.throwIfAborted();
266
+ return this.loadCurrentMemoryBase(base);
267
+ }, signal);
268
+ }
269
+
270
+ private loadCurrentMemoryBase(base: TemporalFileBase): Snapshot | undefined {
271
+ const files = new Map(base.files.map((file) => [file.path, file.content]));
272
+ const paths = sessionRuntimePaths(this.cwd, this.sessionId, this.root, this.sessionKey);
273
+ const session = temporalScopePaths(this.cwd, this.sessionId, "session", this.root, this.sessionKey);
274
+ if ([paths.config, paths.runtime, session.checkpoint, session.patches, session.meta].every((path) => files.get(path) === undefined)) return undefined;
275
+ const document = parseSessionRuntime(files.get(paths.config), files.get(paths.runtime), this.cwd, this.sessionId);
276
+ if (!document) throw new RevisionUnavailableError("Current State Flow session runtime is unavailable");
277
+ const origin = `start:${randomUUID()}`;
278
+ const scopes = Object.fromEntries(SCOPES.map((scope) => {
279
+ const paths = temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey);
280
+ const stream = parseScopeStream(files.get(paths.checkpoint), files.get(paths.patches), scope,
281
+ scope === "cwd" ? this.cwd : undefined, files.get(paths.meta));
282
+ if (!stream && scope === "session") throw new RevisionUnavailableError("Current State Flow session memory is unavailable");
283
+ return [scope, stream ?? freshEmptyScopeStream(scope, origin, this.historyLimit)];
284
+ })) as Record<StateScope, ScopeStream>;
285
+ validateScopeLineage(scopes.session, "session", document.meta.lineage, MAX_HISTORY_LIMIT);
286
+ let view: TemporalState;
287
+ try {
288
+ view = constrainTemporalState({ scopes, lineage: document.meta.lineage }, this.historyLimit);
289
+ } catch {
290
+ // Independently validated live shared streams can belong to another writer's lineage.
291
+ view = adoptTemporalStreams(scopes, origin, this.historyLimit);
292
+ }
293
+ const provenance = Object.fromEntries(SCOPES.map((scope) => {
294
+ const meta = temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey).meta;
295
+ return [scope, parseScopeProvenance(files.get(meta), meta)];
296
+ })) as Record<StateScope, ArtifactProvenanceRegistry>;
297
+ const snapshot: Snapshot = {
298
+ config: { enabled: false },
299
+ meta: { step: document.meta.step, ...(document.meta.bootstrap === true ? { bootstrap: true } : {}) },
300
+ };
301
+ this.view = view;
302
+ this.base = base;
303
+ this.provenanceByScope = provenance;
304
+ this.semanticRevision = undefined;
305
+ this.savedRuntime = undefined;
306
+ this.restoredOriginPending = false;
307
+ this.absentSharedScopes.clear();
308
+ return snapshot;
309
+ }
310
+
233
311
  /** Copy one retained source-session boundary over the child's current shared scopes. */
234
312
  prepareBoundaryFork(source: SessionAddress, checkpoint: RetainedBoundaryCheckpoint): { snapshot: Snapshot; fork: () => { snapshot: Snapshot; publication: RuntimePublication } } {
235
313
  const parent = Object.freeze({ ...source });
@@ -299,7 +377,7 @@ export class TemporalRuntime {
299
377
  const paths = sessionRuntimePaths(this.cwd, this.sessionId, this.root, this.sessionKey);
300
378
  const existingRuntime = parseSessionRuntime(files.get(paths.config), files.get(paths.runtime), this.cwd, this.sessionId);
301
379
  if (existingRuntime && !newSessionOrigin) throw new Error("Existing session runtime requires retained-boundary restoration");
302
- // Explicit start before any branch runtime is a new origin, never inheritance of a later session layer.
380
+ // A pre-runtime branch may establish an empty origin, never import a later session layer.
303
381
  if (newSessionOrigin) streams.session = undefined;
304
382
  if (copy) streams.session = copy.stream;
305
383
  const fresh = createTemporalState({ global: emptyState(), cwd: emptyState(), session: emptyState() }, randomUUID(), this.historyLimit);
@@ -329,7 +407,7 @@ export class TemporalRuntime {
329
407
  /** Copy the private origin and apply configured retention folding, preserving live shared values/provenance. */
330
408
  private publishForkOrigin(snapshot: Snapshot): RuntimePublication {
331
409
  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);
410
+ const publication = (this.transaction?.publish ?? publishTemporalStateToFiles)(this.cwd, this.sessionId, this.view!, SCOPES, this.base!, this.root, runtime, this.sessionKey, this.provenanceByScope);
333
411
  this.base = publication.base;
334
412
  this.semanticRevision = publication.revision;
335
413
  this.savedRuntime = hashJson(runtime);
@@ -344,12 +422,12 @@ export class TemporalRuntime {
344
422
  * actually changes remains a fail-closed write conflict. Divergence in non-adoptable
345
423
  * session or runtime files also fails closed under the existing race rule.
346
424
  */
347
- private reconcileSharedDrift(changedScopes: ReadonlySet<StateScope>): {
425
+ private reconcileSharedDrift(changedScopes: ReadonlySet<StateScope>, current?: TemporalFileBase): {
348
426
  view: TemporalState;
349
427
  base: TemporalFileBase;
350
428
  provenance: Record<StateScope, ArtifactProvenanceRegistry>;
351
429
  } {
352
- const captured = captureTemporalFileBase(this.cwd, this.sessionId, this.root, this.sessionKey);
430
+ const captured = current ?? captureTemporalFileBase(this.cwd, this.sessionId, this.root, this.sessionKey);
353
431
  const liveFiles = new Map(captured.files.map((file) => [file.path, file]));
354
432
  const priorFiles = new Map(this.base!.files.map((file) => [file.path, file]));
355
433
  const adoptable = new Set<string>();
@@ -415,15 +493,202 @@ export class TemporalRuntime {
415
493
  return { view: reconciledView(), base: captured, provenance };
416
494
  }
417
495
 
418
- /** Refresh live shared scopes in memory without publishing or advancing private semantic history. */
419
- refreshShared(): boolean {
420
- if (!this.view || !this.base) throw new Error("State Flow shared refresh requires a selected temporal runtime");
421
- const before = this.view;
422
- const reconciled = this.reconcileSharedDrift(new Set());
423
- this.view = reconciled.view;
424
- this.base = reconciled.base;
425
- this.provenanceByScope = reconciled.provenance;
426
- return before !== this.view;
496
+ /** Await coherent shared inspection, lazily loading an empty private view only when none is selected. */
497
+ async refreshShared(signal?: AbortSignal): Promise<boolean> {
498
+ signal?.throwIfAborted();
499
+ if (!this.view && !lstatSync(this.root, { throwIfNoEntry: false })) return false;
500
+ return withStorageTransaction(this.root, (storage) => {
501
+ const current = storage.capture(this.cwd, this.sessionId, this.root, this.sessionKey);
502
+ if (!this.view) return this.loadPassiveBase(current);
503
+ if (!this.base) throw new Error("State Flow shared refresh requires a selected temporal runtime");
504
+ const before = this.view;
505
+ const reconciled = this.reconcileSharedDrift(new Set(), current);
506
+ this.view = reconciled.view;
507
+ this.base = reconciled.base;
508
+ this.provenanceByScope = reconciled.provenance;
509
+ return before !== this.view;
510
+ }, signal);
511
+ }
512
+
513
+ /** Prepare current shared state while retaining the exact accepted private publication basis. */
514
+ private publicationCandidate(storage: StorageTransaction, current?: TemporalFileBase): TemporalRuntime {
515
+ if (this.restoredOriginPending) throw new RevisionUnavailableError("State Flow restored origin must be accepted before patching memory");
516
+ current ??= storage.capture(this.cwd, this.sessionId, this.root, this.sessionKey);
517
+ const candidate = new TemporalRuntime(this.cwd, this.session, this.root, undefined, this.historyLimit);
518
+ candidate.transaction = storage;
519
+ if (this.semanticRevision && this.view && this.base) {
520
+ candidate.view = this.view;
521
+ candidate.base = this.base;
522
+ candidate.provenanceByScope = this.provenanceByScope;
523
+ const reconciled = candidate.reconcileSharedDrift(new Set(), current);
524
+ candidate.view = reconciled.view;
525
+ candidate.base = reconciled.base;
526
+ candidate.provenanceByScope = reconciled.provenance;
527
+ return candidate;
528
+ }
529
+ const files = new Map(current.files.map((file) => [file.path, file.content]));
530
+ const paths = sessionRuntimePaths(this.cwd, this.sessionId, this.root, this.sessionKey);
531
+ const session = temporalScopePaths(this.cwd, this.sessionId, "session", this.root, this.sessionKey);
532
+ if ([paths.config, paths.runtime, session.checkpoint, session.patches, session.meta].some((path) => files.get(path) !== undefined)) {
533
+ const document = parseSessionRuntime(files.get(paths.config), files.get(paths.runtime), this.cwd, this.sessionId);
534
+ const stream = parseScopeStream(files.get(session.checkpoint), files.get(session.patches), "session", undefined, files.get(session.meta));
535
+ if (!document || !stream) throw new RevisionUnavailableError("Current State Flow session memory is incomplete");
536
+ validateScopeLineage(stream, "session", document.meta.lineage, MAX_HISTORY_LIMIT);
537
+ throw new RevisionUnavailableError("Existing State Flow session memory is not selected; select an accepted boundary or use /state-flow-start");
538
+ }
539
+ const origin = `patch:${randomUUID()}`;
540
+ const streams = Object.fromEntries(SCOPES.map((scope) => {
541
+ const paths = temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey);
542
+ return [scope, parseScopeStream(files.get(paths.checkpoint), files.get(paths.patches), scope,
543
+ scope === "cwd" ? this.cwd : undefined, files.get(paths.meta)) ?? freshEmptyScopeStream(scope, `${origin}:empty`, this.historyLimit)];
544
+ })) as Record<StateScope, ScopeStream>;
545
+ candidate.view = adoptTemporalStreams(streams, origin, this.historyLimit);
546
+ candidate.base = current;
547
+ candidate.provenanceByScope = Object.fromEntries(SCOPES.map((scope) => {
548
+ const paths = temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey);
549
+ // Absent semantic pairs cannot confer compilation evidence on a new registration.
550
+ return [scope, files.get(paths.checkpoint) === undefined ? {} : parseScopeProvenance(files.get(paths.meta), paths.meta)];
551
+ })) as Record<StateScope, ArtifactProvenanceRegistry>;
552
+ return candidate;
553
+ }
554
+
555
+ /** Stage and accept synchronously inside an awaited lock; expose neither selection nor raw storage operations. */
556
+ async withPatchTransaction<T>(action: (transaction: RuntimePatchTransaction) => T, signal?: AbortSignal): Promise<T> {
557
+ return this.withPublicationTransaction((candidate, publish) => action({
558
+ states: candidate.states(),
559
+ causalBasis: candidate.causalBasis(),
560
+ provenance: structuredClone(candidate.provenanceByScope),
561
+ publish,
562
+ }), { kind: "patch" }, signal);
563
+ }
564
+
565
+ /** Activate current owned memory; the caller must authorize a wholly absent private origin after waiting. */
566
+ async withStartTransaction<T>(action: (current: Snapshot | undefined, publish: (snapshot: Snapshot) => RuntimePublication) => T, signal?: AbortSignal, allowCreateOrigin = false): Promise<T> {
567
+ return this.withPublicationTransaction((_candidate, publish, current) => action(current, (snapshot) => publish(snapshot)), { kind: "start", allowCreateOrigin }, signal);
568
+ }
569
+
570
+ /** Select one retained private boundary beside current shared streams, then accept only after caller policy is rechecked. */
571
+ async withRestoreTransaction<T>(checkpoint: RetainedBoundaryCheckpoint, action: (selected: Snapshot, publish: (snapshot: Snapshot) => RuntimePublication) => T, signal?: AbortSignal): Promise<T> {
572
+ signal?.throwIfAborted();
573
+ return this.withPublicationTransaction((_candidate, publish, selected) => action(selected!, (snapshot) => publish(snapshot)),
574
+ { kind: "restore", checkpoint: structuredClone(checkpoint) }, signal);
575
+ }
576
+
577
+ /** Copy exact retained parent authority into an unoccupied child; the caller rechecks native selection after waiting. */
578
+ async withForkTransaction<T>(source: SessionAddress, checkpoint: RetainedBoundaryCheckpoint, action: (selected: Snapshot, publish: (snapshot: Snapshot) => RuntimePublication) => T, signal?: AbortSignal): Promise<T> {
579
+ signal?.throwIfAborted();
580
+ return this.withPublicationTransaction((_candidate, publish, selected) => action(selected!, (snapshot) => publish(snapshot)),
581
+ { kind: "fork", source: { ...source }, checkpoint: structuredClone(checkpoint) }, signal);
582
+ }
583
+
584
+ private prepareForkCandidate(storage: StorageTransaction, source: SessionAddress, checkpoint: RetainedBoundaryCheckpoint): {
585
+ candidate: TemporalRuntime; current: Snapshot; assertSource: () => void;
586
+ } {
587
+ const sourceBase = storage.capture(this.cwd, source.id, this.root, source.key);
588
+ const parent = new TemporalRuntime(this.cwd, source, this.root, undefined, this.historyLimit);
589
+ const selected = parent.prepareBoundaryRestoreBase(checkpoint, sourceBase).restore();
590
+ const base = storage.capture(this.cwd, this.sessionId, this.root, this.sessionKey);
591
+ const files = new Map(base.files.map((file) => [file.path, file.content]));
592
+ const paths = temporalScopePaths(this.cwd, this.sessionId, "session", this.root, this.sessionKey);
593
+ if ([paths.checkpoint, paths.patches, paths.meta, join(paths.directory, "config.json"), join(paths.directory, "runtime.json")].some((path) => files.get(path) !== undefined)) {
594
+ throw new Error("State Flow fork target already has session storage");
595
+ }
596
+ if (SCOPES.some((scope) => lstatSync(join(temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey).directory, "state.json"), { throwIfNoEntry: false }))) {
597
+ throw new Error("Unsupported State Flow storage exists; preserve or convert it before initialization");
598
+ }
599
+ const candidate = new TemporalRuntime(this.cwd, this.session, this.root, undefined, this.historyLimit);
600
+ candidate.transaction = storage;
601
+ candidate.base = base;
602
+ candidate.view = adoptTemporalStreams(parent.view!.scopes, `files:${randomUUID()}`, this.historyLimit);
603
+ candidate.provenanceByScope = { ...parent.provenanceByScope };
604
+ for (const scope of SHARED_SCOPES) {
605
+ const meta = temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey).meta;
606
+ candidate.provenanceByScope[scope] = parseScopeProvenance(files.get(meta), meta);
607
+ }
608
+ return {
609
+ candidate,
610
+ current: { config: selected.config, meta: { step: 0, ...(selected.meta.bootstrap === undefined ? {} : { bootstrap: selected.meta.bootstrap }) } },
611
+ assertSource: () => assertTemporalFileBase(sourceBase, storage.capture(this.cwd, source.id, this.root, source.key)),
612
+ };
613
+ }
614
+
615
+ /** Recheck caller policy after waiting, then accept only config/runtime over an already accepted private basis. */
616
+ async withLifecycleTransaction<T>(action: (publish: (snapshot: Snapshot) => RuntimePublication) => T, signal?: AbortSignal): Promise<T> {
617
+ return this.withPublicationTransaction((_candidate, publish) => action((snapshot) => publish(snapshot)), { kind: "lifecycle" }, signal);
618
+ }
619
+
620
+ private async withPublicationTransaction<T>(
621
+ action: (candidate: TemporalRuntime, publish: RuntimePatchTransaction["publish"], current?: Snapshot) => T,
622
+ selection: PublicationSelection, signal?: AbortSignal,
623
+ ): Promise<T> {
624
+ const { kind } = selection;
625
+ const allowCreateOrigin = selection.kind === "start" && selection.allowCreateOrigin;
626
+ const semantic = kind !== "lifecycle";
627
+ const assertAuthority = (): void => {
628
+ if (selection.kind === "fork") {
629
+ if (selection.source.id === this.sessionId || selection.source.key === this.sessionKey) throw new Error("State Flow fork requires a distinct session identity and key");
630
+ if (this.view) throw new Error("State Flow fork target already has session storage");
631
+ }
632
+ if (!semantic && (!this.semanticRevision || !this.view || !this.base || this.restoredOriginPending)) {
633
+ throw new Error("State Flow lifecycle transaction requires an accepted runtime");
634
+ }
635
+ };
636
+ signal?.throwIfAborted();
637
+ assertAuthority();
638
+ if (kind === "patch" || allowCreateOrigin) initializeFileStore(this.root);
639
+ if ((kind === "start" || kind === "restore" || kind === "fork") && !lstatSync(this.root, { throwIfNoEntry: false })) {
640
+ throw new RevisionUnavailableError(kind !== "start" ? "Selected State Flow boundary storage is unavailable" : "Current State Flow session storage is unavailable");
641
+ }
642
+ return withStorageTransaction(this.root, (storage) => {
643
+ assertAuthority();
644
+ let current: Snapshot | undefined;
645
+ let candidate: TemporalRuntime;
646
+ let assertSource: (() => void) | undefined;
647
+ if (selection.kind === "fork") {
648
+ ({ candidate, current, assertSource } = this.prepareForkCandidate(storage, selection.source, selection.checkpoint));
649
+ } else if (selection.kind === "restore" || selection.kind === "start") {
650
+ candidate = new TemporalRuntime(this.cwd, this.session, this.root, undefined, this.historyLimit);
651
+ candidate.transaction = storage;
652
+ const base = storage.capture(this.cwd, this.sessionId, this.root, this.sessionKey);
653
+ current = selection.kind === "restore"
654
+ ? candidate.prepareBoundaryRestoreBase(selection.checkpoint, base).restore()
655
+ : candidate.loadCurrentMemoryBase(base);
656
+ if (!current) {
657
+ if (!allowCreateOrigin) throw new RevisionUnavailableError("Current State Flow session memory is unavailable");
658
+ if (SCOPES.some((scope) => lstatSync(join(temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey).directory, "state.json"), { throwIfNoEntry: false }))) {
659
+ throw new Error("Unsupported State Flow storage exists; preserve or convert it before initialization");
660
+ }
661
+ candidate = candidate.publicationCandidate(storage, base);
662
+ }
663
+ } else candidate = this.publicationCandidate(storage);
664
+ let consumed = false;
665
+ let published = false;
666
+ const result = action(candidate, (snapshot, accepted, provenance) => {
667
+ if (consumed) throw new Error(`State Flow ${kind} transaction publication was already consumed`);
668
+ consumed = true;
669
+ signal?.throwIfAborted();
670
+ assertSource?.();
671
+ // Patch preparation without semantic work preserves complete accepted cohorts; selection accepts origins with configured folding.
672
+ // New authority or wholly absent shared pairs still need normal atomic initialization; partial evidence already failed.
673
+ const writeSemantic = semantic && (kind === "start" || kind === "restore" || kind === "fork" || accepted !== undefined || !this.semanticRevision
674
+ || SCOPES.some((scope) => Object.keys(provenance?.[scope] ?? {}).length > 0)
675
+ || candidate.base!.files.some((file) => file.content === undefined));
676
+ // Every capability acceptance validates its captured cohort, including semantic no-ops.
677
+ const publication = kind === "fork" ? candidate.publishForkOrigin(snapshot) : candidate.publish(snapshot, writeSemantic, accepted, { provenance });
678
+ if (!publication) throw new Error(`State Flow ${kind} transaction produced no canonical publication`);
679
+ this.view = candidate.view;
680
+ this.base = candidate.base;
681
+ this.provenanceByScope = candidate.provenanceByScope;
682
+ this.semanticRevision = candidate.semanticRevision;
683
+ this.savedRuntime = candidate.savedRuntime;
684
+ this.restoredOriginPending = false;
685
+ this.absentSharedScopes.clear();
686
+ published = true;
687
+ return publication;
688
+ }, current);
689
+ if (!published) throw new Error(`State Flow ${kind} transaction requires one synchronous publication`);
690
+ return result;
691
+ }, signal);
427
692
  }
428
693
 
429
694
  /** Canonically accept a prepared retained-boundary origin before lifecycle-only persistence. */
@@ -449,8 +714,8 @@ export class TemporalRuntime {
449
714
  let basis = this.view;
450
715
  let base: TemporalFileBase = this.base;
451
716
  let basisProvenance = this.provenanceByScope;
452
- // First passive writes also reconcile their selected basis; origin-only acceptance keeps its strict CAS.
453
- if (this.semanticRevision || accepted !== undefined || provenanceScopes.length > 0) {
717
+ // A transaction already selected its current basis under exclusion; raw replay callers still guard their older basis.
718
+ if (!this.transaction && (this.semanticRevision || accepted !== undefined || provenanceScopes.length > 0)) {
454
719
  const changedScopes = new Set<StateScope>([
455
720
  ...(accepted?.transitions ?? []).map(({ scope }) => scope),
456
721
  ...provenanceScopes,
@@ -473,7 +738,7 @@ export class TemporalRuntime {
473
738
  const runtime = createSessionRuntime(snapshot, this.cwd, this.sessionId, next.lineage, nextProvenance.session);
474
739
  const fingerprint = hashJson(runtime);
475
740
  if (!semantic && fingerprint === this.savedRuntime && !provenanceChanged) return undefined;
476
- const result = publishTemporalStateToFiles(this.cwd, this.sessionId, next, runtimeOnly ? [] : SCOPES, base, this.root, runtime, this.sessionKey, runtimeOnly ? undefined : nextProvenance, runtimeOnly);
741
+ const result = (this.transaction?.publish ?? publishTemporalStateToFiles)(this.cwd, this.sessionId, next, runtimeOnly ? [] : SCOPES, base, this.root, runtime, this.sessionKey, runtimeOnly ? undefined : nextProvenance, runtimeOnly);
477
742
  this.base = result.base;
478
743
  this.view = next;
479
744
  this.provenanceByScope = nextProvenance;
@@ -1,3 +1,5 @@
1
+ import { parseRetainedPiCheckpoint } from "./snapshot.ts";
2
+
1
3
  export const SNAPSHOT_ENTRY_TYPE = "state-flow-snapshot";
2
4
 
3
5
  interface BranchEntry {
@@ -15,6 +17,9 @@ export interface SessionEntryLookup {
15
17
  export interface PassiveStopBoundary {
16
18
  at: number;
17
19
  from?: number;
20
+ preserveContext?: true;
21
+ /** A same-owner failed Stop remains a write fence until a later accepted checkpoint. */
22
+ persistenceError?: string;
18
23
  }
19
24
 
20
25
  export interface SnapshotDiscovery {
@@ -58,6 +63,26 @@ export function hasPriorConversation(branch: readonly BranchEntry[]): boolean {
58
63
  return false;
59
64
  }
60
65
 
66
+ /** Native conversation after the latest valid checkpoint may contain uncompiled work, not a new semantic authority. */
67
+ export function hasUncheckpointedConversation(branch: readonly BranchEntry[]): boolean {
68
+ for (let index = branch.length - 1; index >= 0; index--) {
69
+ try {
70
+ const entry = branch[index];
71
+ if (entry?.type === "message") {
72
+ const role = entry.message?.role;
73
+ if (role === "user" || role === "assistant" || role === "toolResult") return true;
74
+ }
75
+ if (entry?.type === "custom" && entry.customType === SNAPSHOT_ENTRY_TYPE) {
76
+ parseRetainedPiCheckpoint(entry.data);
77
+ return false;
78
+ }
79
+ } catch {
80
+ // Malformed entries cannot prove that pending input was checkpointed.
81
+ }
82
+ }
83
+ return false;
84
+ }
85
+
61
86
  /** Auto-start eligibility is session identity/lifecycle, not the presence of CWD materialization. */
62
87
  export function isNewSession(reason: unknown, branch: readonly BranchEntry[]): boolean {
63
88
  if (reason === "new") return true;
@@ -79,14 +104,24 @@ export function findAssistantToolBatch(session: SessionEntryLookup, toolCallId:
79
104
  }
80
105
 
81
106
  export function findPassiveStopBoundary(branch: readonly BranchEntry[], sessionId: string, entryType: string): PassiveStopBoundary | undefined {
107
+ let checkpointSeen = false;
82
108
  for (const entry of [...branch].reverse()) {
83
109
  try {
84
- if (entry?.type !== "custom" || entry.customType !== entryType) continue;
85
- const { at, from, reset, owner } = (entry.data as { at?: unknown; from?: unknown; reset?: unknown; owner?: unknown } | undefined) ?? {};
110
+ if (entry?.type !== "custom") continue;
111
+ if (entry.customType === SNAPSHOT_ENTRY_TYPE) {
112
+ parseRetainedPiCheckpoint(entry.data);
113
+ checkpointSeen = true;
114
+ continue;
115
+ }
116
+ if (entry.customType !== entryType) continue;
117
+ const { at, from, reset, owner, preserveContext, persistenceError } = (entry.data as { at?: unknown; from?: unknown; reset?: unknown; owner?: unknown; preserveContext?: unknown; persistenceError?: unknown } | undefined) ?? {};
86
118
  if (reset === true && owner === sessionId) return undefined;
119
+ if (owner !== undefined && owner !== sessionId) continue;
87
120
  if (typeof at === "number" && Number.isSafeInteger(at) && at >= 0) return {
88
121
  at,
89
122
  ...(typeof from === "number" && Number.isSafeInteger(from) && from >= 0 ? { from } : {}),
123
+ ...(preserveContext === true ? { preserveContext: true } : {}),
124
+ ...(!checkpointSeen && owner === sessionId && typeof persistenceError === "string" && persistenceError.trim().length > 0 ? { persistenceError } : {}),
90
125
  };
91
126
  } catch {
92
127
  // A hostile unrelated branch entry cannot manufacture or suppress a valid marker.
@@ -10,6 +10,9 @@ const MAX_LEGACY_VALIDATION_ATTEMPT = 7;
10
10
  /** Missing operational capability is not evidence that a checkpoint target is invalid. */
11
11
  export class RevisionUnavailableError extends Error {}
12
12
 
13
+ /** Expired history cannot be restored, but explicit activation may use validated current memory. */
14
+ export class HistoryBoundaryExpiredError extends RevisionUnavailableError {}
15
+
13
16
  export interface SnapshotConfig {
14
17
  enabled: boolean;
15
18
  }
@@ -1,6 +1,7 @@
1
1
  import type { ArtifactInvalidationReason } from "./artifact.ts";
2
2
  import { projectRecentTransitionsWithLimit, type RecentTransitionWindow } from "./history.ts";
3
3
  import { retainedMemoryScopes } from "./memory.ts";
4
+ import { conciseDiagnostic } from "./protocol.ts";
4
5
  import type { Snapshot } from "./snapshot.ts";
5
6
  import { overlayStates, type ScopedStates, type StateScope } from "./state.ts";
6
7
  import type { ScopeRevisions, TransitionBoundary } from "./temporal.ts";
@@ -27,6 +28,7 @@ export interface StatusDiagnostics {
27
28
  temporal?: { head: TransitionBoundary; historyDepth: number; tailCounts: Record<StateScope, number>; revisions: ScopeRevisions };
28
29
  staleArtifacts: readonly StaleArtifactDiagnostic[];
29
30
  durableStateError?: string;
31
+ publicationError?: string;
30
32
  }
31
33
 
32
34
  export function formatScopeRevisionVector(revisions: ScopeRevisions): string {
@@ -63,7 +65,7 @@ export function detailedStatus(snapshot: Snapshot, diagnostics: StatusDiagnostic
63
65
  ];
64
66
  const temporal = available ? diagnostics.temporal : undefined;
65
67
  const temporalLines = temporal === undefined
66
- ? [`Temporal materialization unavailable: ${diagnostics.durableStateError ?? "no selected branch runtime"}`,
68
+ ? [`Temporal materialization unavailable: ${conciseDiagnostic(diagnostics.durableStateError ?? "no selected branch runtime")}`,
67
69
  `Hot history: unavailable; configured maximum depth ${diagnostics.historyLimit}`,
68
70
  "Retained patch tails: unavailable"]
69
71
  : [`Temporal head: ${JSON.stringify(temporal.head.id)}; branch-local position ${temporal.head.position}`,
@@ -82,6 +84,7 @@ export function detailedStatus(snapshot: Snapshot, diagnostics: StatusDiagnostic
82
84
  "Memory: owner state-flow; global retention enabled; global fallback active",
83
85
  `Memory-bearing scopes: global ${memoryScopes?.global ?? "unknown"}; CWD ${memoryScopes?.cwd ?? "unknown"}; session ${memoryScopes?.session ?? "unknown"}`,
84
86
  ...temporalLines,
87
+ ...(diagnostics.publicationError === undefined ? [] : [`Memory writes paused after Stop: ${conciseDiagnostic(diagnostics.publicationError)}`]),
85
88
  `Artifacts: global ${artifacts("global")}; CWD ${artifacts("cwd")}; session ${artifacts("session")}; pending invalidations ${invalidated}`,
86
89
  available ? `Recent transitions: global ${diagnostics.recent.filter(({ transitions }) => transitions.some(({ scope }) => scope === "global")).length}; CWD ${diagnostics.recent.filter(({ transitions }) => transitions.some(({ scope }) => scope === "cwd")).length}; session ${diagnostics.recent.filter(({ transitions }) => transitions.some(({ scope }) => scope === "session")).length}; active ${projectedRecent.length}` : "Recent transitions: unavailable",
87
90
  ...invalidationLines,