@llblab/pi-kit 0.28.0 → 0.29.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 (38) hide show
  1. package/BACKLOG.md +3 -1
  2. package/CHANGELOG.md +8 -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 +1 -29
  6. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +12 -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/logging.js +1 -1
  10. package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.d.ts +2 -2
  11. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +8 -27
  12. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +16 -151
  13. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +3 -4
  14. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +4 -3
  15. package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +1 -1
  16. package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +4 -1
  17. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +2 -0
  18. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +6 -2
  19. package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.d.ts +0 -4
  20. package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.js +0 -36
  21. package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
  22. package/node_modules/@llblab/pi-state-flow/docs/agent-contract-relocation.md +1 -1
  23. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +20 -31
  24. package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +8 -9
  25. package/node_modules/@llblab/pi-state-flow/docs/filesystem-recovery.md +4 -4
  26. package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +18 -23
  27. package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +19 -21
  28. package/node_modules/@llblab/pi-state-flow/docs/usage.md +26 -27
  29. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +89 -48
  30. package/node_modules/@llblab/pi-state-flow/lib/logging.ts +1 -1
  31. package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +2 -2
  32. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +22 -146
  33. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +6 -3
  34. package/node_modules/@llblab/pi-state-flow/lib/status.ts +4 -2
  35. package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +8 -2
  36. package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +0 -36
  37. package/node_modules/@llblab/pi-state-flow/package.json +1 -1
  38. package/package.json +2 -2
@@ -30,7 +30,7 @@ import type { RehydrationPhase } from "./rehydration.ts";
30
30
  import { TemporalRuntime, type RuntimePublication } from "./runtime.ts";
31
31
  import { discoverSnapshotData, findAssistantToolBatch, findBranchPolicy, findPassiveStopBoundary, hasPendingFork, hasPriorConversation, hasUncheckpointedConversation, isNewSession, retainsPhysicalSessionProjection, SNAPSHOT_ENTRY_TYPE } from "./session.ts";
32
32
  import { hasCompiledSkillArtifact, hashSkillSource, registeredSkillResolver, SkillReadTracker, type SuccessfulSkillRead } from "./skills.ts";
33
- import { HistoryBoundaryExpiredError, emptySnapshot, migrationFailure, preRuntimeCheckpoint, type InactiveMode, type RetainedBoundaryCheckpoint, type RetainedPiCheckpoint, type Snapshot, type StateFlowMode } from "./snapshot.ts";
33
+ import { emptySnapshot, isActiveRestorationBlocked, migrationFailure, preRuntimeCheckpoint, type InactiveMode, type RetainedBoundaryCheckpoint, type RetainedPiCheckpoint, type Snapshot, type StateFlowMode } from "./snapshot.ts";
34
34
  import { emptyState, projectStateForModel, type AtomicScopePatches, type SemanticState, type ModelState, type ScopedStates, type StateScope } from "./state.ts";
35
35
  import { compactStatus, detailedStatus, STATUS_KEY, type StatusDiagnostics } from "./status.ts";
36
36
  import { PublicationBusyError } from "./storage.ts";
@@ -80,7 +80,7 @@ interface InactivePersistence extends ModeOperation {
80
80
  type BranchSelection =
81
81
  | { kind: "settled" }
82
82
  | { kind: "current"; mode: InactiveMode }
83
- | { kind: "restore"; checkpoint: RetainedBoundaryCheckpoint }
83
+ | { kind: "attach"; checkpoint: RetainedBoundaryCheckpoint }
84
84
  | { kind: "fork"; checkpoint: RetainedBoundaryCheckpoint; source: SessionAddress }
85
85
  | { kind: "auto-start" };
86
86
 
@@ -93,7 +93,6 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
93
93
  let scopeStates: ScopedStates = { global: emptyState(), cwd: emptyState(), session: emptyState() };
94
94
  let effectiveState: SemanticState = {};
95
95
  let branchStartsWithoutRuntime = false;
96
- let selectedBoundaryError: { expired: boolean; blockInference: boolean } | undefined;
97
96
  let modePersistenceError: string | undefined;
98
97
  /** One pending inactive-mode persistence; later inactive choices coalesce until it publishes. */
99
98
  const inactivePersistence = new OwnedOperationSlot<InactivePersistence>();
@@ -177,11 +176,12 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
177
176
  if (modePersistenceError) throw new Error(`Memory writes paused after mode change: ${modePersistenceError}; use /state-flow-active`);
178
177
  }
179
178
 
179
+ /** Active lifecycle work requires validated memory, not merely a selected Active policy. */
180
180
  function isActive(): boolean {
181
- return snapshot.config.mode === "active";
181
+ return snapshot.config.mode === "active" && !isActiveRestorationBlocked(snapshot);
182
182
  }
183
183
 
184
- /** An unavailable or pending active selection keeps the configured inactive policy, never an invented one. */
184
+ /** Pending acquisition has no accepted Active memory yet. */
185
185
  function inactiveSelection(mode: StateFlowMode): InactiveMode {
186
186
  return mode === "active" ? config.inactiveMode : mode;
187
187
  }
@@ -448,7 +448,6 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
448
448
  runtime = undefined;
449
449
  snapshot = emptySnapshot("off");
450
450
  branchStartsWithoutRuntime = withoutRuntime;
451
- selectedBoundaryError = undefined;
452
451
  modePersistenceError = persistenceError;
453
452
  forkInitialization = reason === "fork";
454
453
  installScopeStates();
@@ -464,7 +463,6 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
464
463
  runtime = createRuntime(ctx);
465
464
  installScopeStates();
466
465
  branchStartsWithoutRuntime = false;
467
- selectedBoundaryError = undefined;
468
466
  forkInitialization = sessionStartReason === "fork";
469
467
  let selection: BranchSelection = { kind: "settled" };
470
468
  let skipped = 0;
@@ -492,12 +490,10 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
492
490
  selection = { kind: "fork", source, checkpoint: sourceFence?.persistenceError !== undefined
493
491
  ? { ...selected.checkpoint, mode: sourceFence.mode ?? config.inactiveMode } : selected.checkpoint };
494
492
  } catch (error) {
495
- selectedBoundaryError = { expired: error instanceof HistoryBoundaryExpiredError,
496
- blockInference: selected.checkpoint.mode === "active" && requestedMode === undefined };
497
- snapshot = selectedBoundaryFailure(diagnosticText(error), inactiveSelection(selected.checkpoint.mode));
493
+ snapshot = selectedBoundaryFailure(diagnosticText(error), requestedMode ?? selected.checkpoint.mode);
498
494
  }
499
495
  } else if (selected.kind === "boundary") {
500
- selection = { kind: "restore", checkpoint: selected.checkpoint };
496
+ selection = { kind: "attach", checkpoint: selected.checkpoint };
501
497
  } else if (selected.kind === "pre-runtime") {
502
498
  snapshot = emptySnapshot(selected.mode);
503
499
  } else if (discovery.candidates.length > 0) {
@@ -551,10 +547,9 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
551
547
  };
552
548
  let accepted = false;
553
549
  const failSelection = (error: unknown): void => {
554
- selectedBoundaryError = { expired: error instanceof HistoryBoundaryExpiredError,
555
- blockInference: pending.requestedMode === undefined && (selection.kind === "auto-start"
556
- || ((selection.kind === "restore" || selection.kind === "fork") && selection.checkpoint.mode === "active")) };
557
- snapshot = selectedBoundaryFailure(diagnosticText(error), inactiveSelection(snapshot.config.mode));
550
+ const mode = pending.requestedMode ?? (selection.kind === "auto-start" ? "active"
551
+ : selection.kind === "attach" || selection.kind === "fork" ? selection.checkpoint.mode : snapshot.config.mode);
552
+ snapshot = selectedBoundaryFailure(diagnosticText(error), mode);
558
553
  installScopeStates();
559
554
  };
560
555
  // Install accepted memory before native writes; later ancillary failures cannot revert or replay it.
@@ -584,9 +579,17 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
584
579
  snapshot = current ? deactivateEpisode(current, pending.requestedMode ?? selection.mode)
585
580
  : migrationFailure({}, "Current State Flow session memory is unavailable", pending.requestedMode ?? selection.mode);
586
581
  installScopeStates();
587
- } else if (selection.kind === "restore") {
588
- await candidate.withRestoreTransaction(selection.checkpoint, (restored, publish) => {
582
+ } else if (selection.kind === "attach") {
583
+ // Memory is the current same-session files; the Pi checkpoint supplies only mode and run lifecycle.
584
+ await candidate.withStartTransaction((current, publish) => {
589
585
  assertCurrent();
586
+ if (!current) throw new Error("Current State Flow session memory is unavailable");
587
+ const { checkpoint } = selection;
588
+ const restored: Snapshot = { config: { mode: checkpoint.mode }, meta: {
589
+ step: current.meta.step,
590
+ ...(checkpoint.bootstrap ? { bootstrap: true } : {}),
591
+ ...(checkpoint.specification === undefined ? {} : { specification: checkpoint.specification }),
592
+ } };
590
593
  // Canceled preparation or boundary continuation may have no specification. Retain uncompiled native context.
591
594
  if (restored.config.mode === "active" && restored.meta.specification === undefined
592
595
  && retainsPhysicalSessionProjection(reason) && hasUncheckpointedConversation(ctx.sessionManager.getBranch())) restored.meta.bootstrap = true;
@@ -595,6 +598,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
595
598
  accept(candidate, next, appendCheckpoint);
596
599
  }, signal);
597
600
  } else if (selection.kind === "fork") {
601
+ // The runtime copies the parent's current memory; the checkpoint supplies only child mode/bootstrap.
598
602
  await candidate.withForkTransaction(selection.source, selection.checkpoint, (child, publish) => {
599
603
  assertCurrent();
600
604
  // Mode is selected independently of creating the child's private memory.
@@ -632,7 +636,8 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
632
636
  if (accepted) return;
633
637
  }
634
638
  settleSelection(ctx, reason, notifyRecovery, skipped, selected !== placeholder);
635
- if (snapshot.config.mode !== "passive" || runtime?.view) return;
639
+ if (snapshot.config.mode === "off" || isActive() || runtime?.view) return;
640
+ // Unavailable Active memory may expose shared reads, never substitute private memory or model context.
636
641
  const passive = await loadPassiveView(ctx, signal, isCurrent, notifyRecovery);
637
642
  if (passive === undefined) return;
638
643
  assertCurrent();
@@ -674,8 +679,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
674
679
  function settleSelection(ctx: ExtensionContext, reason: unknown, notifyRecovery: boolean, skipped: number, selectedMemory: boolean): void {
675
680
  const failure = !isActive() && snapshot.meta.validation?.attempt === 0 ? snapshot.meta.validation.error : undefined;
676
681
  if (failure !== undefined && notifyRecovery && !modePersistenceError) {
677
- if (selectedBoundaryError?.expired) notifyProblem(ctx, "State Flow history is outside the retained temporal window; /state-flow-active can use current session memory.", "warning");
678
- else notifyProblem(ctx, `State Flow restore failed: ${failure}`, "error");
682
+ notifyProblem(ctx, `State Flow restore failed: ${failure}`, "error");
679
683
  }
680
684
  const continuation = selectedMemory ? selectedContinuation(ctx, reason) : undefined;
681
685
  if (continuation !== undefined) {
@@ -884,19 +888,26 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
884
888
  }
885
889
  }
886
890
 
887
- async function activateCurrentState(ctx: ExtensionContext, pending: AbortController): Promise<StateFlowTelegramControlResult> {
891
+ /** Explicit Active or Passive accepts validated current same-session memory without claiming unavailable history. */
892
+ async function activateCurrentState(
893
+ ctx: ExtensionContext, pending: AbortController, mode: "active" | "passive" = "active",
894
+ owns = () => startActivation.current?.controller === pending,
895
+ ): Promise<StateFlowTelegramControlResult> {
888
896
  let selected = runtime;
889
897
  const owner = ctx.sessionManager.getSessionId();
890
898
  const sameSession = sessionIdentity(ctx);
891
- const initiallyActive = isActive();
899
+ const policy = () => `${snapshot.config.mode}:${isActive()}`;
900
+ const initialPolicy = policy();
901
+ const acceptedPolicy = `${mode}:${mode === "active"}`;
902
+ const active = mode === "active";
892
903
  let accepted = false;
893
904
  let receipt = AbortSignal.any([pending.signal, sharedInspectionLifetime.signal]);
894
905
  const signal = ctx.signal ? AbortSignal.any([pending.signal, ctx.signal]) : pending.signal;
895
- const isCurrent = () => startActivation.current?.controller === pending && !signal.aborted && runtime === selected && !shuttingDown
896
- && sameSession() && isActive() === (accepted || initiallyActive);
906
+ const isCurrent = () => owns() && !signal.aborted && runtime === selected && !shuttingDown
907
+ && sameSession() && policy() === (accepted ? acceptedPolicy : initialPolicy);
897
908
  const superseded = (): StateFlowTelegramControlResult => {
898
909
  pending.abort();
899
- return { ok: false, message: "State Flow activation was superseded", signal: pending.signal };
910
+ return { ok: false, message: active ? "State Flow activation was superseded" : "State Flow mode change was superseded", signal: pending.signal };
900
911
  };
901
912
  try {
902
913
  const activation = createRuntime(ctx);
@@ -910,28 +921,29 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
910
921
  const result = await activation.withStartTransaction((current, publish) => {
911
922
  signal.throwIfAborted();
912
923
  if (!isCurrent()) throw new Error("State Flow activation selection changed while awaiting publication");
913
- if (!current && !branchStartsWithoutRuntime) throw new Error(snapshot.meta.validation?.error ?? "Current State Flow session memory is unavailable");
924
+ if (!current && (!active || !branchStartsWithoutRuntime)) throw new Error(snapshot.meta.validation?.error ?? "Current State Flow session memory is unavailable");
914
925
  const boundary = deferred && retainsPhysicalSessionProjection(deferred.reason)
915
926
  ? findPassiveStopBoundary(ctx.sessionManager.getBranch(), owner, PASSIVE_STOP_ENTRY_TYPE) : undefined;
916
927
  const bootstrap = hasPriorConversation(ctx.sessionManager.getBranch()) || passiveContinuation !== undefined || boundary !== undefined;
917
- const activated = current ? resumeEpisode(current, bootstrap) : startEpisode(bootstrap);
918
- const recoveredCurrent = selectedBoundaryError?.expired;
928
+ const activated = !active ? deactivateEpisode(current!, "passive")
929
+ : current ? resumeEpisode(current, bootstrap) : startEpisode(bootstrap);
930
+ const resumedWrites = modePersistenceError !== undefined;
919
931
  publish(activated);
920
932
  accepted = true;
921
- inactivePersistence.cancel();
933
+ // Passive acceptance runs inside its own inactive-persistence owner.
934
+ if (active) inactivePersistence.cancel();
922
935
  runtime = selected = activation;
923
936
  snapshot = activated;
924
937
  activeContext = ctx;
925
- selectedBoundaryError = undefined;
926
938
  modePersistenceError = undefined;
927
939
  installScopeStates();
928
940
  const continuation = passiveContinuation ?? bootstrapContinuation ?? (deferred ? selectedContinuation(ctx, deferred.reason) : undefined);
929
941
  deferredBranch = undefined;
930
942
  clearRunTransient();
931
943
  contextProjection.reset();
932
- passiveContinuation = undefined;
933
- bootstrapContinuation = snapshot.meta.bootstrap ? continuation : undefined;
934
- deferInferencePreparation();
944
+ passiveContinuation = active ? undefined : continuation;
945
+ bootstrapContinuation = active && snapshot.meta.bootstrap ? continuation : undefined;
946
+ if (active) deferInferencePreparation();
935
947
  receipt = AbortSignal.any([pending.signal, sharedInspectionLifetime.signal]);
936
948
  recordPublication();
937
949
  syncStateFlowTools();
@@ -941,21 +953,21 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
941
953
  pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, { reset: true, owner });
942
954
  }
943
955
  appendCheckpoint();
944
- ctx.ui.notify(
945
- recoveredCurrent
946
- ? "State Flow active from current session memory; unavailable historical state was not restored."
947
- : snapshot.meta.bootstrap
948
- ? "State Flow active. The next complete agent run will migrate active context into state."
949
- : "State Flow active. The next prompt starts a stateful agent run.",
950
- "info",
951
- );
956
+ if (!active) {
957
+ if (resumedWrites) ctx.ui.notify("State Flow passive; memory writes resumed from current session memory.", "info");
958
+ return { ok: true, message: "State Flow passive", signal: receipt };
959
+ }
960
+ ctx.ui.notify(snapshot.meta.bootstrap
961
+ ? "State Flow active. The next complete agent run will migrate active context into state."
962
+ : "State Flow active. The next prompt starts a stateful agent run.", "info");
952
963
  return { ok: true, message: "State Flow active", signal: receipt };
953
- }, signal, branchStartsWithoutRuntime);
964
+ }, signal, active && branchStartsWithoutRuntime);
954
965
  return isCurrent() ? result : superseded();
955
966
  } catch (error) {
956
967
  if (!isCurrent()) return superseded();
957
- const message = conciseDiagnostic(`${accepted ? "State Flow active; lifecycle update failed" : "State Flow activation failed"}: ${diagnosticText(error)}`);
958
- notifyProblem(ctx, message, accepted ? "warning" : "error");
968
+ const message = conciseDiagnostic(`${accepted ? `State Flow ${mode}; lifecycle update failed` : active ? "State Flow activation failed" : "State Flow passive acceptance failed"}: ${diagnosticText(error)}`);
969
+ // Failed Passive acceptance falls back to its caller's write-fence report.
970
+ if (active || accepted) notifyProblem(ctx, message, accepted ? "warning" : "error");
959
971
  return { ok: accepted, message, signal: receipt };
960
972
  }
961
973
  }
@@ -987,10 +999,14 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
987
999
  },
988
1000
  });
989
1001
 
1002
+ /** Unavailable selection or failed persistence needs current-memory acceptance, not a sticky Passive write fence. */
1003
+ function passiveRecoveryNeeded(mode: InactiveMode): boolean {
1004
+ return mode === "passive" && (modePersistenceError !== undefined || snapshot.meta.validation?.attempt === 0);
1005
+ }
1006
+
990
1007
  /** Local policy, tools and UI change before any canonical wait. */
991
1008
  function applyInactiveMode(ctx: ExtensionContext, mode: InactiveMode): void {
992
1009
  snapshot = deactivateEpisode(snapshot, mode);
993
- if (selectedBoundaryError) selectedBoundaryError.blockInference = false;
994
1010
  clearRunTransient();
995
1011
  syncStateFlowTools();
996
1012
  updateUi(ctx);
@@ -1019,7 +1035,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1019
1035
  if (snapshot.config.mode === mode) {
1020
1036
  const retained = selectRetainedCheckpoint(discoverSnapshotData(ctx.sessionManager.getBranch()).candidates, config.inactiveMode);
1021
1037
  const recordedMode = retained.kind === "pre-runtime" ? retained.mode : retained.kind === "boundary" ? retained.checkpoint.mode : undefined;
1022
- if (modePersistenceError || recordedMode === mode) return Promise.resolve({ ok: true, message: `State Flow is already ${mode}` });
1038
+ if (!passiveRecoveryNeeded(mode) && (modePersistenceError || recordedMode === mode)) return Promise.resolve({ ok: true, message: `State Flow is already ${mode}` });
1023
1039
  }
1024
1040
  inactivePersistence.cancel();
1025
1041
  const pending = inactivePersistence.claim({ controller: new AbortController() });
@@ -1071,7 +1087,6 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1071
1087
  acquisition.reset();
1072
1088
  runtime = undefined;
1073
1089
  snapshot = emptySnapshot("off");
1074
- selectedBoundaryError = undefined;
1075
1090
  branchStartsWithoutRuntime = preRuntime;
1076
1091
  forkInitialization = pendingFork;
1077
1092
  deferredBranch = { owner: address.key, cwd: ctx.cwd, reason: pendingFork ? "fork" : undefined };
@@ -1113,8 +1128,33 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1113
1128
  const wasActive = current.config.mode === "active";
1114
1129
  let unfinished = current.meta.specification !== undefined
1115
1130
  || (inferencePreparation.current?.prompt !== undefined && !inferencePreparation.current.accepted);
1131
+ const recoverPassive = passiveRecoveryNeeded(mode);
1116
1132
  applyInactiveMode(ctx, mode);
1117
1133
  const stoppedAt = Date.now();
1134
+ if (recoverPassive) {
1135
+ const superseded = (): StateFlowTelegramControlResult => ({ ok: false, message: "State Flow mode change was superseded" });
1136
+ const owns = () => inactivePersistence.owns(pending) && !pending.controller.signal.aborted && !shuttingDown;
1137
+ try {
1138
+ // Join pending read-only recovery so its installation cannot supersede this acceptance.
1139
+ const restoring = branchRestoration.current?.operation;
1140
+ if (restoring) await waitForRecovery(restoring, pending.controller.signal);
1141
+ } catch {
1142
+ return superseded();
1143
+ }
1144
+ if (!owns()) return superseded();
1145
+ // Passive is a stable mode: explicit selection accepts validated current memory instead of keeping a write fence.
1146
+ const recovered = await activateCurrentState(ctx, pending.controller, "passive", () => inactivePersistence.owns(pending));
1147
+ if (recovered.ok) pending.published = true;
1148
+ if (recovered.ok || !owns()) return recovered;
1149
+ if (current.config.mode === mode && modePersistenceError) {
1150
+ // The existing native fence already records this policy; report why acceptance still failed.
1151
+ pending.published = true;
1152
+ const message = conciseDiagnostic(`State Flow passive; memory writes remain paused: ${recovered.message}`);
1153
+ notifyProblem(ctx, message, "warning");
1154
+ return { ok: true, message };
1155
+ }
1156
+ selected = runtime;
1157
+ }
1118
1158
  if (modePersistenceError) {
1119
1159
  // Writes stay paused; the native marker records only the newly selected inactive policy.
1120
1160
  pending.published = true;
@@ -1217,6 +1257,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1217
1257
  port: {
1218
1258
  snapshot: () => ({
1219
1259
  mode: snapshot.config.mode,
1260
+ inferenceBlocked: isActiveRestorationBlocked(snapshot),
1220
1261
  step: snapshot.meta.step,
1221
1262
  revisions: scopeRevisions(),
1222
1263
  bootstrap: snapshot.meta.bootstrap === true,
@@ -1304,7 +1345,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1304
1345
 
1305
1346
  function prepareContext(messages: AgentMessage[], ctx: ExtensionContext): ReturnType<typeof projectContext> | Promise<ReturnType<typeof projectContext>> {
1306
1347
  // Pi continues after hook errors; public Abort fences inference instead of exposing the native history fallback.
1307
- if (selectedBoundaryError?.blockInference && ctx.signal && !shuttingDown) {
1348
+ if (isActiveRestorationBlocked(snapshot) && ctx.signal && !shuttingDown) {
1308
1349
  ctx.abort();
1309
1350
  notifyProblem(ctx, "State Flow inference blocked: Active memory restoration failed. Select a valid branch or use /state-flow-active for current memory; /state-flow-passive or /state-flow-off explicitly permits native context.", "error");
1310
1351
  return;
@@ -38,7 +38,7 @@ export function projectDiagnosticContent(content: unknown): StateFlowDiagnosticB
38
38
 
39
39
  /** Diagnostic JSONL lives beneath the active Pi agent directory, never inside the state repository. */
40
40
  export function stateFlowLogPath(agentDir: string): string {
41
- return join(agentDir, "tmp", "state-flow", "logs.jsonl");
41
+ return join(agentDir, "tmp", "pi-state-flow", "logs.jsonl");
42
42
  }
43
43
 
44
44
  function ensureDiagnosticDirectory(path: string): void {
@@ -1,4 +1,4 @@
1
- import { parseRetainedPiCheckpoint, migrationFailure, type InactiveMode, type RetainedBoundaryCheckpoint, type RetainedPiCheckpoint, type Snapshot } from "./snapshot.ts";
1
+ import { parseRetainedPiCheckpoint, migrationFailure, type InactiveMode, type RetainedBoundaryCheckpoint, type RetainedPiCheckpoint, type Snapshot, type StateFlowMode } from "./snapshot.ts";
2
2
 
3
3
  export type RetainedCheckpointSelection =
4
4
  | { kind: "boundary"; checkpoint: RetainedBoundaryCheckpoint; skipped: string[] }
@@ -45,6 +45,6 @@ export function waitForRecovery<T>(operation: Promise<T>, signal: AbortSignal):
45
45
  }
46
46
 
47
47
  /** A selected boundary that cannot be resolved stays unavailable; callers never fall through to older evidence. */
48
- export function selectedBoundaryFailure(cause: string, mode: InactiveMode = "passive"): Snapshot {
48
+ export function selectedBoundaryFailure(cause: string, mode: StateFlowMode = "passive"): Snapshot {
49
49
  return migrationFailure({}, `Snapshot restoration failed: ${cause}`, mode);
50
50
  }
@@ -6,9 +6,9 @@ import { pruneArtifactProvenance, type ArtifactProvenance, type ArtifactProvenan
6
6
  import { assertTemporalFileBase, captureTemporalFileBase, initializeFileStore, publishTemporalStateToFiles, withStorageTransaction, type StorageTransaction, type TemporalFileBase } from "./storage.ts";
7
7
  import { DEFAULT_HISTORY_LIMIT, MAX_HISTORY_LIMIT, type AcceptedTransition, type RecentTransitionWindow } from "./history.ts";
8
8
  import { hashJson, sameJson } from "./json.ts";
9
- import { HistoryBoundaryExpiredError, RevisionUnavailableError, createSessionRuntime, parseSessionRuntime, preRuntimeCheckpoint, retainedBoundaryCheckpoint, type RetainedBoundaryCheckpoint, type RetainedPiCheckpoint, type Snapshot } from "./snapshot.ts";
9
+ import { RevisionUnavailableError, createSessionRuntime, parseSessionRuntime, preRuntimeCheckpoint, retainedBoundaryCheckpoint, type RetainedPiCheckpoint, type Snapshot, type StateFlowMode } from "./snapshot.ts";
10
10
  import { type MaterializedState, type SemanticState, type ScopedSemanticStates, type ScopedStates, type StateScope } from "./state.ts";
11
- import { adoptTemporalStreams, advanceTemporalState, constrainTemporalState, createTemporalState, readTemporalScopes, readTemporalState, readTemporalView, selectScopeStreamAtBoundary, validateScopeLineage, type ScopeStream, type TemporalState } from "./temporal.ts";
11
+ import { adoptTemporalStreams, advanceTemporalState, constrainTemporalState, createTemporalState, readTemporalScopes, readTemporalState, readTemporalView, validateScopeLineage, type ScopeStream, type TemporalState } from "./temporal.ts";
12
12
 
13
13
  const SCOPES = ["global", "cwd", "session"] as const;
14
14
  const SHARED_SCOPES = ["global", "cwd"] as const;
@@ -24,8 +24,13 @@ export interface RuntimePatchTransaction {
24
24
  type PublicationSelection =
25
25
  | { kind: "patch" | "lifecycle" }
26
26
  | { kind: "start"; allowCreateOrigin: boolean }
27
- | { kind: "restore"; checkpoint: RetainedBoundaryCheckpoint }
28
- | { kind: "fork"; source: SessionAddress; checkpoint: RetainedBoundaryCheckpoint };
27
+ | { kind: "fork"; source: SessionAddress; lifecycle: ForkLifecycle };
28
+
29
+ /** Child policy recorded by the native fork; memory always comes from the parent's current files. */
30
+ export interface ForkLifecycle {
31
+ mode: StateFlowMode;
32
+ bootstrap?: true;
33
+ }
29
34
 
30
35
  interface SessionCopy {
31
36
  stream: ScopeStream;
@@ -78,7 +83,6 @@ export class TemporalRuntime {
78
83
  private semanticRevision: string | undefined;
79
84
  private savedRuntime: string | undefined;
80
85
  private transaction: StorageTransaction | undefined;
81
- private restoredOriginPending = false;
82
86
  private provenanceByScope: Record<StateScope, ArtifactProvenanceRegistry> = emptyProvenance();
83
87
  /** Shared scopes whose wholly absent live basis was accepted after one stale-target refusal. */
84
88
  private readonly absentSharedScopes = new Set<StateScope>();
@@ -130,7 +134,6 @@ export class TemporalRuntime {
130
134
  this.provenanceByScope = provenance;
131
135
  this.semanticRevision = undefined;
132
136
  this.savedRuntime = undefined;
133
- this.restoredOriginPending = false;
134
137
  this.absentSharedScopes.clear();
135
138
  return true;
136
139
  }
@@ -187,80 +190,6 @@ export class TemporalRuntime {
187
190
  return result;
188
191
  }
189
192
 
190
- /** Prepare a retained session boundary from current canonical files; shared scopes remain live. */
191
- prepareBoundaryRestore(checkpoint: RetainedBoundaryCheckpoint): { snapshot: Snapshot; restore: () => Snapshot } {
192
- if (!lstatSync(this.root, { throwIfNoEntry: false })) throw new RevisionUnavailableError("Selected State Flow boundary storage is unavailable");
193
- return this.prepareBoundaryRestoreBase(checkpoint, captureTemporalFileBase(this.cwd, this.sessionId, this.root, this.sessionKey));
194
- }
195
-
196
- private prepareBoundaryRestoreBase(checkpoint: RetainedBoundaryCheckpoint, base: TemporalFileBase): { snapshot: Snapshot; restore: () => Snapshot } {
197
- const files = new Map(base.files.map((file) => [file.path, file.content]));
198
- const scopes = Object.fromEntries(SCOPES.map((scope) => {
199
- const paths = temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey);
200
- const stream = parseScopeStream(files.get(paths.checkpoint), files.get(paths.patches), scope,
201
- scope === "cwd" ? this.cwd : undefined, files.get(paths.meta));
202
- if (!stream) throw new RevisionUnavailableError(`State Flow ${scopeLabel(scope)} scope is unavailable for retained-boundary restoration`);
203
- return [scope, stream];
204
- })) as Record<StateScope, ScopeStream>;
205
- const runtimePaths = sessionRuntimePaths(this.cwd, this.sessionId, this.root, this.sessionKey);
206
- const document = parseSessionRuntime(files.get(runtimePaths.config), files.get(runtimePaths.runtime), this.cwd, this.sessionId);
207
- if (!document) throw new RevisionUnavailableError("State Flow session runtime is unavailable for retained-boundary restoration");
208
- // The codecs validate persisted retention against the format maximum, not the new operator limit.
209
- validateScopeLineage(scopes.session, "session", document.meta.lineage, MAX_HISTORY_LIMIT);
210
- const boundary = document.meta.lineage.slice(-(this.historyLimit + 1)).find(({ id }) => id === checkpoint.boundary);
211
- if (!boundary) throw new HistoryBoundaryExpiredError("Selected State Flow history boundary is outside the retained temporal window");
212
- const selectedSession = selectScopeStreamAtBoundary(scopes.session, "session", boundary, MAX_HISTORY_LIMIT);
213
- const view = adoptTemporalStreams({
214
- global: scopes.global,
215
- cwd: scopes.cwd,
216
- session: selectedSession,
217
- }, `restore:${checkpoint.boundary}:${randomUUID()}`, this.historyLimit);
218
- const snapshot: Snapshot = {
219
- config: { mode: checkpoint.mode },
220
- meta: {
221
- step: checkpoint.step,
222
- ...(checkpoint.bootstrap === true ? { bootstrap: true } : {}),
223
- ...(checkpoint.specification === undefined ? {} : { specification: checkpoint.specification }),
224
- },
225
- };
226
- const provenance = Object.fromEntries(SCOPES.map((scope) => {
227
- const paths = temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey);
228
- const parsed = parseScopeProvenance(files.get(paths.meta), paths.meta);
229
- const retained = pruneArtifactProvenance(parsed, readTemporalState(view, 0, scope, this.historyLimit).artifacts);
230
- if (scope === "session") {
231
- // Live evidence cannot prove an earlier artifact version, even after a change-away-and-back.
232
- for (const record of scopes.session.patches) {
233
- if (record.transition.position <= boundary.position) continue;
234
- for (const path of Object.keys(record.patch.artifacts === null ? retained : record.patch.artifacts ?? {})) delete retained[path];
235
- }
236
- }
237
- return [scope, retained];
238
- })) as Record<StateScope, ArtifactProvenanceRegistry>;
239
- let consumed = false;
240
- return {
241
- snapshot: structuredClone(snapshot),
242
- restore: () => {
243
- if (consumed) throw new Error("Prepared State Flow restore was already consumed");
244
- consumed = true;
245
- this.view = view;
246
- this.base = base;
247
- this.provenanceByScope = provenance;
248
- this.semanticRevision = undefined;
249
- this.savedRuntime = hashJson(createSessionRuntime(snapshot, this.cwd, this.sessionId, view.lineage, provenance.session));
250
- this.restoredOriginPending = true;
251
- this.absentSharedScopes.clear();
252
- return structuredClone(snapshot);
253
- },
254
- };
255
- }
256
-
257
- /** Restore and canonically accept one retained boundary as a single lifecycle operation. */
258
- restoreBoundary(checkpoint: RetainedBoundaryCheckpoint): { snapshot: Snapshot; publication: RuntimePublication } {
259
- const prepared = this.prepareBoundaryRestore(checkpoint);
260
- const snapshot = prepared.restore();
261
- return { snapshot, publication: this.acceptRestoredOrigin(snapshot) };
262
- }
263
-
264
193
  /** Await a coherent read-only recovery view; this neither activates policy nor accepts publication authority. */
265
194
  async refreshCurrentMemory(signal?: AbortSignal): Promise<Snapshot | undefined> {
266
195
  signal?.throwIfAborted();
@@ -309,42 +238,10 @@ export class TemporalRuntime {
309
238
  this.provenanceByScope = provenance;
310
239
  this.semanticRevision = undefined;
311
240
  this.savedRuntime = undefined;
312
- this.restoredOriginPending = false;
313
241
  this.absentSharedScopes.clear();
314
242
  return snapshot;
315
243
  }
316
244
 
317
- /** Copy one retained source-session boundary over the child's current shared scopes. */
318
- prepareBoundaryFork(source: SessionAddress, checkpoint: RetainedBoundaryCheckpoint): { snapshot: Snapshot; fork: () => { snapshot: Snapshot; publication: RuntimePublication } } {
319
- const parent = Object.freeze({ ...source });
320
- if (parent.id === this.sessionId || parent.key === this.sessionKey) throw new Error("State Flow fork requires a distinct session identity and key");
321
- const sourceRuntime = new TemporalRuntime(this.cwd, parent, this.root, undefined, this.historyLimit);
322
- const prepared = sourceRuntime.prepareBoundaryRestore(checkpoint);
323
- prepared.restore();
324
- const snapshot: Snapshot = {
325
- config: structuredClone(prepared.snapshot.config),
326
- meta: {
327
- step: 0,
328
- ...(prepared.snapshot.meta.bootstrap === undefined ? {} : { bootstrap: prepared.snapshot.meta.bootstrap }),
329
- },
330
- };
331
- let consumed = false;
332
- return {
333
- snapshot: structuredClone(snapshot),
334
- fork: () => {
335
- if (consumed) throw new Error("Prepared State Flow fork was already consumed");
336
- consumed = true;
337
- if (this.view) throw new Error("State Flow fork target already has session storage");
338
- const publication = this.initializeOrigin(snapshot, { allowCreateCwd: false, copy: {
339
- stream: sourceRuntime.view!.scopes.session,
340
- provenance: sourceRuntime.artifactProvenance("session"),
341
- } });
342
- if (!publication?.revision) throw new Error("State Flow fork requires existing shared scope storage");
343
- return { snapshot: structuredClone(snapshot), publication };
344
- },
345
- };
346
- }
347
-
348
245
  initialize(snapshot: Snapshot, allowCreateCwd: boolean, expectedShared?: Pick<ScopedStates, "global" | "cwd">, newSessionOrigin = false): RuntimePublication | undefined {
349
246
  return this.initializeOrigin(snapshot, { allowCreateCwd, expectedShared, newSessionOrigin });
350
247
  }
@@ -518,7 +415,6 @@ export class TemporalRuntime {
518
415
 
519
416
  /** Prepare current shared state while retaining the exact accepted private publication basis. */
520
417
  private publicationCandidate(storage: StorageTransaction, current?: TemporalFileBase): TemporalRuntime {
521
- if (this.restoredOriginPending) throw new RevisionUnavailableError("State Flow restored origin must be accepted before patching memory");
522
418
  current ??= storage.capture(this.cwd, this.sessionId, this.root, this.sessionKey);
523
419
  const candidate = new TemporalRuntime(this.cwd, this.session, this.root, undefined, this.historyLimit);
524
420
  candidate.transaction = storage;
@@ -573,26 +469,19 @@ export class TemporalRuntime {
573
469
  return this.withPublicationTransaction((_candidate, publish, current) => action(current, (snapshot) => publish(snapshot)), { kind: "start", allowCreateOrigin }, signal);
574
470
  }
575
471
 
576
- /** Select one retained private boundary beside current shared streams, then accept only after caller policy is rechecked. */
577
- async withRestoreTransaction<T>(checkpoint: RetainedBoundaryCheckpoint, action: (selected: Snapshot, publish: (snapshot: Snapshot) => RuntimePublication) => T, signal?: AbortSignal): Promise<T> {
578
- signal?.throwIfAborted();
579
- return this.withPublicationTransaction((_candidate, publish, selected) => action(selected!, (snapshot) => publish(snapshot)),
580
- { kind: "restore", checkpoint: structuredClone(checkpoint) }, signal);
581
- }
582
-
583
- /** Copy exact retained parent authority into an unoccupied child; the caller rechecks native selection after waiting. */
584
- async withForkTransaction<T>(source: SessionAddress, checkpoint: RetainedBoundaryCheckpoint, action: (selected: Snapshot, publish: (snapshot: Snapshot) => RuntimePublication) => T, signal?: AbortSignal): Promise<T> {
472
+ /** Copy the parent's current session memory into an unoccupied child; the caller rechecks native selection after waiting. */
473
+ async withForkTransaction<T>(source: SessionAddress, lifecycle: ForkLifecycle, action: (selected: Snapshot, publish: (snapshot: Snapshot) => RuntimePublication) => T, signal?: AbortSignal): Promise<T> {
585
474
  signal?.throwIfAborted();
586
475
  return this.withPublicationTransaction((_candidate, publish, selected) => action(selected!, (snapshot) => publish(snapshot)),
587
- { kind: "fork", source: { ...source }, checkpoint: structuredClone(checkpoint) }, signal);
476
+ { kind: "fork", source: { ...source }, lifecycle: { mode: lifecycle.mode, ...(lifecycle.bootstrap ? { bootstrap: true as const } : {}) } }, signal);
588
477
  }
589
478
 
590
- private prepareForkCandidate(storage: StorageTransaction, source: SessionAddress, checkpoint: RetainedBoundaryCheckpoint): {
479
+ private prepareForkCandidate(storage: StorageTransaction, source: SessionAddress, lifecycle: ForkLifecycle): {
591
480
  candidate: TemporalRuntime; current: Snapshot; assertSource: () => void;
592
481
  } {
593
482
  const sourceBase = storage.capture(this.cwd, source.id, this.root, source.key);
594
483
  const parent = new TemporalRuntime(this.cwd, source, this.root, undefined, this.historyLimit);
595
- const selected = parent.prepareBoundaryRestoreBase(checkpoint, sourceBase).restore();
484
+ if (!parent.loadCurrentMemoryBase(sourceBase)) throw new RevisionUnavailableError("State Flow fork parent memory is unavailable");
596
485
  const base = storage.capture(this.cwd, this.sessionId, this.root, this.sessionKey);
597
486
  const files = new Map(base.files.map((file) => [file.path, file.content]));
598
487
  const paths = temporalScopePaths(this.cwd, this.sessionId, "session", this.root, this.sessionKey);
@@ -613,7 +502,7 @@ export class TemporalRuntime {
613
502
  }
614
503
  return {
615
504
  candidate,
616
- current: { config: selected.config, meta: { step: 0, ...(selected.meta.bootstrap === undefined ? {} : { bootstrap: selected.meta.bootstrap }) } },
505
+ current: { config: { mode: lifecycle.mode }, meta: { step: 0, ...(lifecycle.bootstrap ? { bootstrap: true } : {}) } },
617
506
  assertSource: () => assertTemporalFileBase(sourceBase, storage.capture(this.cwd, source.id, this.root, source.key)),
618
507
  };
619
508
  }
@@ -635,15 +524,15 @@ export class TemporalRuntime {
635
524
  if (selection.source.id === this.sessionId || selection.source.key === this.sessionKey) throw new Error("State Flow fork requires a distinct session identity and key");
636
525
  if (this.view) throw new Error("State Flow fork target already has session storage");
637
526
  }
638
- if (!semantic && (!this.semanticRevision || !this.view || !this.base || this.restoredOriginPending)) {
527
+ if (!semantic && (!this.semanticRevision || !this.view || !this.base)) {
639
528
  throw new Error("State Flow lifecycle transaction requires an accepted runtime");
640
529
  }
641
530
  };
642
531
  signal?.throwIfAborted();
643
532
  assertAuthority();
644
533
  if (kind === "patch" || allowCreateOrigin) initializeFileStore(this.root);
645
- if ((kind === "start" || kind === "restore" || kind === "fork") && !lstatSync(this.root, { throwIfNoEntry: false })) {
646
- throw new RevisionUnavailableError(kind !== "start" ? "Selected State Flow boundary storage is unavailable" : "Current State Flow session storage is unavailable");
534
+ if ((kind === "start" || kind === "fork") && !lstatSync(this.root, { throwIfNoEntry: false })) {
535
+ throw new RevisionUnavailableError(kind === "fork" ? "State Flow fork parent storage is unavailable" : "Current State Flow session storage is unavailable");
647
536
  }
648
537
  return withStorageTransaction(this.root, (storage) => {
649
538
  assertAuthority();
@@ -651,14 +540,12 @@ export class TemporalRuntime {
651
540
  let candidate: TemporalRuntime;
652
541
  let assertSource: (() => void) | undefined;
653
542
  if (selection.kind === "fork") {
654
- ({ candidate, current, assertSource } = this.prepareForkCandidate(storage, selection.source, selection.checkpoint));
655
- } else if (selection.kind === "restore" || selection.kind === "start") {
543
+ ({ candidate, current, assertSource } = this.prepareForkCandidate(storage, selection.source, selection.lifecycle));
544
+ } else if (selection.kind === "start") {
656
545
  candidate = new TemporalRuntime(this.cwd, this.session, this.root, undefined, this.historyLimit);
657
546
  candidate.transaction = storage;
658
547
  const base = storage.capture(this.cwd, this.sessionId, this.root, this.sessionKey);
659
- current = selection.kind === "restore"
660
- ? candidate.prepareBoundaryRestoreBase(selection.checkpoint, base).restore()
661
- : candidate.loadCurrentMemoryBase(base);
548
+ current = candidate.loadCurrentMemoryBase(base);
662
549
  if (!current) {
663
550
  if (!allowCreateOrigin) throw new RevisionUnavailableError("Current State Flow session memory is unavailable");
664
551
  if (SCOPES.some((scope) => lstatSync(join(temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey).directory, "state.json"), { throwIfNoEntry: false }))) {
@@ -676,7 +563,7 @@ export class TemporalRuntime {
676
563
  assertSource?.();
677
564
  // Patch preparation without semantic work preserves complete accepted cohorts; selection accepts origins with configured folding.
678
565
  // New authority or wholly absent shared pairs still need normal atomic initialization; partial evidence already failed.
679
- const writeSemantic = semantic && (kind === "start" || kind === "restore" || kind === "fork" || accepted !== undefined || !this.semanticRevision
566
+ const writeSemantic = semantic && (kind === "start" || kind === "fork" || accepted !== undefined || !this.semanticRevision
680
567
  || SCOPES.some((scope) => Object.keys(provenance?.[scope] ?? {}).length > 0)
681
568
  || candidate.base!.files.some((file) => file.content === undefined));
682
569
  // Every capability acceptance validates its captured cohort, including semantic no-ops.
@@ -687,7 +574,6 @@ export class TemporalRuntime {
687
574
  this.provenanceByScope = candidate.provenanceByScope;
688
575
  this.semanticRevision = candidate.semanticRevision;
689
576
  this.savedRuntime = candidate.savedRuntime;
690
- this.restoredOriginPending = false;
691
577
  this.absentSharedScopes.clear();
692
578
  published = true;
693
579
  return publication;
@@ -697,15 +583,6 @@ export class TemporalRuntime {
697
583
  }, signal);
698
584
  }
699
585
 
700
- /** Canonically accept a prepared retained-boundary origin before lifecycle-only persistence. */
701
- acceptRestoredOrigin(snapshot: Snapshot): RuntimePublication {
702
- if (!this.restoredOriginPending) throw new Error("State Flow has no prepared restored origin to accept");
703
- const publication = this.publish(snapshot, true);
704
- if (!publication) throw new Error("State Flow restored origin produced no canonical publication");
705
- this.restoredOriginPending = false;
706
- return publication;
707
- }
708
-
709
586
  publish(
710
587
  snapshot: Snapshot,
711
588
  semantic = false,
@@ -713,7 +590,6 @@ export class TemporalRuntime {
713
590
  options: { provenance?: Partial<Record<StateScope, Record<string, ArtifactProvenance>>> } = {},
714
591
  ): RuntimePublication | undefined {
715
592
  if (!this.view || !this.base) throw new Error("State Flow temporal publication is unavailable; restore or initialize before accepting a transition");
716
- if (this.restoredOriginPending && !semantic) throw new Error("State Flow restored origin must be accepted before runtime-only persistence");
717
593
  // Explicit evidence updates still own a scope CAS, even if they equal the stale cache.
718
594
  const provenanceScopes = SCOPES.filter((scope) => Object.keys(options.provenance?.[scope] ?? {}).length > 0);
719
595
  const runtimeOnly = !semantic && accepted === undefined && provenanceScopes.length === 0;