@llblab/pi-kit 0.24.1 → 0.26.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 (162) hide show
  1. package/AGENTS.md +1 -1
  2. package/BACKLOG.md +5 -1
  3. package/CHANGELOG.md +12 -0
  4. package/README.md +11 -8
  5. package/node_modules/@llblab/pi-actors/AGENTS.md +2 -0
  6. package/node_modules/@llblab/pi-actors/CHANGELOG.md +4 -1
  7. package/node_modules/@llblab/pi-actors/LICENSE +21 -0
  8. package/node_modules/@llblab/pi-actors/README.md +1 -1
  9. package/node_modules/@llblab/pi-actors/docs/coordinator-delivery.md +1 -1
  10. package/node_modules/@llblab/pi-actors/package.json +4 -3
  11. package/node_modules/@llblab/pi-claude-usage/AGENTS.md +23 -0
  12. package/node_modules/@llblab/pi-claude-usage/BACKLOG.md +4 -0
  13. package/node_modules/@llblab/pi-claude-usage/CHANGELOG.md +21 -0
  14. package/node_modules/@llblab/pi-claude-usage/LICENSE +22 -0
  15. package/node_modules/@llblab/pi-claude-usage/README.md +155 -0
  16. package/node_modules/@llblab/pi-claude-usage/banner.jpg +0 -0
  17. package/node_modules/@llblab/pi-claude-usage/index.ts +8 -0
  18. package/node_modules/@llblab/pi-claude-usage/lib/extension.ts +30 -0
  19. package/node_modules/@llblab/pi-claude-usage/lib/fast.ts +24 -0
  20. package/node_modules/@llblab/pi-claude-usage/lib/query.ts +146 -0
  21. package/node_modules/@llblab/pi-claude-usage/lib/status-format.ts +297 -0
  22. package/node_modules/@llblab/pi-claude-usage/lib/status.ts +366 -0
  23. package/node_modules/@llblab/pi-claude-usage/lib/telegram.ts +44 -0
  24. package/node_modules/@llblab/pi-claude-usage/lib/usage-store.ts +221 -0
  25. package/node_modules/@llblab/pi-claude-usage/lib/usage.ts +128 -0
  26. package/node_modules/@llblab/pi-claude-usage/package.json +64 -0
  27. package/node_modules/@llblab/pi-clean-room/AGENTS.md +1 -0
  28. package/node_modules/@llblab/pi-clean-room/CHANGELOG.md +5 -0
  29. package/node_modules/@llblab/pi-clean-room/LICENSE +21 -0
  30. package/node_modules/@llblab/pi-clean-room/README.md +1 -1
  31. package/node_modules/@llblab/pi-clean-room/package.json +3 -2
  32. package/node_modules/@llblab/pi-codex-usage/AGENTS.md +9 -6
  33. package/node_modules/@llblab/pi-codex-usage/BACKLOG.md +2 -1
  34. package/node_modules/@llblab/pi-codex-usage/CHANGELOG.md +17 -0
  35. package/node_modules/@llblab/pi-codex-usage/README.md +75 -17
  36. package/node_modules/@llblab/pi-codex-usage/index.ts +8 -1602
  37. package/node_modules/@llblab/pi-codex-usage/lib/extension.ts +25 -0
  38. package/node_modules/@llblab/pi-codex-usage/lib/fast.ts +23 -0
  39. package/node_modules/@llblab/pi-codex-usage/lib/query.ts +368 -0
  40. package/node_modules/@llblab/pi-codex-usage/lib/status-format.ts +347 -0
  41. package/node_modules/@llblab/pi-codex-usage/lib/status.ts +435 -0
  42. package/node_modules/@llblab/pi-codex-usage/lib/telegram.ts +45 -0
  43. package/node_modules/@llblab/pi-codex-usage/lib/usage-store.ts +229 -0
  44. package/node_modules/@llblab/pi-codex-usage/lib/usage.ts +425 -0
  45. package/node_modules/@llblab/pi-codex-usage/package.json +11 -6
  46. package/node_modules/@llblab/pi-command-fast/AGENTS.md +7 -0
  47. package/node_modules/@llblab/pi-command-fast/BACKLOG.md +9 -0
  48. package/node_modules/@llblab/pi-command-fast/CHANGELOG.md +7 -0
  49. package/node_modules/@llblab/pi-command-fast/LICENSE +21 -0
  50. package/node_modules/@llblab/pi-command-fast/README.md +42 -0
  51. package/node_modules/@llblab/pi-command-fast/dist/command.d.ts +8 -0
  52. package/node_modules/@llblab/pi-command-fast/dist/command.js +52 -0
  53. package/node_modules/@llblab/pi-command-fast/dist/index.d.ts +3 -0
  54. package/node_modules/@llblab/pi-command-fast/dist/index.js +3 -0
  55. package/node_modules/@llblab/pi-command-fast/dist/models-json.d.ts +10 -0
  56. package/node_modules/@llblab/pi-command-fast/dist/models-json.js +81 -0
  57. package/node_modules/@llblab/pi-command-fast/package.json +49 -0
  58. package/node_modules/@llblab/pi-grow-loop/AGENTS.md +1 -0
  59. package/node_modules/@llblab/pi-grow-loop/CHANGELOG.md +4 -1
  60. package/node_modules/@llblab/pi-grow-loop/LICENSE +21 -0
  61. package/node_modules/@llblab/pi-grow-loop/README.md +1 -1
  62. package/node_modules/@llblab/pi-grow-loop/package.json +3 -2
  63. package/node_modules/@llblab/pi-state-flow/AGENTS.md +43 -56
  64. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +17 -3
  65. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +25 -0
  66. package/node_modules/@llblab/pi-state-flow/LICENSE +21 -0
  67. package/node_modules/@llblab/pi-state-flow/README.md +18 -15
  68. package/node_modules/@llblab/pi-state-flow/dist/index.d.ts +2 -2
  69. package/node_modules/@llblab/pi-state-flow/dist/index.js +2 -2
  70. package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.d.ts +1 -1
  71. package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.js +1 -1
  72. package/node_modules/@llblab/pi-state-flow/dist/lib/config.d.ts +7 -3
  73. package/node_modules/@llblab/pi-state-flow/dist/lib/config.js +16 -7
  74. package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +9 -9
  75. package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +5 -4
  76. package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +2 -2
  77. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.d.ts +1 -1
  78. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +7 -4
  79. package/node_modules/@llblab/pi-state-flow/dist/lib/episode.d.ts +3 -3
  80. package/node_modules/@llblab/pi-state-flow/dist/lib/episode.js +5 -5
  81. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.d.ts +3 -5
  82. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +503 -235
  83. package/node_modules/@llblab/pi-state-flow/dist/lib/git.d.ts +2 -2
  84. package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +16 -5
  85. package/node_modules/@llblab/pi-state-flow/dist/lib/history.d.ts +11 -4
  86. package/node_modules/@llblab/pi-state-flow/dist/lib/history.js +6 -7
  87. package/node_modules/@llblab/pi-state-flow/dist/lib/logging.d.ts +4 -1
  88. package/node_modules/@llblab/pi-state-flow/dist/lib/logging.js +1 -0
  89. package/node_modules/@llblab/pi-state-flow/dist/lib/query.d.ts +4 -5
  90. package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +13 -13
  91. package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.d.ts +7 -6
  92. package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.js +9 -9
  93. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +3 -2
  94. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +17 -12
  95. package/node_modules/@llblab/pi-state-flow/dist/lib/session.d.ts +13 -1
  96. package/node_modules/@llblab/pi-state-flow/dist/lib/session.js +62 -2
  97. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +23 -8
  98. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +52 -20
  99. package/node_modules/@llblab/pi-state-flow/dist/lib/state.d.ts +22 -3
  100. package/node_modules/@llblab/pi-state-flow/dist/lib/state.js +30 -10
  101. package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +5 -3
  102. package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +19 -28
  103. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +17 -15
  104. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +52 -52
  105. package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.d.ts +8 -4
  106. package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.js +34 -18
  107. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.d.ts +5 -5
  108. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +13 -19
  109. package/node_modules/@llblab/pi-state-flow/dist/package.json +12 -11
  110. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +2 -2
  111. package/node_modules/@llblab/pi-state-flow/docs/README.md +2 -1
  112. package/node_modules/@llblab/pi-state-flow/docs/agent-contract-relocation.md +72 -0
  113. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +44 -36
  114. package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +14 -6
  115. package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +7 -5
  116. package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +6 -6
  117. package/node_modules/@llblab/pi-state-flow/docs/performance.md +1 -1
  118. package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +13 -12
  119. package/node_modules/@llblab/pi-state-flow/docs/usage.md +37 -33
  120. package/node_modules/@llblab/pi-state-flow/index.ts +3 -2
  121. package/node_modules/@llblab/pi-state-flow/lib/compaction.ts +2 -2
  122. package/node_modules/@llblab/pi-state-flow/lib/config.ts +20 -10
  123. package/node_modules/@llblab/pi-state-flow/lib/context.ts +15 -14
  124. package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +1 -1
  125. package/node_modules/@llblab/pi-state-flow/lib/durable.ts +8 -6
  126. package/node_modules/@llblab/pi-state-flow/lib/episode.ts +6 -6
  127. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +484 -232
  128. package/node_modules/@llblab/pi-state-flow/lib/git.ts +14 -5
  129. package/node_modules/@llblab/pi-state-flow/lib/history.ts +16 -11
  130. package/node_modules/@llblab/pi-state-flow/lib/logging.ts +5 -1
  131. package/node_modules/@llblab/pi-state-flow/lib/query.ts +16 -16
  132. package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +11 -11
  133. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +19 -13
  134. package/node_modules/@llblab/pi-state-flow/lib/session.ts +57 -3
  135. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +57 -22
  136. package/node_modules/@llblab/pi-state-flow/lib/state.ts +46 -13
  137. package/node_modules/@llblab/pi-state-flow/lib/status.ts +23 -32
  138. package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +66 -65
  139. package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +39 -19
  140. package/node_modules/@llblab/pi-state-flow/lib/transition.ts +19 -27
  141. package/node_modules/@llblab/pi-state-flow/package.json +12 -11
  142. package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +2 -2
  143. package/node_modules/jsonc-parser/CHANGELOG.md +76 -0
  144. package/node_modules/jsonc-parser/LICENSE.md +21 -0
  145. package/node_modules/jsonc-parser/README.md +364 -0
  146. package/node_modules/jsonc-parser/SECURITY.md +41 -0
  147. package/node_modules/jsonc-parser/lib/esm/impl/edit.js +185 -0
  148. package/node_modules/jsonc-parser/lib/esm/impl/format.js +261 -0
  149. package/node_modules/jsonc-parser/lib/esm/impl/parser.js +659 -0
  150. package/node_modules/jsonc-parser/lib/esm/impl/scanner.js +443 -0
  151. package/node_modules/jsonc-parser/lib/esm/impl/string-intern.js +29 -0
  152. package/node_modules/jsonc-parser/lib/esm/main.d.ts +351 -0
  153. package/node_modules/jsonc-parser/lib/esm/main.js +178 -0
  154. package/node_modules/jsonc-parser/lib/umd/impl/edit.js +201 -0
  155. package/node_modules/jsonc-parser/lib/umd/impl/format.js +275 -0
  156. package/node_modules/jsonc-parser/lib/umd/impl/parser.js +682 -0
  157. package/node_modules/jsonc-parser/lib/umd/impl/scanner.js +456 -0
  158. package/node_modules/jsonc-parser/lib/umd/impl/string-intern.js +42 -0
  159. package/node_modules/jsonc-parser/lib/umd/main.d.ts +351 -0
  160. package/node_modules/jsonc-parser/lib/umd/main.js +194 -0
  161. package/node_modules/jsonc-parser/package.json +37 -0
  162. package/package.json +10 -6
@@ -7,11 +7,11 @@ import { isAbsolute, join, resolve } from "node:path";
7
7
  import { ArtifactReadTracker } from "./acquisition.js";
8
8
  import { classifyArtifactCompilationNeed, inspectRegisteredArtifactPaths, ORDINARY_ARTIFACT_COMPILER, sameArtifactSourceFingerprint, } from "./artifact.js";
9
9
  import { hasCompactionSizedTranscript, planStateFlowCompaction, shouldRequestStateFlowCompaction, stateFlowCompactionResult } from "./compaction.js";
10
- import { loadStateFlowConfig } from "./config.js";
10
+ import { inactiveModeFor, loadStateFlowConfig } from "./config.js";
11
11
  import { ContextProjection, contextView, createPassiveContinuation, currentRunTrajectory, passiveContinuationMessages, projectSystemProtocol, runtimeContextHead, syntheticUser } from "./context.js";
12
12
  import { readNativeSessionHeader } from "./continuation.js";
13
13
  import { cwdScopeKey, resolveSessionAddress, sessionScopeKey, } from "./durable.js";
14
- import { completeRun, prepareRun, resumeEpisode, startEpisode, stopEpisode } from "./episode.js";
14
+ import { completeRun, deactivateEpisode, prepareRun, resumeEpisode, startEpisode } from "./episode.js";
15
15
  import { awaitInFlightBackupPushes, backupCurrentStateFlowFiles, startStateFlowBackupPush } from "./git.js";
16
16
  import { projectRecentTransitionsWithLimit } from "./history.js";
17
17
  import { isObject, presentationJson, sameJson } from "./json.js";
@@ -20,10 +20,10 @@ import { assistantToolCallCount, conciseDiagnostic, diagnosticText, finalizedAss
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
- import { HistoryBoundaryExpiredError, emptySnapshot, migrationFailure } from "./snapshot.js";
26
- import { emptyState, overlayStates, projectStateForModel } from "./state.js";
25
+ import { HistoryBoundaryExpiredError, emptySnapshot, migrationFailure, preRuntimeCheckpoint } from "./snapshot.js";
26
+ import { emptyState, projectStateForModel } from "./state.js";
27
27
  import { compactStatus, detailedStatus, STATUS_KEY } from "./status.js";
28
28
  import { PublicationBusyError } from "./storage.js";
29
29
  import { createStateFlowTelegramAdapter } from "./telegram.js";
@@ -36,29 +36,31 @@ const PASSIVE_STOP_ENTRY_TYPE = "state-flow-passive-stop";
36
36
  export default function stateFlowExtension(pi, options = {}) {
37
37
  const agentDir = options.agentDir ?? getAgentDir();
38
38
  const loadedConfig = loadStateFlowConfig(agentDir, options.repositoryRoot);
39
- const config = {
40
- ...loadedConfig,
41
- passiveBootstrap: options.passive?.bootstrap ?? loadedConfig.passiveBootstrap,
42
- passiveTools: options.passive?.tools ?? loadedConfig.passiveTools,
43
- };
44
- let snapshot = emptySnapshot();
39
+ const config = options.mode === undefined ? loadedConfig
40
+ : { ...loadedConfig, mode: options.mode, inactiveMode: inactiveModeFor(options.mode) };
41
+ let snapshot = emptySnapshot(config.inactiveMode);
45
42
  let scopeStates = { global: emptyState(), cwd: emptyState(), session: emptyState() };
43
+ let effectiveState = {};
46
44
  let branchStartsWithoutRuntime = false;
47
45
  let selectedHistoryExpired = false;
48
- let stopPersistenceError;
49
- let stopPersistence;
46
+ let modePersistenceError;
47
+ /** One pending inactive-mode persistence; later inactive choices coalesce until it publishes. */
48
+ let inactivePersistence;
50
49
  let startActivation;
51
50
  let forkInitialization = false;
52
51
  let branchRestoration;
52
+ let deferredBranch;
53
53
  const restorationOperations = new Set();
54
54
  let responseReconciliation;
55
55
  let sharedInspectionLifetime = new AbortController();
56
+ let memoryToolLifetime = new AbortController();
56
57
  let completedRunAccepted = false;
57
58
  let turnAcceptedForBackup = false;
58
59
  let compactionPlan;
59
60
  let compactionInFlight = false;
60
61
  let compactionStopped = false;
61
- const compactionMarker = `state-flow-boundary:${randomUUID()}`;
62
+ const compactionPrefix = `state-flow-boundary:${randomUUID()}:`;
63
+ let compactionMarker;
62
64
  let passiveContinuation;
63
65
  let bootstrapContinuation;
64
66
  const contextProjection = new ContextProjection();
@@ -70,7 +72,7 @@ export default function stateFlowExtension(pi, options = {}) {
70
72
  const repositoryRoot = resolve(options.repositoryRoot ?? config.directory);
71
73
  const diagnosticWriter = new StateFlowDiagnosticWriter(config.logging, stateFlowLogPath(agentDir), repositoryRoot, (message) => notifyActiveContext(message));
72
74
  let backupPending = false;
73
- const backupLifetime = new AbortController();
75
+ let backupLifetime = new AbortController();
74
76
  const backupOperations = new Set();
75
77
  let shuttingDown = false;
76
78
  let pushFailureNotified = false;
@@ -113,18 +115,26 @@ export default function stateFlowExtension(pi, options = {}) {
113
115
  assertSelectedBranchAvailable();
114
116
  if (!runtime)
115
117
  throw new Error("State Flow temporal runtime is unavailable");
116
- return projectModelState(runtime.read(offset, scope));
118
+ const { lazy: _lazy, ...defaults } = emptyState();
119
+ return { ...defaults, ...projectModelState(runtime.readView(offset, scope)) };
117
120
  } });
118
121
  function assertPublicationAvailable() {
119
122
  assertSelectedBranchAvailable();
120
- if (stopPersistenceError)
121
- throw new Error(`Memory writes paused after Stop: ${stopPersistenceError}; use /state-flow-start`);
123
+ if (modePersistenceError)
124
+ throw new Error(`Memory writes paused after mode change: ${modePersistenceError}; use /state-flow-active`);
125
+ }
126
+ function isActive() {
127
+ return snapshot.config.mode === "active";
128
+ }
129
+ /** An unavailable or pending active selection keeps the configured inactive policy, never an invented one. */
130
+ function inactiveSelection(mode) {
131
+ return mode === "active" ? config.inactiveMode : mode;
122
132
  }
123
133
  function appendCheckpoint() {
124
- const checkpoint = branchStartsWithoutRuntime ? { disabled: true } : runtime?.retainedCheckpoint(snapshot) ?? { disabled: true };
125
- if ("disabled" in checkpoint && !branchStartsWithoutRuntime) {
126
- throw new Error("State Flow cannot checkpoint an unproven branch as ordinary disabled; restore a valid checkpoint first");
127
- }
134
+ const checkpoint = branchStartsWithoutRuntime ? preRuntimeCheckpoint(snapshot.config.mode)
135
+ : runtime?.view ? runtime.retainedCheckpoint(snapshot) : undefined;
136
+ if (!checkpoint)
137
+ throw new Error("State Flow cannot checkpoint an unproven branch; restore a valid checkpoint first");
128
138
  pi.appendEntry(SNAPSHOT_ENTRY_TYPE, checkpoint);
129
139
  if ("boundary" in checkpoint)
130
140
  branchStartsWithoutRuntime = false;
@@ -137,10 +147,10 @@ export default function stateFlowExtension(pi, options = {}) {
137
147
  function updateUi(ctx) {
138
148
  ctx.ui.setStatus(STATUS_KEY, compactStatus(snapshot, scopeRevisions(), (color, text) => ctx.ui.theme.fg(color, text)));
139
149
  }
140
- function cancelStopPersistence() {
141
- const pending = stopPersistence;
150
+ function cancelInactivePersistence() {
151
+ const pending = inactivePersistence;
142
152
  pending?.controller.abort();
143
- stopPersistence = undefined;
153
+ inactivePersistence = undefined;
144
154
  return pending?.operation;
145
155
  }
146
156
  function cancelStartActivation() {
@@ -163,6 +173,11 @@ export default function stateFlowExtension(pi, options = {}) {
163
173
  sharedInspectionLifetime.abort(new Error("State Flow inspection was cancelled; select the scope again"));
164
174
  sharedInspectionLifetime = new AbortController();
165
175
  }
176
+ function cancelBackupWork() {
177
+ backupLifetime.abort();
178
+ backupLifetime = new AbortController();
179
+ backupPending = false;
180
+ }
166
181
  function cancelInferencePreparation() {
167
182
  inferencePreparation?.controller.abort();
168
183
  inferencePreparation = undefined;
@@ -223,7 +238,7 @@ export default function stateFlowExtension(pi, options = {}) {
223
238
  function missingArtifactRemovals(states) {
224
239
  const owners = new Map();
225
240
  for (const scope of ["global", "cwd", "session"])
226
- for (const path of Object.keys(states[scope].artifacts)) {
241
+ for (const path of Object.keys(states[scope].artifacts ?? {})) {
227
242
  owners.set(path, [...owners.get(path) ?? [], scope]);
228
243
  }
229
244
  const removals = {};
@@ -239,36 +254,53 @@ export default function stateFlowExtension(pi, options = {}) {
239
254
  }
240
255
  function installScopeStates() {
241
256
  scopeStates = runtime?.view ? runtime.states() : { global: emptyState(), cwd: emptyState(), session: emptyState() };
257
+ effectiveState = runtime?.view ? runtime.readView() : {};
258
+ }
259
+ /** Active and passive expose both memory tools; Off exposes neither. */
260
+ function memoryToolsAvailable() {
261
+ return snapshot.config.mode !== "off";
242
262
  }
243
- function passiveToolsAvailable() {
244
- return snapshot.config.enabled || config.passiveTools;
263
+ /** Passive memory context needs a loaded view; Off and Active never inject it. */
264
+ function passiveMemoryAvailable() {
265
+ return snapshot.config.mode === "passive" && runtime?.view !== undefined;
245
266
  }
246
267
  async function inspectTelegramState(scope) {
247
268
  if (!activeContext || shuttingDown)
248
269
  throw new Error("State Flow is not attached to an active session yet");
249
- if (scope === "session" || scope === "effective")
270
+ const ctx = activeContext;
271
+ const owner = sessionAddress(ctx).key;
272
+ const cwd = ctx.cwd;
273
+ const off = snapshot.config.mode === "off";
274
+ const privateScope = scope === "session" || scope === "effective";
275
+ if (!off && privateScope)
250
276
  assertSelectedBranchAvailable();
251
- const selected = runtime ??= createRuntime(activeContext);
277
+ // Off operator reads are disposable: they confer no selected-branch/cache authority.
278
+ const selected = off ? createRuntime(ctx) : runtime ??= createRuntime(ctx);
252
279
  const signal = sharedInspectionLifetime.signal;
253
- if (!selected.view || (scope !== "session" && !stopPersistenceError))
280
+ let privateAvailable = true;
281
+ if (off && privateScope)
282
+ privateAvailable = await selected.refreshCurrentMemory(signal) !== undefined;
283
+ else if (!selected.view || (scope !== "session" && !modePersistenceError))
254
284
  await selected.refreshShared(signal);
255
285
  signal.throwIfAborted();
256
- if (runtime !== selected)
286
+ if (activeContext !== ctx || ctx.cwd !== cwd || sessionAddress(ctx).key !== owner || (!off && runtime !== selected)) {
257
287
  throw new Error("State Flow inspection selection changed; select the scope again");
258
- if (scope === "session" || scope === "effective")
288
+ }
289
+ if (!privateAvailable)
290
+ throw new Error("Current State Flow session memory is unavailable");
291
+ if (!off && privateScope)
259
292
  assertSelectedBranchAvailable();
260
293
  if (!selected.view)
261
294
  throw new Error("State Flow temporal runtime is unavailable");
262
- installScopeStates();
263
- const state = scope === "effective"
264
- ? overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session)
265
- : scopeStates[scope];
266
- return { state: { ...projectModelState(state), lazy: structuredClone(state.lazy) }, revisions: scopeRevisions(), signal };
295
+ if (!off)
296
+ installScopeStates();
297
+ const state = selected.readView(0, scope === "effective" ? undefined : scope);
298
+ return { state: { ...projectModelState(state), ...(state.lazy === undefined ? {} : { lazy: structuredClone(state.lazy) }) }, revisions: temporalScopeRevisions(selected.view), signal };
267
299
  }
268
300
  function syncStateFlowTools() {
269
301
  const active = pi.getActiveTools();
270
302
  const owned = [PATCH_STATE_TOOL_NAME, READ_STATE_TOOL_NAME];
271
- const available = passiveToolsAvailable();
303
+ const available = memoryToolsAvailable();
272
304
  if (owned.every((name) => active.includes(name) === available))
273
305
  return;
274
306
  pi.setActiveTools(available
@@ -313,7 +345,7 @@ export default function stateFlowExtension(pi, options = {}) {
313
345
  throw new Error("Temporal State Flow runtime is unavailable; reload before publishing");
314
346
  await selected.withPatchTransaction((transaction) => {
315
347
  signal.throwIfAborted();
316
- if (runtime !== selected || inferencePreparation !== pending || !snapshot.config.enabled || shuttingDown) {
348
+ if (runtime !== selected || inferencePreparation !== pending || !isActive() || shuttingDown) {
317
349
  throw new Error("State Flow inference selection changed while awaiting publication");
318
350
  }
319
351
  assertPublicationAvailable();
@@ -379,6 +411,7 @@ export default function stateFlowExtension(pi, options = {}) {
379
411
  cwdScopeKey: cwdScopeKey(cwd),
380
412
  sessionScopeKey: sessionScopeKey(session.key),
381
413
  scopeStates: diagnosticStates,
414
+ effectiveState,
382
415
  recent: diagnosticRecent,
383
416
  historyLimit: config.historyLimit,
384
417
  ...(view === undefined ? {} : { temporal: {
@@ -389,7 +422,7 @@ export default function stateFlowExtension(pi, options = {}) {
389
422
  } }),
390
423
  staleArtifacts,
391
424
  ...(durableStateError === undefined ? {} : { durableStateError }),
392
- ...(stopPersistenceError === undefined ? {} : { publicationError: stopPersistenceError }),
425
+ ...(modePersistenceError === undefined ? {} : { publicationError: modePersistenceError }),
393
426
  };
394
427
  }
395
428
  function forkSource(ctx) {
@@ -402,13 +435,15 @@ export default function stateFlowExtension(pi, options = {}) {
402
435
  return resolveSessionAddress(parent.file, parent.id, parent.timestamp);
403
436
  }
404
437
  /** Select the active native branch under one owned restoration lifetime; only current accepted work installs memory. */
405
- function restoreActiveBranch(ctx, sessionStartReason, notifyRecovery = true, startOwner) {
438
+ function restoreActiveBranch(ctx, sessionStartReason, notifyRecovery = true, startOwner, requestedMode) {
406
439
  contextProjection.reset();
440
+ memoryToolLifetime.abort();
441
+ memoryToolLifetime = new AbortController();
407
442
  cancelBranchRestoration();
408
443
  // Start-owned attachment/fork recovery keeps its owner; only accepted Start cancels Stop.
409
444
  if (!startOwner) {
410
445
  cancelStartActivation();
411
- cancelStopPersistence();
446
+ cancelInactivePersistence();
412
447
  }
413
448
  clearRunTransient();
414
449
  passiveContinuation = undefined;
@@ -417,6 +452,54 @@ export default function stateFlowExtension(pi, options = {}) {
417
452
  artifactReads.setCandidates([]);
418
453
  telegramStartPending = false;
419
454
  activeContext = ctx;
455
+ if (!startOwner) {
456
+ let mode = config.inactiveMode;
457
+ let adoptDefault = false;
458
+ let withoutRuntime = false;
459
+ let persistenceError;
460
+ let pendingFork = false;
461
+ try {
462
+ const branch = ctx.sessionManager.getBranch();
463
+ const discovery = discoverSnapshotData(branch);
464
+ withoutRuntime = discovery.candidates.length === 0 && discovery.errors.length === 0;
465
+ adoptDefault = withoutRuntime && isNewSession(sessionStartReason, branch);
466
+ pendingFork = hasPendingFork(branch, ctx.sessionManager.getSessionId(), PASSIVE_STOP_ENTRY_TYPE);
467
+ const inherited = sessionStartReason === "fork" || pendingFork;
468
+ const policy = findBranchPolicy(branch, inherited ? undefined : ctx.sessionManager.getSessionId(), inherited || retainsPhysicalSessionProjection(sessionStartReason) ? PASSIVE_STOP_ENTRY_TYPE : undefined, config.inactiveMode);
469
+ mode = policy?.mode ?? (adoptDefault ? config.mode : config.inactiveMode);
470
+ persistenceError = inherited
471
+ ? findBranchPolicy(branch, ctx.sessionManager.getSessionId(), PASSIVE_STOP_ENTRY_TYPE, config.inactiveMode)?.persistenceError
472
+ : policy?.persistenceError;
473
+ }
474
+ catch {
475
+ // Off needs only policy; unavailable native evidence grants no memory authority.
476
+ }
477
+ if (mode === "off") {
478
+ cancelBackupWork();
479
+ const owner = sessionAddress(ctx).key;
480
+ const reason = pendingFork || (deferredBranch?.owner === owner && deferredBranch.cwd === ctx.cwd && deferredBranch.reason === "fork"
481
+ && retainsPhysicalSessionProjection(sessionStartReason)) ? "fork" : sessionStartReason;
482
+ if (reason === "fork" && !pendingFork)
483
+ pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, { owner: ctx.sessionManager.getSessionId(), forkPending: true });
484
+ deferredBranch = { reason, owner, cwd: ctx.cwd };
485
+ runtime = undefined;
486
+ snapshot = emptySnapshot("off");
487
+ branchStartsWithoutRuntime = withoutRuntime;
488
+ selectedHistoryExpired = false;
489
+ modePersistenceError = persistenceError;
490
+ forkInitialization = reason === "fork";
491
+ installScopeStates();
492
+ artifactHints = {};
493
+ if (adoptDefault)
494
+ appendCheckpoint();
495
+ syncStateFlowTools();
496
+ updateUi(ctx);
497
+ return Promise.resolve();
498
+ }
499
+ if (pendingFork)
500
+ sessionStartReason = "fork";
501
+ }
502
+ deferredBranch = undefined;
420
503
  runtime = createRuntime(ctx);
421
504
  installScopeStates();
422
505
  branchStartsWithoutRuntime = false;
@@ -427,56 +510,72 @@ export default function stateFlowExtension(pi, options = {}) {
427
510
  try {
428
511
  const branch = ctx.sessionManager.getBranch();
429
512
  const fence = retainsPhysicalSessionProjection(sessionStartReason)
430
- ? findPassiveStopBoundary(branch, ctx.sessionManager.getSessionId(), PASSIVE_STOP_ENTRY_TYPE)?.persistenceError
513
+ ? findPassiveStopBoundary(branch, ctx.sessionManager.getSessionId(), PASSIVE_STOP_ENTRY_TYPE)
431
514
  : undefined;
432
- stopPersistenceError = fence ?? (startOwner ? stopPersistenceError : undefined);
433
- if (fence) {
434
- // The native Stop owns policy only: read proven memory, never replay or publish a stale selection.
435
- selection = { kind: "current" };
515
+ modePersistenceError = fence?.persistenceError ?? (startOwner ? modePersistenceError : undefined);
516
+ if (fence?.persistenceError) {
517
+ // The native marker owns policy only: read proven memory, never replay or publish a stale selection.
518
+ selection = { kind: "current", mode: fence.mode ?? config.inactiveMode };
436
519
  }
437
520
  else {
438
521
  const discovery = discoverSnapshotData(branch);
439
- const selected = selectRetainedCheckpoint(discovery.candidates);
522
+ const selected = selectRetainedCheckpoint(discovery.candidates, config.inactiveMode);
440
523
  skipped = discovery.errors.length + selected.skipped.length;
441
- branchStartsWithoutRuntime = selected.kind === "disabled" || (discovery.candidates.length === 0 && discovery.errors.length === 0);
524
+ branchStartsWithoutRuntime = selected.kind === "pre-runtime" || (discovery.candidates.length === 0 && discovery.errors.length === 0);
442
525
  if (discovery.candidates.length === 0 && discovery.errors.length > 0) {
443
- snapshot = migrationFailure({}, `Snapshot restoration failed: ${discovery.errors[0]}`);
526
+ snapshot = migrationFailure({}, `Snapshot restoration failed: ${discovery.errors[0]}`, config.inactiveMode);
444
527
  }
445
528
  else if (selected.kind === "boundary" && forkInitialization) {
446
529
  try {
447
530
  // Native parent acquisition stays outside canonical exclusion.
448
531
  const source = forkSource(ctx);
449
- const sourceStopped = findPassiveStopBoundary(branch, source.id, PASSIVE_STOP_ENTRY_TYPE)?.persistenceError !== undefined;
450
- selection = { kind: "fork", source, checkpoint: sourceStopped ? { ...selected.checkpoint, enabled: false } : selected.checkpoint };
532
+ const sourceFence = findPassiveStopBoundary(branch, source.id, PASSIVE_STOP_ENTRY_TYPE);
533
+ selection = { kind: "fork", source, checkpoint: sourceFence?.persistenceError !== undefined
534
+ ? { ...selected.checkpoint, mode: sourceFence.mode ?? config.inactiveMode } : selected.checkpoint };
451
535
  }
452
536
  catch (error) {
453
- snapshot = selectedBoundaryFailure(diagnosticText(error));
537
+ snapshot = selectedBoundaryFailure(diagnosticText(error), inactiveSelection(selected.checkpoint.mode));
454
538
  }
455
539
  }
456
540
  else if (selected.kind === "boundary") {
457
541
  selection = { kind: "restore", checkpoint: selected.checkpoint };
458
542
  }
543
+ else if (selected.kind === "pre-runtime") {
544
+ snapshot = emptySnapshot(selected.mode);
545
+ }
459
546
  else if (discovery.candidates.length > 0) {
460
- snapshot = selected.kind === "disabled" ? emptySnapshot() : selected.snapshot;
547
+ snapshot = selected.snapshot;
461
548
  }
462
- else if (config.autoStart && isNewSession(sessionStartReason, branch)) {
463
- selection = { kind: "auto-start" };
549
+ else if (isNewSession(sessionStartReason, branch)) {
550
+ // The repository mode is only a default for genuinely new sessions.
551
+ if (config.mode === "active")
552
+ selection = { kind: "auto-start" };
553
+ else {
554
+ snapshot = emptySnapshot(config.mode);
555
+ // Adopt the default once without initializing semantic storage.
556
+ appendCheckpoint();
557
+ }
464
558
  }
465
559
  else {
466
- snapshot = emptySnapshot();
560
+ snapshot = emptySnapshot(config.inactiveMode);
467
561
  }
468
562
  }
469
563
  }
470
564
  catch (error) {
471
565
  selection = { kind: "settled" };
472
- snapshot = selectedBoundaryFailure(diagnosticText(error));
566
+ snapshot = selectedBoundaryFailure(diagnosticText(error), config.inactiveMode);
473
567
  }
474
568
  // Pending selection grants neither private reads nor publication until its acceptance installs memory.
475
- if (selection.kind !== "settled")
476
- snapshot = migrationFailure({}, "State Flow branch restoration is pending");
569
+ if (selection.kind !== "settled") {
570
+ snapshot = migrationFailure({}, "State Flow branch restoration is pending", selection.kind === "current" ? selection.mode
571
+ : selection.kind === "auto-start" ? config.inactiveMode : inactiveSelection(selection.checkpoint.mode));
572
+ }
573
+ if (requestedMode)
574
+ snapshot = deactivateEpisode(snapshot, requestedMode);
477
575
  const pending = {
478
576
  controller: new AbortController(),
479
577
  awaitingAcceptance: selection.kind !== "settled" && selection.kind !== "current",
578
+ ...(requestedMode === undefined ? {} : { requestedMode }),
480
579
  };
481
580
  branchRestoration = pending;
482
581
  const operation = attachBranch(ctx, pending, selection, sessionStartReason, notifyRecovery, skipped);
@@ -534,17 +633,18 @@ export default function stateFlowExtension(pi, options = {}) {
534
633
  assertCurrent();
535
634
  if (current)
536
635
  runtime = selected = candidate;
537
- snapshot = current ?? migrationFailure({}, "Current State Flow session memory is unavailable");
636
+ snapshot = current ? deactivateEpisode(current, pending.requestedMode ?? selection.mode)
637
+ : migrationFailure({}, "Current State Flow session memory is unavailable", pending.requestedMode ?? selection.mode);
538
638
  installScopeStates();
539
639
  }
540
640
  else if (selection.kind === "restore") {
541
641
  await candidate.withRestoreTransaction(selection.checkpoint, (restored, publish) => {
542
642
  assertCurrent();
543
643
  // Canceled preparation or boundary continuation may have no specification. Retain uncompiled native context.
544
- if (restored.config.enabled && restored.meta.specification === undefined
644
+ if (restored.config.mode === "active" && restored.meta.specification === undefined
545
645
  && retainsPhysicalSessionProjection(reason) && hasUncheckpointedConversation(ctx.sessionManager.getBranch()))
546
646
  restored.meta.bootstrap = true;
547
- const next = pending.passiveRequested ? stopEpisode(restored) : restored;
647
+ const next = pending.requestedMode ? deactivateEpisode(restored, pending.requestedMode) : restored;
548
648
  accept(candidate, next, publish(next), appendCheckpoint);
549
649
  }, signal);
550
650
  }
@@ -552,7 +652,7 @@ export default function stateFlowExtension(pi, options = {}) {
552
652
  await candidate.withForkTransaction(selection.source, selection.checkpoint, (child, publish) => {
553
653
  assertCurrent();
554
654
  // Mode is selected independently of creating the child's private memory.
555
- const next = pending.passiveRequested ? stopEpisode(child) : child;
655
+ const next = pending.requestedMode ? deactivateEpisode(child, pending.requestedMode) : child;
556
656
  const publication = publish(next);
557
657
  forkInitialization = false;
558
658
  accept(candidate, next, publication, () => {
@@ -570,7 +670,7 @@ export default function stateFlowExtension(pi, options = {}) {
570
670
  if (current || discovery.candidates.length > 0 || discovery.errors.length > 0)
571
671
  throw new Error("Existing session runtime requires retained-boundary restoration");
572
672
  const activated = startEpisode(hasPriorConversation(branch));
573
- const next = pending.passiveRequested ? stopEpisode(activated) : activated;
673
+ const next = pending.requestedMode ? deactivateEpisode(activated, pending.requestedMode) : activated;
574
674
  accept(candidate, next, publish(next), appendCheckpoint);
575
675
  }, signal, true);
576
676
  }
@@ -585,7 +685,7 @@ export default function stateFlowExtension(pi, options = {}) {
585
685
  return;
586
686
  if (error instanceof HistoryBoundaryExpiredError)
587
687
  selectedHistoryExpired = true;
588
- snapshot = selectedBoundaryFailure(diagnosticText(error));
688
+ snapshot = selectedBoundaryFailure(diagnosticText(error), snapshot.config.mode === "active" ? config.inactiveMode : snapshot.config.mode);
589
689
  installScopeStates();
590
690
  }
591
691
  finally {
@@ -595,21 +695,14 @@ export default function stateFlowExtension(pi, options = {}) {
595
695
  return;
596
696
  }
597
697
  settleSelection(ctx, reason, notifyRecovery, skipped, selected !== placeholder);
598
- if (snapshot.config.enabled || !(config.passiveBootstrap || config.passiveTools) || runtime?.view)
698
+ if (snapshot.config.mode !== "passive" || runtime?.view)
599
699
  return;
600
- const passive = createRuntime(ctx);
601
- const priorView = selected?.view;
602
- try {
603
- await passive.refreshShared(signal);
604
- }
605
- catch (error) {
606
- if (isCurrent() && !signal.aborted && notifyRecovery && !stopPersistenceError)
607
- notifyProblem(ctx, `State Flow passive memory is unavailable: ${diagnosticText(error)}`, "warning");
700
+ const passive = await loadPassiveView(ctx, signal, isCurrent, notifyRecovery);
701
+ if (passive === undefined)
608
702
  return;
609
- }
610
703
  assertCurrent();
611
704
  // An intervening passive patch or inspection owns its newer cache, even on the same physical branch.
612
- if (selected?.view !== priorView)
705
+ if (runtime?.view)
613
706
  return;
614
707
  runtime = selected = passive;
615
708
  installScopeStates();
@@ -619,7 +712,7 @@ export default function stateFlowExtension(pi, options = {}) {
619
712
  // Host failures before acceptance leave the selection unavailable, never invented empty memory.
620
713
  if (accepted || !isCurrent())
621
714
  return;
622
- snapshot = selectedBoundaryFailure(diagnosticText(error));
715
+ snapshot = selectedBoundaryFailure(diagnosticText(error), snapshot.config.mode === "active" ? config.inactiveMode : snapshot.config.mode);
623
716
  installScopeStates();
624
717
  }
625
718
  finally {
@@ -628,28 +721,44 @@ export default function stateFlowExtension(pi, options = {}) {
628
721
  branchRestoration = undefined;
629
722
  }
630
723
  }
724
+ /** Load current shared memory for passive projection without initializing or publishing it. */
725
+ async function loadPassiveView(ctx, signal, isCurrent, notify) {
726
+ const passive = createRuntime(ctx);
727
+ try {
728
+ await passive.refreshShared(signal);
729
+ }
730
+ catch (error) {
731
+ if (isCurrent() && !signal.aborted && notify && !modePersistenceError)
732
+ notifyProblem(ctx, `State Flow passive memory is unavailable: ${diagnosticText(error)}`, "warning");
733
+ return undefined;
734
+ }
735
+ return passive;
736
+ }
737
+ function selectedContinuation(ctx, reason) {
738
+ if (!retainsPhysicalSessionProjection(reason) || !runtime?.view)
739
+ return undefined;
740
+ const boundary = findPassiveStopBoundary(ctx.sessionManager.getBranch(), ctx.sessionManager.getSessionId(), PASSIVE_STOP_ENTRY_TYPE);
741
+ return boundary === undefined ? undefined : createPassiveContinuation(projectModelState(effectiveState), boundary.at, boundary.from, boundary.preserveContext);
742
+ }
631
743
  function settleSelection(ctx, reason, notifyRecovery, skipped, selectedMemory) {
632
- const failure = !snapshot.config.enabled && snapshot.meta.validation?.attempt === 0 ? snapshot.meta.validation.error : undefined;
633
- if (failure !== undefined && notifyRecovery && !stopPersistenceError) {
744
+ const failure = !isActive() && snapshot.meta.validation?.attempt === 0 ? snapshot.meta.validation.error : undefined;
745
+ if (failure !== undefined && notifyRecovery && !modePersistenceError) {
634
746
  if (selectedHistoryExpired)
635
- notifyProblem(ctx, "State Flow history is outside the retained temporal window; /state-flow-start can use current session memory.", "warning");
747
+ notifyProblem(ctx, "State Flow history is outside the retained temporal window; /state-flow-active can use current session memory.", "warning");
636
748
  else
637
749
  notifyProblem(ctx, `State Flow restore failed: ${failure}`, "error");
638
750
  }
639
- if (selectedMemory && retainsPhysicalSessionProjection(reason) && runtime?.view) {
640
- const boundary = findPassiveStopBoundary(ctx.sessionManager.getBranch(), ctx.sessionManager.getSessionId(), PASSIVE_STOP_ENTRY_TYPE);
641
- if (boundary !== undefined) {
642
- const continuation = createPassiveContinuation(projectModelState(overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session)), boundary.at, boundary.from, boundary.preserveContext);
643
- if (!snapshot.config.enabled)
644
- passiveContinuation = continuation;
645
- else if (snapshot.meta.bootstrap)
646
- bootstrapContinuation = continuation;
647
- }
751
+ const continuation = selectedMemory ? selectedContinuation(ctx, reason) : undefined;
752
+ if (continuation !== undefined) {
753
+ if (!isActive())
754
+ passiveContinuation = continuation;
755
+ else if (snapshot.meta.bootstrap)
756
+ bootstrapContinuation = continuation;
648
757
  }
649
- if (notifyRecovery && snapshot.config.enabled && skipped > 0) {
758
+ if (notifyRecovery && isActive() && skipped > 0) {
650
759
  notifyProblem(ctx, `State Flow skipped ${skipped} malformed snapshot(s); restored the last valid one.`, "warning");
651
760
  }
652
- if (snapshot.config.enabled)
761
+ if (isActive())
653
762
  deferInferencePreparation();
654
763
  syncStateFlowTools();
655
764
  updateUi(ctx);
@@ -669,8 +778,8 @@ export default function stateFlowExtension(pi, options = {}) {
669
778
  }, { additionalProperties: false }),
670
779
  async execute(_toolCallId, params, signal) {
671
780
  try {
672
- if (!passiveToolsAvailable())
673
- throw new Error("State Flow tools are disabled by configuration");
781
+ if (!memoryToolsAvailable())
782
+ throw new Error("State Flow tools are off for this session");
674
783
  if (signal?.aborted)
675
784
  throw new Error("State Flow read was aborted");
676
785
  if (!runtime?.view)
@@ -731,9 +840,10 @@ export default function stateFlowExtension(pi, options = {}) {
731
840
  return new Text(separatedOutput(theme.fg("dim", formatPatchStateArguments(context.args))), 0, 0);
732
841
  },
733
842
  async execute(toolCallId, params, signal, _onUpdate, ctx) {
843
+ signal = signal ? AbortSignal.any([signal, memoryToolLifetime.signal]) : memoryToolLifetime.signal;
734
844
  try {
735
- if (!passiveToolsAvailable())
736
- throw new Error("State Flow tools are disabled by configuration");
845
+ if (!memoryToolsAvailable())
846
+ throw new Error("State Flow tools are off for this session");
737
847
  assertPublicationAvailable();
738
848
  if (signal?.aborted)
739
849
  throw new Error("State Flow patch was aborted before materialization");
@@ -762,12 +872,12 @@ export default function stateFlowExtension(pi, options = {}) {
762
872
  const selected = runtime ??= createRuntime(ctx);
763
873
  const acquiredArtifacts = structuredClone([...artifactReads.successful.values()]);
764
874
  const acquiredSkills = structuredClone([...skillReads.successful.values()]);
765
- const previousEffective = overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session);
875
+ const previousEffective = effectiveState;
766
876
  return await selected.withPatchTransaction((transaction) => {
767
877
  if (runtime !== selected)
768
878
  throw new Error("State Flow session selection changed while awaiting publication");
769
- if (!passiveToolsAvailable())
770
- throw new Error("State Flow tools are disabled by configuration");
879
+ if (!memoryToolsAvailable())
880
+ throw new Error("State Flow tools are off for this session");
771
881
  assertPublicationAvailable();
772
882
  for (const acquired of acquiredArtifacts) {
773
883
  const observation = inspectRegisteredArtifactPaths([acquired.path])[0];
@@ -792,7 +902,7 @@ export default function stateFlowExtension(pi, options = {}) {
792
902
  }
793
903
  clearAcceptedAcquisitions(new Set(acquiredArtifacts.map(({ path }) => path)));
794
904
  updateUi(ctx);
795
- const updates = contextProjection.acceptPatch(previousEffective, overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session), patches, artifactHints);
905
+ const updates = contextProjection.acceptPatch(previousEffective, effectiveState, patches, artifactHints);
796
906
  const acknowledgement = changed
797
907
  ? `\nState materialized atomically at ${scopes.join("+")} scope${scopes.length === 1 ? "" : "s"}.`
798
908
  : "\nState already current.";
@@ -811,11 +921,12 @@ export default function stateFlowExtension(pi, options = {}) {
811
921
  attempted = undefined;
812
922
  }
813
923
  const cause = diagnosticText(error);
814
- recordDiagnostic(cause, /concurrently|advanced/.test(cause) ? "publication-conflict" : "invalid-patch", ctx, {
815
- input: attempted,
816
- tool: PATCH_STATE_TOOL_NAME,
817
- toolCallId,
818
- });
924
+ if (!signal.aborted && memoryToolsAvailable())
925
+ recordDiagnostic(cause, /concurrently|advanced/.test(cause) ? "publication-conflict" : "invalid-patch", ctx, {
926
+ input: attempted,
927
+ tool: PATCH_STATE_TOOL_NAME,
928
+ toolCallId,
929
+ });
819
930
  throw separatedFailure(error);
820
931
  }
821
932
  },
@@ -826,8 +937,8 @@ export default function stateFlowExtension(pi, options = {}) {
826
937
  if (startActivation?.operation)
827
938
  return startActivation.operation;
828
939
  telegramStartPending = false;
829
- if (activeContext && !branchRestoration && snapshot.config.enabled) {
830
- return Promise.resolve({ ok: true, message: "State Flow is already enabled", signal: sharedInspectionLifetime.signal });
940
+ if (activeContext && !branchRestoration && isActive()) {
941
+ return Promise.resolve({ ok: true, message: "State Flow is already active", signal: sharedInspectionLifetime.signal });
831
942
  }
832
943
  const pending = { controller: new AbortController() };
833
944
  startActivation = pending;
@@ -850,20 +961,22 @@ export default function stateFlowExtension(pi, options = {}) {
850
961
  && ctx.sessionManager.getHeader()?.timestamp === timestamp;
851
962
  const superseded = () => {
852
963
  pending.abort();
853
- return { ok: false, message: "State Flow Start was superseded", signal: pending.signal };
964
+ return { ok: false, message: "State Flow activation was superseded", signal: pending.signal };
854
965
  };
855
966
  try {
856
- if (!activeContext)
967
+ if (deferredBranch?.reason === "fork")
968
+ await waitForRecovery(restoreActiveBranch(ctx, "fork", false, pending), signal);
969
+ else if (!activeContext)
857
970
  await waitForRecovery(restoreActiveBranch(ctx, undefined, false, pending), signal);
858
971
  else if (branchRestoration?.operation)
859
972
  await waitForRecovery(branchRestoration.operation, signal);
860
973
  if (!isOwner())
861
974
  return superseded();
862
- if (snapshot.config.enabled)
863
- return { ok: true, message: "State Flow is already enabled", signal: sharedInspectionLifetime.signal };
975
+ if (isActive())
976
+ return { ok: true, message: "State Flow is already active", signal: sharedInspectionLifetime.signal };
864
977
  if (forkInitialization) {
865
- // Withdraw this join on cancellation without revoking the independently owned Stop.
866
- const stopping = stopPersistence?.operation;
978
+ // Withdraw this join on cancellation without revoking the independently owned mode persistence.
979
+ const stopping = inactivePersistence?.operation;
867
980
  if (stopping)
868
981
  await waitForRecovery(stopping, signal);
869
982
  if (!isOwner())
@@ -878,7 +991,7 @@ export default function stateFlowExtension(pi, options = {}) {
878
991
  catch (error) {
879
992
  if (!isOwner())
880
993
  return superseded();
881
- const message = conciseDiagnostic(`State Flow Start failed: ${diagnosticText(error)}`);
994
+ const message = conciseDiagnostic(`State Flow activation failed: ${diagnosticText(error)}`);
882
995
  notifyProblem(ctx, message, "error");
883
996
  return { ok: false, message, signal: AbortSignal.any([pending.signal, sharedInspectionLifetime.signal]) };
884
997
  }
@@ -889,38 +1002,48 @@ export default function stateFlowExtension(pi, options = {}) {
889
1002
  const cwd = ctx.cwd;
890
1003
  const file = ctx.sessionManager.getSessionFile();
891
1004
  const timestamp = ctx.sessionManager.getHeader()?.timestamp;
892
- const initiallyEnabled = snapshot.config.enabled;
1005
+ const initiallyActive = isActive();
893
1006
  let accepted = false;
894
1007
  let receipt = AbortSignal.any([pending.signal, sharedInspectionLifetime.signal]);
895
- const isCurrent = () => startActivation?.controller === pending && runtime === selected && !shuttingDown
1008
+ const signal = ctx.signal ? AbortSignal.any([pending.signal, ctx.signal]) : pending.signal;
1009
+ const isCurrent = () => startActivation?.controller === pending && !signal.aborted && runtime === selected && !shuttingDown
896
1010
  && ctx.cwd === cwd && ctx.sessionManager.getSessionId() === owner && ctx.sessionManager.getSessionFile() === file
897
- && ctx.sessionManager.getHeader()?.timestamp === timestamp && snapshot.config.enabled === (accepted || initiallyEnabled);
1011
+ && ctx.sessionManager.getHeader()?.timestamp === timestamp && isActive() === (accepted || initiallyActive);
898
1012
  const superseded = () => {
899
1013
  pending.abort();
900
- return { ok: false, message: "State Flow Start was superseded", signal: pending.signal };
1014
+ return { ok: false, message: "State Flow activation was superseded", signal: pending.signal };
901
1015
  };
902
1016
  try {
903
1017
  const activation = createRuntime(ctx);
904
- const signal = ctx.signal ? AbortSignal.any([pending.signal, ctx.signal]) : pending.signal;
1018
+ const deferred = deferredBranch;
1019
+ if (deferred) {
1020
+ const discovery = discoverSnapshotData(ctx.sessionManager.getBranch());
1021
+ const retained = selectRetainedCheckpoint(discovery.candidates, config.inactiveMode);
1022
+ branchStartsWithoutRuntime = retained.kind === "pre-runtime" || (discovery.candidates.length === 0 && discovery.errors.length === 0);
1023
+ }
1024
+ // Off activation owns one cancellable current-head publication, not an independent read-only restore.
905
1025
  const result = await activation.withStartTransaction((current, publish) => {
906
1026
  signal.throwIfAborted();
907
1027
  if (!isCurrent())
908
- throw new Error("State Flow Start selection changed while awaiting publication");
1028
+ throw new Error("State Flow activation selection changed while awaiting publication");
909
1029
  if (!current && !branchStartsWithoutRuntime)
910
1030
  throw new Error(snapshot.meta.validation?.error ?? "Current State Flow session memory is unavailable");
911
- const continuation = passiveContinuation ?? bootstrapContinuation;
912
- const bootstrap = hasPriorConversation(ctx.sessionManager.getBranch()) || passiveContinuation !== undefined;
1031
+ const boundary = deferred && retainsPhysicalSessionProjection(deferred.reason)
1032
+ ? findPassiveStopBoundary(ctx.sessionManager.getBranch(), owner, PASSIVE_STOP_ENTRY_TYPE) : undefined;
1033
+ const bootstrap = hasPriorConversation(ctx.sessionManager.getBranch()) || passiveContinuation !== undefined || boundary !== undefined;
913
1034
  const activated = current ? resumeEpisode(current, bootstrap) : startEpisode(bootstrap);
914
1035
  const recoveredCurrent = selectedHistoryExpired;
915
1036
  const publication = publish(activated);
916
1037
  accepted = true;
917
- cancelStopPersistence();
1038
+ cancelInactivePersistence();
918
1039
  runtime = selected = activation;
919
1040
  snapshot = activated;
920
1041
  activeContext = ctx;
921
1042
  selectedHistoryExpired = false;
922
- stopPersistenceError = undefined;
1043
+ modePersistenceError = undefined;
923
1044
  installScopeStates();
1045
+ const continuation = passiveContinuation ?? bootstrapContinuation ?? (deferred ? selectedContinuation(ctx, deferred.reason) : undefined);
1046
+ deferredBranch = undefined;
924
1047
  clearRunTransient();
925
1048
  contextProjection.reset();
926
1049
  passiveContinuation = undefined;
@@ -930,129 +1053,270 @@ export default function stateFlowExtension(pi, options = {}) {
930
1053
  recordPolicyPublication(publication, ctx);
931
1054
  syncStateFlowTools();
932
1055
  updateUi(ctx);
1056
+ if (forkInitialization) {
1057
+ forkInitialization = false;
1058
+ pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, { reset: true, owner });
1059
+ }
933
1060
  appendCheckpoint();
934
1061
  ctx.ui.notify(recoveredCurrent
935
- ? "State Flow enabled from current session memory; unavailable historical state was not restored."
1062
+ ? "State Flow active from current session memory; unavailable historical state was not restored."
936
1063
  : snapshot.meta.bootstrap
937
- ? "State Flow enabled. The next complete agent run will migrate active context into state."
938
- : "State Flow enabled. The next prompt starts a stateful agent run.", "info");
939
- return { ok: true, message: "State Flow enabled", signal: receipt };
1064
+ ? "State Flow active. The next complete agent run will migrate active context into state."
1065
+ : "State Flow active. The next prompt starts a stateful agent run.", "info");
1066
+ return { ok: true, message: "State Flow active", signal: receipt };
940
1067
  }, signal, branchStartsWithoutRuntime);
941
1068
  return isCurrent() ? result : superseded();
942
1069
  }
943
1070
  catch (error) {
944
1071
  if (!isCurrent())
945
1072
  return superseded();
946
- const message = conciseDiagnostic(`${accepted ? "State Flow enabled; lifecycle update failed" : "State Flow Start failed"}: ${diagnosticText(error)}`);
1073
+ const message = conciseDiagnostic(`${accepted ? "State Flow active; lifecycle update failed" : "State Flow activation failed"}: ${diagnosticText(error)}`);
947
1074
  notifyProblem(ctx, message, accepted ? "warning" : "error");
948
1075
  return { ok: accepted, message, signal: receipt };
949
1076
  }
950
1077
  }
951
- pi.registerCommand("state-flow-start", {
952
- description: "Start State Flow mode",
953
- handler: async (_args, ctx) => {
954
- await startStateFlow(ctx);
955
- },
956
- });
1078
+ const MODE_COMMAND_DESCRIPTIONS = {
1079
+ active: "Make State Flow active on the current session branch",
1080
+ passive: "Use passive State Flow memory on the current session branch",
1081
+ off: "Turn State Flow tools and context off on the current session branch",
1082
+ };
1083
+ /** Terminal commands and Telegram controls share these lifecycle owners. */
1084
+ function selectMode(ctx, mode) {
1085
+ return mode === "active" ? startStateFlow(ctx) : deactivateStateFlow(ctx, mode);
1086
+ }
1087
+ for (const mode of ["active", "passive", "off"]) {
1088
+ pi.registerCommand(`state-flow-${mode}`, {
1089
+ description: MODE_COMMAND_DESCRIPTIONS[mode],
1090
+ handler: async (_args, ctx) => {
1091
+ await selectMode(ctx, mode);
1092
+ },
1093
+ });
1094
+ }
957
1095
  pi.registerCommand("state-flow-status", {
958
1096
  description: "Show State Flow runtime status",
959
1097
  handler: async (_args, ctx) => {
960
1098
  ctx.ui.notify(detailedStatus(snapshot, statusDiagnostics(ctx)), "info");
961
1099
  },
962
1100
  });
963
- function stopStateFlow(ctx) {
964
- cancelStartActivation();
1101
+ /** Local policy, tools and UI change before any canonical wait. */
1102
+ function applyInactiveMode(ctx, mode) {
1103
+ snapshot = deactivateEpisode(snapshot, mode);
1104
+ clearRunTransient();
1105
+ syncStateFlowTools();
1106
+ updateUi(ctx);
1107
+ }
1108
+ function deactivateStateFlow(ctx, mode) {
965
1109
  if (shuttingDown)
966
1110
  return Promise.resolve({ ok: false, message: "State Flow is shutting down" });
967
- if (stopPersistence?.operation)
968
- return stopPersistence.operation;
1111
+ if (mode === "off")
1112
+ return selectOff(ctx);
1113
+ cancelStartActivation();
1114
+ telegramStartPending = false;
1115
+ const persisting = inactivePersistence;
1116
+ if (persisting?.operation && !persisting.published) {
1117
+ // Pending inactive choices coalesce: acceptance persists whichever inactive mode is current then.
1118
+ if (snapshot.config.mode !== mode)
1119
+ applyInactiveMode(ctx, mode);
1120
+ return persisting.operation;
1121
+ }
969
1122
  if (!activeContext)
970
1123
  void restoreActiveBranch(ctx, undefined, false);
971
1124
  const restoring = branchRestoration;
1125
+ // Read-only recovery also retains the latest policy; its native write fence is updated below.
1126
+ if (restoring)
1127
+ restoring.requestedMode = mode;
972
1128
  if (restoring?.awaitingAcceptance) {
973
- // Stop changes policy without cancelling memory restoration or initialization.
974
- restoring.passiveRequested = true;
975
- snapshot = stopEpisode(snapshot);
976
- clearRunTransient();
977
- syncStateFlowTools();
978
- updateUi(ctx);
979
- return Promise.resolve({ ok: true, message: "State Flow disabled; memory restoration continues" });
1129
+ // Passive keeps independently owned restoration; Off withdraws it through selectOff.
1130
+ applyInactiveMode(ctx, mode);
1131
+ return Promise.resolve({ ok: true, message: `State Flow ${mode}; memory restoration continues` });
980
1132
  }
1133
+ if (snapshot.config.mode === mode) {
1134
+ const retained = selectRetainedCheckpoint(discoverSnapshotData(ctx.sessionManager.getBranch()).candidates, config.inactiveMode);
1135
+ const recordedMode = retained.kind === "pre-runtime" ? retained.mode : retained.kind === "boundary" ? retained.checkpoint.mode : undefined;
1136
+ if (modePersistenceError || recordedMode === mode)
1137
+ return Promise.resolve({ ok: true, message: `State Flow is already ${mode}` });
1138
+ }
1139
+ cancelInactivePersistence();
981
1140
  const pending = { controller: new AbortController() };
982
- stopPersistence = pending;
983
- const operation = persistStoppedState(ctx, pending.controller);
1141
+ inactivePersistence = pending;
1142
+ const operation = deferredBranch && mode === "passive" && !branchStartsWithoutRuntime
1143
+ ? acquirePassiveMode(ctx, pending, deferredBranch.reason)
1144
+ : persistInactiveMode(ctx, pending, mode);
984
1145
  pending.operation = operation;
985
- const finished = () => { if (stopPersistence === pending)
986
- stopPersistence = undefined; };
1146
+ const finished = () => { if (inactivePersistence === pending)
1147
+ inactivePersistence = undefined; };
987
1148
  void operation.then(finished, finished);
988
1149
  return operation;
989
1150
  }
990
- async function persistStoppedState(ctx, pending) {
991
- telegramStartPending = false;
1151
+ /** Off owns cancellation and native policy only; no canonical transaction or semantic handoff. */
1152
+ function selectOff(ctx) {
1153
+ const address = sessionAddress(ctx);
1154
+ const owner = ctx.sessionManager.getSessionId();
1155
+ const branch = ctx.sessionManager.getBranch();
992
1156
  const selected = runtime;
1157
+ const sameOwner = selected?.sessionKey === address.key && selected.cwd === ctx.cwd;
1158
+ const forkRecorded = hasPendingFork(branch, owner, PASSIVE_STOP_ENTRY_TYPE);
1159
+ const pendingFork = forkRecorded || (sameOwner && forkInitialization) || (deferredBranch?.owner === address.key && deferredBranch.cwd === ctx.cwd && deferredBranch.reason === "fork");
1160
+ const policy = findBranchPolicy(branch, owner, PASSIVE_STOP_ENTRY_TYPE, config.inactiveMode);
1161
+ const sameDeferred = deferredBranch?.owner === address.key && deferredBranch.cwd === ctx.cwd;
1162
+ const current = sameOwner || sameDeferred ? snapshot : emptySnapshot("off");
1163
+ if (!sameOwner && !sameDeferred)
1164
+ modePersistenceError = policy?.persistenceError;
1165
+ const alreadyOff = current.config.mode === "off" && policy?.mode === "off";
1166
+ const incoming = sameOwner || sameDeferred ? current.meta.bootstrap ? bootstrapContinuation : passiveContinuation : undefined;
1167
+ const unfinished = current.meta.specification !== undefined || (inferencePreparation?.prompt !== undefined && !inferencePreparation.accepted)
1168
+ || (isActive() && hasUncheckpointedConversation(branch));
1169
+ const at = incoming?.startedAt ?? Date.now();
1170
+ const from = incoming ? incoming.activeRunStartedAt : (sameOwner || sameDeferred) && (!ctx.isIdle() || unfinished) ? runAnchorTimestamp : undefined;
1171
+ const preserveContext = modePersistenceError !== undefined || (incoming ? incoming.preserveContext : current.meta.bootstrap === true || (unfinished && from === undefined));
1172
+ const discovery = discoverSnapshotData(branch);
1173
+ const preRuntime = (sameOwner && branchStartsWithoutRuntime) || (discovery.candidates.length === 0 && discovery.errors.length === 0);
1174
+ let checkpoint;
1175
+ try {
1176
+ checkpoint = !modePersistenceError && current.meta.validation?.attempt !== 0
1177
+ ? preRuntime ? preRuntimeCheckpoint("off") : sameOwner && selected?.view ? selected.retainedCheckpoint(deactivateEpisode(current, "off")) : undefined
1178
+ : undefined;
1179
+ }
1180
+ catch {
1181
+ // An unusable cached bookmark cannot prevent native Off policy or authorize replacement memory.
1182
+ }
1183
+ cancelStartActivation();
1184
+ cancelBranchRestoration();
1185
+ cancelInactivePersistence();
1186
+ cancelBackupWork();
1187
+ memoryToolLifetime.abort();
1188
+ memoryToolLifetime = new AbortController();
1189
+ clearRunTransient();
1190
+ contextProjection.reset();
1191
+ telegramStartPending = false;
1192
+ bootstrapContinuation = undefined;
1193
+ passiveContinuation = undefined;
1194
+ artifactInvalidations = [];
1195
+ artifactHints = {};
1196
+ artifactReads.setCandidates([]);
1197
+ runtime = undefined;
1198
+ snapshot = emptySnapshot("off");
1199
+ selectedHistoryExpired = false;
1200
+ branchStartsWithoutRuntime = preRuntime;
1201
+ forkInitialization = pendingFork;
1202
+ deferredBranch = { owner: address.key, cwd: ctx.cwd, reason: pendingFork ? "fork" : undefined };
1203
+ activeContext = ctx;
1204
+ installScopeStates();
1205
+ syncStateFlowTools();
1206
+ updateUi(ctx);
1207
+ try {
1208
+ if (!alreadyOff || (pendingFork && !forkRecorded)) {
1209
+ if (!checkpoint || !preRuntime || pendingFork || preserveContext || from !== undefined || modePersistenceError)
1210
+ pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, {
1211
+ owner, at, mode: "off", memoryDeferred: true,
1212
+ ...(from === undefined ? {} : { from }),
1213
+ ...(preserveContext ? { preserveContext: true } : {}),
1214
+ ...(pendingFork ? { forkPending: true } : {}),
1215
+ ...(modePersistenceError === undefined ? {} : { persistenceError: modePersistenceError }),
1216
+ });
1217
+ if (checkpoint)
1218
+ pi.appendEntry(SNAPSHOT_ENTRY_TYPE, checkpoint);
1219
+ }
1220
+ return Promise.resolve({ ok: true, message: alreadyOff ? "State Flow is already off" : "State Flow off" });
1221
+ }
1222
+ catch (error) {
1223
+ const message = conciseDiagnostic(`State Flow off; native mode recording failed: ${diagnosticText(error)}`);
1224
+ notifyProblem(ctx, message, "error");
1225
+ return Promise.resolve({ ok: false, message });
1226
+ }
1227
+ }
1228
+ async function acquirePassiveMode(ctx, pending, reason) {
1229
+ await restoreActiveBranch(ctx, reason, true, pending.controller, "passive");
1230
+ if (inactivePersistence !== pending || pending.controller.signal.aborted || shuttingDown) {
1231
+ return { ok: false, message: "State Flow mode change was superseded" };
1232
+ }
1233
+ return persistInactiveMode(ctx, pending, "passive");
1234
+ }
1235
+ async function persistInactiveMode(ctx, pending, mode) {
1236
+ let selected = runtime;
993
1237
  const owner = ctx.sessionManager.getSessionId();
994
1238
  const current = snapshot;
1239
+ const wasActive = current.config.mode === "active";
995
1240
  let unfinished = current.meta.specification !== undefined
996
1241
  || (inferencePreparation?.prompt !== undefined && !inferencePreparation.accepted);
997
- snapshot = stopEpisode(current);
998
- clearRunTransient();
999
- syncStateFlowTools();
1000
- updateUi(ctx);
1001
- if (stopPersistenceError)
1002
- return { ok: true, message: "State Flow disabled; memory writes remain paused" };
1242
+ applyInactiveMode(ctx, mode);
1003
1243
  const stoppedAt = Date.now();
1244
+ if (modePersistenceError) {
1245
+ // Writes stay paused; the native marker records only the newly selected inactive policy.
1246
+ pending.published = true;
1247
+ pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, {
1248
+ at: passiveContinuation?.startedAt ?? stoppedAt,
1249
+ ...(passiveContinuation?.activeRunStartedAt === undefined ? {} : { from: passiveContinuation.activeRunStartedAt }),
1250
+ preserveContext: true, owner, persistenceError: modePersistenceError, mode,
1251
+ });
1252
+ return { ok: true, message: `State Flow ${mode}; memory writes remain paused` };
1253
+ }
1004
1254
  const incomingBoundary = current.meta.bootstrap ? bootstrapContinuation : undefined;
1005
1255
  const anchor = runAnchorTimestamp;
1006
1256
  const idle = ctx.isIdle();
1007
- const isCurrent = () => stopPersistence?.controller === pending && runtime === selected
1008
- && !shuttingDown && ctx.sessionManager.getSessionId() === owner && !snapshot.config.enabled;
1009
- const superseded = () => ({ ok: false, message: "State Flow Stop was superseded" });
1257
+ const isCurrent = () => inactivePersistence === pending && runtime === selected
1258
+ && !shuttingDown && ctx.sessionManager.getSessionId() === owner && !isActive();
1259
+ const superseded = () => ({ ok: false, message: "State Flow mode change was superseded" });
1010
1260
  const freezeHandoff = () => {
1011
1261
  contextProjection.reset();
1012
- const handoff = (current.config.enabled || stopPersistenceError) && selected?.view && current.meta.validation?.attempt !== 0
1013
- ? createPassiveContinuation(projectModelState(overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session)), incomingBoundary?.startedAt ?? stoppedAt, incomingBoundary ? incomingBoundary.activeRunStartedAt : !idle || unfinished ? anchor : undefined, stopPersistenceError !== undefined || (incomingBoundary ? incomingBoundary.preserveContext : current.meta.bootstrap === true || (unfinished && anchor === undefined)))
1262
+ const handoff = (wasActive || modePersistenceError) && selected?.view && current.meta.validation?.attempt !== 0
1263
+ ? 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)))
1014
1264
  : undefined;
1015
- passiveContinuation = handoff ?? (!current.config.enabled ? passiveContinuation : undefined);
1265
+ // Repeated Passive choices keep the already selected continuation boundary.
1266
+ passiveContinuation = handoff ?? (!wasActive ? passiveContinuation : undefined);
1016
1267
  return handoff;
1017
1268
  };
1018
1269
  const complete = () => {
1270
+ pending.published = true;
1019
1271
  const exitHandoff = freezeHandoff();
1020
- if (exitHandoff || stopPersistenceError)
1272
+ const selectedMode = snapshot.config.mode;
1273
+ if (exitHandoff || modePersistenceError)
1021
1274
  pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, {
1022
1275
  at: exitHandoff?.startedAt ?? stoppedAt,
1023
1276
  ...(exitHandoff?.activeRunStartedAt === undefined ? {} : { from: exitHandoff.activeRunStartedAt }),
1024
- ...(exitHandoff?.preserveContext || stopPersistenceError ? { preserveContext: true } : {}),
1025
- ...(stopPersistenceError === undefined ? {} : { owner, persistenceError: stopPersistenceError }),
1277
+ ...(exitHandoff?.preserveContext || modePersistenceError ? { preserveContext: true } : {}),
1278
+ ...(modePersistenceError === undefined ? {} : { owner, persistenceError: modePersistenceError, mode: selectedMode }),
1026
1279
  });
1027
- if (!stopPersistenceError) {
1280
+ if (!modePersistenceError) {
1028
1281
  appendCheckpoint();
1029
- return { ok: true, message: "State Flow disabled" };
1282
+ return { ok: true, message: `State Flow ${selectedMode}` };
1030
1283
  }
1031
- const message = conciseDiagnostic(`State Flow disabled; memory writes paused: ${stopPersistenceError}`);
1284
+ const message = conciseDiagnostic(`State Flow ${selectedMode}; memory writes paused: ${modePersistenceError}`);
1032
1285
  notifyProblem(ctx, message, "warning");
1033
1286
  return { ok: true, message };
1034
1287
  };
1035
1288
  let accepted = false;
1036
1289
  try {
1037
- unfinished ||= current.config.enabled && hasUncheckpointedConversation(ctx.sessionManager.getBranch());
1290
+ unfinished ||= wasActive && hasUncheckpointedConversation(ctx.sessionManager.getBranch());
1038
1291
  // Projection changes before any wait; capture the old run's boundary before later native input can replace it.
1039
1292
  freezeHandoff();
1040
1293
  bootstrapContinuation = undefined;
1041
1294
  artifactInvalidations = [];
1042
1295
  artifactReads.setCandidates([]);
1043
1296
  assertPublicationAvailable();
1044
- if (branchStartsWithoutRuntime || !selected?.view)
1045
- return complete();
1046
- const signal = ctx.signal ? AbortSignal.any([pending.signal, ctx.signal]) : pending.signal;
1297
+ const signal = ctx.signal ? AbortSignal.any([pending.controller.signal, ctx.signal]) : pending.controller.signal;
1298
+ if (branchStartsWithoutRuntime || !selected?.view) {
1299
+ // A pre-runtime choice is native-only; Passive then loads shared memory without initializing storage.
1300
+ const result = complete();
1301
+ if (snapshot.config.mode !== "passive" || runtime?.view)
1302
+ return result;
1303
+ const passive = await loadPassiveView(ctx, signal, isCurrent, true);
1304
+ if (passive && isCurrent() && !signal.aborted && !runtime?.view) {
1305
+ runtime = selected = passive;
1306
+ installScopeStates();
1307
+ updateUi(ctx);
1308
+ }
1309
+ return isCurrent() ? result : superseded();
1310
+ }
1047
1311
  const result = await selected.withLifecycleTransaction((publish) => {
1048
1312
  signal.throwIfAborted();
1049
1313
  if (!isCurrent())
1050
- throw new Error("State Flow Stop selection changed while awaiting publication");
1314
+ throw new Error("State Flow mode selection changed while awaiting publication");
1051
1315
  assertPublicationAvailable();
1052
- const stopped = stopEpisode(snapshot);
1053
- const publication = publish(stopped);
1316
+ const next = structuredClone(snapshot);
1317
+ const publication = publish(next);
1054
1318
  accepted = true;
1055
- snapshot = stopped;
1319
+ snapshot = next;
1056
1320
  installScopeStates();
1057
1321
  recordPolicyPublication(publication, ctx);
1058
1322
  return complete();
@@ -1064,28 +1328,22 @@ export default function stateFlowExtension(pi, options = {}) {
1064
1328
  return superseded();
1065
1329
  const cause = diagnosticText(error);
1066
1330
  if (accepted) {
1067
- const message = conciseDiagnostic(`State Flow disabled; lifecycle update failed: ${cause}`);
1331
+ const message = conciseDiagnostic(`State Flow ${snapshot.config.mode}; lifecycle update failed: ${cause}`);
1068
1332
  notifyProblem(ctx, message, "warning");
1069
1333
  return { ok: true, message };
1070
1334
  }
1071
- stopPersistenceError = cause.trim() ? cause : "Canonical State Flow persistence failed";
1335
+ modePersistenceError = cause.trim() ? cause : "Canonical State Flow persistence failed";
1072
1336
  bootstrapContinuation = undefined;
1073
1337
  artifactInvalidations = [];
1074
1338
  artifactReads.setCandidates([]);
1075
1339
  return complete();
1076
1340
  }
1077
1341
  }
1078
- pi.registerCommand("state-flow-stop", {
1079
- description: "Stop State Flow on the current session branch",
1080
- handler: async (_args, ctx) => {
1081
- await stopStateFlow(ctx);
1082
- },
1083
- });
1084
1342
  const telegram = createStateFlowTelegramAdapter({
1085
1343
  ...(options.telegram?.load === undefined ? {} : { load: options.telegram.load }),
1086
1344
  port: {
1087
1345
  snapshot: () => ({
1088
- enabled: snapshot.config.enabled,
1346
+ mode: snapshot.config.mode,
1089
1347
  step: snapshot.meta.step,
1090
1348
  revisions: scopeRevisions(),
1091
1349
  bootstrap: snapshot.meta.bootstrap === true,
@@ -1093,15 +1351,13 @@ export default function stateFlowExtension(pi, options = {}) {
1093
1351
  }),
1094
1352
  inspect: inspectTelegramState,
1095
1353
  canStartNow: () => activeContext === undefined || activeContext.isIdle(),
1096
- start: () => {
1354
+ select: async (mode) => {
1097
1355
  if (!activeContext)
1098
1356
  throw new Error("State Flow is not attached to an active session yet");
1099
- return startStateFlow(activeContext);
1100
- },
1101
- stop: async () => {
1102
- if (!activeContext)
1103
- throw new Error("State Flow is not attached to an active session yet");
1104
- const operation = stopStateFlow(activeContext);
1357
+ const operation = selectMode(activeContext, mode);
1358
+ if (mode === "active")
1359
+ return operation;
1360
+ // Inactive choices revoke older presentation at once; the receipt belongs to the new lifetime.
1105
1361
  const signal = sharedInspectionLifetime.signal;
1106
1362
  try {
1107
1363
  return { ...await operation, signal };
@@ -1123,8 +1379,8 @@ export default function stateFlowExtension(pi, options = {}) {
1123
1379
  pi.on("before_agent_start", (event) => {
1124
1380
  // Native run identity is independent of semantic enablement and survives mode toggles.
1125
1381
  runAnchorTimestamp = undefined;
1126
- if (!snapshot.config.enabled) {
1127
- if (!config.passiveBootstrap || !runtime?.view)
1382
+ if (!isActive()) {
1383
+ if (!passiveMemoryAvailable())
1128
1384
  return;
1129
1385
  (event.systemPromptOptions.sections ??= {}).state_flow = PASSIVE_MEMORY_PROTOCOL;
1130
1386
  return;
@@ -1139,21 +1395,25 @@ export default function stateFlowExtension(pi, options = {}) {
1139
1395
  (event.systemPromptOptions.sections ??= {}).state_flow = stateFlowProtocol(snapshot.meta.bootstrap === true);
1140
1396
  });
1141
1397
  pi.on("context_with_system", (event) => {
1142
- const protocol = snapshot.config.enabled ? stateFlowProtocol(snapshot.meta.bootstrap === true)
1143
- : config.passiveBootstrap && runtime?.view ? PASSIVE_MEMORY_PROTOCOL : undefined;
1398
+ // Off removes the owned section, including one contributed before an in-flight mode change.
1399
+ const protocol = isActive() ? stateFlowProtocol(snapshot.meta.bootstrap === true)
1400
+ : passiveMemoryAvailable() ? PASSIVE_MEMORY_PROTOCOL : undefined;
1144
1401
  return { messages: projectSystemProtocol(event.messages, protocol) };
1145
1402
  });
1146
1403
  function projectContext(messages) {
1404
+ // Off injects no State Flow context, including a retained passive handoff.
1405
+ if (snapshot.config.mode === "off")
1406
+ return;
1147
1407
  if (runtime?.view)
1148
1408
  refreshArtifactHints();
1149
- if (!snapshot.config.enabled && !passiveContinuation && (!config.passiveBootstrap || !runtime?.view))
1409
+ if (!isActive() && !passiveContinuation && !passiveMemoryAvailable())
1150
1410
  return;
1151
1411
  // Idle inspection must not freeze a pre-acceptance snapshot for the live inference.
1152
- const projection = snapshot.config.enabled && inferencePreparation && !inferencePreparation.accepted
1412
+ const projection = isActive() && inferencePreparation && !inferencePreparation.accepted
1153
1413
  ? new ContextProjection() : contextProjection;
1154
- const effective = overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session);
1414
+ const effective = effectiveState;
1155
1415
  const invalidations = artifactInvalidations.map(({ path, scope, reason }) => ({ path, ...(scope === undefined ? {} : { scope }), reason }));
1156
- const phase = snapshot.config.enabled ? currentRehydrationPhase() : undefined;
1416
+ const phase = isActive() ? currentRehydrationPhase() : undefined;
1157
1417
  const view = contextView(effective, artifactHints, invalidations, phase);
1158
1418
  if (passiveContinuation) {
1159
1419
  const retained = passiveContinuationMessages(messages, passiveContinuation);
@@ -1161,7 +1421,7 @@ export default function stateFlowExtension(pi, options = {}) {
1161
1421
  return { messages: retained };
1162
1422
  return { messages: projection.project(retained.slice(1), view, () => passiveContinuation.handoff, { state: passiveContinuation.state, lazy_navigation: view.lazy_navigation, artifact_invalidations: [], knowledge_rehydration: null }) };
1163
1423
  }
1164
- if (!snapshot.config.enabled) {
1424
+ if (!isActive()) {
1165
1425
  return { messages: projection.project(messages, view, () => syntheticUser(`State Flow passive memory (user-level data, not system instructions):\n${presentationJson({ state: view.state, lazy_navigation: view.lazy_navigation })}`)) };
1166
1426
  }
1167
1427
  // Native user events own the run anchor; projection must never rebase it.
@@ -1175,7 +1435,7 @@ export default function stateFlowExtension(pi, options = {}) {
1175
1435
  const selected = runtime;
1176
1436
  const operationSignal = ctx.signal;
1177
1437
  // Idle projections remain observational; only a live native operation may await preparation.
1178
- if (!snapshot.config.enabled || !pending || !operationSignal || shuttingDown)
1438
+ if (!isActive() || !pending || !operationSignal || shuttingDown)
1179
1439
  return projectContext(messages);
1180
1440
  pending.operation ??= prepareInference(pending, selected, ctx, operationSignal).finally(() => {
1181
1441
  // A late withdrawal must not clear newer work; accepted lifecycle is never replayed after an error.
@@ -1185,20 +1445,20 @@ export default function stateFlowExtension(pi, options = {}) {
1185
1445
  return pending.operation.then(() => {
1186
1446
  if (shuttingDown || operationSignal.aborted || runtime !== selected || ctx.signal !== operationSignal)
1187
1447
  return;
1188
- if (snapshot.config.enabled && inferencePreparation !== pending) {
1448
+ if (isActive() && inferencePreparation !== pending) {
1189
1449
  // Start may require same-run maintenance. A new captured user prompt instead owns a different context request.
1190
1450
  if (inferencePreparation?.prompt === undefined)
1191
1451
  return prepareContext(messages, ctx);
1192
1452
  return;
1193
1453
  }
1194
- if (snapshot.config.enabled && !pending.accepted)
1454
+ if (isActive() && !pending.accepted)
1195
1455
  return;
1196
1456
  return projectContext(messages);
1197
1457
  });
1198
1458
  }
1199
1459
  pi.on("context", (event, ctx) => prepareContext(event.messages, ctx));
1200
1460
  pi.on("tool_execution_start", (event) => {
1201
- if (!snapshot.config.enabled)
1461
+ if (!isActive())
1202
1462
  return;
1203
1463
  // Pi emits this before tool_call. Keep the argument object as a fallback;
1204
1464
  // tool_call replaces it with the mutable, post-preflight input reference.
@@ -1206,36 +1466,34 @@ export default function stateFlowExtension(pi, options = {}) {
1206
1466
  artifactReads.recordStart(event.toolCallId, event.toolName, event.args);
1207
1467
  });
1208
1468
  pi.on("tool_call", (event, ctx) => {
1209
- if (!snapshot.config.enabled)
1469
+ if (!isActive())
1210
1470
  return;
1211
1471
  const batch = findAssistantToolBatch(ctx.sessionManager, event.toolCallId);
1212
1472
  const patchCalls = batch?.filter((name) => name === PATCH_STATE_TOOL_NAME).length ?? 0;
1213
1473
  if (patchCalls > 0) {
1214
1474
  if (patchCalls !== 1) {
1215
- return {
1216
- block: true,
1217
- reason: "A State Flow barrier response must contain exactly one patch_state call",
1218
- };
1475
+ const reason = "A State Flow barrier response must contain exactly one patch_state call";
1476
+ recordDiagnostic(reason, "barrier-block", ctx, { tool: event.toolName, toolCallId: event.toolCallId, batchToolNames: batch });
1477
+ return { block: true, reason };
1219
1478
  }
1220
1479
  if (event.toolName !== PATCH_STATE_TOOL_NAME) {
1221
- return {
1222
- block: true,
1223
- reason: "Blocked by the patch_state barrier; reconsider this action after State Flow rematerializes context",
1224
- };
1480
+ const reason = "Blocked by the patch_state barrier; reconsider this action after State Flow rematerializes context";
1481
+ recordDiagnostic(reason, "barrier-block", ctx, { tool: event.toolName, toolCallId: event.toolCallId, batchToolNames: batch });
1482
+ return { block: true, reason };
1225
1483
  }
1226
1484
  }
1227
1485
  skillReads.recordCall(event.toolCallId, event.toolName, event.input);
1228
1486
  artifactReads.recordCall(event.toolCallId, event.toolName, event.input);
1229
1487
  });
1230
1488
  pi.on("tool_execution_end", (event) => {
1231
- if (!snapshot.config.enabled)
1489
+ if (!isActive())
1232
1490
  return;
1233
1491
  skillReads.recordEnd(event.toolCallId, event.toolName, event.isError);
1234
1492
  dropCurrentSkillReads();
1235
1493
  artifactReads.recordEnd(event.toolCallId, event.toolName, event.isError);
1236
1494
  });
1237
1495
  pi.on("tool_result", (event) => {
1238
- if (!snapshot.config.enabled)
1496
+ if (!isActive())
1239
1497
  return;
1240
1498
  const read = skillReads.recordResult(event.toolName, event.input, event.isError);
1241
1499
  if (!read)
@@ -1250,7 +1508,7 @@ export default function stateFlowExtension(pi, options = {}) {
1250
1508
  // Observe actual user events even while disabled; Start/Stop cannot invent or erase them.
1251
1509
  if (event.message.role === "user" && runAnchorTimestamp === undefined)
1252
1510
  runAnchorTimestamp = event.message.timestamp;
1253
- if (shuttingDown || !snapshot.config.enabled)
1511
+ if (shuttingDown || !isActive())
1254
1512
  return;
1255
1513
  if (event.message.role !== "assistant")
1256
1514
  return;
@@ -1268,7 +1526,7 @@ export default function stateFlowExtension(pi, options = {}) {
1268
1526
  if (shuttingDown)
1269
1527
  return;
1270
1528
  const pending = responseReconciliation;
1271
- if (!snapshot.config.enabled || !pending) {
1529
+ if (!isActive() || !pending) {
1272
1530
  updateUi(ctx);
1273
1531
  return;
1274
1532
  }
@@ -1281,7 +1539,7 @@ export default function stateFlowExtension(pi, options = {}) {
1281
1539
  throw new Error("Temporal State Flow runtime is unavailable; reload before publishing");
1282
1540
  await selected.withPatchTransaction((transaction) => {
1283
1541
  signal.throwIfAborted();
1284
- if (runtime !== selected || responseReconciliation !== pending || !snapshot.config.enabled) {
1542
+ if (runtime !== selected || responseReconciliation !== pending || !isActive()) {
1285
1543
  throw new Error("State Flow response selection changed while awaiting publication");
1286
1544
  }
1287
1545
  assertPublicationAvailable();
@@ -1324,24 +1582,26 @@ export default function stateFlowExtension(pi, options = {}) {
1324
1582
  }
1325
1583
  });
1326
1584
  pi.on("session_before_compact", (event) => {
1327
- if (!compactionPlan)
1585
+ if (event.reason !== "manual" || !event.customInstructions?.startsWith("state-flow-boundary:"))
1328
1586
  return;
1329
- if (compactionStopped && event.reason === "manual" && event.customInstructions === compactionMarker)
1587
+ const marker = compactionMarker;
1588
+ if (shuttingDown || compactionStopped || !isActive() || !compactionPlan || !marker || event.customInstructions !== marker)
1330
1589
  return { cancel: true };
1331
- const result = stateFlowCompactionResult(compactionPlan, compactionMarker, event);
1590
+ const result = stateFlowCompactionResult(compactionPlan, marker, event);
1332
1591
  if (result === undefined || "cancel" in result)
1333
1592
  return result;
1334
1593
  return { compaction: result };
1335
1594
  });
1336
1595
  pi.on("agent_before_settle", async (_event, ctx) => {
1337
- if (shuttingDown || !turnAcceptedForBackup)
1596
+ if (shuttingDown || !memoryToolsAvailable() || !turnAcceptedForBackup)
1338
1597
  return;
1339
1598
  turnAcceptedForBackup = false;
1340
1599
  if (!backupPending)
1341
1600
  return;
1342
1601
  backupPending = false;
1343
1602
  const operationSignal = ctx.signal;
1344
- const signal = operationSignal ? AbortSignal.any([backupLifetime.signal, operationSignal]) : backupLifetime.signal;
1603
+ const lifetime = backupLifetime.signal;
1604
+ const signal = operationSignal ? AbortSignal.any([lifetime, operationSignal]) : lifetime;
1345
1605
  const selected = runtime;
1346
1606
  try {
1347
1607
  if (existsSync(join(repositoryRoot, ".git"))) {
@@ -1356,10 +1616,10 @@ export default function stateFlowExtension(pi, options = {}) {
1356
1616
  finally {
1357
1617
  backupOperations.delete(operation);
1358
1618
  }
1359
- if (signal.aborted)
1619
+ if (signal.aborted || snapshot.config.mode === "off")
1360
1620
  return;
1361
1621
  startStateFlowBackupPush(repositoryRoot, (error) => {
1362
- if (shuttingDown)
1622
+ if (shuttingDown || lifetime.aborted || snapshot.config.mode === "off")
1363
1623
  return;
1364
1624
  const message = diagnosticText(error);
1365
1625
  const recorded = diagnosticWriter.recordBackupPushFailure(pushSessionId, pushCwd, message);
@@ -1369,7 +1629,7 @@ export default function stateFlowExtension(pi, options = {}) {
1369
1629
  notifyActiveContext(recorded
1370
1630
  ? `State Flow Git backup push failed; state is saved locally. Details: ${stateFlowLogPath(agentDir)}. A later accepted turn retries.`
1371
1631
  : `State Flow Git backup push failed; local diagnostics unavailable: ${message}`);
1372
- }, () => { pushFailureNotified = false; });
1632
+ }, () => { pushFailureNotified = false; }, lifetime);
1373
1633
  }
1374
1634
  }
1375
1635
  catch (error) {
@@ -1383,12 +1643,12 @@ export default function stateFlowExtension(pi, options = {}) {
1383
1643
  }
1384
1644
  });
1385
1645
  pi.on("agent_settled", async (_event, ctx) => {
1386
- if (telegramStartPending && !snapshot.config.enabled) {
1646
+ if (telegramStartPending && !isActive()) {
1387
1647
  telegramStartPending = false;
1388
1648
  await startStateFlow(ctx);
1389
1649
  return;
1390
1650
  }
1391
- if (!completedRunAccepted || compactionStopped || !snapshot.config.enabled || snapshot.meta.bootstrap
1651
+ if (!completedRunAccepted || compactionStopped || !isActive() || snapshot.meta.bootstrap
1392
1652
  || compactionInFlight || !ctx.isIdle() || ctx.hasPendingMessages() || !runtime?.view
1393
1653
  || !shouldRequestStateFlowCompaction(ctx.getContextUsage()))
1394
1654
  return;
@@ -1400,11 +1660,18 @@ export default function stateFlowExtension(pi, options = {}) {
1400
1660
  if (!plan)
1401
1661
  return;
1402
1662
  compactionPlan = plan;
1663
+ const marker = compactionMarker = `${compactionPrefix}${randomUUID()}`;
1403
1664
  compactionInFlight = true;
1404
1665
  // Pi dispatches deferred companion prompts after all settled handlers return.
1405
1666
  await new Promise((resolve) => {
1406
- const finished = () => { compactionPlan = undefined; compactionInFlight = false; resolve(); };
1407
- ctx.compact({ customInstructions: compactionMarker, onComplete: finished, onError: finished });
1667
+ const finished = () => {
1668
+ if (compactionMarker === marker) {
1669
+ compactionPlan = undefined;
1670
+ compactionInFlight = false;
1671
+ }
1672
+ resolve();
1673
+ };
1674
+ ctx.compact({ customInstructions: marker, onComplete: finished, onError: finished });
1408
1675
  });
1409
1676
  });
1410
1677
  pi.on("session_compact", () => { contextProjection.reset(); });
@@ -1421,10 +1688,11 @@ export default function stateFlowExtension(pi, options = {}) {
1421
1688
  });
1422
1689
  pi.on("session_shutdown", async (_event, _ctx) => {
1423
1690
  shuttingDown = true;
1691
+ memoryToolLifetime.abort();
1424
1692
  contextProjection.reset();
1425
1693
  const restoring = cancelBranchRestoration();
1426
1694
  const starting = cancelStartActivation();
1427
- const stopping = cancelStopPersistence();
1695
+ const stopping = cancelInactivePersistence();
1428
1696
  backupLifetime.abort();
1429
1697
  cancelInferencePreparation();
1430
1698
  cancelResponseReconciliation();