@llblab/pi-kit 0.22.2 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +4 -4
- package/node_modules/@llblab/pi-grow-loop/AGENTS.md +10 -6
- package/node_modules/@llblab/pi-grow-loop/CHANGELOG.md +7 -0
- package/node_modules/@llblab/pi-grow-loop/README.md +2 -0
- package/node_modules/@llblab/pi-grow-loop/dist/index.d.ts +33 -0
- package/node_modules/@llblab/pi-grow-loop/dist/index.js +286 -0
- package/node_modules/@llblab/pi-grow-loop/dist/pi-grow-loop/index.d.ts +1 -0
- package/node_modules/@llblab/pi-grow-loop/dist/pi-grow-loop/index.js +1 -0
- package/node_modules/@llblab/pi-grow-loop/dist/skills/grow-loop/SKILL.md +117 -0
- package/node_modules/@llblab/pi-grow-loop/dist/skills/while-true/SKILL.md +233 -0
- package/node_modules/@llblab/pi-grow-loop/index.ts +67 -12
- package/node_modules/@llblab/pi-grow-loop/package.json +9 -8
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +23 -17
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +5 -3
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +11 -0
- package/node_modules/@llblab/pi-state-flow/README.md +18 -6
- package/node_modules/@llblab/pi-state-flow/dist/index.d.ts +2 -1
- package/node_modules/@llblab/pi-state-flow/dist/index.js +1 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.d.ts +2 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.js +5 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +3 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +7 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.d.ts +5 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +54 -40
- package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +20 -20
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +755 -325
- package/node_modules/@llblab/pi-state-flow/dist/lib/git.d.ts +2 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +14 -17
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.d.ts +4 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +65 -23
- package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.d.ts +16 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.js +32 -15
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +28 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +276 -26
- package/node_modules/@llblab/pi-state-flow/dist/lib/session.d.ts +5 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/session.js +36 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +3 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +3 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +1 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +3 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/storage.d.ts +11 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/storage.js +150 -24
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +14 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +52 -18
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +8 -8
- package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +3 -1
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +53 -7
- package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +84 -44
- package/node_modules/@llblab/pi-state-flow/docs/filesystem-recovery.md +12 -2
- package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +6 -4
- package/node_modules/@llblab/pi-state-flow/docs/performance.md +2 -2
- package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +28 -8
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +41 -14
- package/node_modules/@llblab/pi-state-flow/index.ts +3 -0
- package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +5 -5
- package/node_modules/@llblab/pi-state-flow/lib/context.ts +8 -3
- package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +57 -40
- package/node_modules/@llblab/pi-state-flow/lib/durable.ts +20 -20
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +719 -316
- package/node_modules/@llblab/pi-state-flow/lib/git.ts +16 -18
- package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +60 -24
- package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +34 -21
- package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +290 -25
- package/node_modules/@llblab/pi-state-flow/lib/session.ts +37 -2
- package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +3 -0
- package/node_modules/@llblab/pi-state-flow/lib/status.ts +4 -1
- package/node_modules/@llblab/pi-state-flow/lib/storage.ts +141 -22
- package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +60 -19
- package/node_modules/@llblab/pi-state-flow/lib/transition.ts +8 -8
- package/node_modules/@llblab/pi-state-flow/package.json +1 -1
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +3 -1
- package/node_modules/@llblab/pi-telegram/AGENTS.md +1 -1
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +5 -0
- package/node_modules/@llblab/pi-telegram/README.md +2 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/skills.d.ts +7 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/skills.js +32 -7
- package/node_modules/@llblab/pi-telegram/dist/package.json +3 -8
- package/node_modules/@llblab/pi-telegram/lib/skills.ts +43 -7
- package/node_modules/@llblab/pi-telegram/package.json +3 -8
- package/node_modules/@llblab/pi-telegram/scripts/build-dist.mjs +103 -32
- package/package.json +6 -6
|
@@ -29,19 +29,20 @@ import { awaitInFlightBackupPushes, backupCurrentStateFlowFiles, startStateFlowB
|
|
|
29
29
|
import { projectRecentTransitionsWithLimit } from "./history.ts";
|
|
30
30
|
import { isObject, presentationJson, sameJson, type JsonObject } from "./json.ts";
|
|
31
31
|
import { StateFlowDiagnosticWriter, stateFlowLogPath, type DiagnosticExtras, type StateFlowDiagnosticCategory } from "./logging.ts";
|
|
32
|
-
import { assistantToolCallCount, finalizedAssistantResponse, formatPatchStateArguments, PASSIVE_MEMORY_PROTOCOL, separatedFailure, separatedOutput, stateFlowProtocol } from "./protocol.ts";
|
|
32
|
+
import { assistantToolCallCount, conciseDiagnostic, diagnosticText, finalizedAssistantResponse, formatPatchStateArguments, PASSIVE_MEMORY_PROTOCOL, separatedFailure, separatedOutput, stateFlowProtocol } from "./protocol.ts";
|
|
33
33
|
import { readProjectedState, readStatePath } from "./query.ts";
|
|
34
|
-
import {
|
|
34
|
+
import { selectedBoundaryFailure, selectRetainedCheckpoint, waitForRecovery } from "./recovery.ts";
|
|
35
35
|
import type { RehydrationPhase } from "./rehydration.ts";
|
|
36
|
-
import {
|
|
37
|
-
import { discoverSnapshotData, findAssistantToolBatch, findPassiveStopBoundary, hasPriorConversation, isNewSession, retainsPhysicalSessionProjection, SNAPSHOT_ENTRY_TYPE } from "./session.ts";
|
|
36
|
+
import { TemporalRuntime, type RuntimePublication } from "./runtime.ts";
|
|
37
|
+
import { discoverSnapshotData, findAssistantToolBatch, findPassiveStopBoundary, hasPriorConversation, hasUncheckpointedConversation, isNewSession, retainsPhysicalSessionProjection, SNAPSHOT_ENTRY_TYPE } from "./session.ts";
|
|
38
38
|
import { hasCompiledSkillArtifact, hashSkillSource, registeredSkillResolver, SkillReadTracker, type SuccessfulSkillRead } from "./skills.ts";
|
|
39
|
-
import { emptySnapshot, migrationFailure, type Snapshot } from "./snapshot.ts";
|
|
39
|
+
import { HistoryBoundaryExpiredError, emptySnapshot, migrationFailure, type RetainedBoundaryCheckpoint, type Snapshot } from "./snapshot.ts";
|
|
40
40
|
import { emptyState, overlayStates, projectStateForModel, type AtomicScopePatches, type MaterializedState, type ModelState, type ScopedStates, type StateScope } from "./state.ts";
|
|
41
41
|
import { compactStatus, detailedStatus, STATUS_KEY, type StatusDiagnostics } from "./status.ts";
|
|
42
|
-
import {
|
|
42
|
+
import { PublicationBusyError } from "./storage.ts";
|
|
43
|
+
import { createStateFlowTelegramAdapter, type StateFlowTelegramControlResult, type StateFlowTelegramInspection, type StateFlowTelegramLoader, type StateFlowTelegramScope } from "./telegram.ts";
|
|
43
44
|
import { temporalScopeRevisions, type ScopeRevisions } from "./temporal.ts";
|
|
44
|
-
import { commitScopedTransition, stageAtomicScopePatches, stageScopedTransition
|
|
45
|
+
import { commitScopedTransition, stageAtomicScopePatches, stageScopedTransition } from "./transition.ts";
|
|
45
46
|
|
|
46
47
|
export interface StateFlowExtensionOptions {
|
|
47
48
|
agentDir?: string;
|
|
@@ -58,6 +59,28 @@ export const PATCH_STATE_TOOL_NAME = "patch_state";
|
|
|
58
59
|
export const READ_STATE_TOOL_NAME = "read_state";
|
|
59
60
|
const PASSIVE_STOP_ENTRY_TYPE = "state-flow-passive-stop";
|
|
60
61
|
|
|
62
|
+
interface InferencePreparation {
|
|
63
|
+
readonly controller: AbortController;
|
|
64
|
+
readonly prompt?: string;
|
|
65
|
+
accepted?: boolean;
|
|
66
|
+
operation?: Promise<void>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface BranchRestoration {
|
|
70
|
+
readonly controller: AbortController;
|
|
71
|
+
/** Memory acceptance is independent of the requested active/passive policy. */
|
|
72
|
+
awaitingAcceptance: boolean;
|
|
73
|
+
passiveRequested?: boolean;
|
|
74
|
+
operation?: Promise<void>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
type BranchSelection =
|
|
78
|
+
| { kind: "settled" }
|
|
79
|
+
| { kind: "current" }
|
|
80
|
+
| { kind: "restore"; checkpoint: RetainedBoundaryCheckpoint }
|
|
81
|
+
| { kind: "fork"; checkpoint: RetainedBoundaryCheckpoint; source: SessionAddress }
|
|
82
|
+
| { kind: "auto-start" };
|
|
83
|
+
|
|
61
84
|
export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowExtensionOptions = {}): void {
|
|
62
85
|
const agentDir = options.agentDir ?? getAgentDir();
|
|
63
86
|
const loadedConfig = loadStateFlowConfig(agentDir, options.repositoryRoot);
|
|
@@ -68,10 +91,16 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
68
91
|
};
|
|
69
92
|
let snapshot: Snapshot = emptySnapshot();
|
|
70
93
|
let scopeStates: ScopedStates = { global: emptyState(), cwd: emptyState(), session: emptyState() };
|
|
71
|
-
let branchHasSnapshot = false;
|
|
72
94
|
let branchStartsWithoutRuntime = false;
|
|
95
|
+
let selectedHistoryExpired = false;
|
|
96
|
+
let stopPersistenceError: string | undefined;
|
|
97
|
+
let stopPersistence: { controller: AbortController; operation?: Promise<StateFlowTelegramControlResult> } | undefined;
|
|
98
|
+
let startActivation: { controller: AbortController; operation?: Promise<StateFlowTelegramControlResult> } | undefined;
|
|
73
99
|
let forkInitialization = false;
|
|
74
|
-
let
|
|
100
|
+
let branchRestoration: BranchRestoration | undefined;
|
|
101
|
+
const restorationOperations = new Set<Promise<void>>();
|
|
102
|
+
let responseReconciliation: AbortController | undefined;
|
|
103
|
+
let sharedInspectionLifetime = new AbortController();
|
|
75
104
|
let completedRunAccepted = false;
|
|
76
105
|
let turnAcceptedForBackup = false;
|
|
77
106
|
let compactionPlan: StateFlowCompactionPlan | undefined;
|
|
@@ -80,7 +109,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
80
109
|
const compactionMarker = `state-flow-boundary:${randomUUID()}`;
|
|
81
110
|
let passiveContinuation: PassiveContinuation | undefined;
|
|
82
111
|
let bootstrapContinuation: PassiveContinuation | undefined;
|
|
83
|
-
let
|
|
112
|
+
let inferencePreparation: InferencePreparation | undefined;
|
|
84
113
|
let runAnchorTimestamp: number | undefined;
|
|
85
114
|
let runtime: TemporalRuntime | undefined;
|
|
86
115
|
let activeContext: ExtensionContext | undefined;
|
|
@@ -88,6 +117,8 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
88
117
|
const repositoryRoot = resolve(options.repositoryRoot ?? config.directory);
|
|
89
118
|
const diagnosticWriter = new StateFlowDiagnosticWriter(config.logging, stateFlowLogPath(agentDir), repositoryRoot, (message) => notifyActiveContext(message));
|
|
90
119
|
let backupPending = false;
|
|
120
|
+
const backupLifetime = new AbortController();
|
|
121
|
+
const backupOperations = new Set<Promise<string | undefined>>();
|
|
91
122
|
let shuttingDown = false;
|
|
92
123
|
let pushFailureNotified = false;
|
|
93
124
|
const skillReads = new SkillReadTracker(hashSkillSource, (path) => activeContext
|
|
@@ -103,9 +134,13 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
103
134
|
return resolveSessionAddress(ctx.sessionManager.getSessionFile(), ctx.sessionManager.getSessionId(), ctx.sessionManager.getHeader()?.timestamp);
|
|
104
135
|
}
|
|
105
136
|
|
|
137
|
+
function notifyProblem(ctx: ExtensionContext, message: string, level: "warning" | "error"): void {
|
|
138
|
+
ctx.ui.notify(conciseDiagnostic(message), level);
|
|
139
|
+
}
|
|
140
|
+
|
|
106
141
|
function notifyActiveContext(message: string): void {
|
|
107
142
|
try {
|
|
108
|
-
activeContext
|
|
143
|
+
if (activeContext) notifyProblem(activeContext, message, "warning");
|
|
109
144
|
} catch {
|
|
110
145
|
// An asynchronous push attempt can outlive the Pi context that launched it.
|
|
111
146
|
}
|
|
@@ -121,7 +156,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
121
156
|
|
|
122
157
|
function assertSelectedBranchAvailable(): void {
|
|
123
158
|
if (snapshot.meta.validation?.attempt === 0) {
|
|
124
|
-
throw new Error(`State Flow selected branch is unavailable: ${snapshot.meta.validation.error}
|
|
159
|
+
throw new Error(`State Flow selected branch is unavailable: ${snapshot.meta.validation.error}`);
|
|
125
160
|
}
|
|
126
161
|
}
|
|
127
162
|
|
|
@@ -131,12 +166,12 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
131
166
|
return projectModelState(runtime.read(offset, scope));
|
|
132
167
|
} });
|
|
133
168
|
|
|
134
|
-
function
|
|
169
|
+
function assertPublicationAvailable(): void {
|
|
135
170
|
assertSelectedBranchAvailable();
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
171
|
+
if (stopPersistenceError) throw new Error(`Memory writes paused after Stop: ${stopPersistenceError}; use /state-flow-start`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function appendCheckpoint(): void {
|
|
140
175
|
const checkpoint = branchStartsWithoutRuntime ? { disabled: true } : runtime?.retainedCheckpoint(snapshot) ?? { disabled: true };
|
|
141
176
|
if ("disabled" in checkpoint && !branchStartsWithoutRuntime) {
|
|
142
177
|
throw new Error("State Flow cannot checkpoint an unproven branch as ordinary disabled; restore a valid checkpoint first");
|
|
@@ -155,8 +190,46 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
155
190
|
ctx.ui.setStatus(STATUS_KEY, compactStatus(snapshot, scopeRevisions(), (color, text) => ctx.ui.theme.fg(color, text)));
|
|
156
191
|
}
|
|
157
192
|
|
|
193
|
+
function cancelStopPersistence(): Promise<StateFlowTelegramControlResult> | undefined {
|
|
194
|
+
const pending = stopPersistence;
|
|
195
|
+
pending?.controller.abort();
|
|
196
|
+
stopPersistence = undefined;
|
|
197
|
+
return pending?.operation;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function cancelStartActivation(): Promise<StateFlowTelegramControlResult> | undefined {
|
|
201
|
+
const pending = startActivation;
|
|
202
|
+
pending?.controller.abort();
|
|
203
|
+
startActivation = undefined;
|
|
204
|
+
return pending?.operation;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function cancelBranchRestoration(): Promise<void> | undefined {
|
|
208
|
+
const pending = branchRestoration;
|
|
209
|
+
pending?.controller.abort();
|
|
210
|
+
branchRestoration = undefined;
|
|
211
|
+
return pending?.operation;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function cancelResponseReconciliation(): void {
|
|
215
|
+
responseReconciliation?.abort();
|
|
216
|
+
responseReconciliation = undefined;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function cancelSharedInspections(): void {
|
|
220
|
+
sharedInspectionLifetime.abort(new Error("State Flow inspection was cancelled; select the scope again"));
|
|
221
|
+
sharedInspectionLifetime = new AbortController();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function cancelInferencePreparation(): void {
|
|
225
|
+
inferencePreparation?.controller.abort();
|
|
226
|
+
inferencePreparation = undefined;
|
|
227
|
+
}
|
|
228
|
+
|
|
158
229
|
function clearRunTransient(): void {
|
|
159
|
-
|
|
230
|
+
cancelInferencePreparation();
|
|
231
|
+
cancelResponseReconciliation();
|
|
232
|
+
cancelSharedInspections();
|
|
160
233
|
completedRunAccepted = false;
|
|
161
234
|
turnAcceptedForBackup = false;
|
|
162
235
|
compactionPlan = undefined;
|
|
@@ -165,10 +238,11 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
165
238
|
artifactReads.clear();
|
|
166
239
|
}
|
|
167
240
|
|
|
168
|
-
function
|
|
241
|
+
function deferInferencePreparation(prompt?: string): void {
|
|
242
|
+
cancelInferencePreparation();
|
|
243
|
+
inferencePreparation = { controller: new AbortController(), prompt };
|
|
169
244
|
artifactInvalidations = [];
|
|
170
245
|
artifactReads.setCandidates([]);
|
|
171
|
-
artifactRefreshPending = true;
|
|
172
246
|
}
|
|
173
247
|
|
|
174
248
|
function refreshArtifactHints(): void {
|
|
@@ -202,10 +276,9 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
202
276
|
artifactReads.setCandidates(artifactInvalidations);
|
|
203
277
|
}
|
|
204
278
|
|
|
205
|
-
function
|
|
206
|
-
if (!snapshot.config.enabled || !runtime?.view) return;
|
|
279
|
+
function missingArtifactRemovals(states: ScopedStates): AtomicScopePatches {
|
|
207
280
|
const owners = new Map<string, StateScope[]>();
|
|
208
|
-
for (const scope of ["global", "cwd", "session"] as const) for (const path of Object.keys(
|
|
281
|
+
for (const scope of ["global", "cwd", "session"] as const) for (const path of Object.keys(states[scope].artifacts)) {
|
|
209
282
|
owners.set(path, [...owners.get(path) ?? [], scope]);
|
|
210
283
|
}
|
|
211
284
|
const removals: AtomicScopePatches = {};
|
|
@@ -216,12 +289,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
216
289
|
removals[scope] = { ...(removals[scope] ?? {}), artifacts: { ...artifacts, [observation.path]: null } };
|
|
217
290
|
}
|
|
218
291
|
}
|
|
219
|
-
|
|
220
|
-
const stage = stageAtomicScopePatches(scopeStates, removals, [], runtime.causalBasis());
|
|
221
|
-
commitStage(stage, ctx, false);
|
|
222
|
-
updateUi(ctx);
|
|
223
|
-
}
|
|
224
|
-
refreshArtifactHints();
|
|
292
|
+
return removals;
|
|
225
293
|
}
|
|
226
294
|
|
|
227
295
|
|
|
@@ -233,16 +301,21 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
233
301
|
return snapshot.config.enabled || config.passiveTools;
|
|
234
302
|
}
|
|
235
303
|
|
|
236
|
-
function
|
|
304
|
+
async function inspectTelegramState(scope: StateFlowTelegramScope): Promise<StateFlowTelegramInspection> {
|
|
305
|
+
if (!activeContext || shuttingDown) throw new Error("State Flow is not attached to an active session yet");
|
|
237
306
|
if (scope === "session" || scope === "effective") assertSelectedBranchAvailable();
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
307
|
+
const selected = runtime ??= createRuntime(activeContext);
|
|
308
|
+
const signal = sharedInspectionLifetime.signal;
|
|
309
|
+
if (!selected.view || (scope !== "session" && !stopPersistenceError)) await selected.refreshShared(signal);
|
|
310
|
+
signal.throwIfAborted();
|
|
311
|
+
if (runtime !== selected) throw new Error("State Flow inspection selection changed; select the scope again");
|
|
312
|
+
if (scope === "session" || scope === "effective") assertSelectedBranchAvailable();
|
|
313
|
+
if (!selected.view) throw new Error("State Flow temporal runtime is unavailable");
|
|
245
314
|
installScopeStates();
|
|
315
|
+
const state = scope === "effective"
|
|
316
|
+
? overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session)
|
|
317
|
+
: scopeStates[scope];
|
|
318
|
+
return { state: { ...projectModelState(state), lazy: structuredClone(state.lazy) }, revisions: scopeRevisions(), signal };
|
|
246
319
|
}
|
|
247
320
|
|
|
248
321
|
function syncStateFlowTools(): void {
|
|
@@ -287,29 +360,57 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
287
360
|
return `State Flow acquisition: this registered Skill belongs at ${target}. If durable compiled guidance is useful, include a non-empty description, kind:"skill", and compilation object there. Unrelated semantic patches do not need to include it.`;
|
|
288
361
|
}
|
|
289
362
|
|
|
290
|
-
function
|
|
291
|
-
const acquiredArtifactPaths = new Set(artifactReads.successful.keys());
|
|
292
|
-
let committed: boolean;
|
|
293
|
-
try {
|
|
294
|
-
committed = commitScopedTransition(snapshot, scopeStates, stage, (accepted, nextSnapshot) => {
|
|
295
|
-
if (!runtime?.view) throw new Error("Temporal State Flow runtime is unavailable; reload before publishing");
|
|
296
|
-
const publication = runtime.publish(nextSnapshot, accepted !== undefined, accepted, {
|
|
297
|
-
provenance: stage.provenanceUpdates,
|
|
298
|
-
});
|
|
299
|
-
if (publication) recordPublication(publication, ctx);
|
|
300
|
-
}, runtime!.causalBasis(), { finalizeRun });
|
|
301
|
-
} catch (error) {
|
|
302
|
-
if (error instanceof SharedScopeRemovalConflictError) installScopeStates();
|
|
303
|
-
throw error;
|
|
304
|
-
}
|
|
305
|
-
if (!committed) return false;
|
|
306
|
-
installScopeStates();
|
|
363
|
+
function clearAcceptedAcquisitions(acquiredArtifactPaths = new Set(artifactReads.successful.keys())): void {
|
|
307
364
|
artifactInvalidations = artifactInvalidations.filter(({ path }) => !acquiredArtifactPaths.has(path));
|
|
308
365
|
artifactReads.setCandidates(artifactInvalidations);
|
|
309
366
|
dropCurrentSkillReads();
|
|
310
367
|
artifactReads.clear();
|
|
311
|
-
|
|
312
|
-
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async function prepareInference(pending: InferencePreparation, selected: TemporalRuntime | undefined, ctx: ExtensionContext, operationSignal: AbortSignal): Promise<void> {
|
|
371
|
+
const signal = AbortSignal.any([pending.controller.signal, operationSignal]);
|
|
372
|
+
try {
|
|
373
|
+
if (!selected?.view) throw new Error("Temporal State Flow runtime is unavailable; reload before publishing");
|
|
374
|
+
await selected.withPatchTransaction((transaction) => {
|
|
375
|
+
signal.throwIfAborted();
|
|
376
|
+
if (runtime !== selected || inferencePreparation !== pending || !snapshot.config.enabled || shuttingDown) {
|
|
377
|
+
throw new Error("State Flow inference selection changed while awaiting publication");
|
|
378
|
+
}
|
|
379
|
+
assertPublicationAvailable();
|
|
380
|
+
const nextSnapshot = structuredClone(snapshot);
|
|
381
|
+
const rotatesRun = pending.prompt !== undefined && nextSnapshot.meta.specification !== undefined;
|
|
382
|
+
if (pending.prompt !== undefined) prepareRun(nextSnapshot, pending.prompt);
|
|
383
|
+
// Exact-path metadata validation belongs to the locked current head, including newly adopted registrations.
|
|
384
|
+
const removals = missingArtifactRemovals(transaction.states);
|
|
385
|
+
let publication: RuntimePublication | undefined;
|
|
386
|
+
if (Object.keys(removals).length > 0) {
|
|
387
|
+
const stage = stageAtomicScopePatches(transaction.states, removals, [], transaction.causalBasis);
|
|
388
|
+
commitScopedTransition(nextSnapshot, transaction.states, stage, (transition, next) => {
|
|
389
|
+
publication = transaction.publish(next, transition);
|
|
390
|
+
pending.accepted = true;
|
|
391
|
+
}, transaction.causalBasis, { finalizeRun: false });
|
|
392
|
+
} else {
|
|
393
|
+
publication = transaction.publish(nextSnapshot);
|
|
394
|
+
pending.accepted = true;
|
|
395
|
+
}
|
|
396
|
+
snapshot = nextSnapshot;
|
|
397
|
+
installScopeStates();
|
|
398
|
+
if (rotatesRun && rehydrationPhase !== "new-bootstrap" && rehydrationPhase !== "resume-bootstrap") rehydrationPhase = "step";
|
|
399
|
+
if (publication?.changed) recordPublication(publication, ctx);
|
|
400
|
+
if (pending.prompt !== undefined || publication?.changed) appendCheckpoint();
|
|
401
|
+
if (Object.keys(removals).length > 0) clearAcceptedAcquisitions();
|
|
402
|
+
updateUi(ctx);
|
|
403
|
+
}, signal);
|
|
404
|
+
} catch (error) {
|
|
405
|
+
if (signal.aborted || runtime !== selected || inferencePreparation !== pending || shuttingDown) return;
|
|
406
|
+
// Pi reports context-hook exceptions and continues. Abort through its public port rather than infer from stale preparation.
|
|
407
|
+
ctx.abort();
|
|
408
|
+
const cause = diagnosticText(error);
|
|
409
|
+
recordDiagnostic(cause, "publication-conflict", ctx);
|
|
410
|
+
notifyProblem(ctx, pending.accepted
|
|
411
|
+
? `State Flow preparation saved; lifecycle update failed: ${cause}`
|
|
412
|
+
: `State Flow inference preparation failed: ${cause}`, "error");
|
|
413
|
+
}
|
|
313
414
|
}
|
|
314
415
|
|
|
315
416
|
|
|
@@ -344,6 +445,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
344
445
|
} }),
|
|
345
446
|
staleArtifacts,
|
|
346
447
|
...(durableStateError === undefined ? {} : { durableStateError }),
|
|
448
|
+
...(stopPersistenceError === undefined ? {} : { publicationError: stopPersistenceError }),
|
|
347
449
|
};
|
|
348
450
|
}
|
|
349
451
|
|
|
@@ -355,116 +457,224 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
355
457
|
return resolveSessionAddress(parent.file, parent.id, parent.timestamp);
|
|
356
458
|
}
|
|
357
459
|
|
|
358
|
-
|
|
460
|
+
/** Select the active native branch under one owned restoration lifetime; only current accepted work installs memory. */
|
|
461
|
+
function restoreActiveBranch(ctx: ExtensionContext, sessionStartReason?: unknown, notifyRecovery = true, startOwner?: AbortController): Promise<void> {
|
|
462
|
+
cancelBranchRestoration();
|
|
463
|
+
// Start-owned attachment/fork recovery keeps its owner; only accepted Start cancels Stop.
|
|
464
|
+
if (!startOwner) {
|
|
465
|
+
cancelStartActivation();
|
|
466
|
+
cancelStopPersistence();
|
|
467
|
+
}
|
|
359
468
|
clearRunTransient();
|
|
360
469
|
passiveContinuation = undefined;
|
|
361
470
|
bootstrapContinuation = undefined;
|
|
362
471
|
artifactInvalidations = [];
|
|
363
472
|
artifactReads.setCandidates([]);
|
|
364
|
-
artifactRefreshPending = false;
|
|
365
473
|
telegramStartPending = false;
|
|
366
474
|
activeContext = ctx;
|
|
367
|
-
|
|
368
|
-
runtime = new TemporalRuntime(ctx.cwd, session, repositoryRoot, undefined, config.historyLimit);
|
|
475
|
+
runtime = createRuntime(ctx);
|
|
369
476
|
installScopeStates();
|
|
370
|
-
branchHasSnapshot = false;
|
|
371
477
|
branchStartsWithoutRuntime = false;
|
|
478
|
+
selectedHistoryExpired = false;
|
|
372
479
|
forkInitialization = sessionStartReason === "fork";
|
|
480
|
+
let selection: BranchSelection = { kind: "settled" };
|
|
481
|
+
let skipped = 0;
|
|
373
482
|
try {
|
|
374
483
|
const branch = ctx.sessionManager.getBranch();
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
snapshot = recovery.snapshot;
|
|
407
|
-
if (branchHasSnapshot && restoreSelected) {
|
|
408
|
-
snapshot = restoreSelected();
|
|
409
|
-
persist();
|
|
410
|
-
} else if (branchHasSnapshot && snapshot.config.enabled) {
|
|
411
|
-
const publication = runtime.initialize(snapshot, true);
|
|
412
|
-
recordPolicyPublication(publication, ctx);
|
|
484
|
+
const fence = retainsPhysicalSessionProjection(sessionStartReason)
|
|
485
|
+
? findPassiveStopBoundary(branch, ctx.sessionManager.getSessionId(), PASSIVE_STOP_ENTRY_TYPE)?.persistenceError
|
|
486
|
+
: undefined;
|
|
487
|
+
stopPersistenceError = fence ?? (startOwner ? stopPersistenceError : undefined);
|
|
488
|
+
if (fence) {
|
|
489
|
+
// The native Stop owns policy only: read proven memory, never replay or publish a stale selection.
|
|
490
|
+
selection = { kind: "current" };
|
|
491
|
+
} else {
|
|
492
|
+
const discovery = discoverSnapshotData(branch);
|
|
493
|
+
const selected = selectRetainedCheckpoint(discovery.candidates);
|
|
494
|
+
skipped = discovery.errors.length + selected.skipped.length;
|
|
495
|
+
branchStartsWithoutRuntime = selected.kind === "disabled" || (discovery.candidates.length === 0 && discovery.errors.length === 0);
|
|
496
|
+
if (discovery.candidates.length === 0 && discovery.errors.length > 0) {
|
|
497
|
+
snapshot = migrationFailure({}, `Snapshot restoration failed: ${discovery.errors[0]}`);
|
|
498
|
+
} else if (selected.kind === "boundary" && forkInitialization) {
|
|
499
|
+
try {
|
|
500
|
+
// Native parent acquisition stays outside canonical exclusion.
|
|
501
|
+
const source = forkSource(ctx);
|
|
502
|
+
const sourceStopped = findPassiveStopBoundary(branch, source.id, PASSIVE_STOP_ENTRY_TYPE)?.persistenceError !== undefined;
|
|
503
|
+
selection = { kind: "fork", source, checkpoint: sourceStopped ? { ...selected.checkpoint, enabled: false } : selected.checkpoint };
|
|
504
|
+
} catch (error) {
|
|
505
|
+
snapshot = selectedBoundaryFailure(diagnosticText(error));
|
|
506
|
+
}
|
|
507
|
+
} else if (selected.kind === "boundary") {
|
|
508
|
+
selection = { kind: "restore", checkpoint: selected.checkpoint };
|
|
509
|
+
} else if (discovery.candidates.length > 0) {
|
|
510
|
+
snapshot = selected.kind === "disabled" ? emptySnapshot() : selected.snapshot;
|
|
511
|
+
} else if (config.autoStart && isNewSession(sessionStartReason, branch)) {
|
|
512
|
+
selection = { kind: "auto-start" };
|
|
513
|
+
} else {
|
|
514
|
+
snapshot = emptySnapshot();
|
|
413
515
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
516
|
+
}
|
|
517
|
+
} catch (error) {
|
|
518
|
+
selection = { kind: "settled" };
|
|
519
|
+
snapshot = selectedBoundaryFailure(diagnosticText(error));
|
|
520
|
+
}
|
|
521
|
+
// Pending selection grants neither private reads nor publication until its acceptance installs memory.
|
|
522
|
+
if (selection.kind !== "settled") snapshot = migrationFailure({}, "State Flow branch restoration is pending");
|
|
523
|
+
const pending: BranchRestoration = {
|
|
524
|
+
controller: new AbortController(),
|
|
525
|
+
awaitingAcceptance: selection.kind !== "settled" && selection.kind !== "current",
|
|
526
|
+
};
|
|
527
|
+
branchRestoration = pending;
|
|
528
|
+
const operation = attachBranch(ctx, pending, selection, sessionStartReason, notifyRecovery, skipped);
|
|
529
|
+
pending.operation = operation;
|
|
530
|
+
restorationOperations.add(operation);
|
|
531
|
+
const finished = () => { restorationOperations.delete(operation); };
|
|
532
|
+
void operation.then(finished, finished);
|
|
533
|
+
return operation;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
async function attachBranch(
|
|
537
|
+
ctx: ExtensionContext, pending: BranchRestoration, selection: BranchSelection,
|
|
538
|
+
reason: unknown, notifyRecovery: boolean, skipped: number,
|
|
539
|
+
): Promise<void> {
|
|
540
|
+
const placeholder = runtime;
|
|
541
|
+
let selected = runtime;
|
|
542
|
+
const owner = ctx.sessionManager.getSessionId();
|
|
543
|
+
const cwd = ctx.cwd;
|
|
544
|
+
const file = ctx.sessionManager.getSessionFile();
|
|
545
|
+
const timestamp = ctx.sessionManager.getHeader()?.timestamp;
|
|
546
|
+
const signal = AbortSignal.any([pending.controller.signal, ...(ctx.signal ? [ctx.signal] : [])]);
|
|
547
|
+
const isCurrent = () => branchRestoration === pending && !pending.controller.signal.aborted && !shuttingDown && runtime === selected
|
|
548
|
+
&& ctx.cwd === cwd && ctx.sessionManager.getSessionId() === owner && ctx.sessionManager.getSessionFile() === file
|
|
549
|
+
&& ctx.sessionManager.getHeader()?.timestamp === timestamp;
|
|
550
|
+
const assertCurrent = () => {
|
|
551
|
+
signal.throwIfAborted();
|
|
552
|
+
if (!isCurrent()) throw new Error("State Flow branch selection changed while awaiting publication");
|
|
553
|
+
};
|
|
554
|
+
let accepted = false;
|
|
555
|
+
// Install accepted memory before native writes; later ancillary failures cannot revert or replay it.
|
|
556
|
+
const accept = (candidate: TemporalRuntime, next: Snapshot, publication: RuntimePublication, nativeWrites: () => void): void => {
|
|
557
|
+
accepted = true;
|
|
558
|
+
pending.awaitingAcceptance = false;
|
|
559
|
+
runtime = selected = candidate;
|
|
560
|
+
snapshot = next;
|
|
561
|
+
installScopeStates();
|
|
562
|
+
recordPolicyPublication(publication, ctx);
|
|
563
|
+
let failure: unknown;
|
|
564
|
+
try { nativeWrites(); } catch (error) { failure = error; }
|
|
565
|
+
settleSelection(ctx, reason, notifyRecovery, skipped, true);
|
|
566
|
+
if (failure !== undefined) throw failure;
|
|
567
|
+
};
|
|
568
|
+
try {
|
|
569
|
+
if (selection.kind !== "settled") {
|
|
570
|
+
// Local policy is already passive while the selection waits for exclusion.
|
|
571
|
+
syncStateFlowTools();
|
|
572
|
+
updateUi(ctx);
|
|
573
|
+
const candidate = createRuntime(ctx);
|
|
574
|
+
try {
|
|
575
|
+
if (selection.kind === "current") {
|
|
576
|
+
const current = await candidate.refreshCurrentMemory(signal);
|
|
577
|
+
assertCurrent();
|
|
578
|
+
if (current) runtime = selected = candidate;
|
|
579
|
+
snapshot = current ?? migrationFailure({}, "Current State Flow session memory is unavailable");
|
|
580
|
+
installScopeStates();
|
|
581
|
+
} else if (selection.kind === "restore") {
|
|
582
|
+
await candidate.withRestoreTransaction(selection.checkpoint, (restored, publish) => {
|
|
583
|
+
assertCurrent();
|
|
584
|
+
// Canceled preparation or boundary continuation may have no specification. Retain uncompiled native context.
|
|
585
|
+
if (restored.config.enabled && restored.meta.specification === undefined
|
|
586
|
+
&& retainsPhysicalSessionProjection(reason) && hasUncheckpointedConversation(ctx.sessionManager.getBranch())) restored.meta.bootstrap = true;
|
|
587
|
+
const next = pending.passiveRequested ? stopEpisode(restored) : restored;
|
|
588
|
+
accept(candidate, next, publish(next), appendCheckpoint);
|
|
589
|
+
}, signal);
|
|
590
|
+
} else if (selection.kind === "fork") {
|
|
591
|
+
await candidate.withForkTransaction(selection.source, selection.checkpoint, (child, publish) => {
|
|
592
|
+
assertCurrent();
|
|
593
|
+
// Mode is selected independently of creating the child's private memory.
|
|
594
|
+
const next = pending.passiveRequested ? stopEpisode(child) : child;
|
|
595
|
+
const publication = publish(next);
|
|
596
|
+
forkInitialization = false;
|
|
597
|
+
accept(candidate, next, publication, () => {
|
|
598
|
+
pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, { reset: true, owner });
|
|
599
|
+
appendCheckpoint();
|
|
600
|
+
});
|
|
601
|
+
}, signal);
|
|
602
|
+
} else {
|
|
603
|
+
await candidate.withStartTransaction((current, publish) => {
|
|
604
|
+
assertCurrent();
|
|
605
|
+
// Truly new branch authority is rechecked after waiting; existing memory needs its own selection.
|
|
606
|
+
const branch = ctx.sessionManager.getBranch();
|
|
607
|
+
const discovery = discoverSnapshotData(branch);
|
|
608
|
+
if (current || discovery.candidates.length > 0 || discovery.errors.length > 0) throw new Error("Existing session runtime requires retained-boundary restoration");
|
|
609
|
+
const activated = startEpisode(hasPriorConversation(branch));
|
|
610
|
+
const next = pending.passiveRequested ? stopEpisode(activated) : activated;
|
|
611
|
+
accept(candidate, next, publish(next), appendCheckpoint);
|
|
612
|
+
}, signal, true);
|
|
613
|
+
}
|
|
614
|
+
} catch (error) {
|
|
615
|
+
if (accepted) {
|
|
616
|
+
if (isCurrent()) notifyProblem(ctx, `State Flow memory restored; lifecycle update failed: ${diagnosticText(error)}`, "warning");
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
if (!isCurrent()) return;
|
|
620
|
+
if (error instanceof HistoryBoundaryExpiredError) selectedHistoryExpired = true;
|
|
621
|
+
snapshot = selectedBoundaryFailure(diagnosticText(error));
|
|
622
|
+
installScopeStates();
|
|
623
|
+
} finally {
|
|
624
|
+
pending.awaitingAcceptance = false;
|
|
425
625
|
}
|
|
426
|
-
|
|
427
|
-
snapshot = emptySnapshot();
|
|
428
|
-
installScopeStates();
|
|
626
|
+
if (accepted) return;
|
|
429
627
|
}
|
|
430
|
-
|
|
431
|
-
|
|
628
|
+
settleSelection(ctx, reason, notifyRecovery, skipped, selected !== placeholder);
|
|
629
|
+
if (snapshot.config.enabled || !(config.passiveBootstrap || config.passiveTools) || runtime?.view) return;
|
|
630
|
+
const passive = createRuntime(ctx);
|
|
631
|
+
const priorView = selected?.view;
|
|
632
|
+
try {
|
|
633
|
+
await passive.refreshShared(signal);
|
|
634
|
+
} catch (error) {
|
|
635
|
+
if (isCurrent() && !signal.aborted && notifyRecovery && !stopPersistenceError) notifyProblem(ctx, `State Flow passive memory is unavailable: ${diagnosticText(error)}`, "warning");
|
|
636
|
+
return;
|
|
432
637
|
}
|
|
638
|
+
assertCurrent();
|
|
639
|
+
// An intervening passive patch or inspection owns its newer cache, even on the same physical branch.
|
|
640
|
+
if (selected?.view !== priorView) return;
|
|
641
|
+
runtime = selected = passive;
|
|
642
|
+
installScopeStates();
|
|
643
|
+
updateUi(ctx);
|
|
433
644
|
} catch (error) {
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
config: { ...snapshot.config, enabled: false },
|
|
438
|
-
meta: { ...snapshot.meta, validation: failure.meta.validation },
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
|
-
if (!snapshot.config.enabled && snapshot.meta.validation?.attempt === 0) {
|
|
442
|
-
// A failed acceptance may have installed a prepared view; only shared passive reads may survive.
|
|
443
|
-
runtime = createRuntime(ctx);
|
|
645
|
+
// Host failures before acceptance leave the selection unavailable, never invented empty memory.
|
|
646
|
+
if (accepted || !isCurrent()) return;
|
|
647
|
+
snapshot = selectedBoundaryFailure(diagnosticText(error));
|
|
444
648
|
installScopeStates();
|
|
445
|
-
|
|
649
|
+
} finally {
|
|
650
|
+
pending.awaitingAcceptance = false;
|
|
651
|
+
if (branchRestoration === pending) branchRestoration = undefined;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function settleSelection(ctx: ExtensionContext, reason: unknown, notifyRecovery: boolean, skipped: number, selectedMemory: boolean): void {
|
|
656
|
+
const failure = !snapshot.config.enabled && snapshot.meta.validation?.attempt === 0 ? snapshot.meta.validation.error : undefined;
|
|
657
|
+
if (failure !== undefined && notifyRecovery && !stopPersistenceError) {
|
|
658
|
+
if (selectedHistoryExpired) notifyProblem(ctx, "State Flow history is outside the retained temporal window; /state-flow-start can use current session memory.", "warning");
|
|
659
|
+
else notifyProblem(ctx, `State Flow restore failed: ${failure}`, "error");
|
|
446
660
|
}
|
|
447
|
-
if (retainsPhysicalSessionProjection(
|
|
661
|
+
if (selectedMemory && retainsPhysicalSessionProjection(reason) && runtime?.view) {
|
|
448
662
|
const boundary = findPassiveStopBoundary(ctx.sessionManager.getBranch(), ctx.sessionManager.getSessionId(), PASSIVE_STOP_ENTRY_TYPE);
|
|
449
663
|
if (boundary !== undefined) {
|
|
450
664
|
const continuation = createPassiveContinuation(
|
|
451
665
|
projectModelState(overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session)),
|
|
452
666
|
boundary.at,
|
|
453
667
|
boundary.from,
|
|
668
|
+
boundary.preserveContext,
|
|
454
669
|
);
|
|
455
670
|
if (!snapshot.config.enabled) passiveContinuation = continuation;
|
|
456
671
|
else if (snapshot.meta.bootstrap) bootstrapContinuation = continuation;
|
|
457
672
|
}
|
|
458
673
|
}
|
|
459
|
-
if (
|
|
460
|
-
|
|
461
|
-
runtime?.loadPassive();
|
|
462
|
-
installScopeStates();
|
|
463
|
-
} catch (error) {
|
|
464
|
-
ctx.ui.notify(`State Flow passive memory is unavailable: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
465
|
-
}
|
|
674
|
+
if (notifyRecovery && snapshot.config.enabled && skipped > 0) {
|
|
675
|
+
notifyProblem(ctx, `State Flow skipped ${skipped} malformed snapshot(s); restored the last valid one.`, "warning");
|
|
466
676
|
}
|
|
467
|
-
if (snapshot.config.enabled)
|
|
677
|
+
if (snapshot.config.enabled) deferInferencePreparation();
|
|
468
678
|
syncStateFlowTools();
|
|
469
679
|
updateUi(ctx);
|
|
470
680
|
}
|
|
@@ -540,9 +750,10 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
540
750
|
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
541
751
|
try {
|
|
542
752
|
if (!passiveToolsAvailable()) throw new Error("State Flow tools are disabled by configuration");
|
|
543
|
-
|
|
753
|
+
assertPublicationAvailable();
|
|
544
754
|
if (signal?.aborted) throw new Error("State Flow patch was aborted before materialization");
|
|
545
755
|
if (!isObject(params)) throw new Error("patch_state requires an object");
|
|
756
|
+
params = structuredClone(params);
|
|
546
757
|
const allowed = new Set(["global", "cwd", "session"]);
|
|
547
758
|
for (const key of Object.keys(params)) {
|
|
548
759
|
if (!allowed.has(key)) throw new Error(`patch_state does not accept field ${key}`);
|
|
@@ -556,34 +767,48 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
556
767
|
patches[scope] = patch;
|
|
557
768
|
}
|
|
558
769
|
const scopes = Object.keys(patches) as StateScope[];
|
|
559
|
-
if (scopes.length === 0) throw new Error("patch_state requires at least one
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
770
|
+
if (scopes.length === 0) throw new Error("patch_state requires at least one scope patch");
|
|
771
|
+
const selected = runtime ??= createRuntime(ctx);
|
|
772
|
+
const acquiredArtifacts = structuredClone([...artifactReads.successful.values()]);
|
|
773
|
+
const acquiredSkills = structuredClone([...skillReads.successful.values()]);
|
|
774
|
+
return await selected.withPatchTransaction((transaction) => {
|
|
775
|
+
if (runtime !== selected) throw new Error("State Flow session selection changed while awaiting publication");
|
|
776
|
+
if (!passiveToolsAvailable()) throw new Error("State Flow tools are disabled by configuration");
|
|
777
|
+
assertPublicationAvailable();
|
|
778
|
+
for (const acquired of acquiredArtifacts) {
|
|
779
|
+
const observation = inspectRegisteredArtifactPaths([acquired.path])[0];
|
|
780
|
+
if (acquired.sourceFingerprint === undefined || observation?.kind !== "present"
|
|
781
|
+
|| !sameArtifactSourceFingerprint(acquired.sourceFingerprint, observation.fingerprint)) {
|
|
782
|
+
throw new Error(`Artifact source changed after acquisition: ${acquired.path}`);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
const stage = stageAtomicScopePatches(transaction.states, patches, acquiredSkills, transaction.causalBasis, acquiredArtifacts);
|
|
786
|
+
const changed = (["global", "cwd", "session"] as const).some((scope) =>
|
|
787
|
+
!sameJson(transaction.states[scope], stage.nextStates[scope])
|
|
788
|
+
|| Object.entries(stage.provenanceUpdates[scope]).some(([path, evidence]) =>
|
|
789
|
+
!Object.hasOwn(transaction.provenance[scope], path) || !sameJson(transaction.provenance[scope][path], evidence)));
|
|
790
|
+
const nextSnapshot = structuredClone(snapshot);
|
|
791
|
+
let publication: RuntimePublication | undefined;
|
|
792
|
+
commitScopedTransition(nextSnapshot, transaction.states, stage, (accepted, next) => {
|
|
793
|
+
publication = transaction.publish(next, accepted, stage.provenanceUpdates);
|
|
794
|
+
}, transaction.causalBasis, { finalizeRun: false });
|
|
795
|
+
snapshot = nextSnapshot;
|
|
565
796
|
installScopeStates();
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
for (const acquired of artifactReads.successful.values()) {
|
|
570
|
-
const observation = inspectRegisteredArtifactPaths([acquired.path])[0];
|
|
571
|
-
if (acquired.sourceFingerprint === undefined || observation?.kind !== "present"
|
|
572
|
-
|| !sameArtifactSourceFingerprint(acquired.sourceFingerprint, observation.fingerprint)) {
|
|
573
|
-
throw new Error(`Artifact source changed after acquisition: ${acquired.path}`);
|
|
797
|
+
if (publication?.changed) {
|
|
798
|
+
recordPublication(publication, ctx);
|
|
799
|
+
appendCheckpoint();
|
|
574
800
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
updateUi(ctx);
|
|
582
|
-
return { content: [{ type: "text", text: `\nState materialized atomically at ${scopes.join("+")} scope${scopes.length === 1 ? "" : "s"}.` }], details: { scopes, step: snapshot.meta.step } };
|
|
801
|
+
clearAcceptedAcquisitions(new Set(acquiredArtifacts.map(({ path }) => path)));
|
|
802
|
+
updateUi(ctx);
|
|
803
|
+
return { content: [{ type: "text" as const, text: changed
|
|
804
|
+
? `\nState materialized atomically at ${scopes.join("+")} scope${scopes.length === 1 ? "" : "s"}.`
|
|
805
|
+
: "\nState already current." }], details: { scopes, step: snapshot.meta.step, changed } };
|
|
806
|
+
}, signal);
|
|
583
807
|
} catch (error) {
|
|
584
808
|
let attempted: unknown;
|
|
585
809
|
try { attempted = structuredClone(params); } catch { attempted = undefined; }
|
|
586
|
-
|
|
810
|
+
const cause = diagnosticText(error);
|
|
811
|
+
recordDiagnostic(cause, /concurrently|advanced/.test(cause) ? "publication-conflict" : "invalid-patch", ctx, {
|
|
587
812
|
input: attempted,
|
|
588
813
|
tool: PATCH_STATE_TOOL_NAME,
|
|
589
814
|
toolCallId,
|
|
@@ -593,74 +818,127 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
593
818
|
},
|
|
594
819
|
});
|
|
595
820
|
|
|
596
|
-
function startStateFlow(ctx: ExtensionContext): StateFlowTelegramControlResult {
|
|
597
|
-
if (
|
|
821
|
+
function startStateFlow(ctx: ExtensionContext): Promise<StateFlowTelegramControlResult> {
|
|
822
|
+
if (shuttingDown) return Promise.resolve({ ok: false, message: "State Flow is shutting down" });
|
|
823
|
+
if (startActivation?.operation) return startActivation.operation;
|
|
598
824
|
telegramStartPending = false;
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
const
|
|
603
|
-
|
|
825
|
+
if (activeContext && !branchRestoration && snapshot.config.enabled) {
|
|
826
|
+
return Promise.resolve({ ok: true, message: "State Flow is already enabled", signal: sharedInspectionLifetime.signal });
|
|
827
|
+
}
|
|
828
|
+
const pending: NonNullable<typeof startActivation> = { controller: new AbortController() };
|
|
829
|
+
startActivation = pending;
|
|
830
|
+
const operation = runStart(ctx, pending.controller);
|
|
831
|
+
pending.operation = operation;
|
|
832
|
+
const finished = () => { if (startActivation === pending) startActivation = undefined; };
|
|
833
|
+
void operation.then(finished, finished);
|
|
834
|
+
return operation;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/** Await restoration-owned attachment and exact-source fork recovery before current-head activation. */
|
|
838
|
+
async function runStart(ctx: ExtensionContext, pending: AbortController): Promise<StateFlowTelegramControlResult> {
|
|
839
|
+
const owner = ctx.sessionManager.getSessionId();
|
|
840
|
+
const cwd = ctx.cwd;
|
|
841
|
+
const file = ctx.sessionManager.getSessionFile();
|
|
842
|
+
const timestamp = ctx.sessionManager.getHeader()?.timestamp;
|
|
843
|
+
const signal = ctx.signal ? AbortSignal.any([pending.signal, ctx.signal]) : pending.signal;
|
|
844
|
+
const isOwner = () => startActivation?.controller === pending && !signal.aborted && !shuttingDown
|
|
845
|
+
&& ctx.cwd === cwd && ctx.sessionManager.getSessionId() === owner && ctx.sessionManager.getSessionFile() === file
|
|
846
|
+
&& ctx.sessionManager.getHeader()?.timestamp === timestamp;
|
|
847
|
+
const superseded = (): StateFlowTelegramControlResult => {
|
|
848
|
+
pending.abort();
|
|
849
|
+
return { ok: false, message: "State Flow Start was superseded", signal: pending.signal };
|
|
850
|
+
};
|
|
604
851
|
try {
|
|
605
|
-
if (!
|
|
606
|
-
|
|
607
|
-
|
|
852
|
+
if (!activeContext) await waitForRecovery(restoreActiveBranch(ctx, undefined, false, pending), signal);
|
|
853
|
+
else if (branchRestoration?.operation) await waitForRecovery(branchRestoration.operation, signal);
|
|
854
|
+
if (!isOwner()) return superseded();
|
|
855
|
+
if (snapshot.config.enabled) return { ok: true, message: "State Flow is already enabled", signal: sharedInspectionLifetime.signal };
|
|
856
|
+
if (forkInitialization) {
|
|
857
|
+
// Withdraw this join on cancellation without revoking the independently owned Stop.
|
|
858
|
+
const stopping = stopPersistence?.operation;
|
|
859
|
+
if (stopping) await waitForRecovery(stopping, signal);
|
|
860
|
+
if (!isOwner()) return superseded();
|
|
861
|
+
await waitForRecovery(restoreActiveBranch(ctx, "fork", false, pending), signal);
|
|
862
|
+
if (!isOwner()) return superseded();
|
|
863
|
+
assertSelectedBranchAvailable();
|
|
608
864
|
}
|
|
609
|
-
|
|
610
|
-
if (!runtime?.view && !branchStartsWithoutRuntime) throw new Error("Selected State Flow boundary is unavailable; restore its canonical files before starting State Flow");
|
|
611
|
-
const branch = ctx.sessionManager.getBranch();
|
|
612
|
-
activeContext = ctx;
|
|
613
|
-
runtime ??= createRuntime(ctx);
|
|
614
|
-
if (branchStartsWithoutRuntime) runtime.prepare();
|
|
615
|
-
const bootstrap = (!branchHasSnapshot || !snapshot.config.enabled)
|
|
616
|
-
&& (hasPriorConversation(branch) || previousPassiveContinuation !== undefined);
|
|
617
|
-
const existingBranch = branchHasSnapshot;
|
|
618
|
-
snapshot = existingBranch
|
|
619
|
-
? resumeEpisode(snapshot, bootstrap)
|
|
620
|
-
: startEpisode(bootstrap);
|
|
621
|
-
branchHasSnapshot = true;
|
|
622
|
-
// Activation reasserts the selected session stream as a complete cohort. A resumed
|
|
623
|
-
// branch may have no reusable temporal revision after an extension upgrade; a
|
|
624
|
-
// runtime-only write would then reject its selected session as an omitted change.
|
|
625
|
-
const publication = runtime.view
|
|
626
|
-
? runtime.publish(snapshot, true)
|
|
627
|
-
: runtime.initialize(snapshot, true, undefined, branchStartsWithoutRuntime);
|
|
628
|
-
recordPolicyPublication(publication, ctx);
|
|
629
|
-
installScopeStates();
|
|
630
|
-
clearRunTransient();
|
|
631
|
-
passiveContinuation = undefined;
|
|
632
|
-
bootstrapContinuation = snapshot.meta.bootstrap
|
|
633
|
-
? previousPassiveContinuation ?? previousBootstrapContinuation
|
|
634
|
-
: undefined;
|
|
635
|
-
deferArtifactRefresh();
|
|
636
|
-
syncStateFlowTools();
|
|
637
|
-
persist();
|
|
638
|
-
updateUi(ctx);
|
|
639
|
-
ctx.ui.notify(
|
|
640
|
-
snapshot.meta.bootstrap
|
|
641
|
-
? "State Flow enabled. The next complete agent run will migrate active context into state."
|
|
642
|
-
: "State Flow enabled. The next prompt starts a stateful agent run.",
|
|
643
|
-
"info",
|
|
644
|
-
);
|
|
645
|
-
return { ok: true, message: "State Flow enabled" };
|
|
865
|
+
return activateCurrentState(ctx, pending);
|
|
646
866
|
} catch (error) {
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
867
|
+
if (!isOwner()) return superseded();
|
|
868
|
+
const message = conciseDiagnostic(`State Flow Start failed: ${diagnosticText(error)}`);
|
|
869
|
+
notifyProblem(ctx, message, "error");
|
|
870
|
+
return { ok: false, message, signal: AbortSignal.any([pending.signal, sharedInspectionLifetime.signal]) };
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
async function activateCurrentState(ctx: ExtensionContext, pending: AbortController): Promise<StateFlowTelegramControlResult> {
|
|
875
|
+
let selected = runtime;
|
|
876
|
+
const owner = ctx.sessionManager.getSessionId();
|
|
877
|
+
const cwd = ctx.cwd;
|
|
878
|
+
const file = ctx.sessionManager.getSessionFile();
|
|
879
|
+
const timestamp = ctx.sessionManager.getHeader()?.timestamp;
|
|
880
|
+
const initiallyEnabled = snapshot.config.enabled;
|
|
881
|
+
let accepted = false;
|
|
882
|
+
let receipt = AbortSignal.any([pending.signal, sharedInspectionLifetime.signal]);
|
|
883
|
+
const isCurrent = () => startActivation?.controller === pending && runtime === selected && !shuttingDown
|
|
884
|
+
&& ctx.cwd === cwd && ctx.sessionManager.getSessionId() === owner && ctx.sessionManager.getSessionFile() === file
|
|
885
|
+
&& ctx.sessionManager.getHeader()?.timestamp === timestamp && snapshot.config.enabled === (accepted || initiallyEnabled);
|
|
886
|
+
const superseded = (): StateFlowTelegramControlResult => {
|
|
887
|
+
pending.abort();
|
|
888
|
+
return { ok: false, message: "State Flow Start was superseded", signal: pending.signal };
|
|
889
|
+
};
|
|
890
|
+
try {
|
|
891
|
+
const activation = createRuntime(ctx);
|
|
892
|
+
const signal = ctx.signal ? AbortSignal.any([pending.signal, ctx.signal]) : pending.signal;
|
|
893
|
+
const result = await activation.withStartTransaction((current, publish) => {
|
|
894
|
+
signal.throwIfAborted();
|
|
895
|
+
if (!isCurrent()) throw new Error("State Flow Start selection changed while awaiting publication");
|
|
896
|
+
if (!current && !branchStartsWithoutRuntime) throw new Error(snapshot.meta.validation?.error ?? "Current State Flow session memory is unavailable");
|
|
897
|
+
const continuation = passiveContinuation ?? bootstrapContinuation;
|
|
898
|
+
const bootstrap = hasPriorConversation(ctx.sessionManager.getBranch()) || passiveContinuation !== undefined;
|
|
899
|
+
const activated = current ? resumeEpisode(current, bootstrap) : startEpisode(bootstrap);
|
|
900
|
+
const recoveredCurrent = selectedHistoryExpired;
|
|
901
|
+
const publication = publish(activated);
|
|
902
|
+
accepted = true;
|
|
903
|
+
cancelStopPersistence();
|
|
904
|
+
runtime = selected = activation;
|
|
905
|
+
snapshot = activated;
|
|
906
|
+
activeContext = ctx;
|
|
907
|
+
selectedHistoryExpired = false;
|
|
908
|
+
stopPersistenceError = undefined;
|
|
909
|
+
installScopeStates();
|
|
910
|
+
clearRunTransient();
|
|
911
|
+
passiveContinuation = undefined;
|
|
912
|
+
bootstrapContinuation = snapshot.meta.bootstrap ? continuation : undefined;
|
|
913
|
+
deferInferencePreparation();
|
|
914
|
+
receipt = AbortSignal.any([pending.signal, sharedInspectionLifetime.signal]);
|
|
915
|
+
recordPolicyPublication(publication, ctx);
|
|
916
|
+
syncStateFlowTools();
|
|
917
|
+
updateUi(ctx);
|
|
918
|
+
appendCheckpoint();
|
|
919
|
+
ctx.ui.notify(
|
|
920
|
+
recoveredCurrent
|
|
921
|
+
? "State Flow enabled from current session memory; unavailable historical state was not restored."
|
|
922
|
+
: snapshot.meta.bootstrap
|
|
923
|
+
? "State Flow enabled. The next complete agent run will migrate active context into state."
|
|
924
|
+
: "State Flow enabled. The next prompt starts a stateful agent run.",
|
|
925
|
+
"info",
|
|
926
|
+
);
|
|
927
|
+
return { ok: true, message: "State Flow enabled", signal: receipt };
|
|
928
|
+
}, signal, branchStartsWithoutRuntime);
|
|
929
|
+
return isCurrent() ? result : superseded();
|
|
930
|
+
} catch (error) {
|
|
931
|
+
if (!isCurrent()) return superseded();
|
|
932
|
+
const message = conciseDiagnostic(`${accepted ? "State Flow enabled; lifecycle update failed" : "State Flow Start failed"}: ${diagnosticText(error)}`);
|
|
933
|
+
notifyProblem(ctx, message, accepted ? "warning" : "error");
|
|
934
|
+
return { ok: accepted, message, signal: receipt };
|
|
657
935
|
}
|
|
658
936
|
}
|
|
659
937
|
|
|
660
938
|
pi.registerCommand("state-flow-start", {
|
|
661
939
|
description: "Start State Flow mode",
|
|
662
940
|
handler: async (_args, ctx) => {
|
|
663
|
-
startStateFlow(ctx);
|
|
941
|
+
await startStateFlow(ctx);
|
|
664
942
|
},
|
|
665
943
|
});
|
|
666
944
|
|
|
@@ -671,47 +949,121 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
671
949
|
},
|
|
672
950
|
});
|
|
673
951
|
|
|
674
|
-
function stopStateFlow(ctx: ExtensionContext): StateFlowTelegramControlResult {
|
|
675
|
-
|
|
676
|
-
|
|
952
|
+
function stopStateFlow(ctx: ExtensionContext): Promise<StateFlowTelegramControlResult> {
|
|
953
|
+
cancelStartActivation();
|
|
954
|
+
if (shuttingDown) return Promise.resolve({ ok: false, message: "State Flow is shutting down" });
|
|
955
|
+
if (stopPersistence?.operation) return stopPersistence.operation;
|
|
956
|
+
if (!activeContext) void restoreActiveBranch(ctx, undefined, false);
|
|
957
|
+
const restoring = branchRestoration;
|
|
958
|
+
if (restoring?.awaitingAcceptance) {
|
|
959
|
+
// Stop changes policy without cancelling memory restoration or initialization.
|
|
960
|
+
restoring.passiveRequested = true;
|
|
961
|
+
snapshot = stopEpisode(snapshot);
|
|
962
|
+
clearRunTransient();
|
|
963
|
+
syncStateFlowTools();
|
|
964
|
+
updateUi(ctx);
|
|
965
|
+
return Promise.resolve({ ok: true, message: "State Flow disabled; memory restoration continues" });
|
|
966
|
+
}
|
|
967
|
+
const pending: NonNullable<typeof stopPersistence> = { controller: new AbortController() };
|
|
968
|
+
stopPersistence = pending;
|
|
969
|
+
const operation = persistStoppedState(ctx, pending.controller);
|
|
970
|
+
pending.operation = operation;
|
|
971
|
+
const finished = () => { if (stopPersistence === pending) stopPersistence = undefined; };
|
|
972
|
+
void operation.then(finished, finished);
|
|
973
|
+
return operation;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
async function persistStoppedState(ctx: ExtensionContext, pending: AbortController): Promise<StateFlowTelegramControlResult> {
|
|
677
977
|
telegramStartPending = false;
|
|
978
|
+
const selected = runtime;
|
|
979
|
+
const owner = ctx.sessionManager.getSessionId();
|
|
678
980
|
const current = snapshot;
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
installScopeStates();
|
|
683
|
-
// Freeze the accepted shared adoption, not the pre-publication cache.
|
|
684
|
-
const exitHandoff = current.config.enabled && runtime?.view
|
|
685
|
-
? createPassiveContinuation(
|
|
686
|
-
projectModelState(overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session)),
|
|
687
|
-
stoppedAt,
|
|
688
|
-
ctx.isIdle() ? undefined : runAnchorTimestamp,
|
|
689
|
-
)
|
|
690
|
-
: undefined;
|
|
691
|
-
const retainedHandoff = exitHandoff ?? (!current.config.enabled ? passiveContinuation : undefined);
|
|
692
|
-
snapshot = stopped;
|
|
693
|
-
recordPolicyPublication(publication, ctx);
|
|
694
|
-
branchHasSnapshot = true;
|
|
981
|
+
let unfinished = current.meta.specification !== undefined
|
|
982
|
+
|| (inferencePreparation?.prompt !== undefined && !inferencePreparation.accepted);
|
|
983
|
+
snapshot = stopEpisode(current);
|
|
695
984
|
clearRunTransient();
|
|
696
|
-
passiveContinuation = retainedHandoff;
|
|
697
|
-
bootstrapContinuation = undefined;
|
|
698
|
-
artifactInvalidations = [];
|
|
699
|
-
artifactReads.setCandidates([]);
|
|
700
|
-
artifactRefreshPending = false;
|
|
701
|
-
if (exitHandoff) pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, {
|
|
702
|
-
at: stoppedAt,
|
|
703
|
-
...(exitHandoff.activeRunStartedAt === undefined ? {} : { from: exitHandoff.activeRunStartedAt }),
|
|
704
|
-
});
|
|
705
985
|
syncStateFlowTools();
|
|
706
|
-
persist();
|
|
707
986
|
updateUi(ctx);
|
|
708
|
-
return { ok: true, message: "State Flow disabled" };
|
|
987
|
+
if (stopPersistenceError) return { ok: true, message: "State Flow disabled; memory writes remain paused" };
|
|
988
|
+
const stoppedAt = Date.now();
|
|
989
|
+
const incomingBoundary = current.meta.bootstrap ? bootstrapContinuation : undefined;
|
|
990
|
+
const anchor = runAnchorTimestamp;
|
|
991
|
+
const idle = ctx.isIdle();
|
|
992
|
+
const isCurrent = () => stopPersistence?.controller === pending && runtime === selected
|
|
993
|
+
&& !shuttingDown && ctx.sessionManager.getSessionId() === owner && !snapshot.config.enabled;
|
|
994
|
+
const superseded = (): StateFlowTelegramControlResult => ({ ok: false, message: "State Flow Stop was superseded" });
|
|
995
|
+
const freezeHandoff = (): PassiveContinuation | undefined => {
|
|
996
|
+
const handoff = (current.config.enabled || stopPersistenceError) && selected?.view && current.meta.validation?.attempt !== 0
|
|
997
|
+
? createPassiveContinuation(
|
|
998
|
+
projectModelState(overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session)),
|
|
999
|
+
incomingBoundary?.startedAt ?? stoppedAt,
|
|
1000
|
+
incomingBoundary ? incomingBoundary.activeRunStartedAt : !idle || unfinished ? anchor : undefined,
|
|
1001
|
+
stopPersistenceError !== undefined || (incomingBoundary ? incomingBoundary.preserveContext : current.meta.bootstrap === true || (unfinished && anchor === undefined)),
|
|
1002
|
+
)
|
|
1003
|
+
: undefined;
|
|
1004
|
+
passiveContinuation = handoff ?? (!current.config.enabled ? passiveContinuation : undefined);
|
|
1005
|
+
return handoff;
|
|
1006
|
+
};
|
|
1007
|
+
const complete = (): StateFlowTelegramControlResult => {
|
|
1008
|
+
const exitHandoff = freezeHandoff();
|
|
1009
|
+
if (exitHandoff || stopPersistenceError) pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, {
|
|
1010
|
+
at: exitHandoff?.startedAt ?? stoppedAt,
|
|
1011
|
+
...(exitHandoff?.activeRunStartedAt === undefined ? {} : { from: exitHandoff.activeRunStartedAt }),
|
|
1012
|
+
...(exitHandoff?.preserveContext || stopPersistenceError ? { preserveContext: true } : {}),
|
|
1013
|
+
...(stopPersistenceError === undefined ? {} : { owner, persistenceError: stopPersistenceError }),
|
|
1014
|
+
});
|
|
1015
|
+
if (!stopPersistenceError) {
|
|
1016
|
+
appendCheckpoint();
|
|
1017
|
+
return { ok: true, message: "State Flow disabled" };
|
|
1018
|
+
}
|
|
1019
|
+
const message = conciseDiagnostic(`State Flow disabled; memory writes paused: ${stopPersistenceError}`);
|
|
1020
|
+
notifyProblem(ctx, message, "warning");
|
|
1021
|
+
return { ok: true, message };
|
|
1022
|
+
};
|
|
1023
|
+
let accepted = false;
|
|
1024
|
+
try {
|
|
1025
|
+
unfinished ||= current.config.enabled && hasUncheckpointedConversation(ctx.sessionManager.getBranch());
|
|
1026
|
+
// Projection changes before any wait; capture the old run's boundary before later native input can replace it.
|
|
1027
|
+
freezeHandoff();
|
|
1028
|
+
bootstrapContinuation = undefined;
|
|
1029
|
+
artifactInvalidations = [];
|
|
1030
|
+
artifactReads.setCandidates([]);
|
|
1031
|
+
assertPublicationAvailable();
|
|
1032
|
+
if (branchStartsWithoutRuntime || !selected?.view) return complete();
|
|
1033
|
+
const signal = ctx.signal ? AbortSignal.any([pending.signal, ctx.signal]) : pending.signal;
|
|
1034
|
+
const result = await selected.withLifecycleTransaction((publish) => {
|
|
1035
|
+
signal.throwIfAborted();
|
|
1036
|
+
if (!isCurrent()) throw new Error("State Flow Stop selection changed while awaiting publication");
|
|
1037
|
+
assertPublicationAvailable();
|
|
1038
|
+
const stopped = stopEpisode(snapshot);
|
|
1039
|
+
const publication = publish(stopped);
|
|
1040
|
+
accepted = true;
|
|
1041
|
+
snapshot = stopped;
|
|
1042
|
+
installScopeStates();
|
|
1043
|
+
recordPolicyPublication(publication, ctx);
|
|
1044
|
+
return complete();
|
|
1045
|
+
}, signal);
|
|
1046
|
+
return isCurrent() ? result : superseded();
|
|
1047
|
+
} catch (error) {
|
|
1048
|
+
if (!isCurrent()) return superseded();
|
|
1049
|
+
const cause = diagnosticText(error);
|
|
1050
|
+
if (accepted) {
|
|
1051
|
+
const message = conciseDiagnostic(`State Flow disabled; lifecycle update failed: ${cause}`);
|
|
1052
|
+
notifyProblem(ctx, message, "warning");
|
|
1053
|
+
return { ok: true, message };
|
|
1054
|
+
}
|
|
1055
|
+
stopPersistenceError = cause.trim() ? cause : "Canonical State Flow persistence failed";
|
|
1056
|
+
bootstrapContinuation = undefined;
|
|
1057
|
+
artifactInvalidations = [];
|
|
1058
|
+
artifactReads.setCandidates([]);
|
|
1059
|
+
return complete();
|
|
1060
|
+
}
|
|
709
1061
|
}
|
|
710
1062
|
|
|
711
1063
|
pi.registerCommand("state-flow-stop", {
|
|
712
1064
|
description: "Stop State Flow on the current session branch",
|
|
713
1065
|
handler: async (_args, ctx) => {
|
|
714
|
-
stopStateFlow(ctx);
|
|
1066
|
+
await stopStateFlow(ctx);
|
|
715
1067
|
},
|
|
716
1068
|
});
|
|
717
1069
|
|
|
@@ -725,25 +1077,20 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
725
1077
|
bootstrap: snapshot.meta.bootstrap === true,
|
|
726
1078
|
startPending: telegramStartPending,
|
|
727
1079
|
}),
|
|
728
|
-
|
|
729
|
-
refreshTelegramStateView(scope);
|
|
730
|
-
const selected = scope === "effective"
|
|
731
|
-
? overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session)
|
|
732
|
-
: scopeStates[scope];
|
|
733
|
-
return { ...projectModelState(selected), lazy: structuredClone(selected.lazy) };
|
|
734
|
-
},
|
|
735
|
-
revisions: () => scopeRevisions(),
|
|
1080
|
+
inspect: inspectTelegramState,
|
|
736
1081
|
canStartNow: () => activeContext === undefined || activeContext.isIdle(),
|
|
737
1082
|
start: () => {
|
|
738
1083
|
if (!activeContext) throw new Error("State Flow is not attached to an active session yet");
|
|
739
1084
|
return startStateFlow(activeContext);
|
|
740
1085
|
},
|
|
741
|
-
stop: () => {
|
|
1086
|
+
stop: async () => {
|
|
742
1087
|
if (!activeContext) throw new Error("State Flow is not attached to an active session yet");
|
|
1088
|
+
const operation = stopStateFlow(activeContext);
|
|
1089
|
+
const signal = sharedInspectionLifetime.signal;
|
|
743
1090
|
try {
|
|
744
|
-
return
|
|
1091
|
+
return { ...await operation, signal };
|
|
745
1092
|
} catch (error) {
|
|
746
|
-
return { ok: false, message:
|
|
1093
|
+
return { ok: false, message: diagnosticText(error), signal };
|
|
747
1094
|
}
|
|
748
1095
|
},
|
|
749
1096
|
deferStart: () => {
|
|
@@ -751,12 +1098,13 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
751
1098
|
},
|
|
752
1099
|
cancelStart: () => {
|
|
753
1100
|
telegramStartPending = false;
|
|
1101
|
+
cancelStartActivation();
|
|
754
1102
|
},
|
|
755
1103
|
},
|
|
756
1104
|
});
|
|
757
1105
|
void telegram.ensure();
|
|
758
1106
|
|
|
759
|
-
pi.on("before_agent_start", (event
|
|
1107
|
+
pi.on("before_agent_start", (event) => {
|
|
760
1108
|
// Native run identity is independent of semantic enablement and survives mode toggles.
|
|
761
1109
|
runAnchorTimestamp = undefined;
|
|
762
1110
|
if (!snapshot.config.enabled) {
|
|
@@ -766,17 +1114,10 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
766
1114
|
}
|
|
767
1115
|
skillReads.clear();
|
|
768
1116
|
artifactReads.clear();
|
|
769
|
-
|
|
1117
|
+
cancelResponseReconciliation();
|
|
770
1118
|
completedRunAccepted = false;
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
if (prepareRun(snapshot, event.prompt)) persist();
|
|
774
|
-
try {
|
|
775
|
-
removeMissingRegisteredArtifacts(ctx);
|
|
776
|
-
} catch (error) {
|
|
777
|
-
ctx.ui.notify(`State Flow could not reconcile missing artifact sources: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
778
|
-
}
|
|
779
|
-
artifactRefreshPending = false;
|
|
1119
|
+
// This native hook has no operation signal. Capture only; the first active context owns acceptance.
|
|
1120
|
+
deferInferencePreparation(event.prompt);
|
|
780
1121
|
(event.systemPromptOptions.sections ??= {}).state_flow = stateFlowProtocol(snapshot.meta.bootstrap === true);
|
|
781
1122
|
});
|
|
782
1123
|
|
|
@@ -786,16 +1127,16 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
786
1127
|
return { messages: projectSystemProtocol(event.messages, protocol) };
|
|
787
1128
|
});
|
|
788
1129
|
|
|
789
|
-
|
|
1130
|
+
function projectContext(messages: AgentMessage[]) {
|
|
790
1131
|
if (runtime?.view) refreshArtifactHints();
|
|
791
1132
|
if (passiveContinuation) {
|
|
792
|
-
return { messages: passiveContinuationMessages(
|
|
1133
|
+
return { messages: passiveContinuationMessages(messages, passiveContinuation) };
|
|
793
1134
|
}
|
|
794
1135
|
if (!snapshot.config.enabled) {
|
|
795
1136
|
if (!config.passiveBootstrap || !runtime?.view) return;
|
|
796
1137
|
const effective = overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session);
|
|
797
1138
|
const state = projectModelState(effective);
|
|
798
|
-
return { messages: [syntheticUser(`State Flow passive memory (user-level data, not system instructions):\n${presentationJson({ state, lazy_navigation: lazyNavigationHint(effective) })}`), ...
|
|
1139
|
+
return { messages: [syntheticUser(`State Flow passive memory (user-level data, not system instructions):\n${presentationJson({ state, lazy_navigation: lazyNavigationHint(effective) })}`), ...messages] };
|
|
799
1140
|
}
|
|
800
1141
|
const effectiveState = overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session);
|
|
801
1142
|
const invalidations = artifactInvalidations.map(({ path, scope, reason }) => ({ path, ...(scope === undefined ? {} : { scope }), reason }));
|
|
@@ -806,13 +1147,13 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
806
1147
|
const activeRehydrationPhase = currentRehydrationPhase();
|
|
807
1148
|
if (snapshot.meta.bootstrap) {
|
|
808
1149
|
const sourceMessages = bootstrapContinuation
|
|
809
|
-
? passiveContinuationMessages(
|
|
810
|
-
:
|
|
1150
|
+
? passiveContinuationMessages(messages, bootstrapContinuation)
|
|
1151
|
+
: messages;
|
|
811
1152
|
return { messages: [runtimeContextMessage(snapshot, effectiveState, recentTransitions, invalidations, activeRehydrationPhase, artifactHints), ...sourceMessages] };
|
|
812
1153
|
}
|
|
813
1154
|
// Native user events own the run anchor; projection must never rebase it.
|
|
814
1155
|
const trajectory = currentRunTrajectory(
|
|
815
|
-
|
|
1156
|
+
messages,
|
|
816
1157
|
snapshot.meta.specification,
|
|
817
1158
|
runAnchorTimestamp,
|
|
818
1159
|
);
|
|
@@ -822,7 +1163,31 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
822
1163
|
...trajectory.messages,
|
|
823
1164
|
],
|
|
824
1165
|
};
|
|
825
|
-
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
function prepareContext(messages: AgentMessage[], ctx: ExtensionContext): ReturnType<typeof projectContext> | Promise<ReturnType<typeof projectContext>> {
|
|
1169
|
+
const pending = inferencePreparation;
|
|
1170
|
+
const selected = runtime;
|
|
1171
|
+
const operationSignal = ctx.signal;
|
|
1172
|
+
// Idle projections remain observational; only a live native operation may await preparation.
|
|
1173
|
+
if (!snapshot.config.enabled || !pending || !operationSignal || shuttingDown) return projectContext(messages);
|
|
1174
|
+
pending.operation ??= prepareInference(pending, selected, ctx, operationSignal).finally(() => {
|
|
1175
|
+
// A late withdrawal must not clear newer work; accepted lifecycle is never replayed after an error.
|
|
1176
|
+
if (!pending.accepted && inferencePreparation === pending) pending.operation = undefined;
|
|
1177
|
+
});
|
|
1178
|
+
return pending.operation.then(() => {
|
|
1179
|
+
if (shuttingDown || operationSignal.aborted || runtime !== selected || ctx.signal !== operationSignal) return;
|
|
1180
|
+
if (snapshot.config.enabled && inferencePreparation !== pending) {
|
|
1181
|
+
// Start may require same-run maintenance. A new captured user prompt instead owns a different context request.
|
|
1182
|
+
if (inferencePreparation?.prompt === undefined) return prepareContext(messages, ctx);
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
if (snapshot.config.enabled && !pending.accepted) return;
|
|
1186
|
+
return projectContext(messages);
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
pi.on("context", (event, ctx) => prepareContext(event.messages as AgentMessage[], ctx));
|
|
826
1191
|
|
|
827
1192
|
pi.on("tool_execution_start", (event) => {
|
|
828
1193
|
if (!snapshot.config.enabled) return;
|
|
@@ -874,49 +1239,68 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
874
1239
|
pi.on("message_end", (event, ctx): any => {
|
|
875
1240
|
// Observe actual user events even while disabled; Start/Stop cannot invent or erase them.
|
|
876
1241
|
if (event.message.role === "user" && runAnchorTimestamp === undefined) runAnchorTimestamp = event.message.timestamp;
|
|
877
|
-
if (!snapshot.config.enabled) return;
|
|
1242
|
+
if (shuttingDown || !snapshot.config.enabled) return;
|
|
878
1243
|
if (event.message.role !== "assistant") return;
|
|
1244
|
+
cancelResponseReconciliation();
|
|
879
1245
|
const message = event.message as unknown as { role: "assistant"; stopReason?: string; content?: unknown };
|
|
880
|
-
if (message.stopReason === "aborted" || assistantToolCallCount(message.content) > 0 || message.stopReason === "toolUse")
|
|
881
|
-
responseAwaitingReconciliation = false;
|
|
882
|
-
return;
|
|
883
|
-
}
|
|
1246
|
+
if (message.stopReason === "aborted" || assistantToolCallCount(message.content) > 0 || message.stopReason === "toolUse") return;
|
|
884
1247
|
if (message.stopReason === "length" || message.stopReason === "error") {
|
|
885
|
-
responseAwaitingReconciliation = false;
|
|
886
1248
|
recordDiagnostic(`Assistant response ended with ${message.stopReason}`, "finalization", ctx, { content: message.content });
|
|
887
1249
|
return;
|
|
888
1250
|
}
|
|
889
|
-
|
|
1251
|
+
responseReconciliation = new AbortController();
|
|
890
1252
|
});
|
|
891
1253
|
|
|
892
|
-
pi.on("turn_end", (event, ctx) => {
|
|
893
|
-
|
|
894
|
-
|
|
1254
|
+
pi.on("turn_end", async (event, ctx) => {
|
|
1255
|
+
if (shuttingDown) return;
|
|
1256
|
+
const pending = responseReconciliation;
|
|
1257
|
+
if (!snapshot.config.enabled || !pending) {
|
|
895
1258
|
updateUi(ctx);
|
|
896
1259
|
return;
|
|
897
1260
|
}
|
|
1261
|
+
const selected = runtime;
|
|
1262
|
+
const signal = ctx.signal ? AbortSignal.any([pending.signal, ctx.signal]) : pending.signal;
|
|
898
1263
|
let responseCommitted = false;
|
|
899
|
-
const specification = snapshot.meta.specification;
|
|
900
1264
|
try {
|
|
901
1265
|
const response = finalizedAssistantResponse(event.message);
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
1266
|
+
if (!selected?.view) throw new Error("Temporal State Flow runtime is unavailable; reload before publishing");
|
|
1267
|
+
await selected.withPatchTransaction((transaction) => {
|
|
1268
|
+
signal.throwIfAborted();
|
|
1269
|
+
if (runtime !== selected || responseReconciliation !== pending || !snapshot.config.enabled) {
|
|
1270
|
+
throw new Error("State Flow response selection changed while awaiting publication");
|
|
1271
|
+
}
|
|
1272
|
+
assertPublicationAvailable();
|
|
1273
|
+
const wasBootstrap = snapshot.meta.bootstrap === true;
|
|
1274
|
+
const nextSnapshot = structuredClone(snapshot);
|
|
1275
|
+
const stage = stageScopedTransition(transaction.states, { transitions: [], response }, [], transaction.causalBasis);
|
|
1276
|
+
completeRun(nextSnapshot);
|
|
1277
|
+
let publication: RuntimePublication | undefined;
|
|
1278
|
+
commitScopedTransition(nextSnapshot, transaction.states, stage, (accepted, next) => {
|
|
1279
|
+
publication = transaction.publish(next, accepted);
|
|
1280
|
+
}, transaction.causalBasis, { finalizeRun: true });
|
|
1281
|
+
snapshot = nextSnapshot;
|
|
1282
|
+
installScopeStates();
|
|
1283
|
+
responseCommitted = true;
|
|
1284
|
+
if (publication?.changed) recordPublication(publication, ctx);
|
|
1285
|
+
appendCheckpoint();
|
|
1286
|
+
clearAcceptedAcquisitions();
|
|
1287
|
+
bootstrapContinuation = undefined;
|
|
1288
|
+
rehydrationPhase = "step";
|
|
1289
|
+
completedRunAccepted = !wasBootstrap;
|
|
1290
|
+
turnAcceptedForBackup = true;
|
|
1291
|
+
updateUi(ctx);
|
|
1292
|
+
}, signal);
|
|
910
1293
|
} catch (error) {
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
1294
|
+
// Superseded completion owns neither the new lifecycle nor its diagnostics/UI.
|
|
1295
|
+
if (signal.aborted || runtime !== selected || responseReconciliation !== pending) return;
|
|
1296
|
+
const cause = diagnosticText(error);
|
|
1297
|
+
recordDiagnostic(cause, "finalization", ctx);
|
|
1298
|
+
notifyProblem(ctx, responseCommitted
|
|
1299
|
+
? `State Flow response saved; lifecycle update failed: ${cause}`
|
|
1300
|
+
: `State Flow response reconciliation failed: ${cause}`, "error");
|
|
916
1301
|
} finally {
|
|
917
|
-
|
|
1302
|
+
if (responseReconciliation === pending) responseReconciliation = undefined;
|
|
918
1303
|
}
|
|
919
|
-
updateUi(ctx);
|
|
920
1304
|
});
|
|
921
1305
|
|
|
922
1306
|
pi.on("session_before_compact", (event) => {
|
|
@@ -927,19 +1311,27 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
927
1311
|
return { compaction: result };
|
|
928
1312
|
});
|
|
929
1313
|
|
|
930
|
-
pi.on("agent_before_settle", (_event, ctx) => {
|
|
931
|
-
if (!turnAcceptedForBackup) return;
|
|
1314
|
+
pi.on("agent_before_settle", async (_event, ctx) => {
|
|
1315
|
+
if (shuttingDown || !turnAcceptedForBackup) return;
|
|
932
1316
|
turnAcceptedForBackup = false;
|
|
933
1317
|
if (!backupPending) return;
|
|
934
1318
|
backupPending = false;
|
|
1319
|
+
const operationSignal = ctx.signal;
|
|
1320
|
+
const signal = operationSignal ? AbortSignal.any([backupLifetime.signal, operationSignal]) : backupLifetime.signal;
|
|
1321
|
+
const selected = runtime;
|
|
935
1322
|
try {
|
|
936
1323
|
if (existsSync(join(repositoryRoot, ".git"))) {
|
|
937
|
-
backupCurrentStateFlowFiles(repositoryRoot);
|
|
938
1324
|
const pushSessionId = sessionAddress(ctx).id;
|
|
939
1325
|
const pushCwd = ctx.cwd;
|
|
1326
|
+
// Pi 0.87 settles after its agent signal ends; waiting there would strand native Abort.
|
|
1327
|
+
const operation = backupCurrentStateFlowFiles(repositoryRoot, signal, operationSignal !== undefined);
|
|
1328
|
+
backupOperations.add(operation);
|
|
1329
|
+
try { await operation; }
|
|
1330
|
+
finally { backupOperations.delete(operation); }
|
|
1331
|
+
if (signal.aborted) return;
|
|
940
1332
|
startStateFlowBackupPush(repositoryRoot, (error) => {
|
|
941
1333
|
if (shuttingDown) return;
|
|
942
|
-
const message =
|
|
1334
|
+
const message = diagnosticText(error);
|
|
943
1335
|
const recorded = diagnosticWriter.recordBackupPushFailure(pushSessionId, pushCwd, message);
|
|
944
1336
|
if (pushFailureNotified) return;
|
|
945
1337
|
pushFailureNotified = true;
|
|
@@ -949,16 +1341,20 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
949
1341
|
}, () => { pushFailureNotified = false; });
|
|
950
1342
|
}
|
|
951
1343
|
} catch (error) {
|
|
952
|
-
|
|
1344
|
+
if (signal.aborted || runtime !== selected) return;
|
|
1345
|
+
const message = diagnosticText(error);
|
|
953
1346
|
recordDiagnostic(message, "publication-conflict", ctx);
|
|
954
|
-
ctx
|
|
1347
|
+
notifyProblem(ctx, error instanceof PublicationBusyError
|
|
1348
|
+
? "State Flow state saved; Git backup deferred: Pi supplied no cancellable settlement wait. A later accepted turn retries."
|
|
1349
|
+
: `State Flow state saved; Git backup failed: ${message}`, "warning");
|
|
955
1350
|
}
|
|
956
1351
|
});
|
|
957
1352
|
|
|
958
1353
|
pi.on("agent_settled", async (_event, ctx) => {
|
|
959
1354
|
if (telegramStartPending && !snapshot.config.enabled) {
|
|
960
1355
|
telegramStartPending = false;
|
|
961
|
-
startStateFlow(ctx);
|
|
1356
|
+
await startStateFlow(ctx);
|
|
1357
|
+
return;
|
|
962
1358
|
}
|
|
963
1359
|
if (!completedRunAccepted || compactionStopped || !snapshot.config.enabled || snapshot.meta.bootstrap
|
|
964
1360
|
|| compactionInFlight || !ctx.isIdle() || ctx.hasPendingMessages() || !runtime?.view
|
|
@@ -977,24 +1373,31 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
977
1373
|
});
|
|
978
1374
|
});
|
|
979
1375
|
|
|
980
|
-
pi.on("session_start", (event, ctx) => {
|
|
1376
|
+
pi.on("session_start", async (event, ctx) => {
|
|
981
1377
|
runAnchorTimestamp = undefined;
|
|
982
1378
|
rehydrationPhase = event.reason === "resume" ? "resume-bootstrap" : "new-bootstrap";
|
|
983
|
-
restoreActiveBranch(ctx, event.reason);
|
|
984
|
-
|
|
985
|
-
void telegram.ensure();
|
|
1379
|
+
await restoreActiveBranch(ctx, event.reason);
|
|
1380
|
+
if (!shuttingDown) void telegram.ensure();
|
|
986
1381
|
});
|
|
987
|
-
pi.on("session_tree", (_event, ctx) => {
|
|
1382
|
+
pi.on("session_tree", async (_event, ctx) => {
|
|
988
1383
|
runAnchorTimestamp = undefined;
|
|
989
|
-
restoreActiveBranch(ctx);
|
|
1384
|
+
await restoreActiveBranch(ctx);
|
|
990
1385
|
});
|
|
991
1386
|
pi.on("session_shutdown", async (_event, _ctx) => {
|
|
992
1387
|
shuttingDown = true;
|
|
1388
|
+
const restoring = cancelBranchRestoration();
|
|
1389
|
+
const starting = cancelStartActivation();
|
|
1390
|
+
const stopping = cancelStopPersistence();
|
|
1391
|
+
backupLifetime.abort();
|
|
1392
|
+
cancelInferencePreparation();
|
|
1393
|
+
cancelResponseReconciliation();
|
|
1394
|
+
cancelSharedInspections();
|
|
993
1395
|
telegramStartPending = false;
|
|
994
1396
|
compactionStopped = true;
|
|
995
1397
|
completedRunAccepted = false;
|
|
996
1398
|
activeContext = undefined;
|
|
997
1399
|
telegram.dispose();
|
|
1400
|
+
await Promise.allSettled([...backupOperations, ...restorationOperations, ...[restoring, stopping, starting].filter((operation) => operation !== undefined)]);
|
|
998
1401
|
await awaitInFlightBackupPushes(repositoryRoot);
|
|
999
1402
|
});
|
|
1000
1403
|
}
|