@llblab/pi-kit 0.25.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 (169) hide show
  1. package/BACKLOG.md +5 -1
  2. package/CHANGELOG.md +11 -0
  3. package/README.md +10 -8
  4. package/node_modules/@llblab/pi-actors/AGENTS.md +2 -0
  5. package/node_modules/@llblab/pi-actors/CHANGELOG.md +4 -1
  6. package/node_modules/@llblab/pi-actors/LICENSE +21 -0
  7. package/node_modules/@llblab/pi-actors/README.md +1 -1
  8. package/node_modules/@llblab/pi-actors/docs/coordinator-delivery.md +1 -1
  9. package/node_modules/@llblab/pi-actors/package.json +4 -3
  10. package/node_modules/@llblab/pi-claude-usage/AGENTS.md +6 -3
  11. package/node_modules/@llblab/pi-claude-usage/BACKLOG.md +2 -1
  12. package/node_modules/@llblab/pi-claude-usage/CHANGELOG.md +8 -0
  13. package/node_modules/@llblab/pi-claude-usage/README.md +48 -3
  14. package/node_modules/@llblab/pi-claude-usage/index.ts +8 -1159
  15. package/node_modules/@llblab/pi-claude-usage/lib/extension.ts +30 -0
  16. package/node_modules/@llblab/pi-claude-usage/lib/fast.ts +24 -0
  17. package/node_modules/@llblab/pi-claude-usage/lib/query.ts +146 -0
  18. package/node_modules/@llblab/pi-claude-usage/lib/status-format.ts +297 -0
  19. package/node_modules/@llblab/pi-claude-usage/lib/status.ts +366 -0
  20. package/node_modules/@llblab/pi-claude-usage/lib/telegram.ts +44 -0
  21. package/node_modules/@llblab/pi-claude-usage/lib/usage-store.ts +221 -0
  22. package/node_modules/@llblab/pi-claude-usage/lib/usage.ts +128 -0
  23. package/node_modules/@llblab/pi-claude-usage/package.json +9 -5
  24. package/node_modules/@llblab/pi-clean-room/AGENTS.md +1 -0
  25. package/node_modules/@llblab/pi-clean-room/CHANGELOG.md +5 -0
  26. package/node_modules/@llblab/pi-clean-room/LICENSE +21 -0
  27. package/node_modules/@llblab/pi-clean-room/README.md +1 -1
  28. package/node_modules/@llblab/pi-clean-room/package.json +3 -2
  29. package/node_modules/@llblab/pi-codex-usage/AGENTS.md +9 -6
  30. package/node_modules/@llblab/pi-codex-usage/BACKLOG.md +2 -1
  31. package/node_modules/@llblab/pi-codex-usage/CHANGELOG.md +17 -0
  32. package/node_modules/@llblab/pi-codex-usage/README.md +75 -17
  33. package/node_modules/@llblab/pi-codex-usage/index.ts +8 -1602
  34. package/node_modules/@llblab/pi-codex-usage/lib/extension.ts +25 -0
  35. package/node_modules/@llblab/pi-codex-usage/lib/fast.ts +23 -0
  36. package/node_modules/@llblab/pi-codex-usage/lib/query.ts +368 -0
  37. package/node_modules/@llblab/pi-codex-usage/lib/status-format.ts +347 -0
  38. package/node_modules/@llblab/pi-codex-usage/lib/status.ts +435 -0
  39. package/node_modules/@llblab/pi-codex-usage/lib/telegram.ts +45 -0
  40. package/node_modules/@llblab/pi-codex-usage/lib/usage-store.ts +229 -0
  41. package/node_modules/@llblab/pi-codex-usage/lib/usage.ts +425 -0
  42. package/node_modules/@llblab/pi-codex-usage/package.json +11 -6
  43. package/node_modules/@llblab/pi-command-fast/AGENTS.md +7 -0
  44. package/node_modules/@llblab/pi-command-fast/BACKLOG.md +9 -0
  45. package/node_modules/@llblab/pi-command-fast/CHANGELOG.md +7 -0
  46. package/node_modules/@llblab/pi-command-fast/LICENSE +21 -0
  47. package/node_modules/@llblab/pi-command-fast/README.md +42 -0
  48. package/node_modules/@llblab/pi-command-fast/dist/command.d.ts +8 -0
  49. package/node_modules/@llblab/pi-command-fast/dist/command.js +52 -0
  50. package/node_modules/@llblab/pi-command-fast/dist/index.d.ts +3 -0
  51. package/node_modules/@llblab/pi-command-fast/dist/index.js +3 -0
  52. package/node_modules/@llblab/pi-command-fast/dist/models-json.d.ts +10 -0
  53. package/node_modules/@llblab/pi-command-fast/dist/models-json.js +81 -0
  54. package/node_modules/@llblab/pi-command-fast/package.json +49 -0
  55. package/node_modules/@llblab/pi-grow-loop/AGENTS.md +1 -0
  56. package/node_modules/@llblab/pi-grow-loop/CHANGELOG.md +4 -1
  57. package/node_modules/@llblab/pi-grow-loop/LICENSE +21 -0
  58. package/node_modules/@llblab/pi-grow-loop/README.md +1 -1
  59. package/node_modules/@llblab/pi-grow-loop/package.json +3 -2
  60. package/node_modules/@llblab/pi-state-flow/AGENTS.md +7 -6
  61. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +13 -5
  62. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +16 -1
  63. package/node_modules/@llblab/pi-state-flow/LICENSE +21 -0
  64. package/node_modules/@llblab/pi-state-flow/README.md +117 -35
  65. package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.d.ts +24 -1
  66. package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.js +80 -1
  67. package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.d.ts +18 -1
  68. package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.js +41 -1
  69. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +346 -303
  70. package/node_modules/@llblab/pi-state-flow/dist/lib/git.d.ts +31 -2
  71. package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +74 -5
  72. package/node_modules/@llblab/pi-state-flow/dist/lib/operation.d.ts +37 -0
  73. package/node_modules/@llblab/pi-state-flow/dist/lib/operation.js +59 -0
  74. package/node_modules/@llblab/pi-state-flow/dist/lib/ownership.d.ts +31 -0
  75. package/node_modules/@llblab/pi-state-flow/dist/lib/ownership.js +117 -0
  76. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +7 -7
  77. package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +1 -1
  78. package/node_modules/@llblab/pi-state-flow/dist/lib/session.d.ts +10 -1
  79. package/node_modules/@llblab/pi-state-flow/dist/lib/session.js +60 -1
  80. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +6 -0
  81. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +8 -5
  82. package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +2 -0
  83. package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +28 -0
  84. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +6 -1
  85. package/node_modules/@llblab/pi-state-flow/dist/package.json +10 -9
  86. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +14 -6
  87. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +2 -2
  88. package/node_modules/@llblab/pi-state-flow/docs/README.md +19 -9
  89. package/node_modules/@llblab/pi-state-flow/docs/agent-contract-relocation.md +4 -4
  90. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +646 -89
  91. package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +116 -37
  92. package/node_modules/@llblab/pi-state-flow/docs/filesystem-recovery.md +118 -21
  93. package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +70 -8
  94. package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +88 -14
  95. package/node_modules/@llblab/pi-state-flow/docs/performance.md +83 -66
  96. package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +391 -62
  97. package/node_modules/@llblab/pi-state-flow/docs/usage.md +317 -61
  98. package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +85 -1
  99. package/node_modules/@llblab/pi-state-flow/lib/compaction.ts +49 -2
  100. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +342 -301
  101. package/node_modules/@llblab/pi-state-flow/lib/git.ts +77 -5
  102. package/node_modules/@llblab/pi-state-flow/lib/operation.ts +75 -0
  103. package/node_modules/@llblab/pi-state-flow/lib/ownership.ts +120 -0
  104. package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +7 -8
  105. package/node_modules/@llblab/pi-state-flow/lib/query.ts +1 -1
  106. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +1 -1
  107. package/node_modules/@llblab/pi-state-flow/lib/session.ts +52 -1
  108. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +8 -7
  109. package/node_modules/@llblab/pi-state-flow/lib/status.ts +29 -0
  110. package/node_modules/@llblab/pi-state-flow/lib/transition.ts +5 -1
  111. package/node_modules/@llblab/pi-state-flow/package.json +10 -9
  112. package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +14 -6
  113. package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +2 -2
  114. package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -0
  115. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +10 -0
  116. package/node_modules/@llblab/pi-telegram/README.md +1 -1
  117. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +4 -1
  118. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +16 -12
  119. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +1 -1
  120. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +7 -1
  121. package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +12 -3
  122. package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +137 -83
  123. package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +26 -0
  124. package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.d.ts +57 -2
  125. package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.js +109 -4
  126. package/node_modules/@llblab/pi-telegram/dist/lib/model.js +2 -4
  127. package/node_modules/@llblab/pi-telegram/dist/lib/status.d.ts +3 -1
  128. package/node_modules/@llblab/pi-telegram/dist/lib/status.js +31 -1
  129. package/node_modules/@llblab/pi-telegram/dist/lib/sync.js +4 -4
  130. package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +1 -0
  131. package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +17 -4
  132. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.d.ts +2 -2
  133. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.js +48 -12
  134. package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
  135. package/node_modules/@llblab/pi-telegram/docs/architecture.md +6 -5
  136. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +2 -0
  137. package/node_modules/@llblab/pi-telegram/docs/public-api.md +2 -2
  138. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +4 -0
  139. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +17 -10
  140. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +8 -2
  141. package/node_modules/@llblab/pi-telegram/lib/commands.ts +135 -97
  142. package/node_modules/@llblab/pi-telegram/lib/extension.ts +25 -0
  143. package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +140 -4
  144. package/node_modules/@llblab/pi-telegram/lib/model.ts +2 -4
  145. package/node_modules/@llblab/pi-telegram/lib/status.ts +30 -1
  146. package/node_modules/@llblab/pi-telegram/lib/sync.ts +4 -4
  147. package/node_modules/@llblab/pi-telegram/lib/threads.ts +21 -3
  148. package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +46 -13
  149. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  150. package/node_modules/jsonc-parser/CHANGELOG.md +76 -0
  151. package/node_modules/jsonc-parser/LICENSE.md +21 -0
  152. package/node_modules/jsonc-parser/README.md +364 -0
  153. package/node_modules/jsonc-parser/SECURITY.md +41 -0
  154. package/node_modules/jsonc-parser/lib/esm/impl/edit.js +185 -0
  155. package/node_modules/jsonc-parser/lib/esm/impl/format.js +261 -0
  156. package/node_modules/jsonc-parser/lib/esm/impl/parser.js +659 -0
  157. package/node_modules/jsonc-parser/lib/esm/impl/scanner.js +443 -0
  158. package/node_modules/jsonc-parser/lib/esm/impl/string-intern.js +29 -0
  159. package/node_modules/jsonc-parser/lib/esm/main.d.ts +351 -0
  160. package/node_modules/jsonc-parser/lib/esm/main.js +178 -0
  161. package/node_modules/jsonc-parser/lib/umd/impl/edit.js +201 -0
  162. package/node_modules/jsonc-parser/lib/umd/impl/format.js +275 -0
  163. package/node_modules/jsonc-parser/lib/umd/impl/parser.js +682 -0
  164. package/node_modules/jsonc-parser/lib/umd/impl/scanner.js +456 -0
  165. package/node_modules/jsonc-parser/lib/umd/impl/string-intern.js +42 -0
  166. package/node_modules/jsonc-parser/lib/umd/main.d.ts +351 -0
  167. package/node_modules/jsonc-parser/lib/umd/main.js +194 -0
  168. package/node_modules/jsonc-parser/package.json +37 -0
  169. package/package.json +8 -8
@@ -1,26 +1,26 @@
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";
23
- import { discoverSnapshotData, findAssistantToolBatch, findPassiveStopBoundary, hasPriorConversation, hasUncheckpointedConversation, isNewSession, retainsPhysicalSessionProjection, SNAPSHOT_ENTRY_TYPE } from "./session.js";
23
+ import { discoverSnapshotData, findAssistantToolBatch, findBranchPolicy, findPassiveStopBoundary, hasPendingFork, hasPriorConversation, hasUncheckpointedConversation, isNewSession, retainsPhysicalSessionProjection, SNAPSHOT_ENTRY_TYPE } from "./session.js";
24
24
  import { hasCompiledSkillArtifact, hashSkillSource, registeredSkillResolver, SkillReadTracker } from "./skills.js";
25
25
  import { HistoryBoundaryExpiredError, emptySnapshot, migrationFailure, preRuntimeCheckpoint } from "./snapshot.js";
26
26
  import { emptyState, projectStateForModel } from "./state.js";
@@ -45,45 +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;
52
- const restorationOperations = new Set();
53
- let responseReconciliation;
54
- let sharedInspectionLifetime = new AbortController();
51
+ const branchRestoration = new OwnedOperationSlot();
52
+ let deferredBranch;
53
+ const responseReconciliation = new OwnedOperationSlot();
54
+ const sharedInspectionLifetime = new RenewableLifetime();
55
+ const memoryToolLifetime = new RenewableLifetime();
55
56
  let completedRunAccepted = false;
56
- let turnAcceptedForBackup = false;
57
- let compactionPlan;
58
- let compactionInFlight = false;
59
- let compactionStopped = false;
60
- const compactionMarker = `state-flow-boundary:${randomUUID()}`;
57
+ const compactionRequests = new StateFlowCompactionRequests();
61
58
  let passiveContinuation;
62
59
  let bootstrapContinuation;
63
60
  const contextProjection = new ContextProjection();
64
- let inferencePreparation;
61
+ const inferencePreparation = new OwnedOperationSlot();
65
62
  let runAnchorTimestamp;
66
63
  let runtime;
67
64
  let activeContext;
68
65
  let rehydrationPhase;
69
66
  const repositoryRoot = resolve(options.repositoryRoot ?? config.directory);
70
67
  const diagnosticWriter = new StateFlowDiagnosticWriter(config.logging, stateFlowLogPath(agentDir), repositoryRoot, (message) => notifyActiveContext(message));
71
- let backupPending = false;
72
- const backupLifetime = new AbortController();
73
- const backupOperations = new Set();
68
+ const backup = new SettledTurnBackup();
74
69
  let shuttingDown = false;
75
- let pushFailureNotified = false;
76
70
  const skillReads = new SkillReadTracker(hashSkillSource, (path) => activeContext
77
71
  ? registeredSkillResolver(activeContext.cwd, pi.getCommands())(path)
78
72
  : undefined);
79
- const artifactReads = new ArtifactReadTracker();
80
- let artifactInvalidations = [];
81
- let artifactHints = {};
82
- 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;
83
75
  let telegramStartPending = false;
84
76
  function sessionAddress(ctx) {
85
77
  return resolveSessionAddress(ctx.sessionManager.getSessionFile(), ctx.sessionManager.getSessionId(), ctx.sessionManager.getHeader()?.timestamp);
86
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
+ }
87
88
  function notifyProblem(ctx, message, level) {
88
89
  ctx.ui.notify(conciseDiagnostic(message), level);
89
90
  }
@@ -100,7 +101,7 @@ export default function stateFlowExtension(pi, options = {}) {
100
101
  return new TemporalRuntime(ctx.cwd, sessionAddress(ctx), repositoryRoot, undefined, config.historyLimit);
101
102
  }
102
103
  function projectModelState(state) {
103
- return projectStateForModel(state, artifactHints);
104
+ return projectStateForModel(state, acquisition.hints);
104
105
  }
105
106
  function assertSelectedBranchAvailable() {
106
107
  if (snapshot.meta.validation?.attempt === 0) {
@@ -144,105 +145,29 @@ export default function stateFlowExtension(pi, options = {}) {
144
145
  function updateUi(ctx) {
145
146
  ctx.ui.setStatus(STATUS_KEY, compactStatus(snapshot, scopeRevisions(), (color, text) => ctx.ui.theme.fg(color, text)));
146
147
  }
147
- function cancelInactivePersistence() {
148
- const pending = inactivePersistence;
149
- pending?.controller.abort();
150
- inactivePersistence = undefined;
151
- return pending?.operation;
152
- }
153
- function cancelStartActivation() {
154
- const pending = startActivation;
155
- pending?.controller.abort();
156
- startActivation = undefined;
157
- return pending?.operation;
158
- }
159
- function cancelBranchRestoration() {
160
- const pending = branchRestoration;
161
- pending?.controller.abort();
162
- branchRestoration = undefined;
163
- return pending?.operation;
164
- }
165
- function cancelResponseReconciliation() {
166
- responseReconciliation?.abort();
167
- responseReconciliation = undefined;
168
- }
169
148
  function cancelSharedInspections() {
170
- sharedInspectionLifetime.abort(new Error("State Flow inspection was cancelled; select the scope again"));
171
- sharedInspectionLifetime = new AbortController();
172
- }
173
- function cancelInferencePreparation() {
174
- inferencePreparation?.controller.abort();
175
- inferencePreparation = undefined;
149
+ sharedInspectionLifetime.renew(new Error("State Flow inspection was cancelled; select the scope again"));
176
150
  }
177
151
  function clearRunTransient() {
178
- cancelInferencePreparation();
179
- cancelResponseReconciliation();
152
+ inferencePreparation.cancel();
153
+ responseReconciliation.cancel();
180
154
  cancelSharedInspections();
181
155
  completedRunAccepted = false;
182
- turnAcceptedForBackup = false;
183
- compactionPlan = undefined;
184
- compactionInFlight = false;
156
+ backup.clearTurn();
157
+ compactionRequests.clear();
185
158
  skillReads.clear();
186
159
  artifactReads.clear();
187
160
  }
188
161
  function deferInferencePreparation(prompt) {
189
- cancelInferencePreparation();
190
- inferencePreparation = { controller: new AbortController(), prompt };
191
- artifactInvalidations = [];
192
- artifactReads.setCandidates([]);
162
+ inferencePreparation.cancel();
163
+ inferencePreparation.claim({ controller: new AbortController(), prompt });
164
+ acquisition.clearInvalidations();
193
165
  }
194
166
  function refreshArtifactHints() {
195
- if (!runtime?.view) {
196
- artifactHints = {};
197
- artifactInvalidations = [];
198
- artifactReads.setCandidates([]);
199
- return;
200
- }
201
- const paths = new Set();
202
- for (const scope of ["global", "cwd", "session"])
203
- for (const path of Object.keys(scopeStates[scope].artifacts))
204
- paths.add(path);
205
- const observations = new Map(inspectRegisteredArtifactPaths(paths).map((observation) => [observation.path, observation]));
206
- const nextHints = {};
207
- const nextInvalidations = new Map();
208
- for (const scope of ["global", "cwd", "session"]) {
209
- const provenance = runtime.artifactProvenance(scope);
210
- for (const [path, metadata] of Object.entries(scopeStates[scope].artifacts)) {
211
- delete nextHints[path];
212
- nextInvalidations.delete(path);
213
- if (metadata.kind === "skill")
214
- continue;
215
- const observed = observations.get(path);
216
- if (observed?.kind !== "present")
217
- continue;
218
- const need = classifyArtifactCompilationNeed({ path, scope, sourceFingerprint: observed.fingerprint }, metadata, ORDINARY_ARTIFACT_COMPILER, false, provenance[path]);
219
- if (need.kind !== "requires-compilation")
220
- continue;
221
- if (need.reason === "source-changed")
222
- nextHints[path] = SOURCE_CHANGED_HINT;
223
- nextInvalidations.set(path, { path, scope, reason: need.reason });
224
- }
225
- }
226
- artifactHints = nextHints;
227
- artifactInvalidations = [...nextInvalidations.values()].sort((left, right) => left.path.localeCompare(right.path));
228
- artifactReads.setCandidates(artifactInvalidations);
229
- }
230
- function missingArtifactRemovals(states) {
231
- const owners = new Map();
232
- for (const scope of ["global", "cwd", "session"])
233
- for (const path of Object.keys(states[scope].artifacts ?? {})) {
234
- owners.set(path, [...owners.get(path) ?? [], scope]);
235
- }
236
- const removals = {};
237
- for (const observation of inspectRegisteredArtifactPaths(owners.keys())) {
238
- if (observation.kind !== "missing")
239
- continue;
240
- for (const scope of owners.get(observation.path) ?? []) {
241
- const artifacts = (removals[scope]?.artifacts ?? {});
242
- removals[scope] = { ...(removals[scope] ?? {}), artifacts: { ...artifacts, [observation.path]: null } };
243
- }
244
- }
245
- return removals;
167
+ if (runtime?.view)
168
+ acquisition.refresh(scopeStates, (scope) => runtime.artifactProvenance(scope));
169
+ else
170
+ acquisition.reset();
246
171
  }
247
172
  function installScopeStates() {
248
173
  scopeStates = runtime?.view ? runtime.states() : { global: emptyState(), cwd: emptyState(), session: emptyState() };
@@ -259,22 +184,35 @@ export default function stateFlowExtension(pi, options = {}) {
259
184
  async function inspectTelegramState(scope) {
260
185
  if (!activeContext || shuttingDown)
261
186
  throw new Error("State Flow is not attached to an active session yet");
262
- if (scope === "session" || scope === "effective")
187
+ const ctx = activeContext;
188
+ const owner = sessionAddress(ctx).key;
189
+ const cwd = ctx.cwd;
190
+ const off = snapshot.config.mode === "off";
191
+ const privateScope = scope === "session" || scope === "effective";
192
+ if (!off && privateScope)
263
193
  assertSelectedBranchAvailable();
264
- const selected = runtime ??= createRuntime(activeContext);
194
+ // Off operator reads are disposable: they confer no selected-branch/cache authority.
195
+ const selected = off ? createRuntime(ctx) : runtime ??= createRuntime(ctx);
265
196
  const signal = sharedInspectionLifetime.signal;
266
- if (!selected.view || (scope !== "session" && !modePersistenceError))
197
+ let privateAvailable = true;
198
+ if (off && privateScope)
199
+ privateAvailable = await selected.refreshCurrentMemory(signal) !== undefined;
200
+ else if (!selected.view || (scope !== "session" && !modePersistenceError))
267
201
  await selected.refreshShared(signal);
268
202
  signal.throwIfAborted();
269
- if (runtime !== selected)
203
+ if (activeContext !== ctx || ctx.cwd !== cwd || sessionAddress(ctx).key !== owner || (!off && runtime !== selected)) {
270
204
  throw new Error("State Flow inspection selection changed; select the scope again");
271
- if (scope === "session" || scope === "effective")
205
+ }
206
+ if (!privateAvailable)
207
+ throw new Error("Current State Flow session memory is unavailable");
208
+ if (!off && privateScope)
272
209
  assertSelectedBranchAvailable();
273
210
  if (!selected.view)
274
211
  throw new Error("State Flow temporal runtime is unavailable");
275
- installScopeStates();
276
- const state = scope === "effective" ? effectiveState : selected.readView(0, scope);
277
- return { state: { ...projectModelState(state), ...(state.lazy === undefined ? {} : { lazy: structuredClone(state.lazy) }) }, revisions: scopeRevisions(), signal };
212
+ if (!off)
213
+ installScopeStates();
214
+ const state = selected.readView(0, scope === "effective" ? undefined : scope);
215
+ return { state: { ...projectModelState(state), ...(state.lazy === undefined ? {} : { lazy: structuredClone(state.lazy) }) }, revisions: temporalScopeRevisions(selected.view), signal };
278
216
  }
279
217
  function syncStateFlowTools() {
280
218
  const active = pi.getActiveTools();
@@ -286,15 +224,11 @@ export default function stateFlowExtension(pi, options = {}) {
286
224
  ? [...new Set([...active, ...owned])]
287
225
  : active.filter((name) => !owned.includes(name)));
288
226
  }
289
- function recordPublication(_publication, _ctx) {
227
+ /** Accepted canonical publication establishes runtime authority and makes the settled-turn Git backup due. */
228
+ function recordPublication() {
290
229
  // A passive view is not runtime authority; only accepted canonical publication establishes it.
291
230
  branchStartsWithoutRuntime = false;
292
- backupPending = true;
293
- }
294
- /** Record accepted lifecycle persistence; Git backup is scheduled only after an accepted turn settles. */
295
- function recordPolicyPublication(publication, ctx) {
296
- if (publication)
297
- recordPublication(publication, ctx);
231
+ backup.markPublished();
298
232
  }
299
233
  function skillReadIsCurrent(read) {
300
234
  return read.hash !== undefined && runtime?.view !== undefined && hasCompiledSkillArtifact(scopeStates[read.scope].artifacts, runtime.artifactProvenance(read.scope)[read.path], read.path, read.hash);
@@ -311,11 +245,9 @@ export default function stateFlowExtension(pi, options = {}) {
311
245
  const target = `${read.scope}.artifacts[${JSON.stringify(read.path)}]`;
312
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.`;
313
247
  }
314
- function clearAcceptedAcquisitions(acquiredArtifactPaths = new Set(artifactReads.successful.keys())) {
315
- artifactInvalidations = artifactInvalidations.filter(({ path }) => !acquiredArtifactPaths.has(path));
316
- artifactReads.setCandidates(artifactInvalidations);
248
+ function clearAcceptedAcquisitions(acquiredArtifactPaths) {
249
+ acquisition.acceptAcquired(acquiredArtifactPaths);
317
250
  dropCurrentSkillReads();
318
- artifactReads.clear();
319
251
  }
320
252
  async function prepareInference(pending, selected, ctx, operationSignal) {
321
253
  const signal = AbortSignal.any([pending.controller.signal, operationSignal]);
@@ -324,7 +256,7 @@ export default function stateFlowExtension(pi, options = {}) {
324
256
  throw new Error("Temporal State Flow runtime is unavailable; reload before publishing");
325
257
  await selected.withPatchTransaction((transaction) => {
326
258
  signal.throwIfAborted();
327
- if (runtime !== selected || inferencePreparation !== pending || !isActive() || shuttingDown) {
259
+ if (runtime !== selected || !inferencePreparation.owns(pending) || !isActive() || shuttingDown) {
328
260
  throw new Error("State Flow inference selection changed while awaiting publication");
329
261
  }
330
262
  assertPublicationAvailable();
@@ -351,7 +283,7 @@ export default function stateFlowExtension(pi, options = {}) {
351
283
  if (rotatesRun && rehydrationPhase !== "new-bootstrap" && rehydrationPhase !== "resume-bootstrap")
352
284
  rehydrationPhase = "step";
353
285
  if (publication?.changed)
354
- recordPublication(publication, ctx);
286
+ recordPublication();
355
287
  if (pending.prompt !== undefined || publication?.changed)
356
288
  appendCheckpoint();
357
289
  if (Object.keys(removals).length > 0)
@@ -360,7 +292,7 @@ export default function stateFlowExtension(pi, options = {}) {
360
292
  }, signal);
361
293
  }
362
294
  catch (error) {
363
- if (signal.aborted || runtime !== selected || inferencePreparation !== pending || shuttingDown)
295
+ if (signal.aborted || runtime !== selected || !inferencePreparation.owns(pending) || shuttingDown)
364
296
  return;
365
297
  // Pi reports context-hook exceptions and continues. Abort through its public port rather than infer from stale preparation.
366
298
  ctx.abort();
@@ -371,25 +303,21 @@ export default function stateFlowExtension(pi, options = {}) {
371
303
  : `State Flow inference preparation failed: ${cause}`, "error");
372
304
  }
373
305
  }
374
- function currentRehydrationPhase() {
375
- return rehydrationPhase;
376
- }
377
306
  function statusDiagnostics(ctx) {
378
307
  const cwd = ctx.cwd;
379
- const diagnosticStates = scopeStates;
380
308
  const view = runtime?.view;
381
309
  const diagnosticRecent = runtime?.recent() ?? [];
382
310
  const durableStateError = snapshot.meta.validation?.attempt === 0 ? snapshot.meta.validation.error
383
311
  : view ? undefined : "no temporal runtime is selected on this branch";
384
312
  const staleArtifacts = durableStateError === undefined
385
- ? artifactInvalidations.map(({ path, scope, reason }) => ({ scope: scope ?? "global", path, reason }))
313
+ ? acquisition.invalidations.map(({ path, scope, reason }) => ({ scope: scope ?? "global", path, reason }))
386
314
  : [];
387
315
  const session = sessionAddress(ctx);
388
316
  return {
389
317
  repositoryRoot,
390
318
  cwdScopeKey: cwdScopeKey(cwd),
391
319
  sessionScopeKey: sessionScopeKey(session.key),
392
- scopeStates: diagnosticStates,
320
+ scopeStates,
393
321
  effectiveState,
394
322
  recent: diagnosticRecent,
395
323
  historyLimit: config.historyLimit,
@@ -414,21 +342,69 @@ export default function stateFlowExtension(pi, options = {}) {
414
342
  return resolveSessionAddress(parent.file, parent.id, parent.timestamp);
415
343
  }
416
344
  /** Select the active native branch under one owned restoration lifetime; only current accepted work installs memory. */
417
- function restoreActiveBranch(ctx, sessionStartReason, notifyRecovery = true, startOwner) {
345
+ function restoreActiveBranch(ctx, sessionStartReason, notifyRecovery = true, startOwner, requestedMode) {
418
346
  contextProjection.reset();
419
- cancelBranchRestoration();
347
+ memoryToolLifetime.renew();
348
+ branchRestoration.cancel();
420
349
  // Start-owned attachment/fork recovery keeps its owner; only accepted Start cancels Stop.
421
350
  if (!startOwner) {
422
- cancelStartActivation();
423
- cancelInactivePersistence();
351
+ startActivation.cancel();
352
+ inactivePersistence.cancel();
424
353
  }
425
354
  clearRunTransient();
426
355
  passiveContinuation = undefined;
427
356
  bootstrapContinuation = undefined;
428
- artifactInvalidations = [];
429
- artifactReads.setCandidates([]);
357
+ acquisition.clearInvalidations();
430
358
  telegramStartPending = false;
431
359
  activeContext = ctx;
360
+ if (!startOwner) {
361
+ let mode = config.inactiveMode;
362
+ let adoptDefault = false;
363
+ let withoutRuntime = false;
364
+ let persistenceError;
365
+ let pendingFork = false;
366
+ try {
367
+ const branch = ctx.sessionManager.getBranch();
368
+ const discovery = discoverSnapshotData(branch);
369
+ withoutRuntime = discovery.candidates.length === 0 && discovery.errors.length === 0;
370
+ adoptDefault = withoutRuntime && isNewSession(sessionStartReason, branch);
371
+ pendingFork = hasPendingFork(branch, ctx.sessionManager.getSessionId(), PASSIVE_STOP_ENTRY_TYPE);
372
+ const inherited = sessionStartReason === "fork" || pendingFork;
373
+ const policy = findBranchPolicy(branch, inherited ? undefined : ctx.sessionManager.getSessionId(), inherited || retainsPhysicalSessionProjection(sessionStartReason) ? PASSIVE_STOP_ENTRY_TYPE : undefined, config.inactiveMode);
374
+ mode = policy?.mode ?? (adoptDefault ? config.mode : config.inactiveMode);
375
+ persistenceError = inherited
376
+ ? findBranchPolicy(branch, ctx.sessionManager.getSessionId(), PASSIVE_STOP_ENTRY_TYPE, config.inactiveMode)?.persistenceError
377
+ : policy?.persistenceError;
378
+ }
379
+ catch {
380
+ // Off needs only policy; unavailable native evidence grants no memory authority.
381
+ }
382
+ if (mode === "off") {
383
+ backup.cancel();
384
+ const owner = sessionAddress(ctx).key;
385
+ const reason = pendingFork || (deferredBranch?.owner === owner && deferredBranch.cwd === ctx.cwd && deferredBranch.reason === "fork"
386
+ && retainsPhysicalSessionProjection(sessionStartReason)) ? "fork" : sessionStartReason;
387
+ if (reason === "fork" && !pendingFork)
388
+ pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, { owner: ctx.sessionManager.getSessionId(), forkPending: true });
389
+ deferredBranch = { reason, owner, cwd: ctx.cwd };
390
+ runtime = undefined;
391
+ snapshot = emptySnapshot("off");
392
+ branchStartsWithoutRuntime = withoutRuntime;
393
+ selectedHistoryExpired = false;
394
+ modePersistenceError = persistenceError;
395
+ forkInitialization = reason === "fork";
396
+ installScopeStates();
397
+ acquisition.reset();
398
+ if (adoptDefault)
399
+ appendCheckpoint();
400
+ syncStateFlowTools();
401
+ updateUi(ctx);
402
+ return Promise.resolve();
403
+ }
404
+ if (pendingFork)
405
+ sessionStartReason = "fork";
406
+ }
407
+ deferredBranch = undefined;
432
408
  runtime = createRuntime(ctx);
433
409
  installScopeStates();
434
410
  branchStartsWithoutRuntime = false;
@@ -499,29 +475,25 @@ export default function stateFlowExtension(pi, options = {}) {
499
475
  snapshot = migrationFailure({}, "State Flow branch restoration is pending", selection.kind === "current" ? selection.mode
500
476
  : selection.kind === "auto-start" ? config.inactiveMode : inactiveSelection(selection.checkpoint.mode));
501
477
  }
478
+ if (requestedMode)
479
+ snapshot = deactivateEpisode(snapshot, requestedMode);
502
480
  const pending = {
503
481
  controller: new AbortController(),
504
482
  awaitingAcceptance: selection.kind !== "settled" && selection.kind !== "current",
483
+ ...(requestedMode === undefined ? {} : { requestedMode }),
505
484
  };
506
- branchRestoration = pending;
507
- const operation = attachBranch(ctx, pending, selection, sessionStartReason, notifyRecovery, skipped);
508
- pending.operation = operation;
509
- restorationOperations.add(operation);
510
- const finished = () => { restorationOperations.delete(operation); };
511
- void operation.then(finished, finished);
512
- 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);
513
488
  }
514
489
  async function attachBranch(ctx, pending, selection, reason, notifyRecovery, skipped) {
515
490
  const placeholder = runtime;
516
491
  let selected = runtime;
517
492
  const owner = ctx.sessionManager.getSessionId();
518
- const cwd = ctx.cwd;
519
- const file = ctx.sessionManager.getSessionFile();
520
- const timestamp = ctx.sessionManager.getHeader()?.timestamp;
493
+ const sameSession = sessionIdentity(ctx);
521
494
  const signal = AbortSignal.any([pending.controller.signal, ...(ctx.signal ? [ctx.signal] : [])]);
522
- const isCurrent = () => branchRestoration === pending && !pending.controller.signal.aborted && !shuttingDown && runtime === selected
523
- && ctx.cwd === cwd && ctx.sessionManager.getSessionId() === owner && ctx.sessionManager.getSessionFile() === file
524
- && ctx.sessionManager.getHeader()?.timestamp === timestamp;
495
+ const isCurrent = () => branchRestoration.owns(pending) && !pending.controller.signal.aborted && !shuttingDown && runtime === selected
496
+ && sameSession();
525
497
  const assertCurrent = () => {
526
498
  signal.throwIfAborted();
527
499
  if (!isCurrent())
@@ -529,13 +501,13 @@ export default function stateFlowExtension(pi, options = {}) {
529
501
  };
530
502
  let accepted = false;
531
503
  // Install accepted memory before native writes; later ancillary failures cannot revert or replay it.
532
- const accept = (candidate, next, publication, nativeWrites) => {
504
+ const accept = (candidate, next, nativeWrites) => {
533
505
  accepted = true;
534
506
  pending.awaitingAcceptance = false;
535
507
  runtime = selected = candidate;
536
508
  snapshot = next;
537
509
  installScopeStates();
538
- recordPolicyPublication(publication, ctx);
510
+ recordPublication();
539
511
  let failure;
540
512
  try {
541
513
  nativeWrites();
@@ -571,7 +543,8 @@ export default function stateFlowExtension(pi, options = {}) {
571
543
  && retainsPhysicalSessionProjection(reason) && hasUncheckpointedConversation(ctx.sessionManager.getBranch()))
572
544
  restored.meta.bootstrap = true;
573
545
  const next = pending.requestedMode ? deactivateEpisode(restored, pending.requestedMode) : restored;
574
- accept(candidate, next, publish(next), appendCheckpoint);
546
+ publish(next);
547
+ accept(candidate, next, appendCheckpoint);
575
548
  }, signal);
576
549
  }
577
550
  else if (selection.kind === "fork") {
@@ -579,9 +552,9 @@ export default function stateFlowExtension(pi, options = {}) {
579
552
  assertCurrent();
580
553
  // Mode is selected independently of creating the child's private memory.
581
554
  const next = pending.requestedMode ? deactivateEpisode(child, pending.requestedMode) : child;
582
- const publication = publish(next);
555
+ publish(next);
583
556
  forkInitialization = false;
584
- accept(candidate, next, publication, () => {
557
+ accept(candidate, next, () => {
585
558
  pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, { reset: true, owner });
586
559
  appendCheckpoint();
587
560
  });
@@ -597,7 +570,8 @@ export default function stateFlowExtension(pi, options = {}) {
597
570
  throw new Error("Existing session runtime requires retained-boundary restoration");
598
571
  const activated = startEpisode(hasPriorConversation(branch));
599
572
  const next = pending.requestedMode ? deactivateEpisode(activated, pending.requestedMode) : activated;
600
- accept(candidate, next, publish(next), appendCheckpoint);
573
+ publish(next);
574
+ accept(candidate, next, appendCheckpoint);
601
575
  }, signal, true);
602
576
  }
603
577
  }
@@ -643,8 +617,7 @@ export default function stateFlowExtension(pi, options = {}) {
643
617
  }
644
618
  finally {
645
619
  pending.awaitingAcceptance = false;
646
- if (branchRestoration === pending)
647
- branchRestoration = undefined;
620
+ branchRestoration.release(pending);
648
621
  }
649
622
  }
650
623
  /** Load current shared memory for passive projection without initializing or publishing it. */
@@ -660,6 +633,12 @@ export default function stateFlowExtension(pi, options = {}) {
660
633
  }
661
634
  return passive;
662
635
  }
636
+ function selectedContinuation(ctx, reason) {
637
+ if (!retainsPhysicalSessionProjection(reason) || !runtime?.view)
638
+ return undefined;
639
+ const boundary = findPassiveStopBoundary(ctx.sessionManager.getBranch(), ctx.sessionManager.getSessionId(), PASSIVE_STOP_ENTRY_TYPE);
640
+ return boundary === undefined ? undefined : createPassiveContinuation(projectModelState(effectiveState), boundary.at, boundary.from, boundary.preserveContext);
641
+ }
663
642
  function settleSelection(ctx, reason, notifyRecovery, skipped, selectedMemory) {
664
643
  const failure = !isActive() && snapshot.meta.validation?.attempt === 0 ? snapshot.meta.validation.error : undefined;
665
644
  if (failure !== undefined && notifyRecovery && !modePersistenceError) {
@@ -668,16 +647,12 @@ export default function stateFlowExtension(pi, options = {}) {
668
647
  else
669
648
  notifyProblem(ctx, `State Flow restore failed: ${failure}`, "error");
670
649
  }
671
- if (selectedMemory && retainsPhysicalSessionProjection(reason) && runtime?.view) {
672
- const boundary = findPassiveStopBoundary(ctx.sessionManager.getBranch(), ctx.sessionManager.getSessionId(), PASSIVE_STOP_ENTRY_TYPE);
673
- if (boundary !== undefined) {
674
- const continuation = createPassiveContinuation(projectModelState(effectiveState), boundary.at, boundary.from, boundary.preserveContext);
675
- // Off retains the handoff boundary for a later Passive or Active choice but never projects it.
676
- if (!isActive())
677
- passiveContinuation = continuation;
678
- else if (snapshot.meta.bootstrap)
679
- bootstrapContinuation = continuation;
680
- }
650
+ const continuation = selectedMemory ? selectedContinuation(ctx, reason) : undefined;
651
+ if (continuation !== undefined) {
652
+ if (!isActive())
653
+ passiveContinuation = continuation;
654
+ else if (snapshot.meta.bootstrap)
655
+ bootstrapContinuation = continuation;
681
656
  }
682
657
  if (notifyRecovery && isActive() && skipped > 0) {
683
658
  notifyProblem(ctx, `State Flow skipped ${skipped} malformed snapshot(s); restored the last valid one.`, "warning");
@@ -693,7 +668,7 @@ export default function stateFlowExtension(pi, options = {}) {
693
668
  pi.registerTool({
694
669
  name: READ_STATE_TOOL_NAME,
695
670
  label: "Read State",
696
- 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.",
697
672
  promptSnippet: "Read exact state values, structures, or historical semantic patches",
698
673
  parameters: Type.Object({
699
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" })),
@@ -742,7 +717,7 @@ export default function stateFlowExtension(pi, options = {}) {
742
717
  pi.registerTool({
743
718
  name: PATCH_STATE_TOOL_NAME,
744
719
  label: "Patch State",
745
- 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.",
746
721
  promptSnippet: "Atomically patch one or more global/cwd/session scopes",
747
722
  promptGuidelines: [
748
723
  "Use patch_state only for material durable semantic changes; ordinary answers need no finalization call.",
@@ -764,6 +739,7 @@ export default function stateFlowExtension(pi, options = {}) {
764
739
  return new Text(separatedOutput(theme.fg("dim", formatPatchStateArguments(context.args))), 0, 0);
765
740
  },
766
741
  async execute(toolCallId, params, signal, _onUpdate, ctx) {
742
+ signal = signal ? AbortSignal.any([signal, memoryToolLifetime.signal]) : memoryToolLifetime.signal;
767
743
  try {
768
744
  if (!memoryToolsAvailable())
769
745
  throw new Error("State Flow tools are off for this session");
@@ -820,12 +796,12 @@ export default function stateFlowExtension(pi, options = {}) {
820
796
  snapshot = nextSnapshot;
821
797
  installScopeStates();
822
798
  if (publication?.changed) {
823
- recordPublication(publication, ctx);
799
+ recordPublication();
824
800
  appendCheckpoint();
825
801
  }
826
802
  clearAcceptedAcquisitions(new Set(acquiredArtifacts.map(({ path }) => path)));
827
803
  updateUi(ctx);
828
- const updates = contextProjection.acceptPatch(previousEffective, effectiveState, patches, artifactHints);
804
+ const updates = contextProjection.acceptPatch(previousEffective, effectiveState, patches, acquisition.hints);
829
805
  const acknowledgement = changed
830
806
  ? `\nState materialized atomically at ${scopes.join("+")} scope${scopes.length === 1 ? "" : "s"}.`
831
807
  : "\nState already current.";
@@ -844,11 +820,12 @@ export default function stateFlowExtension(pi, options = {}) {
844
820
  attempted = undefined;
845
821
  }
846
822
  const cause = diagnosticText(error);
847
- recordDiagnostic(cause, /concurrently|advanced/.test(cause) ? "publication-conflict" : "invalid-patch", ctx, {
848
- input: attempted,
849
- tool: PATCH_STATE_TOOL_NAME,
850
- toolCallId,
851
- });
823
+ if (!signal.aborted && memoryToolsAvailable())
824
+ recordDiagnostic(cause, /concurrently|advanced/.test(cause) ? "publication-conflict" : "invalid-patch", ctx, {
825
+ input: attempted,
826
+ tool: PATCH_STATE_TOOL_NAME,
827
+ toolCallId,
828
+ });
852
829
  throw separatedFailure(error);
853
830
  }
854
831
  },
@@ -856,47 +833,38 @@ export default function stateFlowExtension(pi, options = {}) {
856
833
  function startStateFlow(ctx) {
857
834
  if (shuttingDown)
858
835
  return Promise.resolve({ ok: false, message: "State Flow is shutting down" });
859
- if (startActivation?.operation)
860
- return startActivation.operation;
836
+ if (startActivation.current?.operation)
837
+ return startActivation.current.operation;
861
838
  telegramStartPending = false;
862
- if (activeContext && !branchRestoration && isActive()) {
839
+ if (activeContext && !branchRestoration.current && isActive()) {
863
840
  return Promise.resolve({ ok: true, message: "State Flow is already active", signal: sharedInspectionLifetime.signal });
864
841
  }
865
- const pending = { controller: new AbortController() };
866
- startActivation = pending;
867
- const operation = runStart(ctx, pending.controller);
868
- pending.operation = operation;
869
- const finished = () => { if (startActivation === pending)
870
- startActivation = undefined; };
871
- void operation.then(finished, finished);
872
- return operation;
842
+ const pending = startActivation.claim({ controller: new AbortController() });
843
+ return startActivation.track(pending, runStart(ctx, pending.controller));
873
844
  }
874
845
  /** Await restoration-owned attachment and exact-source fork recovery before current-head activation. */
875
846
  async function runStart(ctx, pending) {
876
- const owner = ctx.sessionManager.getSessionId();
877
- const cwd = ctx.cwd;
878
- const file = ctx.sessionManager.getSessionFile();
879
- const timestamp = ctx.sessionManager.getHeader()?.timestamp;
847
+ const sameSession = sessionIdentity(ctx);
880
848
  const signal = ctx.signal ? AbortSignal.any([pending.signal, ctx.signal]) : pending.signal;
881
- const isOwner = () => startActivation?.controller === pending && !signal.aborted && !shuttingDown
882
- && ctx.cwd === cwd && ctx.sessionManager.getSessionId() === owner && ctx.sessionManager.getSessionFile() === file
883
- && ctx.sessionManager.getHeader()?.timestamp === timestamp;
849
+ const isOwner = () => startActivation.current?.controller === pending && !signal.aborted && !shuttingDown && sameSession();
884
850
  const superseded = () => {
885
851
  pending.abort();
886
852
  return { ok: false, message: "State Flow activation was superseded", signal: pending.signal };
887
853
  };
888
854
  try {
889
- if (!activeContext)
855
+ if (deferredBranch?.reason === "fork")
856
+ await waitForRecovery(restoreActiveBranch(ctx, "fork", false, pending), signal);
857
+ else if (!activeContext)
890
858
  await waitForRecovery(restoreActiveBranch(ctx, undefined, false, pending), signal);
891
- else if (branchRestoration?.operation)
892
- await waitForRecovery(branchRestoration.operation, signal);
859
+ else if (branchRestoration.current?.operation)
860
+ await waitForRecovery(branchRestoration.current.operation, signal);
893
861
  if (!isOwner())
894
862
  return superseded();
895
863
  if (isActive())
896
864
  return { ok: true, message: "State Flow is already active", signal: sharedInspectionLifetime.signal };
897
865
  if (forkInitialization) {
898
866
  // Withdraw this join on cancellation without revoking the independently owned mode persistence.
899
- const stopping = inactivePersistence?.operation;
867
+ const stopping = inactivePersistence.current?.operation;
900
868
  if (stopping)
901
869
  await waitForRecovery(stopping, signal);
902
870
  if (!isOwner())
@@ -919,50 +887,61 @@ export default function stateFlowExtension(pi, options = {}) {
919
887
  async function activateCurrentState(ctx, pending) {
920
888
  let selected = runtime;
921
889
  const owner = ctx.sessionManager.getSessionId();
922
- const cwd = ctx.cwd;
923
- const file = ctx.sessionManager.getSessionFile();
924
- const timestamp = ctx.sessionManager.getHeader()?.timestamp;
890
+ const sameSession = sessionIdentity(ctx);
925
891
  const initiallyActive = isActive();
926
892
  let accepted = false;
927
893
  let receipt = AbortSignal.any([pending.signal, sharedInspectionLifetime.signal]);
928
- const isCurrent = () => startActivation?.controller === pending && runtime === selected && !shuttingDown
929
- && ctx.cwd === cwd && ctx.sessionManager.getSessionId() === owner && ctx.sessionManager.getSessionFile() === file
930
- && ctx.sessionManager.getHeader()?.timestamp === timestamp && isActive() === (accepted || initiallyActive);
894
+ const signal = ctx.signal ? AbortSignal.any([pending.signal, ctx.signal]) : pending.signal;
895
+ const isCurrent = () => startActivation.current?.controller === pending && !signal.aborted && runtime === selected && !shuttingDown
896
+ && sameSession() && isActive() === (accepted || initiallyActive);
931
897
  const superseded = () => {
932
898
  pending.abort();
933
899
  return { ok: false, message: "State Flow activation was superseded", signal: pending.signal };
934
900
  };
935
901
  try {
936
902
  const activation = createRuntime(ctx);
937
- const signal = ctx.signal ? AbortSignal.any([pending.signal, ctx.signal]) : pending.signal;
903
+ const deferred = deferredBranch;
904
+ if (deferred) {
905
+ const discovery = discoverSnapshotData(ctx.sessionManager.getBranch());
906
+ const retained = selectRetainedCheckpoint(discovery.candidates, config.inactiveMode);
907
+ branchStartsWithoutRuntime = retained.kind === "pre-runtime" || (discovery.candidates.length === 0 && discovery.errors.length === 0);
908
+ }
909
+ // Off activation owns one cancellable current-head publication, not an independent read-only restore.
938
910
  const result = await activation.withStartTransaction((current, publish) => {
939
911
  signal.throwIfAborted();
940
912
  if (!isCurrent())
941
913
  throw new Error("State Flow activation selection changed while awaiting publication");
942
914
  if (!current && !branchStartsWithoutRuntime)
943
915
  throw new Error(snapshot.meta.validation?.error ?? "Current State Flow session memory is unavailable");
944
- const continuation = passiveContinuation ?? bootstrapContinuation;
945
- const bootstrap = hasPriorConversation(ctx.sessionManager.getBranch()) || passiveContinuation !== undefined;
916
+ const boundary = deferred && retainsPhysicalSessionProjection(deferred.reason)
917
+ ? findPassiveStopBoundary(ctx.sessionManager.getBranch(), owner, PASSIVE_STOP_ENTRY_TYPE) : undefined;
918
+ const bootstrap = hasPriorConversation(ctx.sessionManager.getBranch()) || passiveContinuation !== undefined || boundary !== undefined;
946
919
  const activated = current ? resumeEpisode(current, bootstrap) : startEpisode(bootstrap);
947
920
  const recoveredCurrent = selectedHistoryExpired;
948
- const publication = publish(activated);
921
+ publish(activated);
949
922
  accepted = true;
950
- cancelInactivePersistence();
923
+ inactivePersistence.cancel();
951
924
  runtime = selected = activation;
952
925
  snapshot = activated;
953
926
  activeContext = ctx;
954
927
  selectedHistoryExpired = false;
955
928
  modePersistenceError = undefined;
956
929
  installScopeStates();
930
+ const continuation = passiveContinuation ?? bootstrapContinuation ?? (deferred ? selectedContinuation(ctx, deferred.reason) : undefined);
931
+ deferredBranch = undefined;
957
932
  clearRunTransient();
958
933
  contextProjection.reset();
959
934
  passiveContinuation = undefined;
960
935
  bootstrapContinuation = snapshot.meta.bootstrap ? continuation : undefined;
961
936
  deferInferencePreparation();
962
937
  receipt = AbortSignal.any([pending.signal, sharedInspectionLifetime.signal]);
963
- recordPolicyPublication(publication, ctx);
938
+ recordPublication();
964
939
  syncStateFlowTools();
965
940
  updateUi(ctx);
941
+ if (forkInitialization) {
942
+ forkInitialization = false;
943
+ pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, { reset: true, owner });
944
+ }
966
945
  appendCheckpoint();
967
946
  ctx.ui.notify(recoveredCurrent
968
947
  ? "State Flow active from current session memory; unavailable historical state was not restored."
@@ -1012,11 +991,13 @@ export default function stateFlowExtension(pi, options = {}) {
1012
991
  updateUi(ctx);
1013
992
  }
1014
993
  function deactivateStateFlow(ctx, mode) {
1015
- cancelStartActivation();
1016
994
  if (shuttingDown)
1017
995
  return Promise.resolve({ ok: false, message: "State Flow is shutting down" });
996
+ if (mode === "off")
997
+ return selectOff(ctx);
998
+ startActivation.cancel();
1018
999
  telegramStartPending = false;
1019
- const persisting = inactivePersistence;
1000
+ const persisting = inactivePersistence.current;
1020
1001
  if (persisting?.operation && !persisting.published) {
1021
1002
  // Pending inactive choices coalesce: acceptance persists whichever inactive mode is current then.
1022
1003
  if (snapshot.config.mode !== mode)
@@ -1025,12 +1006,12 @@ export default function stateFlowExtension(pi, options = {}) {
1025
1006
  }
1026
1007
  if (!activeContext)
1027
1008
  void restoreActiveBranch(ctx, undefined, false);
1028
- const restoring = branchRestoration;
1009
+ const restoring = branchRestoration.current;
1029
1010
  // Read-only recovery also retains the latest policy; its native write fence is updated below.
1030
1011
  if (restoring)
1031
1012
  restoring.requestedMode = mode;
1032
1013
  if (restoring?.awaitingAcceptance) {
1033
- // A mode change never cancels memory restoration or initialization.
1014
+ // Passive keeps independently owned restoration; Off withdraws it through selectOff.
1034
1015
  applyInactiveMode(ctx, mode);
1035
1016
  return Promise.resolve({ ok: true, message: `State Flow ${mode}; memory restoration continues` });
1036
1017
  }
@@ -1040,15 +1021,92 @@ export default function stateFlowExtension(pi, options = {}) {
1040
1021
  if (modePersistenceError || recordedMode === mode)
1041
1022
  return Promise.resolve({ ok: true, message: `State Flow is already ${mode}` });
1042
1023
  }
1043
- cancelInactivePersistence();
1044
- const pending = { controller: new AbortController() };
1045
- inactivePersistence = pending;
1046
- const operation = persistInactiveMode(ctx, pending, mode);
1047
- pending.operation = operation;
1048
- const finished = () => { if (inactivePersistence === pending)
1049
- inactivePersistence = undefined; };
1050
- void operation.then(finished, finished);
1051
- return operation;
1024
+ inactivePersistence.cancel();
1025
+ const pending = inactivePersistence.claim({ controller: new AbortController() });
1026
+ return inactivePersistence.track(pending, deferredBranch && mode === "passive" && !branchStartsWithoutRuntime
1027
+ ? acquirePassiveMode(ctx, pending, deferredBranch.reason)
1028
+ : persistInactiveMode(ctx, pending, mode));
1029
+ }
1030
+ /** Off owns cancellation and native policy only; no canonical transaction or semantic handoff. */
1031
+ function selectOff(ctx) {
1032
+ const address = sessionAddress(ctx);
1033
+ const owner = ctx.sessionManager.getSessionId();
1034
+ const branch = ctx.sessionManager.getBranch();
1035
+ const selected = runtime;
1036
+ const sameOwner = selected?.sessionKey === address.key && selected.cwd === ctx.cwd;
1037
+ const forkRecorded = hasPendingFork(branch, owner, PASSIVE_STOP_ENTRY_TYPE);
1038
+ const pendingFork = forkRecorded || (sameOwner && forkInitialization) || (deferredBranch?.owner === address.key && deferredBranch.cwd === ctx.cwd && deferredBranch.reason === "fork");
1039
+ const policy = findBranchPolicy(branch, owner, PASSIVE_STOP_ENTRY_TYPE, config.inactiveMode);
1040
+ const sameDeferred = deferredBranch?.owner === address.key && deferredBranch.cwd === ctx.cwd;
1041
+ const current = sameOwner || sameDeferred ? snapshot : emptySnapshot("off");
1042
+ if (!sameOwner && !sameDeferred)
1043
+ modePersistenceError = policy?.persistenceError;
1044
+ const alreadyOff = current.config.mode === "off" && policy?.mode === "off";
1045
+ const incoming = sameOwner || sameDeferred ? current.meta.bootstrap ? bootstrapContinuation : passiveContinuation : undefined;
1046
+ const unfinished = current.meta.specification !== undefined || (inferencePreparation.current?.prompt !== undefined && !inferencePreparation.current.accepted)
1047
+ || (isActive() && hasUncheckpointedConversation(branch));
1048
+ const at = incoming?.startedAt ?? Date.now();
1049
+ const from = incoming ? incoming.activeRunStartedAt : (sameOwner || sameDeferred) && (!ctx.isIdle() || unfinished) ? runAnchorTimestamp : undefined;
1050
+ const preserveContext = modePersistenceError !== undefined || (incoming ? incoming.preserveContext : current.meta.bootstrap === true || (unfinished && from === undefined));
1051
+ const discovery = discoverSnapshotData(branch);
1052
+ const preRuntime = (sameOwner && branchStartsWithoutRuntime) || (discovery.candidates.length === 0 && discovery.errors.length === 0);
1053
+ let checkpoint;
1054
+ try {
1055
+ checkpoint = !modePersistenceError && current.meta.validation?.attempt !== 0
1056
+ ? preRuntime ? preRuntimeCheckpoint("off") : sameOwner && selected?.view ? selected.retainedCheckpoint(deactivateEpisode(current, "off")) : undefined
1057
+ : undefined;
1058
+ }
1059
+ catch {
1060
+ // An unusable cached bookmark cannot prevent native Off policy or authorize replacement memory.
1061
+ }
1062
+ startActivation.cancel();
1063
+ branchRestoration.cancel();
1064
+ inactivePersistence.cancel();
1065
+ backup.cancel();
1066
+ memoryToolLifetime.renew();
1067
+ clearRunTransient();
1068
+ contextProjection.reset();
1069
+ telegramStartPending = false;
1070
+ bootstrapContinuation = undefined;
1071
+ passiveContinuation = undefined;
1072
+ acquisition.reset();
1073
+ runtime = undefined;
1074
+ snapshot = emptySnapshot("off");
1075
+ selectedHistoryExpired = false;
1076
+ branchStartsWithoutRuntime = preRuntime;
1077
+ forkInitialization = pendingFork;
1078
+ deferredBranch = { owner: address.key, cwd: ctx.cwd, reason: pendingFork ? "fork" : undefined };
1079
+ activeContext = ctx;
1080
+ installScopeStates();
1081
+ syncStateFlowTools();
1082
+ updateUi(ctx);
1083
+ try {
1084
+ if (!alreadyOff || (pendingFork && !forkRecorded)) {
1085
+ if (!checkpoint || !preRuntime || pendingFork || preserveContext || from !== undefined || modePersistenceError)
1086
+ pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, {
1087
+ owner, at, mode: "off", memoryDeferred: true,
1088
+ ...(from === undefined ? {} : { from }),
1089
+ ...(preserveContext ? { preserveContext: true } : {}),
1090
+ ...(pendingFork ? { forkPending: true } : {}),
1091
+ ...(modePersistenceError === undefined ? {} : { persistenceError: modePersistenceError }),
1092
+ });
1093
+ if (checkpoint)
1094
+ pi.appendEntry(SNAPSHOT_ENTRY_TYPE, checkpoint);
1095
+ }
1096
+ return Promise.resolve({ ok: true, message: alreadyOff ? "State Flow is already off" : "State Flow off" });
1097
+ }
1098
+ catch (error) {
1099
+ const message = conciseDiagnostic(`State Flow off; native mode recording failed: ${diagnosticText(error)}`);
1100
+ notifyProblem(ctx, message, "error");
1101
+ return Promise.resolve({ ok: false, message });
1102
+ }
1103
+ }
1104
+ async function acquirePassiveMode(ctx, pending, reason) {
1105
+ await restoreActiveBranch(ctx, reason, true, pending.controller, "passive");
1106
+ if (!inactivePersistence.owns(pending) || pending.controller.signal.aborted || shuttingDown) {
1107
+ return { ok: false, message: "State Flow mode change was superseded" };
1108
+ }
1109
+ return persistInactiveMode(ctx, pending, "passive");
1052
1110
  }
1053
1111
  async function persistInactiveMode(ctx, pending, mode) {
1054
1112
  let selected = runtime;
@@ -1056,7 +1114,7 @@ export default function stateFlowExtension(pi, options = {}) {
1056
1114
  const current = snapshot;
1057
1115
  const wasActive = current.config.mode === "active";
1058
1116
  let unfinished = current.meta.specification !== undefined
1059
- || (inferencePreparation?.prompt !== undefined && !inferencePreparation.accepted);
1117
+ || (inferencePreparation.current?.prompt !== undefined && !inferencePreparation.current.accepted);
1060
1118
  applyInactiveMode(ctx, mode);
1061
1119
  const stoppedAt = Date.now();
1062
1120
  if (modePersistenceError) {
@@ -1072,7 +1130,7 @@ export default function stateFlowExtension(pi, options = {}) {
1072
1130
  const incomingBoundary = current.meta.bootstrap ? bootstrapContinuation : undefined;
1073
1131
  const anchor = runAnchorTimestamp;
1074
1132
  const idle = ctx.isIdle();
1075
- const isCurrent = () => inactivePersistence === pending && runtime === selected
1133
+ const isCurrent = () => inactivePersistence.owns(pending) && runtime === selected
1076
1134
  && !shuttingDown && ctx.sessionManager.getSessionId() === owner && !isActive();
1077
1135
  const superseded = () => ({ ok: false, message: "State Flow mode change was superseded" });
1078
1136
  const freezeHandoff = () => {
@@ -1080,7 +1138,7 @@ export default function stateFlowExtension(pi, options = {}) {
1080
1138
  const handoff = (wasActive || modePersistenceError) && selected?.view && current.meta.validation?.attempt !== 0
1081
1139
  ? createPassiveContinuation(projectModelState(effectiveState), incomingBoundary?.startedAt ?? stoppedAt, incomingBoundary ? incomingBoundary.activeRunStartedAt : !idle || unfinished ? anchor : undefined, modePersistenceError !== undefined || (incomingBoundary ? incomingBoundary.preserveContext : current.meta.bootstrap === true || (unfinished && anchor === undefined)))
1082
1140
  : undefined;
1083
- // Off keeps the frozen boundary for a later Passive/Active choice but never projects it.
1141
+ // Repeated Passive choices keep the already selected continuation boundary.
1084
1142
  passiveContinuation = handoff ?? (!wasActive ? passiveContinuation : undefined);
1085
1143
  return handoff;
1086
1144
  };
@@ -1109,8 +1167,7 @@ export default function stateFlowExtension(pi, options = {}) {
1109
1167
  // Projection changes before any wait; capture the old run's boundary before later native input can replace it.
1110
1168
  freezeHandoff();
1111
1169
  bootstrapContinuation = undefined;
1112
- artifactInvalidations = [];
1113
- artifactReads.setCandidates([]);
1170
+ acquisition.clearInvalidations();
1114
1171
  assertPublicationAvailable();
1115
1172
  const signal = ctx.signal ? AbortSignal.any([pending.controller.signal, ctx.signal]) : pending.controller.signal;
1116
1173
  if (branchStartsWithoutRuntime || !selected?.view) {
@@ -1132,11 +1189,11 @@ export default function stateFlowExtension(pi, options = {}) {
1132
1189
  throw new Error("State Flow mode selection changed while awaiting publication");
1133
1190
  assertPublicationAvailable();
1134
1191
  const next = structuredClone(snapshot);
1135
- const publication = publish(next);
1192
+ publish(next);
1136
1193
  accepted = true;
1137
1194
  snapshot = next;
1138
1195
  installScopeStates();
1139
- recordPolicyPublication(publication, ctx);
1196
+ recordPublication();
1140
1197
  return complete();
1141
1198
  }, signal);
1142
1199
  return isCurrent() ? result : superseded();
@@ -1152,8 +1209,7 @@ export default function stateFlowExtension(pi, options = {}) {
1152
1209
  }
1153
1210
  modePersistenceError = cause.trim() ? cause : "Canonical State Flow persistence failed";
1154
1211
  bootstrapContinuation = undefined;
1155
- artifactInvalidations = [];
1156
- artifactReads.setCandidates([]);
1212
+ acquisition.clearInvalidations();
1157
1213
  return complete();
1158
1214
  }
1159
1215
  }
@@ -1189,7 +1245,7 @@ export default function stateFlowExtension(pi, options = {}) {
1189
1245
  },
1190
1246
  cancelStart: () => {
1191
1247
  telegramStartPending = false;
1192
- cancelStartActivation();
1248
+ startActivation.cancel();
1193
1249
  },
1194
1250
  },
1195
1251
  });
@@ -1206,7 +1262,7 @@ export default function stateFlowExtension(pi, options = {}) {
1206
1262
  contextProjection.reset();
1207
1263
  skillReads.clear();
1208
1264
  artifactReads.clear();
1209
- cancelResponseReconciliation();
1265
+ responseReconciliation.cancel();
1210
1266
  completedRunAccepted = false;
1211
1267
  // This native hook has no operation signal. Capture only; the first active context owns acceptance.
1212
1268
  deferInferencePreparation(event.prompt);
@@ -1227,12 +1283,12 @@ export default function stateFlowExtension(pi, options = {}) {
1227
1283
  if (!isActive() && !passiveContinuation && !passiveMemoryAvailable())
1228
1284
  return;
1229
1285
  // Idle inspection must not freeze a pre-acceptance snapshot for the live inference.
1230
- const projection = isActive() && inferencePreparation && !inferencePreparation.accepted
1286
+ const projection = isActive() && inferencePreparation.current && !inferencePreparation.current.accepted
1231
1287
  ? new ContextProjection() : contextProjection;
1232
1288
  const effective = effectiveState;
1233
- const invalidations = artifactInvalidations.map(({ path, scope, reason }) => ({ path, ...(scope === undefined ? {} : { scope }), reason }));
1234
- const phase = isActive() ? currentRehydrationPhase() : undefined;
1235
- 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);
1236
1292
  if (passiveContinuation) {
1237
1293
  const retained = passiveContinuationMessages(messages, passiveContinuation);
1238
1294
  if (!runtime?.view)
@@ -1249,7 +1305,7 @@ export default function stateFlowExtension(pi, options = {}) {
1249
1305
  return { messages: projection.project(source, view, () => runtimeContextHead(snapshot, view, projectRecentTransitionsWithLimit(config.historyLimit, runtime?.recent() ?? []))) };
1250
1306
  }
1251
1307
  function prepareContext(messages, ctx) {
1252
- const pending = inferencePreparation;
1308
+ const pending = inferencePreparation.current;
1253
1309
  const selected = runtime;
1254
1310
  const operationSignal = ctx.signal;
1255
1311
  // Idle projections remain observational; only a live native operation may await preparation.
@@ -1257,15 +1313,15 @@ export default function stateFlowExtension(pi, options = {}) {
1257
1313
  return projectContext(messages);
1258
1314
  pending.operation ??= prepareInference(pending, selected, ctx, operationSignal).finally(() => {
1259
1315
  // A late withdrawal must not clear newer work; accepted lifecycle is never replayed after an error.
1260
- if (!pending.accepted && inferencePreparation === pending)
1316
+ if (!pending.accepted && inferencePreparation.owns(pending))
1261
1317
  pending.operation = undefined;
1262
1318
  });
1263
1319
  return pending.operation.then(() => {
1264
1320
  if (shuttingDown || operationSignal.aborted || runtime !== selected || ctx.signal !== operationSignal)
1265
1321
  return;
1266
- if (isActive() && inferencePreparation !== pending) {
1322
+ if (isActive() && !inferencePreparation.owns(pending)) {
1267
1323
  // Start may require same-run maintenance. A new captured user prompt instead owns a different context request.
1268
- if (inferencePreparation?.prompt === undefined)
1324
+ if (inferencePreparation.current?.prompt === undefined)
1269
1325
  return prepareContext(messages, ctx);
1270
1326
  return;
1271
1327
  }
@@ -1330,7 +1386,7 @@ export default function stateFlowExtension(pi, options = {}) {
1330
1386
  return;
1331
1387
  if (event.message.role !== "assistant")
1332
1388
  return;
1333
- cancelResponseReconciliation();
1389
+ responseReconciliation.cancel();
1334
1390
  const message = event.message;
1335
1391
  if (message.stopReason === "aborted" || assistantToolCallCount(message.content) > 0 || message.stopReason === "toolUse")
1336
1392
  return;
@@ -1338,18 +1394,18 @@ export default function stateFlowExtension(pi, options = {}) {
1338
1394
  recordDiagnostic(`Assistant response ended with ${message.stopReason}`, "finalization", ctx, { content: message.content });
1339
1395
  return;
1340
1396
  }
1341
- responseReconciliation = new AbortController();
1397
+ responseReconciliation.claim({ controller: new AbortController() });
1342
1398
  });
1343
1399
  pi.on("turn_end", async (event, ctx) => {
1344
1400
  if (shuttingDown)
1345
1401
  return;
1346
- const pending = responseReconciliation;
1402
+ const pending = responseReconciliation.current;
1347
1403
  if (!isActive() || !pending) {
1348
1404
  updateUi(ctx);
1349
1405
  return;
1350
1406
  }
1351
1407
  const selected = runtime;
1352
- 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;
1353
1409
  let responseCommitted = false;
1354
1410
  try {
1355
1411
  const response = finalizedAssistantResponse(event.message);
@@ -1357,7 +1413,7 @@ export default function stateFlowExtension(pi, options = {}) {
1357
1413
  throw new Error("Temporal State Flow runtime is unavailable; reload before publishing");
1358
1414
  await selected.withPatchTransaction((transaction) => {
1359
1415
  signal.throwIfAborted();
1360
- if (runtime !== selected || responseReconciliation !== pending || !isActive()) {
1416
+ if (runtime !== selected || !responseReconciliation.owns(pending) || !isActive()) {
1361
1417
  throw new Error("State Flow response selection changed while awaiting publication");
1362
1418
  }
1363
1419
  assertPublicationAvailable();
@@ -1374,19 +1430,19 @@ export default function stateFlowExtension(pi, options = {}) {
1374
1430
  responseCommitted = true;
1375
1431
  contextProjection.reset();
1376
1432
  if (publication?.changed)
1377
- recordPublication(publication, ctx);
1433
+ recordPublication();
1378
1434
  appendCheckpoint();
1379
1435
  clearAcceptedAcquisitions();
1380
1436
  bootstrapContinuation = undefined;
1381
1437
  rehydrationPhase = "step";
1382
1438
  completedRunAccepted = !wasBootstrap;
1383
- turnAcceptedForBackup = true;
1439
+ backup.markTurnAccepted();
1384
1440
  updateUi(ctx);
1385
1441
  }, signal);
1386
1442
  }
1387
1443
  catch (error) {
1388
1444
  // Superseded completion owns neither the new lifecycle nor its diagnostics/UI.
1389
- if (signal.aborted || runtime !== selected || responseReconciliation !== pending)
1445
+ if (signal.aborted || runtime !== selected || !responseReconciliation.owns(pending))
1390
1446
  return;
1391
1447
  const cause = diagnosticText(error);
1392
1448
  recordDiagnostic(cause, "finalization", ctx);
@@ -1395,57 +1451,41 @@ export default function stateFlowExtension(pi, options = {}) {
1395
1451
  : `State Flow response reconciliation failed: ${cause}`, "error");
1396
1452
  }
1397
1453
  finally {
1398
- if (responseReconciliation === pending)
1399
- responseReconciliation = undefined;
1454
+ responseReconciliation.release(pending);
1400
1455
  }
1401
1456
  });
1402
1457
  pi.on("session_before_compact", (event) => {
1403
- if (!compactionPlan)
1404
- return;
1405
- if (compactionStopped && event.reason === "manual" && event.customInstructions === compactionMarker)
1406
- return { cancel: true };
1407
- const result = stateFlowCompactionResult(compactionPlan, compactionMarker, event);
1458
+ const result = compactionRequests.resolve(event, !shuttingDown && isActive());
1408
1459
  if (result === undefined || "cancel" in result)
1409
1460
  return result;
1410
1461
  return { compaction: result };
1411
1462
  });
1412
1463
  pi.on("agent_before_settle", async (_event, ctx) => {
1413
- if (shuttingDown || !turnAcceptedForBackup)
1464
+ if (shuttingDown || !memoryToolsAvailable() || !backup.takeSettledTurn())
1414
1465
  return;
1415
- turnAcceptedForBackup = false;
1416
- if (!backupPending)
1417
- return;
1418
- backupPending = false;
1419
1466
  const operationSignal = ctx.signal;
1420
- const signal = operationSignal ? AbortSignal.any([backupLifetime.signal, operationSignal]) : backupLifetime.signal;
1467
+ const lifetime = backup.lifetime;
1468
+ const signal = operationSignal ? AbortSignal.any([lifetime, operationSignal]) : lifetime;
1421
1469
  const selected = runtime;
1422
1470
  try {
1423
1471
  if (existsSync(join(repositoryRoot, ".git"))) {
1424
1472
  const pushSessionId = sessionAddress(ctx).id;
1425
1473
  const pushCwd = ctx.cwd;
1426
1474
  // Pi 0.87 settles after its agent signal ends; waiting there would strand native Abort.
1427
- const operation = backupCurrentStateFlowFiles(repositoryRoot, signal, operationSignal !== undefined);
1428
- backupOperations.add(operation);
1429
- try {
1430
- await operation;
1431
- }
1432
- finally {
1433
- backupOperations.delete(operation);
1434
- }
1435
- if (signal.aborted)
1475
+ await backup.track(backupCurrentStateFlowFiles(repositoryRoot, signal, operationSignal !== undefined));
1476
+ if (signal.aborted || snapshot.config.mode === "off")
1436
1477
  return;
1437
1478
  startStateFlowBackupPush(repositoryRoot, (error) => {
1438
- if (shuttingDown)
1479
+ if (shuttingDown || lifetime.aborted || snapshot.config.mode === "off")
1439
1480
  return;
1440
1481
  const message = diagnosticText(error);
1441
1482
  const recorded = diagnosticWriter.recordBackupPushFailure(pushSessionId, pushCwd, message);
1442
- if (pushFailureNotified)
1483
+ if (!backup.claimPushFailureNotice())
1443
1484
  return;
1444
- pushFailureNotified = true;
1445
1485
  notifyActiveContext(recorded
1446
1486
  ? `State Flow Git backup push failed; state is saved locally. Details: ${stateFlowLogPath(agentDir)}. A later accepted turn retries.`
1447
1487
  : `State Flow Git backup push failed; local diagnostics unavailable: ${message}`);
1448
- }, () => { pushFailureNotified = false; });
1488
+ }, () => backup.resetPushFailureNotice(), lifetime);
1449
1489
  }
1450
1490
  }
1451
1491
  catch (error) {
@@ -1464,8 +1504,8 @@ export default function stateFlowExtension(pi, options = {}) {
1464
1504
  await startStateFlow(ctx);
1465
1505
  return;
1466
1506
  }
1467
- if (!completedRunAccepted || compactionStopped || !isActive() || snapshot.meta.bootstrap
1468
- || 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
1469
1509
  || !shouldRequestStateFlowCompaction(ctx.getContextUsage()))
1470
1510
  return;
1471
1511
  completedRunAccepted = false;
@@ -1475,12 +1515,14 @@ export default function stateFlowExtension(pi, options = {}) {
1475
1515
  const plan = planStateFlowCompaction(entries, runtime.causalBasis(), snapshot.meta.step, runAnchorTimestamp);
1476
1516
  if (!plan)
1477
1517
  return;
1478
- compactionPlan = plan;
1479
- compactionInFlight = true;
1518
+ const marker = compactionRequests.begin(plan);
1480
1519
  // Pi dispatches deferred companion prompts after all settled handlers return.
1481
1520
  await new Promise((resolve) => {
1482
- const finished = () => { compactionPlan = undefined; compactionInFlight = false; resolve(); };
1483
- ctx.compact({ customInstructions: compactionMarker, onComplete: finished, onError: finished });
1521
+ const finished = () => {
1522
+ compactionRequests.finish(marker);
1523
+ resolve();
1524
+ };
1525
+ ctx.compact({ customInstructions: marker, onComplete: finished, onError: finished });
1484
1526
  });
1485
1527
  });
1486
1528
  pi.on("session_compact", () => { contextProjection.reset(); });
@@ -1497,20 +1539,21 @@ export default function stateFlowExtension(pi, options = {}) {
1497
1539
  });
1498
1540
  pi.on("session_shutdown", async (_event, _ctx) => {
1499
1541
  shuttingDown = true;
1542
+ memoryToolLifetime.end();
1500
1543
  contextProjection.reset();
1501
- const restoring = cancelBranchRestoration();
1502
- const starting = cancelStartActivation();
1503
- const stopping = cancelInactivePersistence();
1504
- backupLifetime.abort();
1505
- cancelInferencePreparation();
1506
- 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();
1507
1550
  cancelSharedInspections();
1508
1551
  telegramStartPending = false;
1509
- compactionStopped = true;
1552
+ compactionRequests.stop();
1510
1553
  completedRunAccepted = false;
1511
1554
  activeContext = undefined;
1512
1555
  telegram.dispose();
1513
- 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)]);
1514
1557
  await awaitInFlightBackupPushes(repositoryRoot);
1515
1558
  });
1516
1559
  }