@llblab/pi-kit 0.28.0 → 0.29.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 (35) hide show
  1. package/BACKLOG.md +2 -1
  2. package/CHANGELOG.md +4 -0
  3. package/README.md +3 -3
  4. package/node_modules/@llblab/pi-state-flow/AGENTS.md +2 -2
  5. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +5 -2
  6. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +7 -0
  7. package/node_modules/@llblab/pi-state-flow/README.md +7 -8
  8. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +94 -47
  9. package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.d.ts +2 -2
  10. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +8 -27
  11. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +16 -151
  12. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +3 -4
  13. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +4 -3
  14. package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +1 -1
  15. package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +4 -1
  16. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +2 -0
  17. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +5 -1
  18. package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.d.ts +0 -4
  19. package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.js +0 -36
  20. package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
  21. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +20 -31
  22. package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +8 -9
  23. package/node_modules/@llblab/pi-state-flow/docs/filesystem-recovery.md +4 -4
  24. package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +18 -23
  25. package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +19 -21
  26. package/node_modules/@llblab/pi-state-flow/docs/usage.md +24 -25
  27. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +89 -48
  28. package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +2 -2
  29. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +22 -146
  30. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +6 -3
  31. package/node_modules/@llblab/pi-state-flow/lib/status.ts +4 -2
  32. package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +7 -1
  33. package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +0 -36
  34. package/node_modules/@llblab/pi-state-flow/package.json +1 -1
  35. package/package.json +2 -2
@@ -6,8 +6,8 @@ import { pruneArtifactProvenance } from "./artifact.js";
6
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 { HistoryBoundaryExpiredError, RevisionUnavailableError, createSessionRuntime, parseSessionRuntime, preRuntimeCheckpoint, retainedBoundaryCheckpoint } from "./snapshot.js";
10
- import { adoptTemporalStreams, advanceTemporalState, constrainTemporalState, createTemporalState, readTemporalScopes, readTemporalState, readTemporalView, selectScopeStreamAtBoundary, validateScopeLineage } from "./temporal.js";
9
+ import { RevisionUnavailableError, createSessionRuntime, parseSessionRuntime, preRuntimeCheckpoint, retainedBoundaryCheckpoint } from "./snapshot.js";
10
+ import { adoptTemporalStreams, advanceTemporalState, constrainTemporalState, createTemporalState, readTemporalScopes, readTemporalState, readTemporalView, validateScopeLineage } from "./temporal.js";
11
11
  const SCOPES = ["global", "cwd", "session"];
12
12
  const SHARED_SCOPES = ["global", "cwd"];
13
13
  function emptyProvenance() {
@@ -50,7 +50,6 @@ export class TemporalRuntime {
50
50
  semanticRevision;
51
51
  savedRuntime;
52
52
  transaction;
53
- restoredOriginPending = false;
54
53
  provenanceByScope = emptyProvenance();
55
54
  /** Shared scopes whose wholly absent live basis was accepted after one stale-target refusal. */
56
55
  absentSharedScopes = new Set();
@@ -101,7 +100,6 @@ export class TemporalRuntime {
101
100
  this.provenanceByScope = provenance;
102
101
  this.semanticRevision = undefined;
103
102
  this.savedRuntime = undefined;
104
- this.restoredOriginPending = false;
105
103
  this.absentSharedScopes.clear();
106
104
  return true;
107
105
  }
@@ -152,83 +150,6 @@ export class TemporalRuntime {
152
150
  }
153
151
  return result;
154
152
  }
155
- /** Prepare a retained session boundary from current canonical files; shared scopes remain live. */
156
- prepareBoundaryRestore(checkpoint) {
157
- if (!lstatSync(this.root, { throwIfNoEntry: false }))
158
- throw new RevisionUnavailableError("Selected State Flow boundary storage is unavailable");
159
- return this.prepareBoundaryRestoreBase(checkpoint, captureTemporalFileBase(this.cwd, this.sessionId, this.root, this.sessionKey));
160
- }
161
- prepareBoundaryRestoreBase(checkpoint, base) {
162
- const files = new Map(base.files.map((file) => [file.path, file.content]));
163
- const scopes = Object.fromEntries(SCOPES.map((scope) => {
164
- const paths = temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey);
165
- const stream = parseScopeStream(files.get(paths.checkpoint), files.get(paths.patches), scope, scope === "cwd" ? this.cwd : undefined, files.get(paths.meta));
166
- if (!stream)
167
- throw new RevisionUnavailableError(`State Flow ${scopeLabel(scope)} scope is unavailable for retained-boundary restoration`);
168
- return [scope, stream];
169
- }));
170
- const runtimePaths = sessionRuntimePaths(this.cwd, this.sessionId, this.root, this.sessionKey);
171
- const document = parseSessionRuntime(files.get(runtimePaths.config), files.get(runtimePaths.runtime), this.cwd, this.sessionId);
172
- if (!document)
173
- throw new RevisionUnavailableError("State Flow session runtime is unavailable for retained-boundary restoration");
174
- // The codecs validate persisted retention against the format maximum, not the new operator limit.
175
- validateScopeLineage(scopes.session, "session", document.meta.lineage, MAX_HISTORY_LIMIT);
176
- const boundary = document.meta.lineage.slice(-(this.historyLimit + 1)).find(({ id }) => id === checkpoint.boundary);
177
- if (!boundary)
178
- throw new HistoryBoundaryExpiredError("Selected State Flow history boundary is outside the retained temporal window");
179
- const selectedSession = selectScopeStreamAtBoundary(scopes.session, "session", boundary, MAX_HISTORY_LIMIT);
180
- const view = adoptTemporalStreams({
181
- global: scopes.global,
182
- cwd: scopes.cwd,
183
- session: selectedSession,
184
- }, `restore:${checkpoint.boundary}:${randomUUID()}`, this.historyLimit);
185
- const snapshot = {
186
- config: { mode: checkpoint.mode },
187
- meta: {
188
- step: checkpoint.step,
189
- ...(checkpoint.bootstrap === true ? { bootstrap: true } : {}),
190
- ...(checkpoint.specification === undefined ? {} : { specification: checkpoint.specification }),
191
- },
192
- };
193
- const provenance = Object.fromEntries(SCOPES.map((scope) => {
194
- const paths = temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey);
195
- const parsed = parseScopeProvenance(files.get(paths.meta), paths.meta);
196
- const retained = pruneArtifactProvenance(parsed, readTemporalState(view, 0, scope, this.historyLimit).artifacts);
197
- if (scope === "session") {
198
- // Live evidence cannot prove an earlier artifact version, even after a change-away-and-back.
199
- for (const record of scopes.session.patches) {
200
- if (record.transition.position <= boundary.position)
201
- continue;
202
- for (const path of Object.keys(record.patch.artifacts === null ? retained : record.patch.artifacts ?? {}))
203
- delete retained[path];
204
- }
205
- }
206
- return [scope, retained];
207
- }));
208
- let consumed = false;
209
- return {
210
- snapshot: structuredClone(snapshot),
211
- restore: () => {
212
- if (consumed)
213
- throw new Error("Prepared State Flow restore was already consumed");
214
- consumed = true;
215
- this.view = view;
216
- this.base = base;
217
- this.provenanceByScope = provenance;
218
- this.semanticRevision = undefined;
219
- this.savedRuntime = hashJson(createSessionRuntime(snapshot, this.cwd, this.sessionId, view.lineage, provenance.session));
220
- this.restoredOriginPending = true;
221
- this.absentSharedScopes.clear();
222
- return structuredClone(snapshot);
223
- },
224
- };
225
- }
226
- /** Restore and canonically accept one retained boundary as a single lifecycle operation. */
227
- restoreBoundary(checkpoint) {
228
- const prepared = this.prepareBoundaryRestore(checkpoint);
229
- const snapshot = prepared.restore();
230
- return { snapshot, publication: this.acceptRestoredOrigin(snapshot) };
231
- }
232
153
  /** Await a coherent read-only recovery view; this neither activates policy nor accepts publication authority. */
233
154
  async refreshCurrentMemory(signal) {
234
155
  signal?.throwIfAborted();
@@ -280,44 +201,9 @@ export class TemporalRuntime {
280
201
  this.provenanceByScope = provenance;
281
202
  this.semanticRevision = undefined;
282
203
  this.savedRuntime = undefined;
283
- this.restoredOriginPending = false;
284
204
  this.absentSharedScopes.clear();
285
205
  return snapshot;
286
206
  }
287
- /** Copy one retained source-session boundary over the child's current shared scopes. */
288
- prepareBoundaryFork(source, checkpoint) {
289
- const parent = Object.freeze({ ...source });
290
- if (parent.id === this.sessionId || parent.key === this.sessionKey)
291
- throw new Error("State Flow fork requires a distinct session identity and key");
292
- const sourceRuntime = new TemporalRuntime(this.cwd, parent, this.root, undefined, this.historyLimit);
293
- const prepared = sourceRuntime.prepareBoundaryRestore(checkpoint);
294
- prepared.restore();
295
- const snapshot = {
296
- config: structuredClone(prepared.snapshot.config),
297
- meta: {
298
- step: 0,
299
- ...(prepared.snapshot.meta.bootstrap === undefined ? {} : { bootstrap: prepared.snapshot.meta.bootstrap }),
300
- },
301
- };
302
- let consumed = false;
303
- return {
304
- snapshot: structuredClone(snapshot),
305
- fork: () => {
306
- if (consumed)
307
- throw new Error("Prepared State Flow fork was already consumed");
308
- consumed = true;
309
- if (this.view)
310
- throw new Error("State Flow fork target already has session storage");
311
- const publication = this.initializeOrigin(snapshot, { allowCreateCwd: false, copy: {
312
- stream: sourceRuntime.view.scopes.session,
313
- provenance: sourceRuntime.artifactProvenance("session"),
314
- } });
315
- if (!publication?.revision)
316
- throw new Error("State Flow fork requires existing shared scope storage");
317
- return { snapshot: structuredClone(snapshot), publication };
318
- },
319
- };
320
- }
321
207
  initialize(snapshot, allowCreateCwd, expectedShared, newSessionOrigin = false) {
322
208
  return this.initializeOrigin(snapshot, { allowCreateCwd, expectedShared, newSessionOrigin });
323
209
  }
@@ -493,8 +379,6 @@ export class TemporalRuntime {
493
379
  }
494
380
  /** Prepare current shared state while retaining the exact accepted private publication basis. */
495
381
  publicationCandidate(storage, current) {
496
- if (this.restoredOriginPending)
497
- throw new RevisionUnavailableError("State Flow restored origin must be accepted before patching memory");
498
382
  current ??= storage.capture(this.cwd, this.sessionId, this.root, this.sessionKey);
499
383
  const candidate = new TemporalRuntime(this.cwd, this.session, this.root, undefined, this.historyLimit);
500
384
  candidate.transaction = storage;
@@ -546,20 +430,16 @@ export class TemporalRuntime {
546
430
  async withStartTransaction(action, signal, allowCreateOrigin = false) {
547
431
  return this.withPublicationTransaction((_candidate, publish, current) => action(current, (snapshot) => publish(snapshot)), { kind: "start", allowCreateOrigin }, signal);
548
432
  }
549
- /** Select one retained private boundary beside current shared streams, then accept only after caller policy is rechecked. */
550
- async withRestoreTransaction(checkpoint, action, signal) {
433
+ /** Copy the parent's current session memory into an unoccupied child; the caller rechecks native selection after waiting. */
434
+ async withForkTransaction(source, lifecycle, action, signal) {
551
435
  signal?.throwIfAborted();
552
- return this.withPublicationTransaction((_candidate, publish, selected) => action(selected, (snapshot) => publish(snapshot)), { kind: "restore", checkpoint: structuredClone(checkpoint) }, signal);
436
+ return this.withPublicationTransaction((_candidate, publish, selected) => action(selected, (snapshot) => publish(snapshot)), { kind: "fork", source: { ...source }, lifecycle: { mode: lifecycle.mode, ...(lifecycle.bootstrap ? { bootstrap: true } : {}) } }, signal);
553
437
  }
554
- /** Copy exact retained parent authority into an unoccupied child; the caller rechecks native selection after waiting. */
555
- async withForkTransaction(source, checkpoint, action, signal) {
556
- signal?.throwIfAborted();
557
- return this.withPublicationTransaction((_candidate, publish, selected) => action(selected, (snapshot) => publish(snapshot)), { kind: "fork", source: { ...source }, checkpoint: structuredClone(checkpoint) }, signal);
558
- }
559
- prepareForkCandidate(storage, source, checkpoint) {
438
+ prepareForkCandidate(storage, source, lifecycle) {
560
439
  const sourceBase = storage.capture(this.cwd, source.id, this.root, source.key);
561
440
  const parent = new TemporalRuntime(this.cwd, source, this.root, undefined, this.historyLimit);
562
- const selected = parent.prepareBoundaryRestoreBase(checkpoint, sourceBase).restore();
441
+ if (!parent.loadCurrentMemoryBase(sourceBase))
442
+ throw new RevisionUnavailableError("State Flow fork parent memory is unavailable");
563
443
  const base = storage.capture(this.cwd, this.sessionId, this.root, this.sessionKey);
564
444
  const files = new Map(base.files.map((file) => [file.path, file.content]));
565
445
  const paths = temporalScopePaths(this.cwd, this.sessionId, "session", this.root, this.sessionKey);
@@ -580,7 +460,7 @@ export class TemporalRuntime {
580
460
  }
581
461
  return {
582
462
  candidate,
583
- current: { config: selected.config, meta: { step: 0, ...(selected.meta.bootstrap === undefined ? {} : { bootstrap: selected.meta.bootstrap }) } },
463
+ current: { config: { mode: lifecycle.mode }, meta: { step: 0, ...(lifecycle.bootstrap ? { bootstrap: true } : {}) } },
584
464
  assertSource: () => assertTemporalFileBase(sourceBase, storage.capture(this.cwd, source.id, this.root, source.key)),
585
465
  };
586
466
  }
@@ -599,7 +479,7 @@ export class TemporalRuntime {
599
479
  if (this.view)
600
480
  throw new Error("State Flow fork target already has session storage");
601
481
  }
602
- if (!semantic && (!this.semanticRevision || !this.view || !this.base || this.restoredOriginPending)) {
482
+ if (!semantic && (!this.semanticRevision || !this.view || !this.base)) {
603
483
  throw new Error("State Flow lifecycle transaction requires an accepted runtime");
604
484
  }
605
485
  };
@@ -607,8 +487,8 @@ export class TemporalRuntime {
607
487
  assertAuthority();
608
488
  if (kind === "patch" || allowCreateOrigin)
609
489
  initializeFileStore(this.root);
610
- if ((kind === "start" || kind === "restore" || kind === "fork") && !lstatSync(this.root, { throwIfNoEntry: false })) {
611
- throw new RevisionUnavailableError(kind !== "start" ? "Selected State Flow boundary storage is unavailable" : "Current State Flow session storage is unavailable");
490
+ if ((kind === "start" || kind === "fork") && !lstatSync(this.root, { throwIfNoEntry: false })) {
491
+ throw new RevisionUnavailableError(kind === "fork" ? "State Flow fork parent storage is unavailable" : "Current State Flow session storage is unavailable");
612
492
  }
613
493
  return withStorageTransaction(this.root, (storage) => {
614
494
  assertAuthority();
@@ -616,15 +496,13 @@ export class TemporalRuntime {
616
496
  let candidate;
617
497
  let assertSource;
618
498
  if (selection.kind === "fork") {
619
- ({ candidate, current, assertSource } = this.prepareForkCandidate(storage, selection.source, selection.checkpoint));
499
+ ({ candidate, current, assertSource } = this.prepareForkCandidate(storage, selection.source, selection.lifecycle));
620
500
  }
621
- else if (selection.kind === "restore" || selection.kind === "start") {
501
+ else if (selection.kind === "start") {
622
502
  candidate = new TemporalRuntime(this.cwd, this.session, this.root, undefined, this.historyLimit);
623
503
  candidate.transaction = storage;
624
504
  const base = storage.capture(this.cwd, this.sessionId, this.root, this.sessionKey);
625
- current = selection.kind === "restore"
626
- ? candidate.prepareBoundaryRestoreBase(selection.checkpoint, base).restore()
627
- : candidate.loadCurrentMemoryBase(base);
505
+ current = candidate.loadCurrentMemoryBase(base);
628
506
  if (!current) {
629
507
  if (!allowCreateOrigin)
630
508
  throw new RevisionUnavailableError("Current State Flow session memory is unavailable");
@@ -646,7 +524,7 @@ export class TemporalRuntime {
646
524
  assertSource?.();
647
525
  // Patch preparation without semantic work preserves complete accepted cohorts; selection accepts origins with configured folding.
648
526
  // New authority or wholly absent shared pairs still need normal atomic initialization; partial evidence already failed.
649
- const writeSemantic = semantic && (kind === "start" || kind === "restore" || kind === "fork" || accepted !== undefined || !this.semanticRevision
527
+ const writeSemantic = semantic && (kind === "start" || kind === "fork" || accepted !== undefined || !this.semanticRevision
650
528
  || SCOPES.some((scope) => Object.keys(provenance?.[scope] ?? {}).length > 0)
651
529
  || candidate.base.files.some((file) => file.content === undefined));
652
530
  // Every capability acceptance validates its captured cohort, including semantic no-ops.
@@ -658,7 +536,6 @@ export class TemporalRuntime {
658
536
  this.provenanceByScope = candidate.provenanceByScope;
659
537
  this.semanticRevision = candidate.semanticRevision;
660
538
  this.savedRuntime = candidate.savedRuntime;
661
- this.restoredOriginPending = false;
662
539
  this.absentSharedScopes.clear();
663
540
  published = true;
664
541
  return publication;
@@ -668,21 +545,9 @@ export class TemporalRuntime {
668
545
  return result;
669
546
  }, signal);
670
547
  }
671
- /** Canonically accept a prepared retained-boundary origin before lifecycle-only persistence. */
672
- acceptRestoredOrigin(snapshot) {
673
- if (!this.restoredOriginPending)
674
- throw new Error("State Flow has no prepared restored origin to accept");
675
- const publication = this.publish(snapshot, true);
676
- if (!publication)
677
- throw new Error("State Flow restored origin produced no canonical publication");
678
- this.restoredOriginPending = false;
679
- return publication;
680
- }
681
548
  publish(snapshot, semantic = false, accepted, options = {}) {
682
549
  if (!this.view || !this.base)
683
550
  throw new Error("State Flow temporal publication is unavailable; restore or initialize before accepting a transition");
684
- if (this.restoredOriginPending && !semantic)
685
- throw new Error("State Flow restored origin must be accepted before runtime-only persistence");
686
551
  // Explicit evidence updates still own a scope CAS, even if they equal the stale cache.
687
552
  const provenanceScopes = SCOPES.filter((scope) => Object.keys(options.provenance?.[scope] ?? {}).length > 0);
688
553
  const runtimeOnly = !semantic && accepted === undefined && provenanceScopes.length === 0;
@@ -4,9 +4,6 @@ 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
- }
10
7
  export type StateFlowMode = "active" | "passive" | "off";
11
8
  export type InactiveMode = Exclude<StateFlowMode, "active">;
12
9
  export declare function isStateFlowMode(value: unknown): value is StateFlowMode;
@@ -38,6 +35,8 @@ export interface StateFlowSnapshot {
38
35
  meta: SnapshotMeta;
39
36
  }
40
37
  export type Snapshot = StateFlowSnapshot;
38
+ /** Failed Active selection retains its policy and fences inference until explicit recovery or an inactive choice. */
39
+ export declare function isActiveRestorationBlocked(snapshot: Snapshot): boolean;
41
40
  export interface SessionRuntime {
42
41
  config: SnapshotConfig;
43
42
  meta: SnapshotMeta & {
@@ -85,5 +84,5 @@ export declare function retainedBoundaryCheckpoint(snapshot: Snapshot, boundary:
85
84
  export declare function preRuntimeCheckpoint(mode: StateFlowMode): PreRuntimeCheckpoint;
86
85
  /** Decode the retained-window checkpoint contract; legacy `enabled`/`{disabled:true}` markers map through `inactiveMode`. */
87
86
  export declare function parseRetainedPiCheckpoint(value: unknown, inactiveMode?: InactiveMode): RetainedPiCheckpoint;
88
- export declare function migrationFailure(data: JsonObject, error: string, mode?: InactiveMode): Snapshot;
87
+ export declare function migrationFailure(data: JsonObject, error: string, mode?: StateFlowMode): Snapshot;
89
88
  export {};
@@ -8,9 +8,6 @@ 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
- }
14
11
  export function isStateFlowMode(value) {
15
12
  return value === "active" || value === "passive" || value === "off";
16
13
  }
@@ -26,6 +23,10 @@ export function readCheckpointMode(value, inactiveMode = "passive") {
26
23
  return Object.hasOwn(value, "enabled") || !isStateFlowMode(value.mode) ? undefined : value.mode;
27
24
  return typeof value.enabled === "boolean" ? value.enabled ? "active" : inactiveMode : undefined;
28
25
  }
26
+ /** Failed Active selection retains its policy and fences inference until explicit recovery or an inactive choice. */
27
+ export function isActiveRestorationBlocked(snapshot) {
28
+ return snapshot.config.mode === "active" && snapshot.meta.validation?.attempt === 0;
29
+ }
29
30
  export function validateSessionRuntime(value, cwd, sessionId) {
30
31
  if (!isJsonValue(value) || !isObject(value) || Object.keys(value).sort().join(",") !== "config,meta"
31
32
  || !isObject(value.config) || !isObject(value.meta))
@@ -1,6 +1,6 @@
1
1
  import type { ArtifactInvalidationReason } from "./artifact.ts";
2
2
  import type { RecentTransitionWindow } from "./history.ts";
3
- import type { Snapshot } from "./snapshot.ts";
3
+ import { type Snapshot } from "./snapshot.ts";
4
4
  import { type SemanticState, type ScopedStates, type StateScope } from "./state.ts";
5
5
  import type { ScopeRevisions, TransitionBoundary } from "./temporal.ts";
6
6
  export declare const STATUS_KEY = "state-flow";
@@ -1,5 +1,6 @@
1
1
  import { ownedTopLevelKeys } from "./ownership.js";
2
2
  import { conciseDiagnostic } from "./protocol.js";
3
+ import { isActiveRestorationBlocked } from "./snapshot.js";
3
4
  import { overlayStates, projectSemanticState } from "./state.js";
4
5
  export const STATUS_KEY = "state-flow";
5
6
  export function formatScopeRevisionVector(revisions) {
@@ -9,7 +10,7 @@ export function compactStatus(snapshot, _revisions, colorize) {
9
10
  const { mode } = snapshot.config;
10
11
  if (mode === "off")
11
12
  return undefined;
12
- return `${colorize("accent", "state-flow")} ${colorize("dim", mode)}`;
13
+ return `${colorize("accent", "state-flow")} ${colorize("dim", isActiveRestorationBlocked(snapshot) ? "active (blocked)" : mode)}`;
13
14
  }
14
15
  function countArtifacts(states, scope) {
15
16
  return Object.keys(states[scope].artifacts).length;
@@ -62,6 +63,8 @@ export function detailedStatus(snapshot, diagnostics) {
62
63
  const artifacts = (scope) => countArtifacts(diagnostics.scopeStates, scope);
63
64
  const hasArtifacts = available && ["global", "cwd", "session"].some((scope) => artifacts(scope) > 0);
64
65
  return [
66
+ `Mode: ${snapshot.config.mode}`,
67
+ ...(isActiveRestorationBlocked(snapshot) ? ["Inference blocked: Active memory restoration failed. Use /state-flow-active for current memory or select Passive/Off for native context."] : []),
65
68
  `Repository: ${diagnostics.repositoryRoot}`,
66
69
  `Scope keys: CWD ${diagnostics.cwdScopeKey}; session ${diagnostics.sessionScopeKey}`,
67
70
  ...temporalLines,
@@ -6,6 +6,8 @@ export declare function stateFlowTelegramSectionSpecifiers(moduleUrl?: string):
6
6
  export interface StateFlowTelegramSnapshot {
7
7
  /** The current session's selected mode. */
8
8
  mode: StateFlowMode;
9
+ /** Failed Active restoration fences inference without silently selecting an inactive mode. */
10
+ inferenceBlocked?: boolean;
9
11
  /** Legacy branch step retained for existing adapter ports; current runtime ports also supply owner revisions. */
10
12
  step: number;
11
13
  revisions?: ScopeRevisions;
@@ -22,7 +22,7 @@ export function stateFlowTelegramSectionSpecifiers(moduleUrl = import.meta.url)
22
22
  }
23
23
  /** Main-menu section label shows only the current session mode. */
24
24
  export function formatStateFlowSectionLabel(snapshot) {
25
- return `🌀 State Flow: ${snapshot.mode}`;
25
+ return `🌀 State Flow: ${snapshot.mode}${snapshot.mode === "active" && snapshot.inferenceBlocked ? " (blocked)" : ""}`;
26
26
  }
27
27
  export const STATE_FLOW_MODES = ["off", "passive", "active"];
28
28
  const STATE_FLOW_MODE_LABELS = { off: "Off", passive: "Passive", active: "Active" };
@@ -44,6 +44,10 @@ export function buildStateFlowSectionView(snapshot, callbackData) {
44
44
  return {
45
45
  text: [
46
46
  `<b>🌀 State Flow:</b> <code>${snapshot.mode}</code>`,
47
+ ...(snapshot.mode === "active" && snapshot.inferenceBlocked ? [
48
+ "",
49
+ "<b>Inference blocked:</b> Active memory restoration failed. Select Active to accept current session memory, or Passive/Off to permit native context.",
50
+ ] : []),
47
51
  "",
48
52
  "<b>Mode</b> — choose a workflow (switching modes never erases stored memory):",
49
53
  "",
@@ -40,10 +40,6 @@ export declare function adoptTemporalStreams(scopes: Record<StateScope, ScopeStr
40
40
  export declare function createTemporalState(states: ScopedSemanticStates, id: string, historyLimit?: number): TemporalState;
41
41
  /** Fold retained tails to a lower configured limit without inventing history. */
42
42
  export declare function constrainTemporalState(view: TemporalState, historyLimit: number): TemporalState;
43
- /** Select one scope at a proven retained boundary from its owning runtime lineage. */
44
- export declare function selectScopeStreamAtBoundary(stream: ScopeStream, scope: StateScope, boundary: TransitionBoundary, historyLimit?: number): ScopeStream;
45
- /** Select one still-retained causal boundary without consulting an external history store. */
46
- export declare function selectTemporalStateBoundary(view: TemporalState, boundaryId: string, historyLimit?: number): TemporalState;
47
43
  /** Current independent scope revisions; Effective uses this vector rather than inventing a scalar owner. */
48
44
  export declare function temporalScopeRevisions(view: TemporalState): ScopeRevisions;
49
45
  /** Exact scope semantics for authored staging; defaults must never become implicit writes. */
@@ -200,42 +200,6 @@ export function constrainTemporalState(view, historyLimit) {
200
200
  validateTemporalState(next, historyLimit);
201
201
  return next;
202
202
  }
203
- /** Select one scope at a proven retained boundary from its owning runtime lineage. */
204
- export function selectScopeStreamAtBoundary(stream, scope, boundary, historyLimit = DEFAULT_HISTORY_LIMIT) {
205
- validateHistoryLimit(historyLimit);
206
- validateBoundary(boundary);
207
- validateScopeStream(stream, scope, historyLimit);
208
- if (boundary.position < stream.checkpoint.through.position) {
209
- throw new Error("Selected State Flow history boundary predates the retained scope checkpoint");
210
- }
211
- const selected = structuredClone(stream);
212
- const retained = selected.patches.filter(({ transition }) => transition.position <= boundary.position);
213
- selected.revision -= selected.patches.length - retained.length;
214
- selected.patches = retained;
215
- validateScopeStream(selected, scope, historyLimit);
216
- return selected;
217
- }
218
- /** Select one still-retained causal boundary without consulting an external history store. */
219
- export function selectTemporalStateBoundary(view, boundaryId, historyLimit = DEFAULT_HISTORY_LIMIT) {
220
- validateHistoryLimit(historyLimit);
221
- validateTemporalState(view, historyLimit);
222
- if (typeof boundaryId !== "string" || boundaryId.trim().length === 0)
223
- throw new Error("State Flow temporal boundary identity must be non-empty");
224
- const index = view.lineage.findIndex(({ id }) => id === boundaryId);
225
- if (index < 0)
226
- throw new Error("Selected State Flow history boundary is outside the retained temporal window");
227
- const target = view.lineage[index];
228
- const selected = structuredClone(view);
229
- selected.lineage = selected.lineage.slice(0, index + 1);
230
- for (const scope of SCOPES) {
231
- const stream = selected.scopes[scope];
232
- const retained = stream.patches.filter(({ transition }) => transition.position <= target.position);
233
- stream.revision -= stream.patches.length - retained.length;
234
- stream.patches = retained;
235
- }
236
- validateTemporalState(selected, historyLimit);
237
- return selected;
238
- }
239
203
  /** Current independent scope revisions; Effective uses this vector rather than inventing a scalar owner. */
240
204
  export function temporalScopeRevisions(view) {
241
205
  const revisions = {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-state-flow",
3
- "version": "0.25.5",
3
+ "version": "0.26.0",
4
4
  "license": "MIT",
5
5
  "private": false,
6
6
  "description": "Incremental scoped state/context/memory compiler for Pi, inspired by SKILL.state",
@@ -236,7 +236,7 @@ Explicit Start independently prepares the validated current same-session canonic
236
236
 
237
237
  - Incompatible but independently valid live shared streams require a fresh origin, not invented cross-writer history.
238
238
  - An empty session origin is allowed only with wholly absent private authority and initialization-safe branch provenance; incomplete or contradictory evidence stays closed.
239
- - Initial physical fork copying still requires its exact-source contract; an already accepted child can activate its own current memory without recopying its parent.
239
+ - Initial physical fork copying copies the parent's current memory; an already accepted child activates its own current memory without recopying its parent.
240
240
  - Repeated Start while already active changes neither the current run nor pending response reconciliation.
241
241
 
242
242
  ### Passive persistence
@@ -259,7 +259,7 @@ Off, session/tree selection, shutdown or an accepted Start revoke obsolete Passi
259
259
 
260
260
  **If persistence fails.** Accepted cached memory and all available native context are kept. The existing native passive-stop marker records `owner`, the selected inactive `mode`, `persistenceError` and `preserveContext: true`.
261
261
 
262
- - This is a same-session policy override and write fence, not semantic authority or a substitute checkpoint. No canonical failure is repaired by resetting memory.
262
+ - This is a same-session policy override and write fence, not semantic authority or a substitute checkpoint. No canonical failure is repaired by resetting memory. A later explicit Passive joins pending read-only recovery, then retries the shared current-memory acceptance used by Start; only accepted publication and its newer checkpoint clear the fence.
263
263
  - Memory-enabled tree/reload/resume with that pending marker use `TemporalRuntime.refreshCurrentMemory()` to validate and read current memory, without historical restoration or publication; unavailable evidence stays unavailable.
264
264
  - A later accepted checkpoint supersedes the fence while keeping the context boundary for bootstrap.
265
265
  - Repeating the same degraded choice is inert; another inactive choice updates only native policy and keeps the fence.
@@ -433,22 +433,16 @@ meta.json
433
433
  - Start uses ordinary origin acceptance, including configured retention folding, not lifecycle-only semantic-byte preservation.
434
434
  - The synchronous prepared Start API keeps its selected-cohort CAS contract but has no production caller.
435
435
 
436
- **`TemporalRuntime.withRestoreTransaction(checkpoint, action, signal?)`** detaches the retained pointer before waiting, then captures and validates its exact private boundary beside current shared streams under one exclusion.
436
+ Native startup, reload, resume and tree attachment use `withStartTransaction` over current same-session memory, applying the branch checkpoint's mode and run lifecycle. There is no API that rewinds memory to an older boundary.
437
437
 
438
- - `action(selectedSnapshot, publish)` rechecks caller selection/policy after waiting and accepts synchronously, once.
439
- - The candidate stays detached until publication. Expiry, incomplete evidence, cancellation and CAS failure never substitute current-head or empty memory.
440
- - Origin acceptance applies configured retention and causal provenance pruning, as the synchronous restore does.
441
- - Accepted memory is installed before control returns to caller checkpointing; a later failure cannot roll it back.
442
- - Native startup/tree restoration uses it through the extension's owned restoration lifetime.
438
+ **`TemporalRuntime.withForkTransaction(source, lifecycle, action, signal?)`** pins the parent identity before waiting; `lifecycle` supplies only the child's mode and bootstrap.
443
439
 
444
- **`TemporalRuntime.withForkTransaction(source, checkpoint, action, signal?)`** pins the parent identity and boundary before waiting.
445
-
446
- - Under one exclusion it selects exact retained parent authority and captures an unoccupied child target.
440
+ - Under one exclusion it loads the parent's current memory and captures an unoccupied child target, so the copy never depends on the native fork step.
447
441
  - The callback receives the child lifecycle (step zero, selected mode/bootstrap, no unfinished specification), rechecks native selection/policy, then publishes synchronously once.
448
442
  - Parent evidence is revalidated immediately before child publication, and the child's exact file cohort is CAS-protected. Only acceptance installs child memory.
449
443
  - Parent-private files stay untouched, shared provenance stays live, and configured folding still applies.
450
- - Expired, malformed or occupied selections never become an empty or newer-parent copy.
451
- - Native fork adoption and Start's exact-source retry use it.
444
+ - Missing, malformed or occupied evidence never becomes an empty or foreign copy.
445
+ - Native fork adoption and Start's fork retry use it.
452
446
 
453
447
  ### Run preparation and answer reconciliation
454
448
 
@@ -499,18 +493,17 @@ Without removals on a complete accepted cohort, publication preserves semantic/p
499
493
 
500
494
  - Off attaches without constructing a temporal runtime, resolving semantic boundaries, reading memory or emitting recovery warnings; native mode/write-fence bookkeeping stays available.
501
495
  - Deferred fork ownership is recorded in a child-owned `forkPending` native marker that survives extension recreation; accepted child initialization resets it.
502
- - Explicit Passive restores the deferred selected boundary (or fenced read-only current authority), while explicit Active keeps the documented current-memory activation after attachment.
503
- - Policy decoding is never proof that a historical boundary is valid.
496
+ - Explicit Passive or Active accepts validated current same-session memory (a pending fork copies the parent's current memory first); a write fence clears only after that acceptance.
504
497
  - Automatic callbacks stay memory-inert in Off; installed-client acceptance gates are tracked separately in [BACKLOG](../BACKLOG.md).
505
498
 
506
499
  **Memory-enabled restoration.** Memory-enabled native `session_start` and `session_tree`, and explicit acquisition after an Off attachment, await one extension-owned branch restoration.
507
500
 
508
- 1. **Synchronous prelude.** It revokes older selection work and pins the session id, file, header timestamp and CWD. It selects active-branch evidence through the recovery domain's pure `selectRetainedCheckpoint`: newer malformed envelopes are skipped, while revision pointers and every failure resolving the selected boundary fail closed.
501
+ 1. **Synchronous prelude.** It revokes older selection work and pins the session id, file, header timestamp and CWD. It reads the branch's newest supported checkpoint through the recovery domain's pure `selectRetainedCheckpoint` only for mode and run lifecycle (unfinished specification, bootstrap); its boundary is never a memory revision to rewind to. Revision pointers and unsupported envelopes fail closed.
509
502
  2. **While waiting,** the mode is the selected inactive policy, and private reads/publication report the pending selection.
510
- 3. **Acceptance.** The selected boundary, the exact-source fork, a truly new auto-start origin (`withStartTransaction` with creation authority, rechecking branch evidence after waiting) or failed-Stop read-only recovery then use the awaited runtime API. Bootstrap and policy are derived after waiting and published in that single acceptance.
503
+ 3. **Acceptance.** Same-session attachment accepts current memory through `withStartTransaction` under the checkpoint's mode; a fork copies the parent's current memory; a truly new auto-start origin uses `withStartTransaction` with creation authority (rechecking branch evidence after waiting); failed-Stop reload uses read-only recovery. Bootstrap and policy are derived after waiting and published in that single acceptance.
511
504
  4. **Installation.** Only the current lifetime installs memory, checkpoint, continuation, tools and UI before yielding; a later native-write failure only warns.
512
505
 
513
- **Failed Active selection.** An unaccepted Active restoration/fork or automatic Start failure carries an inference fence independently of the inactive fallback mode. The live `context` hook calls public `ctx.abort()` before returning; merely throwing would let Pi continue with native history. Re-selecting/reloading the failed Active boundary reconstructs the fence. Accepted Start or a valid branch releases it; explicit Passive/Off permits native context but grants no missing historical/publication authority. Signal-less inspection does not cancel or clear the fence.
506
+ **Failed Active selection.** Unreadable current memory (missing, corrupt, partial or contradictory files), a failed fork copy or automatic Start failure retains the selected Active mode with unavailable-memory validation. The inference fence is derived from that mode and validation, not an independent flag under an inactive fallback. No Active lifecycle work or fabricated projection runs before validated acceptance; shared reads may remain available. Terminal and Telegram show `active (blocked)` with explicit recovery guidance. The live `context` hook calls public `ctx.abort()` before returning; merely throwing would let Pi continue with native history. Reloading while current memory stays unreadable reconstructs the fence. Accepted Start or accepted explicit Passive releases it; explicit Off permits native context but grants no memory authority. Signal-less inspection does not cancel or clear the fence.
514
507
 
515
508
  **Interaction with mode choices:**
516
509
 
@@ -518,25 +511,21 @@ Without removals on a complete accepted cohort, publication preserves semantic/p
518
511
  - Passive keeps independently owned restoration, new-session initialization and fork copying, including attachment requested by a now-cancelled Active waiter. Off cancels those owned waits and keeps only native policy/source bookkeeping for later explicit acquisition.
519
512
  - The pending publisher applies the latest inactive policy inside its single acceptance. Passive can then patch memory without an artificial error fence, while Off exposes no model access. Repeated selections are inert with respect to that acceptance.
520
513
  - Start joins retained Passive acquisition. After Off, non-fork Active goes directly through one cancellable current-head Start transaction, keeping the deferred native selection and local Off policy until publication.
521
- - A superseding Passive cancels that activation and acquires its original retained private boundary, not the current-memory candidate. Native Abort silently withdraws Active, without inventing a write fence or losing later acquisition choices.
522
- - Accepted activation reconstructs any native continuation from its proven current memory. Exact-source fork acquisition keeps its independent restoration owner.
514
+ - A superseding Passive cancels that activation and acquires current memory itself. Native Abort silently withdraws Active, without inventing a write fence or losing later acquisition choices.
515
+ - Accepted activation reconstructs any native continuation from its proven current memory. Fork acquisition keeps its independent restoration owner.
523
516
  - Selection changes, shutdown and native operation cancellation still revoke obsolete work. Real validation/publication failures stay unavailable rather than becoming empty memory.
524
517
  - Start joins pending restoration, owns initial attachment and fork retry without cancelling itself, and is inert when the result is already active. A cancelled Start withdraws its join without cancelling independently owned restoration or Stop persistence.
525
518
  - Read-only recovery uses detached candidates and rechecks cancellation and physical identity before host installation. Passive attachment cannot overwrite a newer cache established by an intervening patch or inspection.
526
519
 
527
- Six synchronous runtime methods remain supported for library consumers and local tests/benchmarks; production lifecycle wiring uses the awaited APIs. Their contracts and cancellation boundaries are documented in [library API compatibility](compatibility.md#state-flow-library-api-compatibility).
520
+ Two synchronous runtime methods (`loadPassive`, `initialize`) remain supported for library consumers and local tests/benchmarks; production lifecycle wiring uses the awaited APIs. Their contracts and cancellation boundaries are documented in [library API compatibility](compatibility.md#state-flow-library-api-compatibility).
528
521
 
529
522
  **What stays outside critical sections.** The advisory [continuation-candidate reader](#session-continuation) also awaits coherent capture, and current-head activation on an attached branch uses the awaited Start transaction. Raw precomputed replay keeps its selected-target guard, unlike current-head authored patch staging. Model inference, source acquisition and Git commands do not belong inside a canonical critical section; artifact freshness validation stays part of publication validation.
530
523
 
531
524
  ## Optional Git backup
532
525
 
533
- Canonical files always own semantic persistence, current materialization and retained hot history. Installing Git beside the store does not change authority or enable cold semantic restoration. Retained-boundary restoration selects private session history from the current canonical lineage while global/CWD scopes stay live; expired boundaries fail closed.
534
-
535
- **Artifact provenance across restore/fork.** Scope artifact provenance records only current evidence.
526
+ Canonical files always own semantic persistence, current materialization and retained hot history. Installing Git beside the store does not change authority or enable cold semantic restoration. Branch navigation attaches current memory, so artifacts keep their current semantics and current evidence together.
536
527
 
537
- - Restore/fork drops session provenance for paths touched by any retained session patch after the selected boundary, including a change-away-and-back or a whole-artifacts-plane deletion. Path existence or equal final values cannot substitute for that causal check.
538
- - Selected artifact semantics stay intact with unavailable evidence until a stable explicit read and compilation.
539
- - Untouched paths, provenance-only refreshes of unchanged semantics, and live shared provenance stay usable.
528
+ **Artifact provenance across attachment and fork.** Scope artifact provenance records only current evidence. Attachment keeps current semantics with their current evidence, and a fork copies the parent's current session provenance, so no historical pruning is needed.
540
529
 
541
530
  ### Local backup commit
542
531
 
@@ -812,9 +801,9 @@ See the [fork contract](fork-contract.md) and [operating limits](usage.md#fork-s
812
801
 
813
802
  Status is a projection of the selected runtime and semantic view, not a second store.
814
803
 
815
- - Compact terminal status uses accent `state-flow` with dim `active` or `passive`, and is hidden in Off.
816
- - Telegram main-menu status uses `State Flow: active`, `State Flow: passive` or `State Flow: off`.
817
- - `/state-flow-status` keeps the `g#c#s#` vector beside concise diagnostics and blank-line-separated top-level semantic JSON.
804
+ - Compact terminal status uses accent `state-flow` with dim `active` or `passive`, and is hidden in Off. Failed Active restoration stays visible as `active (blocked)`.
805
+ - Telegram main-menu status uses `State Flow: active`, `State Flow: passive` or `State Flow: off`, with the same blocked qualifier for failed Active restoration and an actionable submenu notice.
806
+ - `/state-flow-status` reports selected mode and any inference fence explicitly, then keeps the `g#c#s#` vector beside concise diagnostics and blank-line-separated top-level semantic JSON.
818
807
  - Mode is read directly from the session's selected enum, never derived from a pending patch or separate passive flags.
819
808
  - Requested Global, CWD and Session Rich snapshots show their independent `#revision`, while Effective shows the vector. Global/CWD Rich views omit the empty structural response placeholder; Session and Effective expose the Session-owned response.
820
809
  - Missing evidence stays unavailable instead of appearing empty.
@@ -832,7 +821,7 @@ Status is a projection of the selected runtime and semantic view, not a second s
832
821
  - Memory-enabled inspection keeps its accepted-private/shared-refresh and failed-Passive cache rules.
833
822
  - Off inspection uses a fresh disposable `TemporalRuntime`. Global/CWD use coherent shared reads; Session/Effective use `refreshCurrentMemory` and reject absent, incomplete or malformed same-session private evidence, rather than manufacturing an empty layer/revision or borrowing foreign private data.
834
823
  - No reader installs the extension runtime/cache, changes native policy or canonical bytes, clears a carried write fence, acquires an unaccepted fork parent or replaces the selected historical target.
835
- - Expired selected history may coexist with inspectable current stored memory. A later Passive still restores the selected past fail-closed, while Active validates current memory.
824
+ - Branch steps never select a past memory revision; inspection and acquisition always use current stored memory.
836
825
  - The awaited `StateFlowTelegramInspectionPort` returns a `StateFlowTelegramInspection` containing state and matching revisions, plus an optional revocation signal. A result checks its revocation lifetime and physical owner immediately before presentation.
837
826
  - Stop, selection changes and shutdown cancel obsolete reads without altering newer memory or clearing write fences.
838
827