@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.
- package/BACKLOG.md +3 -1
- package/CHANGELOG.md +8 -0
- package/README.md +3 -3
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +2 -2
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +1 -29
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +12 -0
- package/node_modules/@llblab/pi-state-flow/README.md +7 -8
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +94 -47
- package/node_modules/@llblab/pi-state-flow/dist/lib/logging.js +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.d.ts +2 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +8 -27
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +16 -151
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +3 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +4 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +4 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +2 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +6 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.d.ts +0 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.js +0 -36
- package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
- package/node_modules/@llblab/pi-state-flow/docs/agent-contract-relocation.md +1 -1
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +20 -31
- package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +8 -9
- package/node_modules/@llblab/pi-state-flow/docs/filesystem-recovery.md +4 -4
- package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +18 -23
- package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +19 -21
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +26 -27
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +89 -48
- package/node_modules/@llblab/pi-state-flow/lib/logging.ts +1 -1
- package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +2 -2
- package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +22 -146
- package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +6 -3
- package/node_modules/@llblab/pi-state-flow/lib/status.ts +4 -2
- package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +8 -2
- package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +0 -36
- package/node_modules/@llblab/pi-state-flow/package.json +1 -1
- 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 {
|
|
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: "
|
|
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
|
-
/**
|
|
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
|
-
|
|
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: "
|
|
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
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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 === "
|
|
588
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 = () =>
|
|
896
|
-
&& sameSession() &&
|
|
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 =
|
|
918
|
-
|
|
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
|
-
|
|
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
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
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 ?
|
|
958
|
-
|
|
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 (
|
|
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:
|
|
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 {
|
|
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,
|
|
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: "
|
|
28
|
-
|
|
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
|
-
/**
|
|
577
|
-
async
|
|
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 },
|
|
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,
|
|
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
|
-
|
|
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:
|
|
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
|
|
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 === "
|
|
646
|
-
throw new RevisionUnavailableError(kind
|
|
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.
|
|
655
|
-
} else if (selection.kind === "
|
|
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 =
|
|
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 === "
|
|
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;
|