@llblab/pi-kit 0.22.2 → 0.23.1

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