@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
@@ -3,18 +3,11 @@ import { StringEnum, Type } from "@earendil-works/pi-ai";
3
3
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
4
4
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
5
5
  import { Text } from "@earendil-works/pi-tui";
6
- import { randomUUID } from "node:crypto";
7
6
  import { existsSync } from "node:fs";
8
7
  import { isAbsolute, join, resolve } from "node:path";
9
- import { ArtifactReadTracker } from "./acquisition.ts";
10
- import {
11
- classifyArtifactCompilationNeed,
12
- inspectRegisteredArtifactPaths,
13
- ORDINARY_ARTIFACT_COMPILER,
14
- sameArtifactSourceFingerprint,
15
- type ArtifactInvalidationRequest,
16
- } from "./artifact.ts";
17
- import { hasCompactionSizedTranscript, planStateFlowCompaction, shouldRequestStateFlowCompaction, stateFlowCompactionResult, type StateFlowCompactionPlan } from "./compaction.ts";
8
+ import { ArtifactAcquisitionState, missingArtifactRemovals } from "./acquisition.ts";
9
+ import { inspectRegisteredArtifactPaths, sameArtifactSourceFingerprint } from "./artifact.ts";
10
+ import { hasCompactionSizedTranscript, planStateFlowCompaction, shouldRequestStateFlowCompaction, StateFlowCompactionRequests } from "./compaction.ts";
18
11
  import { inactiveModeFor, loadStateFlowConfig } from "./config.ts";
19
12
  import { ContextProjection, contextView, createPassiveContinuation, currentRunTrajectory, passiveContinuationMessages, projectSystemProtocol, runtimeContextHead, syntheticUser, type PassiveContinuation } from "./context.ts";
20
13
  import { readNativeSessionHeader } from "./continuation.ts";
@@ -25,11 +18,12 @@ import {
25
18
  type SessionAddress,
26
19
  } from "./durable.ts";
27
20
  import { completeRun, deactivateEpisode, prepareRun, resumeEpisode, startEpisode } from "./episode.ts";
28
- import { awaitInFlightBackupPushes, backupCurrentStateFlowFiles, startStateFlowBackupPush } from "./git.ts";
21
+ import { awaitInFlightBackupPushes, backupCurrentStateFlowFiles, SettledTurnBackup, startStateFlowBackupPush } from "./git.ts";
29
22
  import { projectRecentTransitionsWithLimit } from "./history.ts";
30
- import { isObject, presentationJson, sameJson, type JsonObject } from "./json.ts";
23
+ import { isObject, presentationJson, sameJson } from "./json.ts";
31
24
  import { StateFlowDiagnosticWriter, stateFlowLogPath, type DiagnosticExtras, type StateFlowDiagnosticCategory } from "./logging.ts";
32
25
  import { assistantToolCallCount, conciseDiagnostic, diagnosticText, finalizedAssistantResponse, formatPatchStateArguments, PASSIVE_MEMORY_PROTOCOL, separatedFailure, separatedOutput, stateFlowProtocol } from "./protocol.ts";
26
+ import { OwnedOperationSlot, RenewableLifetime, type OwnedOperation } from "./operation.ts";
33
27
  import { readProjectedState, readStatePath } from "./query.ts";
34
28
  import { selectedBoundaryFailure, selectRetainedCheckpoint, waitForRecovery } from "./recovery.ts";
35
29
  import type { RehydrationPhase } from "./rehydration.ts";
@@ -37,7 +31,7 @@ import { TemporalRuntime, type RuntimePublication } from "./runtime.ts";
37
31
  import { discoverSnapshotData, findAssistantToolBatch, findBranchPolicy, findPassiveStopBoundary, hasPendingFork, hasPriorConversation, hasUncheckpointedConversation, isNewSession, retainsPhysicalSessionProjection, SNAPSHOT_ENTRY_TYPE } from "./session.ts";
38
32
  import { hasCompiledSkillArtifact, hashSkillSource, registeredSkillResolver, SkillReadTracker, type SuccessfulSkillRead } from "./skills.ts";
39
33
  import { HistoryBoundaryExpiredError, emptySnapshot, migrationFailure, preRuntimeCheckpoint, type InactiveMode, type RetainedBoundaryCheckpoint, type RetainedPiCheckpoint, type Snapshot, type StateFlowMode } from "./snapshot.ts";
40
- import { emptyState, projectStateForModel, type AtomicScopePatches, type SemanticState, type ModelState, type ScopedSemanticStates, type ScopedStates, type StateScope } from "./state.ts";
34
+ import { emptyState, projectStateForModel, type AtomicScopePatches, type SemanticState, type ModelState, type ScopedStates, type StateScope } from "./state.ts";
41
35
  import { compactStatus, detailedStatus, STATUS_KEY, type StatusDiagnostics } from "./status.ts";
42
36
  import { PublicationBusyError } from "./storage.ts";
43
37
  import { createStateFlowTelegramAdapter, type StateFlowTelegramControlResult, type StateFlowTelegramInspection, type StateFlowTelegramLoader, type StateFlowTelegramScope } from "./telegram.ts";
@@ -59,14 +53,14 @@ export const PATCH_STATE_TOOL_NAME = "patch_state";
59
53
  export const READ_STATE_TOOL_NAME = "read_state";
60
54
  const PASSIVE_STOP_ENTRY_TYPE = "state-flow-passive-stop";
61
55
 
62
- interface InferencePreparation {
56
+ interface InferencePreparation extends OwnedOperation {
63
57
  readonly controller: AbortController;
64
58
  readonly prompt?: string;
65
59
  accepted?: boolean;
66
60
  operation?: Promise<void>;
67
61
  }
68
62
 
69
- interface BranchRestoration {
63
+ interface BranchRestoration extends OwnedOperation {
70
64
  readonly controller: AbortController;
71
65
  /** Memory acceptance is independent of the requested session mode. */
72
66
  awaitingAcceptance: boolean;
@@ -74,6 +68,15 @@ interface BranchRestoration {
74
68
  operation?: Promise<void>;
75
69
  }
76
70
 
71
+ interface ModeOperation extends OwnedOperation {
72
+ operation?: Promise<StateFlowTelegramControlResult>;
73
+ }
74
+
75
+ interface InactivePersistence extends ModeOperation {
76
+ /** The native mode marker is recorded; later inactive choices start a new persistence. */
77
+ published?: boolean;
78
+ }
79
+
77
80
  type BranchSelection =
78
81
  | { kind: "settled" }
79
82
  | { kind: "current"; mode: InactiveMode }
@@ -93,50 +96,49 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
93
96
  let selectedHistoryExpired = false;
94
97
  let modePersistenceError: string | undefined;
95
98
  /** One pending inactive-mode persistence; later inactive choices coalesce until it publishes. */
96
- let inactivePersistence: { controller: AbortController; published?: boolean; operation?: Promise<StateFlowTelegramControlResult> } | undefined;
97
- let startActivation: { controller: AbortController; operation?: Promise<StateFlowTelegramControlResult> } | undefined;
99
+ const inactivePersistence = new OwnedOperationSlot<InactivePersistence>();
100
+ const startActivation = new OwnedOperationSlot<ModeOperation>();
98
101
  let forkInitialization = false;
99
- let branchRestoration: BranchRestoration | undefined;
102
+ const branchRestoration = new OwnedOperationSlot<BranchRestoration>();
100
103
  let deferredBranch: { reason: unknown; owner: string; cwd: string } | undefined;
101
- const restorationOperations = new Set<Promise<void>>();
102
- let responseReconciliation: AbortController | undefined;
103
- let sharedInspectionLifetime = new AbortController();
104
- let memoryToolLifetime = new AbortController();
104
+ const responseReconciliation = new OwnedOperationSlot<OwnedOperation>();
105
+ const sharedInspectionLifetime = new RenewableLifetime();
106
+ const memoryToolLifetime = new RenewableLifetime();
105
107
  let completedRunAccepted = false;
106
- let turnAcceptedForBackup = false;
107
- let compactionPlan: StateFlowCompactionPlan | undefined;
108
- let compactionInFlight = false;
109
- let compactionStopped = false;
110
- const compactionPrefix = `state-flow-boundary:${randomUUID()}:`;
111
- let compactionMarker: string | undefined;
108
+ const compactionRequests = new StateFlowCompactionRequests();
112
109
  let passiveContinuation: PassiveContinuation | undefined;
113
110
  let bootstrapContinuation: PassiveContinuation | undefined;
114
111
  const contextProjection = new ContextProjection();
115
- let inferencePreparation: InferencePreparation | undefined;
112
+ const inferencePreparation = new OwnedOperationSlot<InferencePreparation>();
116
113
  let runAnchorTimestamp: number | undefined;
117
114
  let runtime: TemporalRuntime | undefined;
118
115
  let activeContext: ExtensionContext | undefined;
119
116
  let rehydrationPhase: RehydrationPhase | undefined;
120
117
  const repositoryRoot = resolve(options.repositoryRoot ?? config.directory);
121
118
  const diagnosticWriter = new StateFlowDiagnosticWriter(config.logging, stateFlowLogPath(agentDir), repositoryRoot, (message) => notifyActiveContext(message));
122
- let backupPending = false;
123
- let backupLifetime = new AbortController();
124
- const backupOperations = new Set<Promise<string | undefined>>();
119
+ const backup = new SettledTurnBackup();
125
120
  let shuttingDown = false;
126
- let pushFailureNotified = false;
127
121
  const skillReads = new SkillReadTracker(hashSkillSource, (path) => activeContext
128
122
  ? registeredSkillResolver(activeContext.cwd, pi.getCommands())(path)
129
123
  : undefined);
130
- const artifactReads = new ArtifactReadTracker();
131
- let artifactInvalidations: ArtifactInvalidationRequest[] = [];
132
- let artifactHints: Record<string, string> = {};
133
- const SOURCE_CHANGED_HINT = "Source changed since this artifact was compiled. Read and recompile it before relying on it.";
124
+ const acquisition = new ArtifactAcquisitionState();
125
+ const artifactReads = acquisition.reads;
134
126
  let telegramStartPending = false;
135
127
 
136
128
  function sessionAddress(ctx: ExtensionContext): SessionAddress {
137
129
  return resolveSessionAddress(ctx.sessionManager.getSessionFile(), ctx.sessionManager.getSessionId(), ctx.sessionManager.getHeader()?.timestamp);
138
130
  }
139
131
 
132
+ /** Native session identity captured before an await; any change supersedes the waiting operation. */
133
+ function sessionIdentity(ctx: ExtensionContext): () => boolean {
134
+ const cwd = ctx.cwd;
135
+ const owner = ctx.sessionManager.getSessionId();
136
+ const file = ctx.sessionManager.getSessionFile();
137
+ const timestamp = ctx.sessionManager.getHeader()?.timestamp;
138
+ return () => ctx.cwd === cwd && ctx.sessionManager.getSessionId() === owner && ctx.sessionManager.getSessionFile() === file
139
+ && ctx.sessionManager.getHeader()?.timestamp === timestamp;
140
+ }
141
+
140
142
  function notifyProblem(ctx: ExtensionContext, message: string, level: "warning" | "error"): void {
141
143
  ctx.ui.notify(conciseDiagnostic(message), level);
142
144
  }
@@ -154,7 +156,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
154
156
  }
155
157
 
156
158
  function projectModelState(state: SemanticState): SemanticState {
157
- return projectStateForModel(state, artifactHints);
159
+ return projectStateForModel(state, acquisition.hints);
158
160
  }
159
161
 
160
162
  function assertSelectedBranchAvailable(): void {
@@ -202,115 +204,32 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
202
204
  ctx.ui.setStatus(STATUS_KEY, compactStatus(snapshot, scopeRevisions(), (color, text) => ctx.ui.theme.fg(color, text)));
203
205
  }
204
206
 
205
- function cancelInactivePersistence(): Promise<StateFlowTelegramControlResult> | undefined {
206
- const pending = inactivePersistence;
207
- pending?.controller.abort();
208
- inactivePersistence = undefined;
209
- return pending?.operation;
210
- }
211
-
212
- function cancelStartActivation(): Promise<StateFlowTelegramControlResult> | undefined {
213
- const pending = startActivation;
214
- pending?.controller.abort();
215
- startActivation = undefined;
216
- return pending?.operation;
217
- }
218
-
219
- function cancelBranchRestoration(): Promise<void> | undefined {
220
- const pending = branchRestoration;
221
- pending?.controller.abort();
222
- branchRestoration = undefined;
223
- return pending?.operation;
224
- }
225
-
226
- function cancelResponseReconciliation(): void {
227
- responseReconciliation?.abort();
228
- responseReconciliation = undefined;
229
- }
230
-
231
207
  function cancelSharedInspections(): void {
232
- sharedInspectionLifetime.abort(new Error("State Flow inspection was cancelled; select the scope again"));
233
- sharedInspectionLifetime = new AbortController();
234
- }
235
-
236
- function cancelBackupWork(): void {
237
- backupLifetime.abort();
238
- backupLifetime = new AbortController();
239
- backupPending = false;
240
- }
241
-
242
- function cancelInferencePreparation(): void {
243
- inferencePreparation?.controller.abort();
244
- inferencePreparation = undefined;
208
+ sharedInspectionLifetime.renew(new Error("State Flow inspection was cancelled; select the scope again"));
245
209
  }
246
210
 
247
211
  function clearRunTransient(): void {
248
- cancelInferencePreparation();
249
- cancelResponseReconciliation();
212
+ inferencePreparation.cancel();
213
+ responseReconciliation.cancel();
250
214
  cancelSharedInspections();
251
215
  completedRunAccepted = false;
252
- turnAcceptedForBackup = false;
253
- compactionPlan = undefined;
254
- compactionInFlight = false;
216
+ backup.clearTurn();
217
+ compactionRequests.clear();
255
218
  skillReads.clear();
256
219
  artifactReads.clear();
257
220
  }
258
221
 
259
222
  function deferInferencePreparation(prompt?: string): void {
260
- cancelInferencePreparation();
261
- inferencePreparation = { controller: new AbortController(), prompt };
262
- artifactInvalidations = [];
263
- artifactReads.setCandidates([]);
223
+ inferencePreparation.cancel();
224
+ inferencePreparation.claim({ controller: new AbortController(), prompt });
225
+ acquisition.clearInvalidations();
264
226
  }
265
227
 
266
228
  function refreshArtifactHints(): void {
267
- if (!runtime?.view) {
268
- artifactHints = {};
269
- artifactInvalidations = [];
270
- artifactReads.setCandidates([]);
271
- return;
272
- }
273
- const paths = new Set<string>();
274
- for (const scope of ["global", "cwd", "session"] as const) for (const path of Object.keys(scopeStates[scope].artifacts)) paths.add(path);
275
- const observations = new Map(inspectRegisteredArtifactPaths(paths).map((observation) => [observation.path, observation]));
276
- const nextHints: Record<string, string> = {};
277
- const nextInvalidations = new Map<string, ArtifactInvalidationRequest>();
278
- for (const scope of ["global", "cwd", "session"] as const) {
279
- const provenance = runtime.artifactProvenance(scope);
280
- for (const [path, metadata] of Object.entries(scopeStates[scope].artifacts)) {
281
- delete nextHints[path];
282
- nextInvalidations.delete(path);
283
- if (metadata.kind === "skill") continue;
284
- const observed = observations.get(path);
285
- if (observed?.kind !== "present") continue;
286
- const need = classifyArtifactCompilationNeed({ path, scope, sourceFingerprint: observed.fingerprint }, metadata, ORDINARY_ARTIFACT_COMPILER, false, provenance[path]);
287
- if (need.kind !== "requires-compilation") continue;
288
- if (need.reason === "source-changed") nextHints[path] = SOURCE_CHANGED_HINT;
289
- nextInvalidations.set(path, { path, scope, reason: need.reason });
290
- }
291
- }
292
- artifactHints = nextHints;
293
- artifactInvalidations = [...nextInvalidations.values()].sort((left, right) => left.path.localeCompare(right.path));
294
- artifactReads.setCandidates(artifactInvalidations);
295
- }
296
-
297
- function missingArtifactRemovals(states: ScopedSemanticStates): AtomicScopePatches {
298
- const owners = new Map<string, StateScope[]>();
299
- for (const scope of ["global", "cwd", "session"] as const) for (const path of Object.keys(states[scope].artifacts ?? {})) {
300
- owners.set(path, [...owners.get(path) ?? [], scope]);
301
- }
302
- const removals: AtomicScopePatches = {};
303
- for (const observation of inspectRegisteredArtifactPaths(owners.keys())) {
304
- if (observation.kind !== "missing") continue;
305
- for (const scope of owners.get(observation.path) ?? []) {
306
- const artifacts = (removals[scope]?.artifacts ?? {}) as JsonObject;
307
- removals[scope] = { ...(removals[scope] ?? {}), artifacts: { ...artifacts, [observation.path]: null } };
308
- }
309
- }
310
- return removals;
229
+ if (runtime?.view) acquisition.refresh(scopeStates, (scope) => runtime!.artifactProvenance(scope));
230
+ else acquisition.reset();
311
231
  }
312
232
 
313
-
314
233
  function installScopeStates(): void {
315
234
  scopeStates = runtime?.view ? runtime.states() : { global: emptyState(), cwd: emptyState(), session: emptyState() };
316
235
  effectiveState = runtime?.view ? runtime.readView() : {};
@@ -362,15 +281,11 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
362
281
  : active.filter((name) => !owned.includes(name)));
363
282
  }
364
283
 
365
- function recordPublication(_publication: RuntimePublication, _ctx: ExtensionContext): void {
284
+ /** Accepted canonical publication establishes runtime authority and makes the settled-turn Git backup due. */
285
+ function recordPublication(): void {
366
286
  // A passive view is not runtime authority; only accepted canonical publication establishes it.
367
287
  branchStartsWithoutRuntime = false;
368
- backupPending = true;
369
- }
370
-
371
- /** Record accepted lifecycle persistence; Git backup is scheduled only after an accepted turn settles. */
372
- function recordPolicyPublication(publication: RuntimePublication | undefined, ctx: ExtensionContext): void {
373
- if (publication) recordPublication(publication, ctx);
288
+ backup.markPublished();
374
289
  }
375
290
 
376
291
  function skillReadIsCurrent(read: SuccessfulSkillRead): boolean {
@@ -394,11 +309,9 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
394
309
  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.`;
395
310
  }
396
311
 
397
- function clearAcceptedAcquisitions(acquiredArtifactPaths = new Set(artifactReads.successful.keys())): void {
398
- artifactInvalidations = artifactInvalidations.filter(({ path }) => !acquiredArtifactPaths.has(path));
399
- artifactReads.setCandidates(artifactInvalidations);
312
+ function clearAcceptedAcquisitions(acquiredArtifactPaths?: ReadonlySet<string>): void {
313
+ acquisition.acceptAcquired(acquiredArtifactPaths);
400
314
  dropCurrentSkillReads();
401
- artifactReads.clear();
402
315
  }
403
316
 
404
317
  async function prepareInference(pending: InferencePreparation, selected: TemporalRuntime | undefined, ctx: ExtensionContext, operationSignal: AbortSignal): Promise<void> {
@@ -407,7 +320,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
407
320
  if (!selected?.view) throw new Error("Temporal State Flow runtime is unavailable; reload before publishing");
408
321
  await selected.withPatchTransaction((transaction) => {
409
322
  signal.throwIfAborted();
410
- if (runtime !== selected || inferencePreparation !== pending || !isActive() || shuttingDown) {
323
+ if (runtime !== selected || !inferencePreparation.owns(pending) || !isActive() || shuttingDown) {
411
324
  throw new Error("State Flow inference selection changed while awaiting publication");
412
325
  }
413
326
  assertPublicationAvailable();
@@ -430,13 +343,13 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
430
343
  snapshot = nextSnapshot;
431
344
  installScopeStates();
432
345
  if (rotatesRun && rehydrationPhase !== "new-bootstrap" && rehydrationPhase !== "resume-bootstrap") rehydrationPhase = "step";
433
- if (publication?.changed) recordPublication(publication, ctx);
346
+ if (publication?.changed) recordPublication();
434
347
  if (pending.prompt !== undefined || publication?.changed) appendCheckpoint();
435
348
  if (Object.keys(removals).length > 0) clearAcceptedAcquisitions();
436
349
  updateUi(ctx);
437
350
  }, signal);
438
351
  } catch (error) {
439
- if (signal.aborted || runtime !== selected || inferencePreparation !== pending || shuttingDown) return;
352
+ if (signal.aborted || runtime !== selected || !inferencePreparation.owns(pending) || shuttingDown) return;
440
353
  // Pi reports context-hook exceptions and continues. Abort through its public port rather than infer from stale preparation.
441
354
  ctx.abort();
442
355
  const cause = diagnosticText(error);
@@ -448,27 +361,22 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
448
361
  }
449
362
 
450
363
 
451
- function currentRehydrationPhase(): RehydrationPhase | undefined {
452
- return rehydrationPhase;
453
- }
454
-
455
364
  function statusDiagnostics(ctx: ExtensionContext): StatusDiagnostics {
456
365
  const cwd = ctx.cwd;
457
- const diagnosticStates = scopeStates;
458
366
  const view = runtime?.view;
459
367
  const diagnosticRecent = runtime?.recent() ?? [];
460
368
  const durableStateError = snapshot.meta.validation?.attempt === 0 ? snapshot.meta.validation.error
461
369
  : view ? undefined : "no temporal runtime is selected on this branch";
462
370
 
463
371
  const staleArtifacts: StatusDiagnostics["staleArtifacts"] = durableStateError === undefined
464
- ? artifactInvalidations.map(({ path, scope, reason }) => ({ scope: scope ?? "global", path, reason }))
372
+ ? acquisition.invalidations.map(({ path, scope, reason }) => ({ scope: scope ?? "global", path, reason }))
465
373
  : [];
466
374
  const session = sessionAddress(ctx);
467
375
  return {
468
376
  repositoryRoot,
469
377
  cwdScopeKey: cwdScopeKey(cwd),
470
378
  sessionScopeKey: sessionScopeKey(session.key),
471
- scopeStates: diagnosticStates,
379
+ scopeStates,
472
380
  effectiveState,
473
381
  recent: diagnosticRecent,
474
382
  historyLimit: config.historyLimit,
@@ -495,19 +403,17 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
495
403
  /** Select the active native branch under one owned restoration lifetime; only current accepted work installs memory. */
496
404
  function restoreActiveBranch(ctx: ExtensionContext, sessionStartReason?: unknown, notifyRecovery = true, startOwner?: AbortController, requestedMode?: InactiveMode): Promise<void> {
497
405
  contextProjection.reset();
498
- memoryToolLifetime.abort();
499
- memoryToolLifetime = new AbortController();
500
- cancelBranchRestoration();
406
+ memoryToolLifetime.renew();
407
+ branchRestoration.cancel();
501
408
  // Start-owned attachment/fork recovery keeps its owner; only accepted Start cancels Stop.
502
409
  if (!startOwner) {
503
- cancelStartActivation();
504
- cancelInactivePersistence();
410
+ startActivation.cancel();
411
+ inactivePersistence.cancel();
505
412
  }
506
413
  clearRunTransient();
507
414
  passiveContinuation = undefined;
508
415
  bootstrapContinuation = undefined;
509
- artifactInvalidations = [];
510
- artifactReads.setCandidates([]);
416
+ acquisition.clearInvalidations();
511
417
  telegramStartPending = false;
512
418
  activeContext = ctx;
513
419
  if (!startOwner) {
@@ -533,7 +439,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
533
439
  // Off needs only policy; unavailable native evidence grants no memory authority.
534
440
  }
535
441
  if (mode === "off") {
536
- cancelBackupWork();
442
+ backup.cancel();
537
443
  const owner = sessionAddress(ctx).key;
538
444
  const reason = pendingFork || (deferredBranch?.owner === owner && deferredBranch.cwd === ctx.cwd && deferredBranch.reason === "fork"
539
445
  && retainsPhysicalSessionProjection(sessionStartReason)) ? "fork" : sessionStartReason;
@@ -546,7 +452,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
546
452
  modePersistenceError = persistenceError;
547
453
  forkInitialization = reason === "fork";
548
454
  installScopeStates();
549
- artifactHints = {};
455
+ acquisition.reset();
550
456
  if (adoptDefault) appendCheckpoint();
551
457
  syncStateFlowTools();
552
458
  updateUi(ctx);
@@ -621,13 +527,9 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
621
527
  awaitingAcceptance: selection.kind !== "settled" && selection.kind !== "current",
622
528
  ...(requestedMode === undefined ? {} : { requestedMode }),
623
529
  };
624
- branchRestoration = pending;
625
- const operation = attachBranch(ctx, pending, selection, sessionStartReason, notifyRecovery, skipped);
626
- pending.operation = operation;
627
- restorationOperations.add(operation);
628
- const finished = () => { restorationOperations.delete(operation); };
629
- void operation.then(finished, finished);
630
- return operation;
530
+ branchRestoration.claim(pending);
531
+ // attachBranch releases its own ownership; settlement only drains it for shutdown.
532
+ return branchRestoration.track(pending, attachBranch(ctx, pending, selection, sessionStartReason, notifyRecovery, skipped), false);
631
533
  }
632
534
 
633
535
  async function attachBranch(
@@ -637,26 +539,23 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
637
539
  const placeholder = runtime;
638
540
  let selected = runtime;
639
541
  const owner = ctx.sessionManager.getSessionId();
640
- const cwd = ctx.cwd;
641
- const file = ctx.sessionManager.getSessionFile();
642
- const timestamp = ctx.sessionManager.getHeader()?.timestamp;
542
+ const sameSession = sessionIdentity(ctx);
643
543
  const signal = AbortSignal.any([pending.controller.signal, ...(ctx.signal ? [ctx.signal] : [])]);
644
- const isCurrent = () => branchRestoration === pending && !pending.controller.signal.aborted && !shuttingDown && runtime === selected
645
- && ctx.cwd === cwd && ctx.sessionManager.getSessionId() === owner && ctx.sessionManager.getSessionFile() === file
646
- && ctx.sessionManager.getHeader()?.timestamp === timestamp;
544
+ const isCurrent = () => branchRestoration.owns(pending) && !pending.controller.signal.aborted && !shuttingDown && runtime === selected
545
+ && sameSession();
647
546
  const assertCurrent = () => {
648
547
  signal.throwIfAborted();
649
548
  if (!isCurrent()) throw new Error("State Flow branch selection changed while awaiting publication");
650
549
  };
651
550
  let accepted = false;
652
551
  // Install accepted memory before native writes; later ancillary failures cannot revert or replay it.
653
- const accept = (candidate: TemporalRuntime, next: Snapshot, publication: RuntimePublication, nativeWrites: () => void): void => {
552
+ const accept = (candidate: TemporalRuntime, next: Snapshot, nativeWrites: () => void): void => {
654
553
  accepted = true;
655
554
  pending.awaitingAcceptance = false;
656
555
  runtime = selected = candidate;
657
556
  snapshot = next;
658
557
  installScopeStates();
659
- recordPolicyPublication(publication, ctx);
558
+ recordPublication();
660
559
  let failure: unknown;
661
560
  try { nativeWrites(); } catch (error) { failure = error; }
662
561
  settleSelection(ctx, reason, notifyRecovery, skipped, true);
@@ -683,16 +582,17 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
683
582
  if (restored.config.mode === "active" && restored.meta.specification === undefined
684
583
  && retainsPhysicalSessionProjection(reason) && hasUncheckpointedConversation(ctx.sessionManager.getBranch())) restored.meta.bootstrap = true;
685
584
  const next = pending.requestedMode ? deactivateEpisode(restored, pending.requestedMode) : restored;
686
- accept(candidate, next, publish(next), appendCheckpoint);
585
+ publish(next);
586
+ accept(candidate, next, appendCheckpoint);
687
587
  }, signal);
688
588
  } else if (selection.kind === "fork") {
689
589
  await candidate.withForkTransaction(selection.source, selection.checkpoint, (child, publish) => {
690
590
  assertCurrent();
691
591
  // Mode is selected independently of creating the child's private memory.
692
592
  const next = pending.requestedMode ? deactivateEpisode(child, pending.requestedMode) : child;
693
- const publication = publish(next);
593
+ publish(next);
694
594
  forkInitialization = false;
695
- accept(candidate, next, publication, () => {
595
+ accept(candidate, next, () => {
696
596
  pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, { reset: true, owner });
697
597
  appendCheckpoint();
698
598
  });
@@ -706,7 +606,8 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
706
606
  if (current || discovery.candidates.length > 0 || discovery.errors.length > 0) throw new Error("Existing session runtime requires retained-boundary restoration");
707
607
  const activated = startEpisode(hasPriorConversation(branch));
708
608
  const next = pending.requestedMode ? deactivateEpisode(activated, pending.requestedMode) : activated;
709
- accept(candidate, next, publish(next), appendCheckpoint);
609
+ publish(next);
610
+ accept(candidate, next, appendCheckpoint);
710
611
  }, signal, true);
711
612
  }
712
613
  } catch (error) {
@@ -740,7 +641,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
740
641
  installScopeStates();
741
642
  } finally {
742
643
  pending.awaitingAcceptance = false;
743
- if (branchRestoration === pending) branchRestoration = undefined;
644
+ branchRestoration.release(pending);
744
645
  }
745
646
  }
746
647
 
@@ -790,7 +691,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
790
691
  pi.registerTool({
791
692
  name: READ_STATE_TOOL_NAME,
792
693
  label: "Read State",
793
- 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.",
694
+ 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.",
794
695
  promptSnippet: "Read exact state values, structures, or historical semantic patches",
795
696
  parameters: Type.Object({
796
697
  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" })),
@@ -832,7 +733,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
832
733
  pi.registerTool({
833
734
  name: PATCH_STATE_TOOL_NAME,
834
735
  label: "Patch State",
835
- 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.",
736
+ 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.",
836
737
  promptSnippet: "Atomically patch one or more global/cwd/session scopes",
837
738
  promptGuidelines: [
838
739
  "Use patch_state only for material durable semantic changes; ordinary answers need no finalization call.",
@@ -901,12 +802,12 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
901
802
  snapshot = nextSnapshot;
902
803
  installScopeStates();
903
804
  if (publication?.changed) {
904
- recordPublication(publication, ctx);
805
+ recordPublication();
905
806
  appendCheckpoint();
906
807
  }
907
808
  clearAcceptedAcquisitions(new Set(acquiredArtifacts.map(({ path }) => path)));
908
809
  updateUi(ctx);
909
- const updates = contextProjection.acceptPatch(previousEffective, effectiveState, patches, artifactHints);
810
+ const updates = contextProjection.acceptPatch(previousEffective, effectiveState, patches, acquisition.hints);
910
811
  const acknowledgement = changed
911
812
  ? `\nState materialized atomically at ${scopes.join("+")} scope${scopes.length === 1 ? "" : "s"}.`
912
813
  : "\nState already current.";
@@ -931,30 +832,20 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
931
832
 
932
833
  function startStateFlow(ctx: ExtensionContext): Promise<StateFlowTelegramControlResult> {
933
834
  if (shuttingDown) return Promise.resolve({ ok: false, message: "State Flow is shutting down" });
934
- if (startActivation?.operation) return startActivation.operation;
835
+ if (startActivation.current?.operation) return startActivation.current.operation;
935
836
  telegramStartPending = false;
936
- if (activeContext && !branchRestoration && isActive()) {
837
+ if (activeContext && !branchRestoration.current && isActive()) {
937
838
  return Promise.resolve({ ok: true, message: "State Flow is already active", signal: sharedInspectionLifetime.signal });
938
839
  }
939
- const pending: NonNullable<typeof startActivation> = { controller: new AbortController() };
940
- startActivation = pending;
941
- const operation = runStart(ctx, pending.controller);
942
- pending.operation = operation;
943
- const finished = () => { if (startActivation === pending) startActivation = undefined; };
944
- void operation.then(finished, finished);
945
- return operation;
840
+ const pending = startActivation.claim({ controller: new AbortController() });
841
+ return startActivation.track(pending, runStart(ctx, pending.controller));
946
842
  }
947
843
 
948
844
  /** Await restoration-owned attachment and exact-source fork recovery before current-head activation. */
949
845
  async function runStart(ctx: ExtensionContext, pending: AbortController): Promise<StateFlowTelegramControlResult> {
950
- const owner = ctx.sessionManager.getSessionId();
951
- const cwd = ctx.cwd;
952
- const file = ctx.sessionManager.getSessionFile();
953
- const timestamp = ctx.sessionManager.getHeader()?.timestamp;
846
+ const sameSession = sessionIdentity(ctx);
954
847
  const signal = ctx.signal ? AbortSignal.any([pending.signal, ctx.signal]) : pending.signal;
955
- const isOwner = () => startActivation?.controller === pending && !signal.aborted && !shuttingDown
956
- && ctx.cwd === cwd && ctx.sessionManager.getSessionId() === owner && ctx.sessionManager.getSessionFile() === file
957
- && ctx.sessionManager.getHeader()?.timestamp === timestamp;
848
+ const isOwner = () => startActivation.current?.controller === pending && !signal.aborted && !shuttingDown && sameSession();
958
849
  const superseded = (): StateFlowTelegramControlResult => {
959
850
  pending.abort();
960
851
  return { ok: false, message: "State Flow activation was superseded", signal: pending.signal };
@@ -962,12 +853,12 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
962
853
  try {
963
854
  if (deferredBranch?.reason === "fork") await waitForRecovery(restoreActiveBranch(ctx, "fork", false, pending), signal);
964
855
  else if (!activeContext) await waitForRecovery(restoreActiveBranch(ctx, undefined, false, pending), signal);
965
- else if (branchRestoration?.operation) await waitForRecovery(branchRestoration.operation, signal);
856
+ else if (branchRestoration.current?.operation) await waitForRecovery(branchRestoration.current.operation, signal);
966
857
  if (!isOwner()) return superseded();
967
858
  if (isActive()) return { ok: true, message: "State Flow is already active", signal: sharedInspectionLifetime.signal };
968
859
  if (forkInitialization) {
969
860
  // Withdraw this join on cancellation without revoking the independently owned mode persistence.
970
- const stopping = inactivePersistence?.operation;
861
+ const stopping = inactivePersistence.current?.operation;
971
862
  if (stopping) await waitForRecovery(stopping, signal);
972
863
  if (!isOwner()) return superseded();
973
864
  await waitForRecovery(restoreActiveBranch(ctx, "fork", false, pending), signal);
@@ -986,16 +877,13 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
986
877
  async function activateCurrentState(ctx: ExtensionContext, pending: AbortController): Promise<StateFlowTelegramControlResult> {
987
878
  let selected = runtime;
988
879
  const owner = ctx.sessionManager.getSessionId();
989
- const cwd = ctx.cwd;
990
- const file = ctx.sessionManager.getSessionFile();
991
- const timestamp = ctx.sessionManager.getHeader()?.timestamp;
880
+ const sameSession = sessionIdentity(ctx);
992
881
  const initiallyActive = isActive();
993
882
  let accepted = false;
994
883
  let receipt = AbortSignal.any([pending.signal, sharedInspectionLifetime.signal]);
995
884
  const signal = ctx.signal ? AbortSignal.any([pending.signal, ctx.signal]) : pending.signal;
996
- const isCurrent = () => startActivation?.controller === pending && !signal.aborted && runtime === selected && !shuttingDown
997
- && ctx.cwd === cwd && ctx.sessionManager.getSessionId() === owner && ctx.sessionManager.getSessionFile() === file
998
- && ctx.sessionManager.getHeader()?.timestamp === timestamp && isActive() === (accepted || initiallyActive);
885
+ const isCurrent = () => startActivation.current?.controller === pending && !signal.aborted && runtime === selected && !shuttingDown
886
+ && sameSession() && isActive() === (accepted || initiallyActive);
999
887
  const superseded = (): StateFlowTelegramControlResult => {
1000
888
  pending.abort();
1001
889
  return { ok: false, message: "State Flow activation was superseded", signal: pending.signal };
@@ -1018,9 +906,9 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1018
906
  const bootstrap = hasPriorConversation(ctx.sessionManager.getBranch()) || passiveContinuation !== undefined || boundary !== undefined;
1019
907
  const activated = current ? resumeEpisode(current, bootstrap) : startEpisode(bootstrap);
1020
908
  const recoveredCurrent = selectedHistoryExpired;
1021
- const publication = publish(activated);
909
+ publish(activated);
1022
910
  accepted = true;
1023
- cancelInactivePersistence();
911
+ inactivePersistence.cancel();
1024
912
  runtime = selected = activation;
1025
913
  snapshot = activated;
1026
914
  activeContext = ctx;
@@ -1035,7 +923,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1035
923
  bootstrapContinuation = snapshot.meta.bootstrap ? continuation : undefined;
1036
924
  deferInferencePreparation();
1037
925
  receipt = AbortSignal.any([pending.signal, sharedInspectionLifetime.signal]);
1038
- recordPolicyPublication(publication, ctx);
926
+ recordPublication();
1039
927
  syncStateFlowTools();
1040
928
  updateUi(ctx);
1041
929
  if (forkInitialization) {
@@ -1100,16 +988,16 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1100
988
  function deactivateStateFlow(ctx: ExtensionContext, mode: InactiveMode): Promise<StateFlowTelegramControlResult> {
1101
989
  if (shuttingDown) return Promise.resolve({ ok: false, message: "State Flow is shutting down" });
1102
990
  if (mode === "off") return selectOff(ctx);
1103
- cancelStartActivation();
991
+ startActivation.cancel();
1104
992
  telegramStartPending = false;
1105
- const persisting = inactivePersistence;
993
+ const persisting = inactivePersistence.current;
1106
994
  if (persisting?.operation && !persisting.published) {
1107
995
  // Pending inactive choices coalesce: acceptance persists whichever inactive mode is current then.
1108
996
  if (snapshot.config.mode !== mode) applyInactiveMode(ctx, mode);
1109
997
  return persisting.operation;
1110
998
  }
1111
999
  if (!activeContext) void restoreActiveBranch(ctx, undefined, false);
1112
- const restoring = branchRestoration;
1000
+ const restoring = branchRestoration.current;
1113
1001
  // Read-only recovery also retains the latest policy; its native write fence is updated below.
1114
1002
  if (restoring) restoring.requestedMode = mode;
1115
1003
  if (restoring?.awaitingAcceptance) {
@@ -1122,16 +1010,11 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1122
1010
  const recordedMode = retained.kind === "pre-runtime" ? retained.mode : retained.kind === "boundary" ? retained.checkpoint.mode : undefined;
1123
1011
  if (modePersistenceError || recordedMode === mode) return Promise.resolve({ ok: true, message: `State Flow is already ${mode}` });
1124
1012
  }
1125
- cancelInactivePersistence();
1126
- const pending: NonNullable<typeof inactivePersistence> = { controller: new AbortController() };
1127
- inactivePersistence = pending;
1128
- const operation = deferredBranch && mode === "passive" && !branchStartsWithoutRuntime
1013
+ inactivePersistence.cancel();
1014
+ const pending = inactivePersistence.claim({ controller: new AbortController() });
1015
+ return inactivePersistence.track(pending, deferredBranch && mode === "passive" && !branchStartsWithoutRuntime
1129
1016
  ? acquirePassiveMode(ctx, pending, deferredBranch.reason)
1130
- : persistInactiveMode(ctx, pending, mode);
1131
- pending.operation = operation;
1132
- const finished = () => { if (inactivePersistence === pending) inactivePersistence = undefined; };
1133
- void operation.then(finished, finished);
1134
- return operation;
1017
+ : persistInactiveMode(ctx, pending, mode));
1135
1018
  }
1136
1019
 
1137
1020
  /** Off owns cancellation and native policy only; no canonical transaction or semantic handoff. */
@@ -1149,7 +1032,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1149
1032
  if (!sameOwner && !sameDeferred) modePersistenceError = policy?.persistenceError;
1150
1033
  const alreadyOff = current.config.mode === "off" && policy?.mode === "off";
1151
1034
  const incoming = sameOwner || sameDeferred ? current.meta.bootstrap ? bootstrapContinuation : passiveContinuation : undefined;
1152
- const unfinished = current.meta.specification !== undefined || (inferencePreparation?.prompt !== undefined && !inferencePreparation.accepted)
1035
+ const unfinished = current.meta.specification !== undefined || (inferencePreparation.current?.prompt !== undefined && !inferencePreparation.current.accepted)
1153
1036
  || (isActive() && hasUncheckpointedConversation(branch));
1154
1037
  const at = incoming?.startedAt ?? Date.now();
1155
1038
  const from = incoming ? incoming.activeRunStartedAt : (sameOwner || sameDeferred) && (!ctx.isIdle() || unfinished) ? runAnchorTimestamp : undefined;
@@ -1164,20 +1047,17 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1164
1047
  } catch {
1165
1048
  // An unusable cached bookmark cannot prevent native Off policy or authorize replacement memory.
1166
1049
  }
1167
- cancelStartActivation();
1168
- cancelBranchRestoration();
1169
- cancelInactivePersistence();
1170
- cancelBackupWork();
1171
- memoryToolLifetime.abort();
1172
- memoryToolLifetime = new AbortController();
1050
+ startActivation.cancel();
1051
+ branchRestoration.cancel();
1052
+ inactivePersistence.cancel();
1053
+ backup.cancel();
1054
+ memoryToolLifetime.renew();
1173
1055
  clearRunTransient();
1174
1056
  contextProjection.reset();
1175
1057
  telegramStartPending = false;
1176
1058
  bootstrapContinuation = undefined;
1177
1059
  passiveContinuation = undefined;
1178
- artifactInvalidations = [];
1179
- artifactHints = {};
1180
- artifactReads.setCandidates([]);
1060
+ acquisition.reset();
1181
1061
  runtime = undefined;
1182
1062
  snapshot = emptySnapshot("off");
1183
1063
  selectedHistoryExpired = false;
@@ -1207,21 +1087,21 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1207
1087
  }
1208
1088
  }
1209
1089
 
1210
- async function acquirePassiveMode(ctx: ExtensionContext, pending: NonNullable<typeof inactivePersistence>, reason: unknown): Promise<StateFlowTelegramControlResult> {
1090
+ async function acquirePassiveMode(ctx: ExtensionContext, pending: InactivePersistence, reason: unknown): Promise<StateFlowTelegramControlResult> {
1211
1091
  await restoreActiveBranch(ctx, reason, true, pending.controller, "passive");
1212
- if (inactivePersistence !== pending || pending.controller.signal.aborted || shuttingDown) {
1092
+ if (!inactivePersistence.owns(pending) || pending.controller.signal.aborted || shuttingDown) {
1213
1093
  return { ok: false, message: "State Flow mode change was superseded" };
1214
1094
  }
1215
1095
  return persistInactiveMode(ctx, pending, "passive");
1216
1096
  }
1217
1097
 
1218
- async function persistInactiveMode(ctx: ExtensionContext, pending: NonNullable<typeof inactivePersistence>, mode: InactiveMode): Promise<StateFlowTelegramControlResult> {
1098
+ async function persistInactiveMode(ctx: ExtensionContext, pending: InactivePersistence, mode: InactiveMode): Promise<StateFlowTelegramControlResult> {
1219
1099
  let selected = runtime;
1220
1100
  const owner = ctx.sessionManager.getSessionId();
1221
1101
  const current = snapshot;
1222
1102
  const wasActive = current.config.mode === "active";
1223
1103
  let unfinished = current.meta.specification !== undefined
1224
- || (inferencePreparation?.prompt !== undefined && !inferencePreparation.accepted);
1104
+ || (inferencePreparation.current?.prompt !== undefined && !inferencePreparation.current.accepted);
1225
1105
  applyInactiveMode(ctx, mode);
1226
1106
  const stoppedAt = Date.now();
1227
1107
  if (modePersistenceError) {
@@ -1237,7 +1117,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1237
1117
  const incomingBoundary = current.meta.bootstrap ? bootstrapContinuation : undefined;
1238
1118
  const anchor = runAnchorTimestamp;
1239
1119
  const idle = ctx.isIdle();
1240
- const isCurrent = () => inactivePersistence === pending && runtime === selected
1120
+ const isCurrent = () => inactivePersistence.owns(pending) && runtime === selected
1241
1121
  && !shuttingDown && ctx.sessionManager.getSessionId() === owner && !isActive();
1242
1122
  const superseded = (): StateFlowTelegramControlResult => ({ ok: false, message: "State Flow mode change was superseded" });
1243
1123
  const freezeHandoff = (): PassiveContinuation | undefined => {
@@ -1278,8 +1158,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1278
1158
  // Projection changes before any wait; capture the old run's boundary before later native input can replace it.
1279
1159
  freezeHandoff();
1280
1160
  bootstrapContinuation = undefined;
1281
- artifactInvalidations = [];
1282
- artifactReads.setCandidates([]);
1161
+ acquisition.clearInvalidations();
1283
1162
  assertPublicationAvailable();
1284
1163
  const signal = ctx.signal ? AbortSignal.any([pending.controller.signal, ctx.signal]) : pending.controller.signal;
1285
1164
  if (branchStartsWithoutRuntime || !selected?.view) {
@@ -1299,11 +1178,11 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1299
1178
  if (!isCurrent()) throw new Error("State Flow mode selection changed while awaiting publication");
1300
1179
  assertPublicationAvailable();
1301
1180
  const next = structuredClone(snapshot);
1302
- const publication = publish(next);
1181
+ publish(next);
1303
1182
  accepted = true;
1304
1183
  snapshot = next;
1305
1184
  installScopeStates();
1306
- recordPolicyPublication(publication, ctx);
1185
+ recordPublication();
1307
1186
  return complete();
1308
1187
  }, signal);
1309
1188
  return isCurrent() ? result : superseded();
@@ -1317,8 +1196,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1317
1196
  }
1318
1197
  modePersistenceError = cause.trim() ? cause : "Canonical State Flow persistence failed";
1319
1198
  bootstrapContinuation = undefined;
1320
- artifactInvalidations = [];
1321
- artifactReads.setCandidates([]);
1199
+ acquisition.clearInvalidations();
1322
1200
  return complete();
1323
1201
  }
1324
1202
  }
@@ -1352,7 +1230,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1352
1230
  },
1353
1231
  cancelStart: () => {
1354
1232
  telegramStartPending = false;
1355
- cancelStartActivation();
1233
+ startActivation.cancel();
1356
1234
  },
1357
1235
  },
1358
1236
  });
@@ -1369,7 +1247,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1369
1247
  contextProjection.reset();
1370
1248
  skillReads.clear();
1371
1249
  artifactReads.clear();
1372
- cancelResponseReconciliation();
1250
+ responseReconciliation.cancel();
1373
1251
  completedRunAccepted = false;
1374
1252
  // This native hook has no operation signal. Capture only; the first active context owns acceptance.
1375
1253
  deferInferencePreparation(event.prompt);
@@ -1389,12 +1267,12 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1389
1267
  if (runtime?.view) refreshArtifactHints();
1390
1268
  if (!isActive() && !passiveContinuation && !passiveMemoryAvailable()) return;
1391
1269
  // Idle inspection must not freeze a pre-acceptance snapshot for the live inference.
1392
- const projection = isActive() && inferencePreparation && !inferencePreparation.accepted
1270
+ const projection = isActive() && inferencePreparation.current && !inferencePreparation.current.accepted
1393
1271
  ? new ContextProjection() : contextProjection;
1394
1272
  const effective = effectiveState;
1395
- const invalidations = artifactInvalidations.map(({ path, scope, reason }) => ({ path, ...(scope === undefined ? {} : { scope }), reason }));
1396
- const phase = isActive() ? currentRehydrationPhase() : undefined;
1397
- const view = contextView(effective, artifactHints, invalidations, phase);
1273
+ const invalidations = acquisition.invalidations.map(({ path, scope, reason }) => ({ path, ...(scope === undefined ? {} : { scope }), reason }));
1274
+ const phase = isActive() ? rehydrationPhase : undefined;
1275
+ const view = contextView(effective, acquisition.hints, invalidations, phase);
1398
1276
  if (passiveContinuation) {
1399
1277
  const retained = passiveContinuationMessages(messages, passiveContinuation);
1400
1278
  if (!runtime?.view) return { messages: retained };
@@ -1414,20 +1292,20 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1414
1292
  }
1415
1293
 
1416
1294
  function prepareContext(messages: AgentMessage[], ctx: ExtensionContext): ReturnType<typeof projectContext> | Promise<ReturnType<typeof projectContext>> {
1417
- const pending = inferencePreparation;
1295
+ const pending = inferencePreparation.current;
1418
1296
  const selected = runtime;
1419
1297
  const operationSignal = ctx.signal;
1420
1298
  // Idle projections remain observational; only a live native operation may await preparation.
1421
1299
  if (!isActive() || !pending || !operationSignal || shuttingDown) return projectContext(messages);
1422
1300
  pending.operation ??= prepareInference(pending, selected, ctx, operationSignal).finally(() => {
1423
1301
  // A late withdrawal must not clear newer work; accepted lifecycle is never replayed after an error.
1424
- if (!pending.accepted && inferencePreparation === pending) pending.operation = undefined;
1302
+ if (!pending.accepted && inferencePreparation.owns(pending)) pending.operation = undefined;
1425
1303
  });
1426
1304
  return pending.operation.then(() => {
1427
1305
  if (shuttingDown || operationSignal.aborted || runtime !== selected || ctx.signal !== operationSignal) return;
1428
- if (isActive() && inferencePreparation !== pending) {
1306
+ if (isActive() && !inferencePreparation.owns(pending)) {
1429
1307
  // Start may require same-run maintenance. A new captured user prompt instead owns a different context request.
1430
- if (inferencePreparation?.prompt === undefined) return prepareContext(messages, ctx);
1308
+ if (inferencePreparation.current?.prompt === undefined) return prepareContext(messages, ctx);
1431
1309
  return;
1432
1310
  }
1433
1311
  if (isActive() && !pending.accepted) return;
@@ -1487,32 +1365,32 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1487
1365
  if (event.message.role === "user" && runAnchorTimestamp === undefined) runAnchorTimestamp = event.message.timestamp;
1488
1366
  if (shuttingDown || !isActive()) return;
1489
1367
  if (event.message.role !== "assistant") return;
1490
- cancelResponseReconciliation();
1368
+ responseReconciliation.cancel();
1491
1369
  const message = event.message as unknown as { role: "assistant"; stopReason?: string; content?: unknown };
1492
1370
  if (message.stopReason === "aborted" || assistantToolCallCount(message.content) > 0 || message.stopReason === "toolUse") return;
1493
1371
  if (message.stopReason === "length" || message.stopReason === "error") {
1494
1372
  recordDiagnostic(`Assistant response ended with ${message.stopReason}`, "finalization", ctx, { content: message.content });
1495
1373
  return;
1496
1374
  }
1497
- responseReconciliation = new AbortController();
1375
+ responseReconciliation.claim({ controller: new AbortController() });
1498
1376
  });
1499
1377
 
1500
1378
  pi.on("turn_end", async (event, ctx) => {
1501
1379
  if (shuttingDown) return;
1502
- const pending = responseReconciliation;
1380
+ const pending = responseReconciliation.current;
1503
1381
  if (!isActive() || !pending) {
1504
1382
  updateUi(ctx);
1505
1383
  return;
1506
1384
  }
1507
1385
  const selected = runtime;
1508
- const signal = ctx.signal ? AbortSignal.any([pending.signal, ctx.signal]) : pending.signal;
1386
+ const signal = ctx.signal ? AbortSignal.any([pending.controller.signal, ctx.signal]) : pending.controller.signal;
1509
1387
  let responseCommitted = false;
1510
1388
  try {
1511
1389
  const response = finalizedAssistantResponse(event.message);
1512
1390
  if (!selected?.view) throw new Error("Temporal State Flow runtime is unavailable; reload before publishing");
1513
1391
  await selected.withPatchTransaction((transaction) => {
1514
1392
  signal.throwIfAborted();
1515
- if (runtime !== selected || responseReconciliation !== pending || !isActive()) {
1393
+ if (runtime !== selected || !responseReconciliation.owns(pending) || !isActive()) {
1516
1394
  throw new Error("State Flow response selection changed while awaiting publication");
1517
1395
  }
1518
1396
  assertPublicationAvailable();
@@ -1528,44 +1406,38 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1528
1406
  installScopeStates();
1529
1407
  responseCommitted = true;
1530
1408
  contextProjection.reset();
1531
- if (publication?.changed) recordPublication(publication, ctx);
1409
+ if (publication?.changed) recordPublication();
1532
1410
  appendCheckpoint();
1533
1411
  clearAcceptedAcquisitions();
1534
1412
  bootstrapContinuation = undefined;
1535
1413
  rehydrationPhase = "step";
1536
1414
  completedRunAccepted = !wasBootstrap;
1537
- turnAcceptedForBackup = true;
1415
+ backup.markTurnAccepted();
1538
1416
  updateUi(ctx);
1539
1417
  }, signal);
1540
1418
  } catch (error) {
1541
1419
  // Superseded completion owns neither the new lifecycle nor its diagnostics/UI.
1542
- if (signal.aborted || runtime !== selected || responseReconciliation !== pending) return;
1420
+ if (signal.aborted || runtime !== selected || !responseReconciliation.owns(pending)) return;
1543
1421
  const cause = diagnosticText(error);
1544
1422
  recordDiagnostic(cause, "finalization", ctx);
1545
1423
  notifyProblem(ctx, responseCommitted
1546
1424
  ? `State Flow response saved; lifecycle update failed: ${cause}`
1547
1425
  : `State Flow response reconciliation failed: ${cause}`, "error");
1548
1426
  } finally {
1549
- if (responseReconciliation === pending) responseReconciliation = undefined;
1427
+ responseReconciliation.release(pending);
1550
1428
  }
1551
1429
  });
1552
1430
 
1553
1431
  pi.on("session_before_compact", (event) => {
1554
- if (event.reason !== "manual" || !event.customInstructions?.startsWith("state-flow-boundary:")) return;
1555
- const marker = compactionMarker;
1556
- if (shuttingDown || compactionStopped || !isActive() || !compactionPlan || !marker || event.customInstructions !== marker) return { cancel: true };
1557
- const result = stateFlowCompactionResult(compactionPlan, marker, event);
1432
+ const result = compactionRequests.resolve(event, !shuttingDown && isActive());
1558
1433
  if (result === undefined || "cancel" in result) return result;
1559
1434
  return { compaction: result };
1560
1435
  });
1561
1436
 
1562
1437
  pi.on("agent_before_settle", async (_event, ctx) => {
1563
- if (shuttingDown || !memoryToolsAvailable() || !turnAcceptedForBackup) return;
1564
- turnAcceptedForBackup = false;
1565
- if (!backupPending) return;
1566
- backupPending = false;
1438
+ if (shuttingDown || !memoryToolsAvailable() || !backup.takeSettledTurn()) return;
1567
1439
  const operationSignal = ctx.signal;
1568
- const lifetime = backupLifetime.signal;
1440
+ const lifetime = backup.lifetime;
1569
1441
  const signal = operationSignal ? AbortSignal.any([lifetime, operationSignal]) : lifetime;
1570
1442
  const selected = runtime;
1571
1443
  try {
@@ -1573,21 +1445,17 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1573
1445
  const pushSessionId = sessionAddress(ctx).id;
1574
1446
  const pushCwd = ctx.cwd;
1575
1447
  // Pi 0.87 settles after its agent signal ends; waiting there would strand native Abort.
1576
- const operation = backupCurrentStateFlowFiles(repositoryRoot, signal, operationSignal !== undefined);
1577
- backupOperations.add(operation);
1578
- try { await operation; }
1579
- finally { backupOperations.delete(operation); }
1448
+ await backup.track(backupCurrentStateFlowFiles(repositoryRoot, signal, operationSignal !== undefined));
1580
1449
  if (signal.aborted || snapshot.config.mode === "off") return;
1581
1450
  startStateFlowBackupPush(repositoryRoot, (error) => {
1582
1451
  if (shuttingDown || lifetime.aborted || snapshot.config.mode === "off") return;
1583
1452
  const message = diagnosticText(error);
1584
1453
  const recorded = diagnosticWriter.recordBackupPushFailure(pushSessionId, pushCwd, message);
1585
- if (pushFailureNotified) return;
1586
- pushFailureNotified = true;
1454
+ if (!backup.claimPushFailureNotice()) return;
1587
1455
  notifyActiveContext(recorded
1588
1456
  ? `State Flow Git backup push failed; state is saved locally. Details: ${stateFlowLogPath(agentDir)}. A later accepted turn retries.`
1589
1457
  : `State Flow Git backup push failed; local diagnostics unavailable: ${message}`);
1590
- }, () => { pushFailureNotified = false; }, lifetime);
1458
+ }, () => backup.resetPushFailureNotice(), lifetime);
1591
1459
  }
1592
1460
  } catch (error) {
1593
1461
  if (signal.aborted || runtime !== selected) return;
@@ -1605,21 +1473,19 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1605
1473
  await startStateFlow(ctx);
1606
1474
  return;
1607
1475
  }
1608
- if (!completedRunAccepted || compactionStopped || !isActive() || snapshot.meta.bootstrap
1609
- || compactionInFlight || !ctx.isIdle() || ctx.hasPendingMessages() || !runtime?.view
1476
+ if (!completedRunAccepted || compactionRequests.stopped || !isActive() || snapshot.meta.bootstrap
1477
+ || compactionRequests.inFlight || !ctx.isIdle() || ctx.hasPendingMessages() || !runtime?.view
1610
1478
  || !shouldRequestStateFlowCompaction(ctx.getContextUsage())) return;
1611
1479
  completedRunAccepted = false;
1612
1480
  const entries = ctx.sessionManager.buildContextEntries();
1613
1481
  if (!hasCompactionSizedTranscript(entries)) return;
1614
1482
  const plan = planStateFlowCompaction(entries, runtime.causalBasis(), snapshot.meta.step, runAnchorTimestamp);
1615
1483
  if (!plan) return;
1616
- compactionPlan = plan;
1617
- const marker = compactionMarker = `${compactionPrefix}${randomUUID()}`;
1618
- compactionInFlight = true;
1484
+ const marker = compactionRequests.begin(plan);
1619
1485
  // Pi dispatches deferred companion prompts after all settled handlers return.
1620
1486
  await new Promise<void>((resolve) => {
1621
1487
  const finished = () => {
1622
- if (compactionMarker === marker) { compactionPlan = undefined; compactionInFlight = false; }
1488
+ compactionRequests.finish(marker);
1623
1489
  resolve();
1624
1490
  };
1625
1491
  ctx.compact({ customInstructions: marker, onComplete: finished, onError: finished });
@@ -1640,21 +1506,21 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1640
1506
  });
1641
1507
  pi.on("session_shutdown", async (_event, _ctx) => {
1642
1508
  shuttingDown = true;
1643
- memoryToolLifetime.abort();
1509
+ memoryToolLifetime.end();
1644
1510
  contextProjection.reset();
1645
- const restoring = cancelBranchRestoration();
1646
- const starting = cancelStartActivation();
1647
- const stopping = cancelInactivePersistence();
1648
- backupLifetime.abort();
1649
- cancelInferencePreparation();
1650
- cancelResponseReconciliation();
1511
+ const restoring = branchRestoration.cancel();
1512
+ const starting = startActivation.cancel();
1513
+ const stopping = inactivePersistence.cancel();
1514
+ backup.abort();
1515
+ inferencePreparation.cancel();
1516
+ responseReconciliation.cancel();
1651
1517
  cancelSharedInspections();
1652
1518
  telegramStartPending = false;
1653
- compactionStopped = true;
1519
+ compactionRequests.stop();
1654
1520
  completedRunAccepted = false;
1655
1521
  activeContext = undefined;
1656
1522
  telegram.dispose();
1657
- await Promise.allSettled([...backupOperations, ...restorationOperations, ...[restoring, stopping, starting].filter((operation) => operation !== undefined)]);
1523
+ await Promise.allSettled([...backup.operations, ...branchRestoration.inflight, ...[restoring, stopping, starting].filter((operation) => operation !== undefined)]);
1658
1524
  await awaitInFlightBackupPushes(repositoryRoot);
1659
1525
  });
1660
1526
  }