@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,12 +3,12 @@ import { lstatSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { classifyScopeStream, hasCwdMaterialization, parseScopeProvenance, parseScopeStream, sessionRuntimePaths, temporalScopePaths } from "./durable.js";
5
5
  import { pruneArtifactProvenance } from "./artifact.js";
6
- import { captureTemporalFileBase, initializeFileStore, publishTemporalStateToFiles } from "./storage.js";
6
+ import { assertTemporalFileBase, captureTemporalFileBase, initializeFileStore, publishTemporalStateToFiles, withStorageTransaction } from "./storage.js";
7
7
  import { DEFAULT_HISTORY_LIMIT, MAX_HISTORY_LIMIT } from "./history.js";
8
8
  import { hashJson, sameJson } from "./json.js";
9
- import { RevisionUnavailableError, createSessionRuntime, parseSessionRuntime, retainedBoundaryCheckpoint } from "./snapshot.js";
9
+ import { HistoryBoundaryExpiredError, RevisionUnavailableError, createSessionRuntime, parseSessionRuntime, retainedBoundaryCheckpoint } from "./snapshot.js";
10
10
  import { emptyState } from "./state.js";
11
- import { adoptTemporalStreams, advanceTemporalState, createTemporalState, readTemporalState, selectScopeStreamAtBoundary, validateScopeLineage } from "./temporal.js";
11
+ import { adoptTemporalStreams, advanceTemporalState, constrainTemporalState, createTemporalState, readTemporalState, selectScopeStreamAtBoundary, validateScopeLineage } from "./temporal.js";
12
12
  const SCOPES = ["global", "cwd", "session"];
13
13
  const SHARED_SCOPES = ["global", "cwd"];
14
14
  function emptyProvenance() {
@@ -50,6 +50,7 @@ export class TemporalRuntime {
50
50
  base;
51
51
  semanticRevision;
52
52
  savedRuntime;
53
+ transaction;
53
54
  restoredOriginPending = false;
54
55
  provenanceByScope = emptyProvenance();
55
56
  /** Shared scopes whose wholly absent live basis was accepted after one stale-target refusal. */
@@ -76,7 +77,9 @@ export class TemporalRuntime {
76
77
  loadPassive() {
77
78
  if (!lstatSync(this.root, { throwIfNoEntry: false }))
78
79
  return false;
79
- const base = captureTemporalFileBase(this.cwd, this.sessionId, this.root, this.sessionKey);
80
+ return this.loadPassiveBase(captureTemporalFileBase(this.cwd, this.sessionId, this.root, this.sessionKey));
81
+ }
82
+ loadPassiveBase(base) {
80
83
  const files = new Map(base.files.map((file) => [file.path, file.content]));
81
84
  const shared = Object.fromEntries(SHARED_SCOPES.map((scope) => {
82
85
  const paths = temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey);
@@ -88,13 +91,19 @@ export class TemporalRuntime {
88
91
  throw new Error("Incomplete passive State Flow shared storage: CWD state exists without global state");
89
92
  const fresh = createTemporalState({ global: emptyState(), cwd: emptyState(), session: emptyState() }, randomUUID(), this.historyLimit);
90
93
  // Global memory is valid before this CWD has ever materialized its own scope.
91
- this.view = adoptTemporalStreams({ global: shared.global, cwd: shared.cwd ?? fresh.scopes.cwd, session: fresh.scopes.session }, `passive:files:${randomUUID()}`, this.historyLimit);
92
- this.base = base;
93
- this.provenanceByScope = {
94
+ const view = adoptTemporalStreams({ global: shared.global, cwd: shared.cwd ?? fresh.scopes.cwd, session: fresh.scopes.session }, `passive:files:${randomUUID()}`, this.historyLimit);
95
+ const provenance = {
94
96
  global: parseScopeProvenance(files.get(temporalScopePaths(this.cwd, this.sessionId, "global", this.root, this.sessionKey).meta), "State Flow global metadata"),
95
97
  cwd: parseScopeProvenance(files.get(temporalScopePaths(this.cwd, this.sessionId, "cwd", this.root, this.sessionKey).meta), "State Flow CWD metadata"),
96
98
  session: {},
97
99
  };
100
+ this.view = view;
101
+ this.base = base;
102
+ this.provenanceByScope = provenance;
103
+ this.semanticRevision = undefined;
104
+ this.savedRuntime = undefined;
105
+ this.restoredOriginPending = false;
106
+ this.absentSharedScopes.clear();
98
107
  return true;
99
108
  }
100
109
  /** Select canonical file acceptance even when Git is available; backup remains a later concern. */
@@ -143,7 +152,9 @@ export class TemporalRuntime {
143
152
  prepareBoundaryRestore(checkpoint) {
144
153
  if (!lstatSync(this.root, { throwIfNoEntry: false }))
145
154
  throw new RevisionUnavailableError("Selected State Flow boundary storage is unavailable");
146
- const base = captureTemporalFileBase(this.cwd, this.sessionId, this.root, this.sessionKey);
155
+ return this.prepareBoundaryRestoreBase(checkpoint, captureTemporalFileBase(this.cwd, this.sessionId, this.root, this.sessionKey));
156
+ }
157
+ prepareBoundaryRestoreBase(checkpoint, base) {
147
158
  const files = new Map(base.files.map((file) => [file.path, file.content]));
148
159
  const scopes = Object.fromEntries(SCOPES.map((scope) => {
149
160
  const paths = temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey);
@@ -160,7 +171,7 @@ export class TemporalRuntime {
160
171
  validateScopeLineage(scopes.session, "session", document.meta.lineage, MAX_HISTORY_LIMIT);
161
172
  const boundary = document.meta.lineage.slice(-(this.historyLimit + 1)).find(({ id }) => id === checkpoint.boundary);
162
173
  if (!boundary)
163
- throw new RevisionUnavailableError("Selected State Flow history boundary is outside the retained temporal window");
174
+ throw new HistoryBoundaryExpiredError("Selected State Flow history boundary is outside the retained temporal window");
164
175
  const selectedSession = selectScopeStreamAtBoundary(scopes.session, "session", boundary, MAX_HISTORY_LIMIT);
165
176
  const view = adoptTemporalStreams({
166
177
  global: scopes.global,
@@ -214,6 +225,60 @@ export class TemporalRuntime {
214
225
  const snapshot = prepared.restore();
215
226
  return { snapshot, publication: this.acceptRestoredOrigin(snapshot) };
216
227
  }
228
+ /** Await a coherent read-only recovery view; this neither activates policy nor accepts publication authority. */
229
+ async refreshCurrentMemory(signal) {
230
+ signal?.throwIfAborted();
231
+ if (!lstatSync(this.root, { throwIfNoEntry: false }))
232
+ return undefined;
233
+ return withStorageTransaction(this.root, (storage) => {
234
+ const base = storage.capture(this.cwd, this.sessionId, this.root, this.sessionKey);
235
+ signal?.throwIfAborted();
236
+ return this.loadCurrentMemoryBase(base);
237
+ }, signal);
238
+ }
239
+ loadCurrentMemoryBase(base) {
240
+ const files = new Map(base.files.map((file) => [file.path, file.content]));
241
+ const paths = sessionRuntimePaths(this.cwd, this.sessionId, this.root, this.sessionKey);
242
+ const session = temporalScopePaths(this.cwd, this.sessionId, "session", this.root, this.sessionKey);
243
+ if ([paths.config, paths.runtime, session.checkpoint, session.patches, session.meta].every((path) => files.get(path) === undefined))
244
+ return undefined;
245
+ const document = parseSessionRuntime(files.get(paths.config), files.get(paths.runtime), this.cwd, this.sessionId);
246
+ if (!document)
247
+ throw new RevisionUnavailableError("Current State Flow session runtime is unavailable");
248
+ const origin = `start:${randomUUID()}`;
249
+ const scopes = Object.fromEntries(SCOPES.map((scope) => {
250
+ const paths = temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey);
251
+ const stream = parseScopeStream(files.get(paths.checkpoint), files.get(paths.patches), scope, scope === "cwd" ? this.cwd : undefined, files.get(paths.meta));
252
+ if (!stream && scope === "session")
253
+ throw new RevisionUnavailableError("Current State Flow session memory is unavailable");
254
+ return [scope, stream ?? freshEmptyScopeStream(scope, origin, this.historyLimit)];
255
+ }));
256
+ validateScopeLineage(scopes.session, "session", document.meta.lineage, MAX_HISTORY_LIMIT);
257
+ let view;
258
+ try {
259
+ view = constrainTemporalState({ scopes, lineage: document.meta.lineage }, this.historyLimit);
260
+ }
261
+ catch {
262
+ // Independently validated live shared streams can belong to another writer's lineage.
263
+ view = adoptTemporalStreams(scopes, origin, this.historyLimit);
264
+ }
265
+ const provenance = Object.fromEntries(SCOPES.map((scope) => {
266
+ const meta = temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey).meta;
267
+ return [scope, parseScopeProvenance(files.get(meta), meta)];
268
+ }));
269
+ const snapshot = {
270
+ config: { enabled: false },
271
+ meta: { step: document.meta.step, ...(document.meta.bootstrap === true ? { bootstrap: true } : {}) },
272
+ };
273
+ this.view = view;
274
+ this.base = base;
275
+ this.provenanceByScope = provenance;
276
+ this.semanticRevision = undefined;
277
+ this.savedRuntime = undefined;
278
+ this.restoredOriginPending = false;
279
+ this.absentSharedScopes.clear();
280
+ return snapshot;
281
+ }
217
282
  /** Copy one retained source-session boundary over the child's current shared scopes. */
218
283
  prepareBoundaryFork(source, checkpoint) {
219
284
  const parent = Object.freeze({ ...source });
@@ -285,7 +350,7 @@ export class TemporalRuntime {
285
350
  const existingRuntime = parseSessionRuntime(files.get(paths.config), files.get(paths.runtime), this.cwd, this.sessionId);
286
351
  if (existingRuntime && !newSessionOrigin)
287
352
  throw new Error("Existing session runtime requires retained-boundary restoration");
288
- // Explicit start before any branch runtime is a new origin, never inheritance of a later session layer.
353
+ // A pre-runtime branch may establish an empty origin, never import a later session layer.
289
354
  if (newSessionOrigin)
290
355
  streams.session = undefined;
291
356
  if (copy)
@@ -316,7 +381,7 @@ export class TemporalRuntime {
316
381
  /** Copy the private origin and apply configured retention folding, preserving live shared values/provenance. */
317
382
  publishForkOrigin(snapshot) {
318
383
  const runtime = createSessionRuntime(snapshot, this.cwd, this.sessionId, this.view.lineage, this.provenanceByScope.session);
319
- const publication = publishTemporalStateToFiles(this.cwd, this.sessionId, this.view, SCOPES, this.base, this.root, runtime, this.sessionKey, this.provenanceByScope);
384
+ const publication = (this.transaction?.publish ?? publishTemporalStateToFiles)(this.cwd, this.sessionId, this.view, SCOPES, this.base, this.root, runtime, this.sessionKey, this.provenanceByScope);
320
385
  this.base = publication.base;
321
386
  this.semanticRevision = publication.revision;
322
387
  this.savedRuntime = hashJson(runtime);
@@ -330,8 +395,8 @@ export class TemporalRuntime {
330
395
  * actually changes remains a fail-closed write conflict. Divergence in non-adoptable
331
396
  * session or runtime files also fails closed under the existing race rule.
332
397
  */
333
- reconcileSharedDrift(changedScopes) {
334
- const captured = captureTemporalFileBase(this.cwd, this.sessionId, this.root, this.sessionKey);
398
+ reconcileSharedDrift(changedScopes, current) {
399
+ const captured = current ?? captureTemporalFileBase(this.cwd, this.sessionId, this.root, this.sessionKey);
335
400
  const liveFiles = new Map(captured.files.map((file) => [file.path, file]));
336
401
  const priorFiles = new Map(this.base.files.map((file) => [file.path, file]));
337
402
  const adoptable = new Set();
@@ -402,16 +467,201 @@ export class TemporalRuntime {
402
467
  throw targetScopeConflict(targets);
403
468
  return { view: reconciledView(), base: captured, provenance };
404
469
  }
405
- /** Refresh live shared scopes in memory without publishing or advancing private semantic history. */
406
- refreshShared() {
407
- if (!this.view || !this.base)
408
- throw new Error("State Flow shared refresh requires a selected temporal runtime");
409
- const before = this.view;
410
- const reconciled = this.reconcileSharedDrift(new Set());
411
- this.view = reconciled.view;
412
- this.base = reconciled.base;
413
- this.provenanceByScope = reconciled.provenance;
414
- return before !== this.view;
470
+ /** Await coherent shared inspection, lazily loading an empty private view only when none is selected. */
471
+ async refreshShared(signal) {
472
+ signal?.throwIfAborted();
473
+ if (!this.view && !lstatSync(this.root, { throwIfNoEntry: false }))
474
+ return false;
475
+ return withStorageTransaction(this.root, (storage) => {
476
+ const current = storage.capture(this.cwd, this.sessionId, this.root, this.sessionKey);
477
+ if (!this.view)
478
+ return this.loadPassiveBase(current);
479
+ if (!this.base)
480
+ throw new Error("State Flow shared refresh requires a selected temporal runtime");
481
+ const before = this.view;
482
+ const reconciled = this.reconcileSharedDrift(new Set(), current);
483
+ this.view = reconciled.view;
484
+ this.base = reconciled.base;
485
+ this.provenanceByScope = reconciled.provenance;
486
+ return before !== this.view;
487
+ }, signal);
488
+ }
489
+ /** Prepare current shared state while retaining the exact accepted private publication basis. */
490
+ publicationCandidate(storage, current) {
491
+ if (this.restoredOriginPending)
492
+ throw new RevisionUnavailableError("State Flow restored origin must be accepted before patching memory");
493
+ current ??= storage.capture(this.cwd, this.sessionId, this.root, this.sessionKey);
494
+ const candidate = new TemporalRuntime(this.cwd, this.session, this.root, undefined, this.historyLimit);
495
+ candidate.transaction = storage;
496
+ if (this.semanticRevision && this.view && this.base) {
497
+ candidate.view = this.view;
498
+ candidate.base = this.base;
499
+ candidate.provenanceByScope = this.provenanceByScope;
500
+ const reconciled = candidate.reconcileSharedDrift(new Set(), current);
501
+ candidate.view = reconciled.view;
502
+ candidate.base = reconciled.base;
503
+ candidate.provenanceByScope = reconciled.provenance;
504
+ return candidate;
505
+ }
506
+ const files = new Map(current.files.map((file) => [file.path, file.content]));
507
+ const paths = sessionRuntimePaths(this.cwd, this.sessionId, this.root, this.sessionKey);
508
+ const session = temporalScopePaths(this.cwd, this.sessionId, "session", this.root, this.sessionKey);
509
+ if ([paths.config, paths.runtime, session.checkpoint, session.patches, session.meta].some((path) => files.get(path) !== undefined)) {
510
+ const document = parseSessionRuntime(files.get(paths.config), files.get(paths.runtime), this.cwd, this.sessionId);
511
+ const stream = parseScopeStream(files.get(session.checkpoint), files.get(session.patches), "session", undefined, files.get(session.meta));
512
+ if (!document || !stream)
513
+ throw new RevisionUnavailableError("Current State Flow session memory is incomplete");
514
+ validateScopeLineage(stream, "session", document.meta.lineage, MAX_HISTORY_LIMIT);
515
+ throw new RevisionUnavailableError("Existing State Flow session memory is not selected; select an accepted boundary or use /state-flow-start");
516
+ }
517
+ const origin = `patch:${randomUUID()}`;
518
+ const streams = Object.fromEntries(SCOPES.map((scope) => {
519
+ const paths = temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey);
520
+ return [scope, parseScopeStream(files.get(paths.checkpoint), files.get(paths.patches), scope, scope === "cwd" ? this.cwd : undefined, files.get(paths.meta)) ?? freshEmptyScopeStream(scope, `${origin}:empty`, this.historyLimit)];
521
+ }));
522
+ candidate.view = adoptTemporalStreams(streams, origin, this.historyLimit);
523
+ candidate.base = current;
524
+ candidate.provenanceByScope = Object.fromEntries(SCOPES.map((scope) => {
525
+ const paths = temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey);
526
+ // Absent semantic pairs cannot confer compilation evidence on a new registration.
527
+ return [scope, files.get(paths.checkpoint) === undefined ? {} : parseScopeProvenance(files.get(paths.meta), paths.meta)];
528
+ }));
529
+ return candidate;
530
+ }
531
+ /** Stage and accept synchronously inside an awaited lock; expose neither selection nor raw storage operations. */
532
+ async withPatchTransaction(action, signal) {
533
+ return this.withPublicationTransaction((candidate, publish) => action({
534
+ states: candidate.states(),
535
+ causalBasis: candidate.causalBasis(),
536
+ provenance: structuredClone(candidate.provenanceByScope),
537
+ publish,
538
+ }), { kind: "patch" }, signal);
539
+ }
540
+ /** Activate current owned memory; the caller must authorize a wholly absent private origin after waiting. */
541
+ async withStartTransaction(action, signal, allowCreateOrigin = false) {
542
+ return this.withPublicationTransaction((_candidate, publish, current) => action(current, (snapshot) => publish(snapshot)), { kind: "start", allowCreateOrigin }, signal);
543
+ }
544
+ /** Select one retained private boundary beside current shared streams, then accept only after caller policy is rechecked. */
545
+ async withRestoreTransaction(checkpoint, action, signal) {
546
+ signal?.throwIfAborted();
547
+ return this.withPublicationTransaction((_candidate, publish, selected) => action(selected, (snapshot) => publish(snapshot)), { kind: "restore", checkpoint: structuredClone(checkpoint) }, signal);
548
+ }
549
+ /** Copy exact retained parent authority into an unoccupied child; the caller rechecks native selection after waiting. */
550
+ async withForkTransaction(source, checkpoint, action, signal) {
551
+ signal?.throwIfAborted();
552
+ return this.withPublicationTransaction((_candidate, publish, selected) => action(selected, (snapshot) => publish(snapshot)), { kind: "fork", source: { ...source }, checkpoint: structuredClone(checkpoint) }, signal);
553
+ }
554
+ prepareForkCandidate(storage, source, checkpoint) {
555
+ const sourceBase = storage.capture(this.cwd, source.id, this.root, source.key);
556
+ const parent = new TemporalRuntime(this.cwd, source, this.root, undefined, this.historyLimit);
557
+ const selected = parent.prepareBoundaryRestoreBase(checkpoint, sourceBase).restore();
558
+ const base = storage.capture(this.cwd, this.sessionId, this.root, this.sessionKey);
559
+ const files = new Map(base.files.map((file) => [file.path, file.content]));
560
+ const paths = temporalScopePaths(this.cwd, this.sessionId, "session", this.root, this.sessionKey);
561
+ if ([paths.checkpoint, paths.patches, paths.meta, join(paths.directory, "config.json"), join(paths.directory, "runtime.json")].some((path) => files.get(path) !== undefined)) {
562
+ throw new Error("State Flow fork target already has session storage");
563
+ }
564
+ if (SCOPES.some((scope) => lstatSync(join(temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey).directory, "state.json"), { throwIfNoEntry: false }))) {
565
+ throw new Error("Unsupported State Flow storage exists; preserve or convert it before initialization");
566
+ }
567
+ const candidate = new TemporalRuntime(this.cwd, this.session, this.root, undefined, this.historyLimit);
568
+ candidate.transaction = storage;
569
+ candidate.base = base;
570
+ candidate.view = adoptTemporalStreams(parent.view.scopes, `files:${randomUUID()}`, this.historyLimit);
571
+ candidate.provenanceByScope = { ...parent.provenanceByScope };
572
+ for (const scope of SHARED_SCOPES) {
573
+ const meta = temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey).meta;
574
+ candidate.provenanceByScope[scope] = parseScopeProvenance(files.get(meta), meta);
575
+ }
576
+ return {
577
+ candidate,
578
+ current: { config: selected.config, meta: { step: 0, ...(selected.meta.bootstrap === undefined ? {} : { bootstrap: selected.meta.bootstrap }) } },
579
+ assertSource: () => assertTemporalFileBase(sourceBase, storage.capture(this.cwd, source.id, this.root, source.key)),
580
+ };
581
+ }
582
+ /** Recheck caller policy after waiting, then accept only config/runtime over an already accepted private basis. */
583
+ async withLifecycleTransaction(action, signal) {
584
+ return this.withPublicationTransaction((_candidate, publish) => action((snapshot) => publish(snapshot)), { kind: "lifecycle" }, signal);
585
+ }
586
+ async withPublicationTransaction(action, selection, signal) {
587
+ const { kind } = selection;
588
+ const allowCreateOrigin = selection.kind === "start" && selection.allowCreateOrigin;
589
+ const semantic = kind !== "lifecycle";
590
+ const assertAuthority = () => {
591
+ if (selection.kind === "fork") {
592
+ if (selection.source.id === this.sessionId || selection.source.key === this.sessionKey)
593
+ throw new Error("State Flow fork requires a distinct session identity and key");
594
+ if (this.view)
595
+ throw new Error("State Flow fork target already has session storage");
596
+ }
597
+ if (!semantic && (!this.semanticRevision || !this.view || !this.base || this.restoredOriginPending)) {
598
+ throw new Error("State Flow lifecycle transaction requires an accepted runtime");
599
+ }
600
+ };
601
+ signal?.throwIfAborted();
602
+ assertAuthority();
603
+ if (kind === "patch" || allowCreateOrigin)
604
+ initializeFileStore(this.root);
605
+ if ((kind === "start" || kind === "restore" || kind === "fork") && !lstatSync(this.root, { throwIfNoEntry: false })) {
606
+ throw new RevisionUnavailableError(kind !== "start" ? "Selected State Flow boundary storage is unavailable" : "Current State Flow session storage is unavailable");
607
+ }
608
+ return withStorageTransaction(this.root, (storage) => {
609
+ assertAuthority();
610
+ let current;
611
+ let candidate;
612
+ let assertSource;
613
+ if (selection.kind === "fork") {
614
+ ({ candidate, current, assertSource } = this.prepareForkCandidate(storage, selection.source, selection.checkpoint));
615
+ }
616
+ else if (selection.kind === "restore" || selection.kind === "start") {
617
+ candidate = new TemporalRuntime(this.cwd, this.session, this.root, undefined, this.historyLimit);
618
+ candidate.transaction = storage;
619
+ const base = storage.capture(this.cwd, this.sessionId, this.root, this.sessionKey);
620
+ current = selection.kind === "restore"
621
+ ? candidate.prepareBoundaryRestoreBase(selection.checkpoint, base).restore()
622
+ : candidate.loadCurrentMemoryBase(base);
623
+ if (!current) {
624
+ if (!allowCreateOrigin)
625
+ throw new RevisionUnavailableError("Current State Flow session memory is unavailable");
626
+ if (SCOPES.some((scope) => lstatSync(join(temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey).directory, "state.json"), { throwIfNoEntry: false }))) {
627
+ throw new Error("Unsupported State Flow storage exists; preserve or convert it before initialization");
628
+ }
629
+ candidate = candidate.publicationCandidate(storage, base);
630
+ }
631
+ }
632
+ else
633
+ candidate = this.publicationCandidate(storage);
634
+ let consumed = false;
635
+ let published = false;
636
+ const result = action(candidate, (snapshot, accepted, provenance) => {
637
+ if (consumed)
638
+ throw new Error(`State Flow ${kind} transaction publication was already consumed`);
639
+ consumed = true;
640
+ signal?.throwIfAborted();
641
+ assertSource?.();
642
+ // Patch preparation without semantic work preserves complete accepted cohorts; selection accepts origins with configured folding.
643
+ // New authority or wholly absent shared pairs still need normal atomic initialization; partial evidence already failed.
644
+ const writeSemantic = semantic && (kind === "start" || kind === "restore" || kind === "fork" || accepted !== undefined || !this.semanticRevision
645
+ || SCOPES.some((scope) => Object.keys(provenance?.[scope] ?? {}).length > 0)
646
+ || candidate.base.files.some((file) => file.content === undefined));
647
+ // Every capability acceptance validates its captured cohort, including semantic no-ops.
648
+ const publication = kind === "fork" ? candidate.publishForkOrigin(snapshot) : candidate.publish(snapshot, writeSemantic, accepted, { provenance });
649
+ if (!publication)
650
+ throw new Error(`State Flow ${kind} transaction produced no canonical publication`);
651
+ this.view = candidate.view;
652
+ this.base = candidate.base;
653
+ this.provenanceByScope = candidate.provenanceByScope;
654
+ this.semanticRevision = candidate.semanticRevision;
655
+ this.savedRuntime = candidate.savedRuntime;
656
+ this.restoredOriginPending = false;
657
+ this.absentSharedScopes.clear();
658
+ published = true;
659
+ return publication;
660
+ }, current);
661
+ if (!published)
662
+ throw new Error(`State Flow ${kind} transaction requires one synchronous publication`);
663
+ return result;
664
+ }, signal);
415
665
  }
416
666
  /** Canonically accept a prepared retained-boundary origin before lifecycle-only persistence. */
417
667
  acceptRestoredOrigin(snapshot) {
@@ -434,8 +684,8 @@ export class TemporalRuntime {
434
684
  let basis = this.view;
435
685
  let base = this.base;
436
686
  let basisProvenance = this.provenanceByScope;
437
- // First passive writes also reconcile their selected basis; origin-only acceptance keeps its strict CAS.
438
- if (this.semanticRevision || accepted !== undefined || provenanceScopes.length > 0) {
687
+ // A transaction already selected its current basis under exclusion; raw replay callers still guard their older basis.
688
+ if (!this.transaction && (this.semanticRevision || accepted !== undefined || provenanceScopes.length > 0)) {
439
689
  const changedScopes = new Set([
440
690
  ...(accepted?.transitions ?? []).map(({ scope }) => scope),
441
691
  ...provenanceScopes,
@@ -462,7 +712,7 @@ export class TemporalRuntime {
462
712
  const fingerprint = hashJson(runtime);
463
713
  if (!semantic && fingerprint === this.savedRuntime && !provenanceChanged)
464
714
  return undefined;
465
- const result = publishTemporalStateToFiles(this.cwd, this.sessionId, next, runtimeOnly ? [] : SCOPES, base, this.root, runtime, this.sessionKey, runtimeOnly ? undefined : nextProvenance, runtimeOnly);
715
+ const result = (this.transaction?.publish ?? publishTemporalStateToFiles)(this.cwd, this.sessionId, next, runtimeOnly ? [] : SCOPES, base, this.root, runtime, this.sessionKey, runtimeOnly ? undefined : nextProvenance, runtimeOnly);
466
716
  this.base = result.base;
467
717
  this.view = next;
468
718
  this.provenanceByScope = nextProvenance;
@@ -20,6 +20,9 @@ export interface SessionEntryLookup {
20
20
  export interface PassiveStopBoundary {
21
21
  at: number;
22
22
  from?: number;
23
+ preserveContext?: true;
24
+ /** A same-owner failed Stop remains a write fence until a later accepted checkpoint. */
25
+ persistenceError?: string;
23
26
  }
24
27
  export interface SnapshotDiscovery {
25
28
  candidates: unknown[];
@@ -30,6 +33,8 @@ export declare function discoverSnapshotData(branch: readonly BranchEntry[]): Sn
30
33
  export declare function snapshotDataNewestFirst(branch: readonly BranchEntry[]): unknown[];
31
34
  export declare function latestSnapshotData(branch: readonly BranchEntry[]): unknown;
32
35
  export declare function hasPriorConversation(branch: readonly BranchEntry[]): boolean;
36
+ /** Native conversation after the latest valid checkpoint may contain uncompiled work, not a new semantic authority. */
37
+ export declare function hasUncheckpointedConversation(branch: readonly BranchEntry[]): boolean;
33
38
  /** Auto-start eligibility is session identity/lifecycle, not the presence of CWD materialization. */
34
39
  export declare function isNewSession(reason: unknown, branch: readonly BranchEntry[]): boolean;
35
40
  export declare function findAssistantToolBatch(session: SessionEntryLookup, toolCallId: string): string[] | undefined;
@@ -1,3 +1,4 @@
1
+ import { parseRetainedPiCheckpoint } from "./snapshot.js";
1
2
  export const SNAPSHOT_ENTRY_TYPE = "state-flow-snapshot";
2
3
  /** Enumerate active-branch snapshots newest-first while containing hostile entries. */
3
4
  export function discoverSnapshotData(branch) {
@@ -36,6 +37,27 @@ export function hasPriorConversation(branch) {
36
37
  }
37
38
  return false;
38
39
  }
40
+ /** Native conversation after the latest valid checkpoint may contain uncompiled work, not a new semantic authority. */
41
+ export function hasUncheckpointedConversation(branch) {
42
+ for (let index = branch.length - 1; index >= 0; index--) {
43
+ try {
44
+ const entry = branch[index];
45
+ if (entry?.type === "message") {
46
+ const role = entry.message?.role;
47
+ if (role === "user" || role === "assistant" || role === "toolResult")
48
+ return true;
49
+ }
50
+ if (entry?.type === "custom" && entry.customType === SNAPSHOT_ENTRY_TYPE) {
51
+ parseRetainedPiCheckpoint(entry.data);
52
+ return false;
53
+ }
54
+ }
55
+ catch {
56
+ // Malformed entries cannot prove that pending input was checkpointed.
57
+ }
58
+ }
59
+ return false;
60
+ }
39
61
  /** Auto-start eligibility is session identity/lifecycle, not the presence of CWD materialization. */
40
62
  export function isNewSession(reason, branch) {
41
63
  if (reason === "new")
@@ -58,17 +80,29 @@ export function findAssistantToolBatch(session, toolCallId) {
58
80
  return undefined;
59
81
  }
60
82
  export function findPassiveStopBoundary(branch, sessionId, entryType) {
83
+ let checkpointSeen = false;
61
84
  for (const entry of [...branch].reverse()) {
62
85
  try {
63
- if (entry?.type !== "custom" || entry.customType !== entryType)
86
+ if (entry?.type !== "custom")
64
87
  continue;
65
- const { at, from, reset, owner } = entry.data ?? {};
88
+ if (entry.customType === SNAPSHOT_ENTRY_TYPE) {
89
+ parseRetainedPiCheckpoint(entry.data);
90
+ checkpointSeen = true;
91
+ continue;
92
+ }
93
+ if (entry.customType !== entryType)
94
+ continue;
95
+ const { at, from, reset, owner, preserveContext, persistenceError } = entry.data ?? {};
66
96
  if (reset === true && owner === sessionId)
67
97
  return undefined;
98
+ if (owner !== undefined && owner !== sessionId)
99
+ continue;
68
100
  if (typeof at === "number" && Number.isSafeInteger(at) && at >= 0)
69
101
  return {
70
102
  at,
71
103
  ...(typeof from === "number" && Number.isSafeInteger(from) && from >= 0 ? { from } : {}),
104
+ ...(preserveContext === true ? { preserveContext: true } : {}),
105
+ ...(!checkpointSeen && owner === sessionId && typeof persistenceError === "string" && persistenceError.trim().length > 0 ? { persistenceError } : {}),
72
106
  };
73
107
  }
74
108
  catch {
@@ -4,6 +4,9 @@ import { type TransitionBoundary } from "./temporal.ts";
4
4
  /** Missing operational capability is not evidence that a checkpoint target is invalid. */
5
5
  export declare class RevisionUnavailableError extends Error {
6
6
  }
7
+ /** Expired history cannot be restored, but explicit activation may use validated current memory. */
8
+ export declare class HistoryBoundaryExpiredError extends RevisionUnavailableError {
9
+ }
7
10
  export interface SnapshotConfig {
8
11
  enabled: boolean;
9
12
  }
@@ -8,6 +8,9 @@ const MAX_LEGACY_VALIDATION_ATTEMPT = 7;
8
8
  /** Missing operational capability is not evidence that a checkpoint target is invalid. */
9
9
  export class RevisionUnavailableError extends Error {
10
10
  }
11
+ /** Expired history cannot be restored, but explicit activation may use validated current memory. */
12
+ export class HistoryBoundaryExpiredError extends RevisionUnavailableError {
13
+ }
11
14
  export function validateSessionRuntime(value, cwd, sessionId) {
12
15
  if (!isJsonValue(value) || !isObject(value) || Object.keys(value).sort().join(",") !== "config,meta"
13
16
  || !isObject(value.config) || !isObject(value.meta))
@@ -26,6 +26,7 @@ export interface StatusDiagnostics {
26
26
  };
27
27
  staleArtifacts: readonly StaleArtifactDiagnostic[];
28
28
  durableStateError?: string;
29
+ publicationError?: string;
29
30
  }
30
31
  export declare function formatScopeRevisionVector(revisions: ScopeRevisions): string;
31
32
  export declare function compactStatus(snapshot: Snapshot, revisions: ScopeRevisions, colorize: Colorize): string | undefined;
@@ -1,5 +1,6 @@
1
1
  import { projectRecentTransitionsWithLimit } from "./history.js";
2
2
  import { retainedMemoryScopes } from "./memory.js";
3
+ import { conciseDiagnostic } from "./protocol.js";
3
4
  import { overlayStates } from "./state.js";
4
5
  export const STATUS_KEY = "state-flow";
5
6
  export function formatScopeRevisionVector(revisions) {
@@ -27,7 +28,7 @@ export function detailedStatus(snapshot, diagnostics) {
27
28
  ];
28
29
  const temporal = available ? diagnostics.temporal : undefined;
29
30
  const temporalLines = temporal === undefined
30
- ? [`Temporal materialization unavailable: ${diagnostics.durableStateError ?? "no selected branch runtime"}`,
31
+ ? [`Temporal materialization unavailable: ${conciseDiagnostic(diagnostics.durableStateError ?? "no selected branch runtime")}`,
31
32
  `Hot history: unavailable; configured maximum depth ${diagnostics.historyLimit}`,
32
33
  "Retained patch tails: unavailable"]
33
34
  : [`Temporal head: ${JSON.stringify(temporal.head.id)}; branch-local position ${temporal.head.position}`,
@@ -45,6 +46,7 @@ export function detailedStatus(snapshot, diagnostics) {
45
46
  "Memory: owner state-flow; global retention enabled; global fallback active",
46
47
  `Memory-bearing scopes: global ${memoryScopes?.global ?? "unknown"}; CWD ${memoryScopes?.cwd ?? "unknown"}; session ${memoryScopes?.session ?? "unknown"}`,
47
48
  ...temporalLines,
49
+ ...(diagnostics.publicationError === undefined ? [] : [`Memory writes paused after Stop: ${conciseDiagnostic(diagnostics.publicationError)}`]),
48
50
  `Artifacts: global ${artifacts("global")}; CWD ${artifacts("cwd")}; session ${artifacts("session")}; pending invalidations ${invalidated}`,
49
51
  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",
50
52
  ...invalidationLines,
@@ -14,6 +14,17 @@ export declare function initializeFileStore(root: string): void;
14
14
  export declare function acquirePublicationLock(path: string, unavailable: (cause: unknown) => Error): number;
15
15
  /** Canonical writers and bounded backup capture share exclusion; no Git work runs under this lock. */
16
16
  export declare function withStoragePublicationLock<T>(repositoryRoot: string, action: (root: string) => T): T;
17
+ export interface StorageTransaction {
18
+ readonly capture: typeof captureTemporalFileBase;
19
+ readonly publish: typeof publishTemporalStateToFiles;
20
+ }
21
+ export declare class PublicationBusyError extends Error {
22
+ constructor(path: string);
23
+ }
24
+ /** Await an exact file mutex; shared by canonical transactions and the independent Git backup owner. */
25
+ export declare function withFilePublicationLock<T>(lockPath: string, action: () => T | Promise<T>, signal?: AbortSignal, unavailable?: (cause: unknown) => Error, waitForLock?: boolean): Promise<T>;
26
+ /** Await store-wide exclusion, then capture/apply/publish through callback-scoped operations. */
27
+ export declare function withStorageTransaction<T>(repositoryRoot: string, action: (transaction: StorageTransaction) => T | Promise<T>, signal?: AbortSignal, waitForLock?: boolean): Promise<T>;
17
28
  export declare function assertTemporalFileBase(expected: TemporalFileBase, current: TemporalFileBase): void;
18
29
  /** Plan exact canonical updates; lifecycle-only writes exclude semantic files and provenance. */
19
30
  export declare function planTemporalPublication(cwd: string, sessionId: string, view: TemporalState, scopes: readonly StateScope[], current: TemporalFileBase, root: string, runtime?: SessionRuntime, runtimeOnly?: boolean, sessionKey?: string, provenance?: Readonly<Record<StateScope, ArtifactProvenanceRegistry>>): {