@llblab/pi-kit 0.26.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/BACKLOG.md +2 -2
  2. package/CHANGELOG.md +5 -0
  3. package/README.md +4 -4
  4. package/node_modules/@llblab/pi-state-flow/AGENTS.md +1 -1
  5. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +12 -5
  6. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +9 -0
  7. package/node_modules/@llblab/pi-state-flow/README.md +116 -34
  8. package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.d.ts +24 -1
  9. package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.js +80 -1
  10. package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.d.ts +17 -0
  11. package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.js +40 -0
  12. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +149 -298
  13. package/node_modules/@llblab/pi-state-flow/dist/lib/git.d.ts +29 -0
  14. package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +58 -0
  15. package/node_modules/@llblab/pi-state-flow/dist/lib/operation.d.ts +37 -0
  16. package/node_modules/@llblab/pi-state-flow/dist/lib/operation.js +59 -0
  17. package/node_modules/@llblab/pi-state-flow/dist/lib/ownership.d.ts +31 -0
  18. package/node_modules/@llblab/pi-state-flow/dist/lib/ownership.js +117 -0
  19. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +7 -7
  20. package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +1 -1
  21. package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +2 -0
  22. package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +28 -0
  23. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +6 -1
  24. package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
  25. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +14 -6
  26. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +2 -2
  27. package/node_modules/@llblab/pi-state-flow/docs/README.md +19 -9
  28. package/node_modules/@llblab/pi-state-flow/docs/agent-contract-relocation.md +4 -4
  29. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +644 -91
  30. package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +116 -37
  31. package/node_modules/@llblab/pi-state-flow/docs/filesystem-recovery.md +118 -21
  32. package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +68 -8
  33. package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +88 -14
  34. package/node_modules/@llblab/pi-state-flow/docs/performance.md +83 -66
  35. package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +391 -62
  36. package/node_modules/@llblab/pi-state-flow/docs/usage.md +317 -62
  37. package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +85 -1
  38. package/node_modules/@llblab/pi-state-flow/lib/compaction.ts +47 -0
  39. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +161 -295
  40. package/node_modules/@llblab/pi-state-flow/lib/git.ts +63 -0
  41. package/node_modules/@llblab/pi-state-flow/lib/operation.ts +75 -0
  42. package/node_modules/@llblab/pi-state-flow/lib/ownership.ts +120 -0
  43. package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +7 -8
  44. package/node_modules/@llblab/pi-state-flow/lib/query.ts +1 -1
  45. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +1 -1
  46. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +1 -2
  47. package/node_modules/@llblab/pi-state-flow/lib/status.ts +29 -0
  48. package/node_modules/@llblab/pi-state-flow/lib/transition.ts +5 -1
  49. package/node_modules/@llblab/pi-state-flow/package.json +1 -1
  50. package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +14 -6
  51. package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +2 -2
  52. package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -0
  53. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +10 -0
  54. package/node_modules/@llblab/pi-telegram/README.md +1 -1
  55. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +4 -1
  56. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +16 -12
  57. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +1 -1
  58. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +7 -1
  59. package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +12 -3
  60. package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +137 -83
  61. package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +26 -0
  62. package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.d.ts +57 -2
  63. package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.js +109 -4
  64. package/node_modules/@llblab/pi-telegram/dist/lib/model.js +2 -4
  65. package/node_modules/@llblab/pi-telegram/dist/lib/status.d.ts +3 -1
  66. package/node_modules/@llblab/pi-telegram/dist/lib/status.js +31 -1
  67. package/node_modules/@llblab/pi-telegram/dist/lib/sync.js +4 -4
  68. package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +1 -0
  69. package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +17 -4
  70. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.d.ts +2 -2
  71. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.js +48 -12
  72. package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
  73. package/node_modules/@llblab/pi-telegram/docs/architecture.md +6 -5
  74. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +2 -0
  75. package/node_modules/@llblab/pi-telegram/docs/public-api.md +2 -2
  76. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +4 -0
  77. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +17 -10
  78. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +8 -2
  79. package/node_modules/@llblab/pi-telegram/lib/commands.ts +135 -97
  80. package/node_modules/@llblab/pi-telegram/lib/extension.ts +25 -0
  81. package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +140 -4
  82. package/node_modules/@llblab/pi-telegram/lib/model.ts +2 -4
  83. package/node_modules/@llblab/pi-telegram/lib/status.ts +30 -1
  84. package/node_modules/@llblab/pi-telegram/lib/sync.ts +4 -4
  85. package/node_modules/@llblab/pi-telegram/lib/threads.ts +21 -3
  86. package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +46 -13
  87. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  88. package/package.json +3 -3
@@ -1,22 +1,22 @@
1
1
  import { StringEnum, Type } from "@earendil-works/pi-ai";
2
2
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
3
3
  import { Text } from "@earendil-works/pi-tui";
4
- import { randomUUID } from "node:crypto";
5
4
  import { existsSync } from "node:fs";
6
5
  import { isAbsolute, join, resolve } from "node:path";
7
- import { ArtifactReadTracker } from "./acquisition.js";
8
- import { classifyArtifactCompilationNeed, inspectRegisteredArtifactPaths, ORDINARY_ARTIFACT_COMPILER, sameArtifactSourceFingerprint, } from "./artifact.js";
9
- import { hasCompactionSizedTranscript, planStateFlowCompaction, shouldRequestStateFlowCompaction, stateFlowCompactionResult } from "./compaction.js";
6
+ import { ArtifactAcquisitionState, missingArtifactRemovals } from "./acquisition.js";
7
+ import { inspectRegisteredArtifactPaths, sameArtifactSourceFingerprint } from "./artifact.js";
8
+ import { hasCompactionSizedTranscript, planStateFlowCompaction, shouldRequestStateFlowCompaction, StateFlowCompactionRequests } from "./compaction.js";
10
9
  import { inactiveModeFor, loadStateFlowConfig } from "./config.js";
11
10
  import { ContextProjection, contextView, createPassiveContinuation, currentRunTrajectory, passiveContinuationMessages, projectSystemProtocol, runtimeContextHead, syntheticUser } from "./context.js";
12
11
  import { readNativeSessionHeader } from "./continuation.js";
13
12
  import { cwdScopeKey, resolveSessionAddress, sessionScopeKey, } from "./durable.js";
14
13
  import { completeRun, deactivateEpisode, prepareRun, resumeEpisode, startEpisode } from "./episode.js";
15
- import { awaitInFlightBackupPushes, backupCurrentStateFlowFiles, startStateFlowBackupPush } from "./git.js";
14
+ import { awaitInFlightBackupPushes, backupCurrentStateFlowFiles, SettledTurnBackup, startStateFlowBackupPush } from "./git.js";
16
15
  import { projectRecentTransitionsWithLimit } from "./history.js";
17
16
  import { isObject, presentationJson, sameJson } from "./json.js";
18
17
  import { StateFlowDiagnosticWriter, stateFlowLogPath } from "./logging.js";
19
18
  import { assistantToolCallCount, conciseDiagnostic, diagnosticText, finalizedAssistantResponse, formatPatchStateArguments, PASSIVE_MEMORY_PROTOCOL, separatedFailure, separatedOutput, stateFlowProtocol } from "./protocol.js";
19
+ import { OwnedOperationSlot, RenewableLifetime } from "./operation.js";
20
20
  import { readProjectedState, readStatePath } from "./query.js";
21
21
  import { selectedBoundaryFailure, selectRetainedCheckpoint, waitForRecovery } from "./recovery.js";
22
22
  import { TemporalRuntime } from "./runtime.js";
@@ -45,48 +45,46 @@ export default function stateFlowExtension(pi, options = {}) {
45
45
  let selectedHistoryExpired = false;
46
46
  let modePersistenceError;
47
47
  /** One pending inactive-mode persistence; later inactive choices coalesce until it publishes. */
48
- let inactivePersistence;
49
- let startActivation;
48
+ const inactivePersistence = new OwnedOperationSlot();
49
+ const startActivation = new OwnedOperationSlot();
50
50
  let forkInitialization = false;
51
- let branchRestoration;
51
+ const branchRestoration = new OwnedOperationSlot();
52
52
  let deferredBranch;
53
- const restorationOperations = new Set();
54
- let responseReconciliation;
55
- let sharedInspectionLifetime = new AbortController();
56
- let memoryToolLifetime = new AbortController();
53
+ const responseReconciliation = new OwnedOperationSlot();
54
+ const sharedInspectionLifetime = new RenewableLifetime();
55
+ const memoryToolLifetime = new RenewableLifetime();
57
56
  let completedRunAccepted = false;
58
- let turnAcceptedForBackup = false;
59
- let compactionPlan;
60
- let compactionInFlight = false;
61
- let compactionStopped = false;
62
- const compactionPrefix = `state-flow-boundary:${randomUUID()}:`;
63
- let compactionMarker;
57
+ const compactionRequests = new StateFlowCompactionRequests();
64
58
  let passiveContinuation;
65
59
  let bootstrapContinuation;
66
60
  const contextProjection = new ContextProjection();
67
- let inferencePreparation;
61
+ const inferencePreparation = new OwnedOperationSlot();
68
62
  let runAnchorTimestamp;
69
63
  let runtime;
70
64
  let activeContext;
71
65
  let rehydrationPhase;
72
66
  const repositoryRoot = resolve(options.repositoryRoot ?? config.directory);
73
67
  const diagnosticWriter = new StateFlowDiagnosticWriter(config.logging, stateFlowLogPath(agentDir), repositoryRoot, (message) => notifyActiveContext(message));
74
- let backupPending = false;
75
- let backupLifetime = new AbortController();
76
- const backupOperations = new Set();
68
+ const backup = new SettledTurnBackup();
77
69
  let shuttingDown = false;
78
- let pushFailureNotified = false;
79
70
  const skillReads = new SkillReadTracker(hashSkillSource, (path) => activeContext
80
71
  ? registeredSkillResolver(activeContext.cwd, pi.getCommands())(path)
81
72
  : undefined);
82
- const artifactReads = new ArtifactReadTracker();
83
- let artifactInvalidations = [];
84
- let artifactHints = {};
85
- const SOURCE_CHANGED_HINT = "Source changed since this artifact was compiled. Read and recompile it before relying on it.";
73
+ const acquisition = new ArtifactAcquisitionState();
74
+ const artifactReads = acquisition.reads;
86
75
  let telegramStartPending = false;
87
76
  function sessionAddress(ctx) {
88
77
  return resolveSessionAddress(ctx.sessionManager.getSessionFile(), ctx.sessionManager.getSessionId(), ctx.sessionManager.getHeader()?.timestamp);
89
78
  }
79
+ /** Native session identity captured before an await; any change supersedes the waiting operation. */
80
+ function sessionIdentity(ctx) {
81
+ const cwd = ctx.cwd;
82
+ const owner = ctx.sessionManager.getSessionId();
83
+ const file = ctx.sessionManager.getSessionFile();
84
+ const timestamp = ctx.sessionManager.getHeader()?.timestamp;
85
+ return () => ctx.cwd === cwd && ctx.sessionManager.getSessionId() === owner && ctx.sessionManager.getSessionFile() === file
86
+ && ctx.sessionManager.getHeader()?.timestamp === timestamp;
87
+ }
90
88
  function notifyProblem(ctx, message, level) {
91
89
  ctx.ui.notify(conciseDiagnostic(message), level);
92
90
  }
@@ -103,7 +101,7 @@ export default function stateFlowExtension(pi, options = {}) {
103
101
  return new TemporalRuntime(ctx.cwd, sessionAddress(ctx), repositoryRoot, undefined, config.historyLimit);
104
102
  }
105
103
  function projectModelState(state) {
106
- return projectStateForModel(state, artifactHints);
104
+ return projectStateForModel(state, acquisition.hints);
107
105
  }
108
106
  function assertSelectedBranchAvailable() {
109
107
  if (snapshot.meta.validation?.attempt === 0) {
@@ -147,110 +145,29 @@ export default function stateFlowExtension(pi, options = {}) {
147
145
  function updateUi(ctx) {
148
146
  ctx.ui.setStatus(STATUS_KEY, compactStatus(snapshot, scopeRevisions(), (color, text) => ctx.ui.theme.fg(color, text)));
149
147
  }
150
- function cancelInactivePersistence() {
151
- const pending = inactivePersistence;
152
- pending?.controller.abort();
153
- inactivePersistence = undefined;
154
- return pending?.operation;
155
- }
156
- function cancelStartActivation() {
157
- const pending = startActivation;
158
- pending?.controller.abort();
159
- startActivation = undefined;
160
- return pending?.operation;
161
- }
162
- function cancelBranchRestoration() {
163
- const pending = branchRestoration;
164
- pending?.controller.abort();
165
- branchRestoration = undefined;
166
- return pending?.operation;
167
- }
168
- function cancelResponseReconciliation() {
169
- responseReconciliation?.abort();
170
- responseReconciliation = undefined;
171
- }
172
148
  function cancelSharedInspections() {
173
- sharedInspectionLifetime.abort(new Error("State Flow inspection was cancelled; select the scope again"));
174
- sharedInspectionLifetime = new AbortController();
175
- }
176
- function cancelBackupWork() {
177
- backupLifetime.abort();
178
- backupLifetime = new AbortController();
179
- backupPending = false;
180
- }
181
- function cancelInferencePreparation() {
182
- inferencePreparation?.controller.abort();
183
- inferencePreparation = undefined;
149
+ sharedInspectionLifetime.renew(new Error("State Flow inspection was cancelled; select the scope again"));
184
150
  }
185
151
  function clearRunTransient() {
186
- cancelInferencePreparation();
187
- cancelResponseReconciliation();
152
+ inferencePreparation.cancel();
153
+ responseReconciliation.cancel();
188
154
  cancelSharedInspections();
189
155
  completedRunAccepted = false;
190
- turnAcceptedForBackup = false;
191
- compactionPlan = undefined;
192
- compactionInFlight = false;
156
+ backup.clearTurn();
157
+ compactionRequests.clear();
193
158
  skillReads.clear();
194
159
  artifactReads.clear();
195
160
  }
196
161
  function deferInferencePreparation(prompt) {
197
- cancelInferencePreparation();
198
- inferencePreparation = { controller: new AbortController(), prompt };
199
- artifactInvalidations = [];
200
- artifactReads.setCandidates([]);
162
+ inferencePreparation.cancel();
163
+ inferencePreparation.claim({ controller: new AbortController(), prompt });
164
+ acquisition.clearInvalidations();
201
165
  }
202
166
  function refreshArtifactHints() {
203
- if (!runtime?.view) {
204
- artifactHints = {};
205
- artifactInvalidations = [];
206
- artifactReads.setCandidates([]);
207
- return;
208
- }
209
- const paths = new Set();
210
- for (const scope of ["global", "cwd", "session"])
211
- for (const path of Object.keys(scopeStates[scope].artifacts))
212
- paths.add(path);
213
- const observations = new Map(inspectRegisteredArtifactPaths(paths).map((observation) => [observation.path, observation]));
214
- const nextHints = {};
215
- const nextInvalidations = new Map();
216
- for (const scope of ["global", "cwd", "session"]) {
217
- const provenance = runtime.artifactProvenance(scope);
218
- for (const [path, metadata] of Object.entries(scopeStates[scope].artifacts)) {
219
- delete nextHints[path];
220
- nextInvalidations.delete(path);
221
- if (metadata.kind === "skill")
222
- continue;
223
- const observed = observations.get(path);
224
- if (observed?.kind !== "present")
225
- continue;
226
- const need = classifyArtifactCompilationNeed({ path, scope, sourceFingerprint: observed.fingerprint }, metadata, ORDINARY_ARTIFACT_COMPILER, false, provenance[path]);
227
- if (need.kind !== "requires-compilation")
228
- continue;
229
- if (need.reason === "source-changed")
230
- nextHints[path] = SOURCE_CHANGED_HINT;
231
- nextInvalidations.set(path, { path, scope, reason: need.reason });
232
- }
233
- }
234
- artifactHints = nextHints;
235
- artifactInvalidations = [...nextInvalidations.values()].sort((left, right) => left.path.localeCompare(right.path));
236
- artifactReads.setCandidates(artifactInvalidations);
237
- }
238
- function missingArtifactRemovals(states) {
239
- const owners = new Map();
240
- for (const scope of ["global", "cwd", "session"])
241
- for (const path of Object.keys(states[scope].artifacts ?? {})) {
242
- owners.set(path, [...owners.get(path) ?? [], scope]);
243
- }
244
- const removals = {};
245
- for (const observation of inspectRegisteredArtifactPaths(owners.keys())) {
246
- if (observation.kind !== "missing")
247
- continue;
248
- for (const scope of owners.get(observation.path) ?? []) {
249
- const artifacts = (removals[scope]?.artifacts ?? {});
250
- removals[scope] = { ...(removals[scope] ?? {}), artifacts: { ...artifacts, [observation.path]: null } };
251
- }
252
- }
253
- return removals;
167
+ if (runtime?.view)
168
+ acquisition.refresh(scopeStates, (scope) => runtime.artifactProvenance(scope));
169
+ else
170
+ acquisition.reset();
254
171
  }
255
172
  function installScopeStates() {
256
173
  scopeStates = runtime?.view ? runtime.states() : { global: emptyState(), cwd: emptyState(), session: emptyState() };
@@ -307,15 +224,11 @@ export default function stateFlowExtension(pi, options = {}) {
307
224
  ? [...new Set([...active, ...owned])]
308
225
  : active.filter((name) => !owned.includes(name)));
309
226
  }
310
- function recordPublication(_publication, _ctx) {
227
+ /** Accepted canonical publication establishes runtime authority and makes the settled-turn Git backup due. */
228
+ function recordPublication() {
311
229
  // A passive view is not runtime authority; only accepted canonical publication establishes it.
312
230
  branchStartsWithoutRuntime = false;
313
- backupPending = true;
314
- }
315
- /** Record accepted lifecycle persistence; Git backup is scheduled only after an accepted turn settles. */
316
- function recordPolicyPublication(publication, ctx) {
317
- if (publication)
318
- recordPublication(publication, ctx);
231
+ backup.markPublished();
319
232
  }
320
233
  function skillReadIsCurrent(read) {
321
234
  return read.hash !== undefined && runtime?.view !== undefined && hasCompiledSkillArtifact(scopeStates[read.scope].artifacts, runtime.artifactProvenance(read.scope)[read.path], read.path, read.hash);
@@ -332,11 +245,9 @@ export default function stateFlowExtension(pi, options = {}) {
332
245
  const target = `${read.scope}.artifacts[${JSON.stringify(read.path)}]`;
333
246
  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.`;
334
247
  }
335
- function clearAcceptedAcquisitions(acquiredArtifactPaths = new Set(artifactReads.successful.keys())) {
336
- artifactInvalidations = artifactInvalidations.filter(({ path }) => !acquiredArtifactPaths.has(path));
337
- artifactReads.setCandidates(artifactInvalidations);
248
+ function clearAcceptedAcquisitions(acquiredArtifactPaths) {
249
+ acquisition.acceptAcquired(acquiredArtifactPaths);
338
250
  dropCurrentSkillReads();
339
- artifactReads.clear();
340
251
  }
341
252
  async function prepareInference(pending, selected, ctx, operationSignal) {
342
253
  const signal = AbortSignal.any([pending.controller.signal, operationSignal]);
@@ -345,7 +256,7 @@ export default function stateFlowExtension(pi, options = {}) {
345
256
  throw new Error("Temporal State Flow runtime is unavailable; reload before publishing");
346
257
  await selected.withPatchTransaction((transaction) => {
347
258
  signal.throwIfAborted();
348
- if (runtime !== selected || inferencePreparation !== pending || !isActive() || shuttingDown) {
259
+ if (runtime !== selected || !inferencePreparation.owns(pending) || !isActive() || shuttingDown) {
349
260
  throw new Error("State Flow inference selection changed while awaiting publication");
350
261
  }
351
262
  assertPublicationAvailable();
@@ -372,7 +283,7 @@ export default function stateFlowExtension(pi, options = {}) {
372
283
  if (rotatesRun && rehydrationPhase !== "new-bootstrap" && rehydrationPhase !== "resume-bootstrap")
373
284
  rehydrationPhase = "step";
374
285
  if (publication?.changed)
375
- recordPublication(publication, ctx);
286
+ recordPublication();
376
287
  if (pending.prompt !== undefined || publication?.changed)
377
288
  appendCheckpoint();
378
289
  if (Object.keys(removals).length > 0)
@@ -381,7 +292,7 @@ export default function stateFlowExtension(pi, options = {}) {
381
292
  }, signal);
382
293
  }
383
294
  catch (error) {
384
- if (signal.aborted || runtime !== selected || inferencePreparation !== pending || shuttingDown)
295
+ if (signal.aborted || runtime !== selected || !inferencePreparation.owns(pending) || shuttingDown)
385
296
  return;
386
297
  // Pi reports context-hook exceptions and continues. Abort through its public port rather than infer from stale preparation.
387
298
  ctx.abort();
@@ -392,25 +303,21 @@ export default function stateFlowExtension(pi, options = {}) {
392
303
  : `State Flow inference preparation failed: ${cause}`, "error");
393
304
  }
394
305
  }
395
- function currentRehydrationPhase() {
396
- return rehydrationPhase;
397
- }
398
306
  function statusDiagnostics(ctx) {
399
307
  const cwd = ctx.cwd;
400
- const diagnosticStates = scopeStates;
401
308
  const view = runtime?.view;
402
309
  const diagnosticRecent = runtime?.recent() ?? [];
403
310
  const durableStateError = snapshot.meta.validation?.attempt === 0 ? snapshot.meta.validation.error
404
311
  : view ? undefined : "no temporal runtime is selected on this branch";
405
312
  const staleArtifacts = durableStateError === undefined
406
- ? artifactInvalidations.map(({ path, scope, reason }) => ({ scope: scope ?? "global", path, reason }))
313
+ ? acquisition.invalidations.map(({ path, scope, reason }) => ({ scope: scope ?? "global", path, reason }))
407
314
  : [];
408
315
  const session = sessionAddress(ctx);
409
316
  return {
410
317
  repositoryRoot,
411
318
  cwdScopeKey: cwdScopeKey(cwd),
412
319
  sessionScopeKey: sessionScopeKey(session.key),
413
- scopeStates: diagnosticStates,
320
+ scopeStates,
414
321
  effectiveState,
415
322
  recent: diagnosticRecent,
416
323
  historyLimit: config.historyLimit,
@@ -437,19 +344,17 @@ export default function stateFlowExtension(pi, options = {}) {
437
344
  /** Select the active native branch under one owned restoration lifetime; only current accepted work installs memory. */
438
345
  function restoreActiveBranch(ctx, sessionStartReason, notifyRecovery = true, startOwner, requestedMode) {
439
346
  contextProjection.reset();
440
- memoryToolLifetime.abort();
441
- memoryToolLifetime = new AbortController();
442
- cancelBranchRestoration();
347
+ memoryToolLifetime.renew();
348
+ branchRestoration.cancel();
443
349
  // Start-owned attachment/fork recovery keeps its owner; only accepted Start cancels Stop.
444
350
  if (!startOwner) {
445
- cancelStartActivation();
446
- cancelInactivePersistence();
351
+ startActivation.cancel();
352
+ inactivePersistence.cancel();
447
353
  }
448
354
  clearRunTransient();
449
355
  passiveContinuation = undefined;
450
356
  bootstrapContinuation = undefined;
451
- artifactInvalidations = [];
452
- artifactReads.setCandidates([]);
357
+ acquisition.clearInvalidations();
453
358
  telegramStartPending = false;
454
359
  activeContext = ctx;
455
360
  if (!startOwner) {
@@ -475,7 +380,7 @@ export default function stateFlowExtension(pi, options = {}) {
475
380
  // Off needs only policy; unavailable native evidence grants no memory authority.
476
381
  }
477
382
  if (mode === "off") {
478
- cancelBackupWork();
383
+ backup.cancel();
479
384
  const owner = sessionAddress(ctx).key;
480
385
  const reason = pendingFork || (deferredBranch?.owner === owner && deferredBranch.cwd === ctx.cwd && deferredBranch.reason === "fork"
481
386
  && retainsPhysicalSessionProjection(sessionStartReason)) ? "fork" : sessionStartReason;
@@ -489,7 +394,7 @@ export default function stateFlowExtension(pi, options = {}) {
489
394
  modePersistenceError = persistenceError;
490
395
  forkInitialization = reason === "fork";
491
396
  installScopeStates();
492
- artifactHints = {};
397
+ acquisition.reset();
493
398
  if (adoptDefault)
494
399
  appendCheckpoint();
495
400
  syncStateFlowTools();
@@ -577,25 +482,18 @@ export default function stateFlowExtension(pi, options = {}) {
577
482
  awaitingAcceptance: selection.kind !== "settled" && selection.kind !== "current",
578
483
  ...(requestedMode === undefined ? {} : { requestedMode }),
579
484
  };
580
- branchRestoration = pending;
581
- const operation = attachBranch(ctx, pending, selection, sessionStartReason, notifyRecovery, skipped);
582
- pending.operation = operation;
583
- restorationOperations.add(operation);
584
- const finished = () => { restorationOperations.delete(operation); };
585
- void operation.then(finished, finished);
586
- return operation;
485
+ branchRestoration.claim(pending);
486
+ // attachBranch releases its own ownership; settlement only drains it for shutdown.
487
+ return branchRestoration.track(pending, attachBranch(ctx, pending, selection, sessionStartReason, notifyRecovery, skipped), false);
587
488
  }
588
489
  async function attachBranch(ctx, pending, selection, reason, notifyRecovery, skipped) {
589
490
  const placeholder = runtime;
590
491
  let selected = runtime;
591
492
  const owner = ctx.sessionManager.getSessionId();
592
- const cwd = ctx.cwd;
593
- const file = ctx.sessionManager.getSessionFile();
594
- const timestamp = ctx.sessionManager.getHeader()?.timestamp;
493
+ const sameSession = sessionIdentity(ctx);
595
494
  const signal = AbortSignal.any([pending.controller.signal, ...(ctx.signal ? [ctx.signal] : [])]);
596
- const isCurrent = () => branchRestoration === pending && !pending.controller.signal.aborted && !shuttingDown && runtime === selected
597
- && ctx.cwd === cwd && ctx.sessionManager.getSessionId() === owner && ctx.sessionManager.getSessionFile() === file
598
- && ctx.sessionManager.getHeader()?.timestamp === timestamp;
495
+ const isCurrent = () => branchRestoration.owns(pending) && !pending.controller.signal.aborted && !shuttingDown && runtime === selected
496
+ && sameSession();
599
497
  const assertCurrent = () => {
600
498
  signal.throwIfAborted();
601
499
  if (!isCurrent())
@@ -603,13 +501,13 @@ export default function stateFlowExtension(pi, options = {}) {
603
501
  };
604
502
  let accepted = false;
605
503
  // Install accepted memory before native writes; later ancillary failures cannot revert or replay it.
606
- const accept = (candidate, next, publication, nativeWrites) => {
504
+ const accept = (candidate, next, nativeWrites) => {
607
505
  accepted = true;
608
506
  pending.awaitingAcceptance = false;
609
507
  runtime = selected = candidate;
610
508
  snapshot = next;
611
509
  installScopeStates();
612
- recordPolicyPublication(publication, ctx);
510
+ recordPublication();
613
511
  let failure;
614
512
  try {
615
513
  nativeWrites();
@@ -645,7 +543,8 @@ export default function stateFlowExtension(pi, options = {}) {
645
543
  && retainsPhysicalSessionProjection(reason) && hasUncheckpointedConversation(ctx.sessionManager.getBranch()))
646
544
  restored.meta.bootstrap = true;
647
545
  const next = pending.requestedMode ? deactivateEpisode(restored, pending.requestedMode) : restored;
648
- accept(candidate, next, publish(next), appendCheckpoint);
546
+ publish(next);
547
+ accept(candidate, next, appendCheckpoint);
649
548
  }, signal);
650
549
  }
651
550
  else if (selection.kind === "fork") {
@@ -653,9 +552,9 @@ export default function stateFlowExtension(pi, options = {}) {
653
552
  assertCurrent();
654
553
  // Mode is selected independently of creating the child's private memory.
655
554
  const next = pending.requestedMode ? deactivateEpisode(child, pending.requestedMode) : child;
656
- const publication = publish(next);
555
+ publish(next);
657
556
  forkInitialization = false;
658
- accept(candidate, next, publication, () => {
557
+ accept(candidate, next, () => {
659
558
  pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, { reset: true, owner });
660
559
  appendCheckpoint();
661
560
  });
@@ -671,7 +570,8 @@ export default function stateFlowExtension(pi, options = {}) {
671
570
  throw new Error("Existing session runtime requires retained-boundary restoration");
672
571
  const activated = startEpisode(hasPriorConversation(branch));
673
572
  const next = pending.requestedMode ? deactivateEpisode(activated, pending.requestedMode) : activated;
674
- accept(candidate, next, publish(next), appendCheckpoint);
573
+ publish(next);
574
+ accept(candidate, next, appendCheckpoint);
675
575
  }, signal, true);
676
576
  }
677
577
  }
@@ -717,8 +617,7 @@ export default function stateFlowExtension(pi, options = {}) {
717
617
  }
718
618
  finally {
719
619
  pending.awaitingAcceptance = false;
720
- if (branchRestoration === pending)
721
- branchRestoration = undefined;
620
+ branchRestoration.release(pending);
722
621
  }
723
622
  }
724
623
  /** Load current shared memory for passive projection without initializing or publishing it. */
@@ -769,7 +668,7 @@ export default function stateFlowExtension(pi, options = {}) {
769
668
  pi.registerTool({
770
669
  name: READ_STATE_TOOL_NAME,
771
670
  label: "Read State",
772
- description: "Read exact State Flow values or historical semantic patches with one path or an ordered path list. Unscoped semantic paths read the effective overlay; effective makes that overlay explicit, while global, cwd, and session select ownership. Array selectors support zero-based indices and half-open [start..end] ranges. Projection value returns semantic data; keys returns minimal structure; patch intersects the selected path at its boundary.",
671
+ description: "Read exact State Flow values or historical semantic patches with one path or an ordered path list. Unscoped semantic paths read the effective overlay; effective makes that overlay explicit, while global, cwd, and session select ownership. Array selectors support zero-based indices and half-open [start..end] ranges. Projection value returns semantic data; keys returns minimal structure; patch intersects the selected path at its boundary. Path object keys must be ASCII identifiers ([A-Za-z_$][A-Za-z0-9_$-]*); non-ASCII keys cannot be addressed.",
773
672
  promptSnippet: "Read exact state values, structures, or historical semantic patches",
774
673
  parameters: Type.Object({
775
674
  path: Type.Optional(Type.String({ description: "One semantic path; unscoped paths use effective, explicit roots may use effective/global/cwd/session and historical [N], and arrays support half-open [start..end] ranges" })),
@@ -818,7 +717,7 @@ export default function stateFlowExtension(pi, options = {}) {
818
717
  pi.registerTool({
819
718
  name: PATCH_STATE_TOOL_NAME,
820
719
  label: "Patch State",
821
- description: "The sole State Flow semantic mutation protocol. Supply one or more global, cwd, or session patches; all supplied scopes commit atomically. This call must be the only State Flow barrier in its assistant response.",
720
+ description: "The sole State Flow semantic mutation protocol. Supply one or more global, cwd, or session patches; all supplied scopes commit atomically. This call must be the only State Flow barrier in its assistant response. Deleting an intent also deletes same-scope working/lazy keys its {\"$ref\"} values own unless another intent refs them. Name keys in ASCII ([A-Za-z_$][A-Za-z0-9_$-]*) so read paths and refs resolve; values may use any language.",
822
721
  promptSnippet: "Atomically patch one or more global/cwd/session scopes",
823
722
  promptGuidelines: [
824
723
  "Use patch_state only for material durable semantic changes; ordinary answers need no finalization call.",
@@ -897,12 +796,12 @@ export default function stateFlowExtension(pi, options = {}) {
897
796
  snapshot = nextSnapshot;
898
797
  installScopeStates();
899
798
  if (publication?.changed) {
900
- recordPublication(publication, ctx);
799
+ recordPublication();
901
800
  appendCheckpoint();
902
801
  }
903
802
  clearAcceptedAcquisitions(new Set(acquiredArtifacts.map(({ path }) => path)));
904
803
  updateUi(ctx);
905
- const updates = contextProjection.acceptPatch(previousEffective, effectiveState, patches, artifactHints);
804
+ const updates = contextProjection.acceptPatch(previousEffective, effectiveState, patches, acquisition.hints);
906
805
  const acknowledgement = changed
907
806
  ? `\nState materialized atomically at ${scopes.join("+")} scope${scopes.length === 1 ? "" : "s"}.`
908
807
  : "\nState already current.";
@@ -934,31 +833,20 @@ export default function stateFlowExtension(pi, options = {}) {
934
833
  function startStateFlow(ctx) {
935
834
  if (shuttingDown)
936
835
  return Promise.resolve({ ok: false, message: "State Flow is shutting down" });
937
- if (startActivation?.operation)
938
- return startActivation.operation;
836
+ if (startActivation.current?.operation)
837
+ return startActivation.current.operation;
939
838
  telegramStartPending = false;
940
- if (activeContext && !branchRestoration && isActive()) {
839
+ if (activeContext && !branchRestoration.current && isActive()) {
941
840
  return Promise.resolve({ ok: true, message: "State Flow is already active", signal: sharedInspectionLifetime.signal });
942
841
  }
943
- const pending = { controller: new AbortController() };
944
- startActivation = pending;
945
- const operation = runStart(ctx, pending.controller);
946
- pending.operation = operation;
947
- const finished = () => { if (startActivation === pending)
948
- startActivation = undefined; };
949
- void operation.then(finished, finished);
950
- return operation;
842
+ const pending = startActivation.claim({ controller: new AbortController() });
843
+ return startActivation.track(pending, runStart(ctx, pending.controller));
951
844
  }
952
845
  /** Await restoration-owned attachment and exact-source fork recovery before current-head activation. */
953
846
  async function runStart(ctx, pending) {
954
- const owner = ctx.sessionManager.getSessionId();
955
- const cwd = ctx.cwd;
956
- const file = ctx.sessionManager.getSessionFile();
957
- const timestamp = ctx.sessionManager.getHeader()?.timestamp;
847
+ const sameSession = sessionIdentity(ctx);
958
848
  const signal = ctx.signal ? AbortSignal.any([pending.signal, ctx.signal]) : pending.signal;
959
- const isOwner = () => startActivation?.controller === pending && !signal.aborted && !shuttingDown
960
- && ctx.cwd === cwd && ctx.sessionManager.getSessionId() === owner && ctx.sessionManager.getSessionFile() === file
961
- && ctx.sessionManager.getHeader()?.timestamp === timestamp;
849
+ const isOwner = () => startActivation.current?.controller === pending && !signal.aborted && !shuttingDown && sameSession();
962
850
  const superseded = () => {
963
851
  pending.abort();
964
852
  return { ok: false, message: "State Flow activation was superseded", signal: pending.signal };
@@ -968,15 +856,15 @@ export default function stateFlowExtension(pi, options = {}) {
968
856
  await waitForRecovery(restoreActiveBranch(ctx, "fork", false, pending), signal);
969
857
  else if (!activeContext)
970
858
  await waitForRecovery(restoreActiveBranch(ctx, undefined, false, pending), signal);
971
- else if (branchRestoration?.operation)
972
- await waitForRecovery(branchRestoration.operation, signal);
859
+ else if (branchRestoration.current?.operation)
860
+ await waitForRecovery(branchRestoration.current.operation, signal);
973
861
  if (!isOwner())
974
862
  return superseded();
975
863
  if (isActive())
976
864
  return { ok: true, message: "State Flow is already active", signal: sharedInspectionLifetime.signal };
977
865
  if (forkInitialization) {
978
866
  // Withdraw this join on cancellation without revoking the independently owned mode persistence.
979
- const stopping = inactivePersistence?.operation;
867
+ const stopping = inactivePersistence.current?.operation;
980
868
  if (stopping)
981
869
  await waitForRecovery(stopping, signal);
982
870
  if (!isOwner())
@@ -999,16 +887,13 @@ export default function stateFlowExtension(pi, options = {}) {
999
887
  async function activateCurrentState(ctx, pending) {
1000
888
  let selected = runtime;
1001
889
  const owner = ctx.sessionManager.getSessionId();
1002
- const cwd = ctx.cwd;
1003
- const file = ctx.sessionManager.getSessionFile();
1004
- const timestamp = ctx.sessionManager.getHeader()?.timestamp;
890
+ const sameSession = sessionIdentity(ctx);
1005
891
  const initiallyActive = isActive();
1006
892
  let accepted = false;
1007
893
  let receipt = AbortSignal.any([pending.signal, sharedInspectionLifetime.signal]);
1008
894
  const signal = ctx.signal ? AbortSignal.any([pending.signal, ctx.signal]) : pending.signal;
1009
- const isCurrent = () => startActivation?.controller === pending && !signal.aborted && runtime === selected && !shuttingDown
1010
- && ctx.cwd === cwd && ctx.sessionManager.getSessionId() === owner && ctx.sessionManager.getSessionFile() === file
1011
- && ctx.sessionManager.getHeader()?.timestamp === timestamp && isActive() === (accepted || initiallyActive);
895
+ const isCurrent = () => startActivation.current?.controller === pending && !signal.aborted && runtime === selected && !shuttingDown
896
+ && sameSession() && isActive() === (accepted || initiallyActive);
1012
897
  const superseded = () => {
1013
898
  pending.abort();
1014
899
  return { ok: false, message: "State Flow activation was superseded", signal: pending.signal };
@@ -1033,9 +918,9 @@ export default function stateFlowExtension(pi, options = {}) {
1033
918
  const bootstrap = hasPriorConversation(ctx.sessionManager.getBranch()) || passiveContinuation !== undefined || boundary !== undefined;
1034
919
  const activated = current ? resumeEpisode(current, bootstrap) : startEpisode(bootstrap);
1035
920
  const recoveredCurrent = selectedHistoryExpired;
1036
- const publication = publish(activated);
921
+ publish(activated);
1037
922
  accepted = true;
1038
- cancelInactivePersistence();
923
+ inactivePersistence.cancel();
1039
924
  runtime = selected = activation;
1040
925
  snapshot = activated;
1041
926
  activeContext = ctx;
@@ -1050,7 +935,7 @@ export default function stateFlowExtension(pi, options = {}) {
1050
935
  bootstrapContinuation = snapshot.meta.bootstrap ? continuation : undefined;
1051
936
  deferInferencePreparation();
1052
937
  receipt = AbortSignal.any([pending.signal, sharedInspectionLifetime.signal]);
1053
- recordPolicyPublication(publication, ctx);
938
+ recordPublication();
1054
939
  syncStateFlowTools();
1055
940
  updateUi(ctx);
1056
941
  if (forkInitialization) {
@@ -1110,9 +995,9 @@ export default function stateFlowExtension(pi, options = {}) {
1110
995
  return Promise.resolve({ ok: false, message: "State Flow is shutting down" });
1111
996
  if (mode === "off")
1112
997
  return selectOff(ctx);
1113
- cancelStartActivation();
998
+ startActivation.cancel();
1114
999
  telegramStartPending = false;
1115
- const persisting = inactivePersistence;
1000
+ const persisting = inactivePersistence.current;
1116
1001
  if (persisting?.operation && !persisting.published) {
1117
1002
  // Pending inactive choices coalesce: acceptance persists whichever inactive mode is current then.
1118
1003
  if (snapshot.config.mode !== mode)
@@ -1121,7 +1006,7 @@ export default function stateFlowExtension(pi, options = {}) {
1121
1006
  }
1122
1007
  if (!activeContext)
1123
1008
  void restoreActiveBranch(ctx, undefined, false);
1124
- const restoring = branchRestoration;
1009
+ const restoring = branchRestoration.current;
1125
1010
  // Read-only recovery also retains the latest policy; its native write fence is updated below.
1126
1011
  if (restoring)
1127
1012
  restoring.requestedMode = mode;
@@ -1136,17 +1021,11 @@ export default function stateFlowExtension(pi, options = {}) {
1136
1021
  if (modePersistenceError || recordedMode === mode)
1137
1022
  return Promise.resolve({ ok: true, message: `State Flow is already ${mode}` });
1138
1023
  }
1139
- cancelInactivePersistence();
1140
- const pending = { controller: new AbortController() };
1141
- inactivePersistence = pending;
1142
- const operation = deferredBranch && mode === "passive" && !branchStartsWithoutRuntime
1024
+ inactivePersistence.cancel();
1025
+ const pending = inactivePersistence.claim({ controller: new AbortController() });
1026
+ return inactivePersistence.track(pending, deferredBranch && mode === "passive" && !branchStartsWithoutRuntime
1143
1027
  ? acquirePassiveMode(ctx, pending, deferredBranch.reason)
1144
- : persistInactiveMode(ctx, pending, mode);
1145
- pending.operation = operation;
1146
- const finished = () => { if (inactivePersistence === pending)
1147
- inactivePersistence = undefined; };
1148
- void operation.then(finished, finished);
1149
- return operation;
1028
+ : persistInactiveMode(ctx, pending, mode));
1150
1029
  }
1151
1030
  /** Off owns cancellation and native policy only; no canonical transaction or semantic handoff. */
1152
1031
  function selectOff(ctx) {
@@ -1164,7 +1043,7 @@ export default function stateFlowExtension(pi, options = {}) {
1164
1043
  modePersistenceError = policy?.persistenceError;
1165
1044
  const alreadyOff = current.config.mode === "off" && policy?.mode === "off";
1166
1045
  const incoming = sameOwner || sameDeferred ? current.meta.bootstrap ? bootstrapContinuation : passiveContinuation : undefined;
1167
- const unfinished = current.meta.specification !== undefined || (inferencePreparation?.prompt !== undefined && !inferencePreparation.accepted)
1046
+ const unfinished = current.meta.specification !== undefined || (inferencePreparation.current?.prompt !== undefined && !inferencePreparation.current.accepted)
1168
1047
  || (isActive() && hasUncheckpointedConversation(branch));
1169
1048
  const at = incoming?.startedAt ?? Date.now();
1170
1049
  const from = incoming ? incoming.activeRunStartedAt : (sameOwner || sameDeferred) && (!ctx.isIdle() || unfinished) ? runAnchorTimestamp : undefined;
@@ -1180,20 +1059,17 @@ export default function stateFlowExtension(pi, options = {}) {
1180
1059
  catch {
1181
1060
  // An unusable cached bookmark cannot prevent native Off policy or authorize replacement memory.
1182
1061
  }
1183
- cancelStartActivation();
1184
- cancelBranchRestoration();
1185
- cancelInactivePersistence();
1186
- cancelBackupWork();
1187
- memoryToolLifetime.abort();
1188
- memoryToolLifetime = new AbortController();
1062
+ startActivation.cancel();
1063
+ branchRestoration.cancel();
1064
+ inactivePersistence.cancel();
1065
+ backup.cancel();
1066
+ memoryToolLifetime.renew();
1189
1067
  clearRunTransient();
1190
1068
  contextProjection.reset();
1191
1069
  telegramStartPending = false;
1192
1070
  bootstrapContinuation = undefined;
1193
1071
  passiveContinuation = undefined;
1194
- artifactInvalidations = [];
1195
- artifactHints = {};
1196
- artifactReads.setCandidates([]);
1072
+ acquisition.reset();
1197
1073
  runtime = undefined;
1198
1074
  snapshot = emptySnapshot("off");
1199
1075
  selectedHistoryExpired = false;
@@ -1227,7 +1103,7 @@ export default function stateFlowExtension(pi, options = {}) {
1227
1103
  }
1228
1104
  async function acquirePassiveMode(ctx, pending, reason) {
1229
1105
  await restoreActiveBranch(ctx, reason, true, pending.controller, "passive");
1230
- if (inactivePersistence !== pending || pending.controller.signal.aborted || shuttingDown) {
1106
+ if (!inactivePersistence.owns(pending) || pending.controller.signal.aborted || shuttingDown) {
1231
1107
  return { ok: false, message: "State Flow mode change was superseded" };
1232
1108
  }
1233
1109
  return persistInactiveMode(ctx, pending, "passive");
@@ -1238,7 +1114,7 @@ export default function stateFlowExtension(pi, options = {}) {
1238
1114
  const current = snapshot;
1239
1115
  const wasActive = current.config.mode === "active";
1240
1116
  let unfinished = current.meta.specification !== undefined
1241
- || (inferencePreparation?.prompt !== undefined && !inferencePreparation.accepted);
1117
+ || (inferencePreparation.current?.prompt !== undefined && !inferencePreparation.current.accepted);
1242
1118
  applyInactiveMode(ctx, mode);
1243
1119
  const stoppedAt = Date.now();
1244
1120
  if (modePersistenceError) {
@@ -1254,7 +1130,7 @@ export default function stateFlowExtension(pi, options = {}) {
1254
1130
  const incomingBoundary = current.meta.bootstrap ? bootstrapContinuation : undefined;
1255
1131
  const anchor = runAnchorTimestamp;
1256
1132
  const idle = ctx.isIdle();
1257
- const isCurrent = () => inactivePersistence === pending && runtime === selected
1133
+ const isCurrent = () => inactivePersistence.owns(pending) && runtime === selected
1258
1134
  && !shuttingDown && ctx.sessionManager.getSessionId() === owner && !isActive();
1259
1135
  const superseded = () => ({ ok: false, message: "State Flow mode change was superseded" });
1260
1136
  const freezeHandoff = () => {
@@ -1291,8 +1167,7 @@ export default function stateFlowExtension(pi, options = {}) {
1291
1167
  // Projection changes before any wait; capture the old run's boundary before later native input can replace it.
1292
1168
  freezeHandoff();
1293
1169
  bootstrapContinuation = undefined;
1294
- artifactInvalidations = [];
1295
- artifactReads.setCandidates([]);
1170
+ acquisition.clearInvalidations();
1296
1171
  assertPublicationAvailable();
1297
1172
  const signal = ctx.signal ? AbortSignal.any([pending.controller.signal, ctx.signal]) : pending.controller.signal;
1298
1173
  if (branchStartsWithoutRuntime || !selected?.view) {
@@ -1314,11 +1189,11 @@ export default function stateFlowExtension(pi, options = {}) {
1314
1189
  throw new Error("State Flow mode selection changed while awaiting publication");
1315
1190
  assertPublicationAvailable();
1316
1191
  const next = structuredClone(snapshot);
1317
- const publication = publish(next);
1192
+ publish(next);
1318
1193
  accepted = true;
1319
1194
  snapshot = next;
1320
1195
  installScopeStates();
1321
- recordPolicyPublication(publication, ctx);
1196
+ recordPublication();
1322
1197
  return complete();
1323
1198
  }, signal);
1324
1199
  return isCurrent() ? result : superseded();
@@ -1334,8 +1209,7 @@ export default function stateFlowExtension(pi, options = {}) {
1334
1209
  }
1335
1210
  modePersistenceError = cause.trim() ? cause : "Canonical State Flow persistence failed";
1336
1211
  bootstrapContinuation = undefined;
1337
- artifactInvalidations = [];
1338
- artifactReads.setCandidates([]);
1212
+ acquisition.clearInvalidations();
1339
1213
  return complete();
1340
1214
  }
1341
1215
  }
@@ -1371,7 +1245,7 @@ export default function stateFlowExtension(pi, options = {}) {
1371
1245
  },
1372
1246
  cancelStart: () => {
1373
1247
  telegramStartPending = false;
1374
- cancelStartActivation();
1248
+ startActivation.cancel();
1375
1249
  },
1376
1250
  },
1377
1251
  });
@@ -1388,7 +1262,7 @@ export default function stateFlowExtension(pi, options = {}) {
1388
1262
  contextProjection.reset();
1389
1263
  skillReads.clear();
1390
1264
  artifactReads.clear();
1391
- cancelResponseReconciliation();
1265
+ responseReconciliation.cancel();
1392
1266
  completedRunAccepted = false;
1393
1267
  // This native hook has no operation signal. Capture only; the first active context owns acceptance.
1394
1268
  deferInferencePreparation(event.prompt);
@@ -1409,12 +1283,12 @@ export default function stateFlowExtension(pi, options = {}) {
1409
1283
  if (!isActive() && !passiveContinuation && !passiveMemoryAvailable())
1410
1284
  return;
1411
1285
  // Idle inspection must not freeze a pre-acceptance snapshot for the live inference.
1412
- const projection = isActive() && inferencePreparation && !inferencePreparation.accepted
1286
+ const projection = isActive() && inferencePreparation.current && !inferencePreparation.current.accepted
1413
1287
  ? new ContextProjection() : contextProjection;
1414
1288
  const effective = effectiveState;
1415
- const invalidations = artifactInvalidations.map(({ path, scope, reason }) => ({ path, ...(scope === undefined ? {} : { scope }), reason }));
1416
- const phase = isActive() ? currentRehydrationPhase() : undefined;
1417
- const view = contextView(effective, artifactHints, invalidations, phase);
1289
+ const invalidations = acquisition.invalidations.map(({ path, scope, reason }) => ({ path, ...(scope === undefined ? {} : { scope }), reason }));
1290
+ const phase = isActive() ? rehydrationPhase : undefined;
1291
+ const view = contextView(effective, acquisition.hints, invalidations, phase);
1418
1292
  if (passiveContinuation) {
1419
1293
  const retained = passiveContinuationMessages(messages, passiveContinuation);
1420
1294
  if (!runtime?.view)
@@ -1431,7 +1305,7 @@ export default function stateFlowExtension(pi, options = {}) {
1431
1305
  return { messages: projection.project(source, view, () => runtimeContextHead(snapshot, view, projectRecentTransitionsWithLimit(config.historyLimit, runtime?.recent() ?? []))) };
1432
1306
  }
1433
1307
  function prepareContext(messages, ctx) {
1434
- const pending = inferencePreparation;
1308
+ const pending = inferencePreparation.current;
1435
1309
  const selected = runtime;
1436
1310
  const operationSignal = ctx.signal;
1437
1311
  // Idle projections remain observational; only a live native operation may await preparation.
@@ -1439,15 +1313,15 @@ export default function stateFlowExtension(pi, options = {}) {
1439
1313
  return projectContext(messages);
1440
1314
  pending.operation ??= prepareInference(pending, selected, ctx, operationSignal).finally(() => {
1441
1315
  // A late withdrawal must not clear newer work; accepted lifecycle is never replayed after an error.
1442
- if (!pending.accepted && inferencePreparation === pending)
1316
+ if (!pending.accepted && inferencePreparation.owns(pending))
1443
1317
  pending.operation = undefined;
1444
1318
  });
1445
1319
  return pending.operation.then(() => {
1446
1320
  if (shuttingDown || operationSignal.aborted || runtime !== selected || ctx.signal !== operationSignal)
1447
1321
  return;
1448
- if (isActive() && inferencePreparation !== pending) {
1322
+ if (isActive() && !inferencePreparation.owns(pending)) {
1449
1323
  // Start may require same-run maintenance. A new captured user prompt instead owns a different context request.
1450
- if (inferencePreparation?.prompt === undefined)
1324
+ if (inferencePreparation.current?.prompt === undefined)
1451
1325
  return prepareContext(messages, ctx);
1452
1326
  return;
1453
1327
  }
@@ -1512,7 +1386,7 @@ export default function stateFlowExtension(pi, options = {}) {
1512
1386
  return;
1513
1387
  if (event.message.role !== "assistant")
1514
1388
  return;
1515
- cancelResponseReconciliation();
1389
+ responseReconciliation.cancel();
1516
1390
  const message = event.message;
1517
1391
  if (message.stopReason === "aborted" || assistantToolCallCount(message.content) > 0 || message.stopReason === "toolUse")
1518
1392
  return;
@@ -1520,18 +1394,18 @@ export default function stateFlowExtension(pi, options = {}) {
1520
1394
  recordDiagnostic(`Assistant response ended with ${message.stopReason}`, "finalization", ctx, { content: message.content });
1521
1395
  return;
1522
1396
  }
1523
- responseReconciliation = new AbortController();
1397
+ responseReconciliation.claim({ controller: new AbortController() });
1524
1398
  });
1525
1399
  pi.on("turn_end", async (event, ctx) => {
1526
1400
  if (shuttingDown)
1527
1401
  return;
1528
- const pending = responseReconciliation;
1402
+ const pending = responseReconciliation.current;
1529
1403
  if (!isActive() || !pending) {
1530
1404
  updateUi(ctx);
1531
1405
  return;
1532
1406
  }
1533
1407
  const selected = runtime;
1534
- const signal = ctx.signal ? AbortSignal.any([pending.signal, ctx.signal]) : pending.signal;
1408
+ const signal = ctx.signal ? AbortSignal.any([pending.controller.signal, ctx.signal]) : pending.controller.signal;
1535
1409
  let responseCommitted = false;
1536
1410
  try {
1537
1411
  const response = finalizedAssistantResponse(event.message);
@@ -1539,7 +1413,7 @@ export default function stateFlowExtension(pi, options = {}) {
1539
1413
  throw new Error("Temporal State Flow runtime is unavailable; reload before publishing");
1540
1414
  await selected.withPatchTransaction((transaction) => {
1541
1415
  signal.throwIfAborted();
1542
- if (runtime !== selected || responseReconciliation !== pending || !isActive()) {
1416
+ if (runtime !== selected || !responseReconciliation.owns(pending) || !isActive()) {
1543
1417
  throw new Error("State Flow response selection changed while awaiting publication");
1544
1418
  }
1545
1419
  assertPublicationAvailable();
@@ -1556,19 +1430,19 @@ export default function stateFlowExtension(pi, options = {}) {
1556
1430
  responseCommitted = true;
1557
1431
  contextProjection.reset();
1558
1432
  if (publication?.changed)
1559
- recordPublication(publication, ctx);
1433
+ recordPublication();
1560
1434
  appendCheckpoint();
1561
1435
  clearAcceptedAcquisitions();
1562
1436
  bootstrapContinuation = undefined;
1563
1437
  rehydrationPhase = "step";
1564
1438
  completedRunAccepted = !wasBootstrap;
1565
- turnAcceptedForBackup = true;
1439
+ backup.markTurnAccepted();
1566
1440
  updateUi(ctx);
1567
1441
  }, signal);
1568
1442
  }
1569
1443
  catch (error) {
1570
1444
  // Superseded completion owns neither the new lifecycle nor its diagnostics/UI.
1571
- if (signal.aborted || runtime !== selected || responseReconciliation !== pending)
1445
+ if (signal.aborted || runtime !== selected || !responseReconciliation.owns(pending))
1572
1446
  return;
1573
1447
  const cause = diagnosticText(error);
1574
1448
  recordDiagnostic(cause, "finalization", ctx);
@@ -1577,30 +1451,20 @@ export default function stateFlowExtension(pi, options = {}) {
1577
1451
  : `State Flow response reconciliation failed: ${cause}`, "error");
1578
1452
  }
1579
1453
  finally {
1580
- if (responseReconciliation === pending)
1581
- responseReconciliation = undefined;
1454
+ responseReconciliation.release(pending);
1582
1455
  }
1583
1456
  });
1584
1457
  pi.on("session_before_compact", (event) => {
1585
- if (event.reason !== "manual" || !event.customInstructions?.startsWith("state-flow-boundary:"))
1586
- return;
1587
- const marker = compactionMarker;
1588
- if (shuttingDown || compactionStopped || !isActive() || !compactionPlan || !marker || event.customInstructions !== marker)
1589
- return { cancel: true };
1590
- const result = stateFlowCompactionResult(compactionPlan, marker, event);
1458
+ const result = compactionRequests.resolve(event, !shuttingDown && isActive());
1591
1459
  if (result === undefined || "cancel" in result)
1592
1460
  return result;
1593
1461
  return { compaction: result };
1594
1462
  });
1595
1463
  pi.on("agent_before_settle", async (_event, ctx) => {
1596
- if (shuttingDown || !memoryToolsAvailable() || !turnAcceptedForBackup)
1597
- return;
1598
- turnAcceptedForBackup = false;
1599
- if (!backupPending)
1464
+ if (shuttingDown || !memoryToolsAvailable() || !backup.takeSettledTurn())
1600
1465
  return;
1601
- backupPending = false;
1602
1466
  const operationSignal = ctx.signal;
1603
- const lifetime = backupLifetime.signal;
1467
+ const lifetime = backup.lifetime;
1604
1468
  const signal = operationSignal ? AbortSignal.any([lifetime, operationSignal]) : lifetime;
1605
1469
  const selected = runtime;
1606
1470
  try {
@@ -1608,14 +1472,7 @@ export default function stateFlowExtension(pi, options = {}) {
1608
1472
  const pushSessionId = sessionAddress(ctx).id;
1609
1473
  const pushCwd = ctx.cwd;
1610
1474
  // Pi 0.87 settles after its agent signal ends; waiting there would strand native Abort.
1611
- const operation = backupCurrentStateFlowFiles(repositoryRoot, signal, operationSignal !== undefined);
1612
- backupOperations.add(operation);
1613
- try {
1614
- await operation;
1615
- }
1616
- finally {
1617
- backupOperations.delete(operation);
1618
- }
1475
+ await backup.track(backupCurrentStateFlowFiles(repositoryRoot, signal, operationSignal !== undefined));
1619
1476
  if (signal.aborted || snapshot.config.mode === "off")
1620
1477
  return;
1621
1478
  startStateFlowBackupPush(repositoryRoot, (error) => {
@@ -1623,13 +1480,12 @@ export default function stateFlowExtension(pi, options = {}) {
1623
1480
  return;
1624
1481
  const message = diagnosticText(error);
1625
1482
  const recorded = diagnosticWriter.recordBackupPushFailure(pushSessionId, pushCwd, message);
1626
- if (pushFailureNotified)
1483
+ if (!backup.claimPushFailureNotice())
1627
1484
  return;
1628
- pushFailureNotified = true;
1629
1485
  notifyActiveContext(recorded
1630
1486
  ? `State Flow Git backup push failed; state is saved locally. Details: ${stateFlowLogPath(agentDir)}. A later accepted turn retries.`
1631
1487
  : `State Flow Git backup push failed; local diagnostics unavailable: ${message}`);
1632
- }, () => { pushFailureNotified = false; }, lifetime);
1488
+ }, () => backup.resetPushFailureNotice(), lifetime);
1633
1489
  }
1634
1490
  }
1635
1491
  catch (error) {
@@ -1648,8 +1504,8 @@ export default function stateFlowExtension(pi, options = {}) {
1648
1504
  await startStateFlow(ctx);
1649
1505
  return;
1650
1506
  }
1651
- if (!completedRunAccepted || compactionStopped || !isActive() || snapshot.meta.bootstrap
1652
- || compactionInFlight || !ctx.isIdle() || ctx.hasPendingMessages() || !runtime?.view
1507
+ if (!completedRunAccepted || compactionRequests.stopped || !isActive() || snapshot.meta.bootstrap
1508
+ || compactionRequests.inFlight || !ctx.isIdle() || ctx.hasPendingMessages() || !runtime?.view
1653
1509
  || !shouldRequestStateFlowCompaction(ctx.getContextUsage()))
1654
1510
  return;
1655
1511
  completedRunAccepted = false;
@@ -1659,16 +1515,11 @@ export default function stateFlowExtension(pi, options = {}) {
1659
1515
  const plan = planStateFlowCompaction(entries, runtime.causalBasis(), snapshot.meta.step, runAnchorTimestamp);
1660
1516
  if (!plan)
1661
1517
  return;
1662
- compactionPlan = plan;
1663
- const marker = compactionMarker = `${compactionPrefix}${randomUUID()}`;
1664
- compactionInFlight = true;
1518
+ const marker = compactionRequests.begin(plan);
1665
1519
  // Pi dispatches deferred companion prompts after all settled handlers return.
1666
1520
  await new Promise((resolve) => {
1667
1521
  const finished = () => {
1668
- if (compactionMarker === marker) {
1669
- compactionPlan = undefined;
1670
- compactionInFlight = false;
1671
- }
1522
+ compactionRequests.finish(marker);
1672
1523
  resolve();
1673
1524
  };
1674
1525
  ctx.compact({ customInstructions: marker, onComplete: finished, onError: finished });
@@ -1688,21 +1539,21 @@ export default function stateFlowExtension(pi, options = {}) {
1688
1539
  });
1689
1540
  pi.on("session_shutdown", async (_event, _ctx) => {
1690
1541
  shuttingDown = true;
1691
- memoryToolLifetime.abort();
1542
+ memoryToolLifetime.end();
1692
1543
  contextProjection.reset();
1693
- const restoring = cancelBranchRestoration();
1694
- const starting = cancelStartActivation();
1695
- const stopping = cancelInactivePersistence();
1696
- backupLifetime.abort();
1697
- cancelInferencePreparation();
1698
- cancelResponseReconciliation();
1544
+ const restoring = branchRestoration.cancel();
1545
+ const starting = startActivation.cancel();
1546
+ const stopping = inactivePersistence.cancel();
1547
+ backup.abort();
1548
+ inferencePreparation.cancel();
1549
+ responseReconciliation.cancel();
1699
1550
  cancelSharedInspections();
1700
1551
  telegramStartPending = false;
1701
- compactionStopped = true;
1552
+ compactionRequests.stop();
1702
1553
  completedRunAccepted = false;
1703
1554
  activeContext = undefined;
1704
1555
  telegram.dispose();
1705
- await Promise.allSettled([...backupOperations, ...restorationOperations, ...[restoring, stopping, starting].filter((operation) => operation !== undefined)]);
1556
+ await Promise.allSettled([...backup.operations, ...branchRestoration.inflight, ...[restoring, stopping, starting].filter((operation) => operation !== undefined)]);
1706
1557
  await awaitInFlightBackupPushes(repositoryRoot);
1707
1558
  });
1708
1559
  }