@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
@@ -15,7 +15,7 @@ import {
15
15
  type ArtifactInvalidationRequest,
16
16
  } from "./artifact.ts";
17
17
  import { hasCompactionSizedTranscript, planStateFlowCompaction, shouldRequestStateFlowCompaction, stateFlowCompactionResult, type StateFlowCompactionPlan } from "./compaction.ts";
18
- import { loadStateFlowConfig } from "./config.ts";
18
+ import { inactiveModeFor, loadStateFlowConfig } from "./config.ts";
19
19
  import { ContextProjection, contextView, createPassiveContinuation, currentRunTrajectory, passiveContinuationMessages, projectSystemProtocol, runtimeContextHead, syntheticUser, type PassiveContinuation } from "./context.ts";
20
20
  import { readNativeSessionHeader } from "./continuation.ts";
21
21
  import {
@@ -24,7 +24,7 @@ import {
24
24
  sessionScopeKey,
25
25
  type SessionAddress,
26
26
  } from "./durable.ts";
27
- import { completeRun, prepareRun, resumeEpisode, startEpisode, stopEpisode } from "./episode.ts";
27
+ import { completeRun, deactivateEpisode, prepareRun, resumeEpisode, startEpisode } from "./episode.ts";
28
28
  import { awaitInFlightBackupPushes, backupCurrentStateFlowFiles, startStateFlowBackupPush } from "./git.ts";
29
29
  import { projectRecentTransitionsWithLimit } from "./history.ts";
30
30
  import { isObject, presentationJson, sameJson, type JsonObject } from "./json.ts";
@@ -34,10 +34,10 @@ import { readProjectedState, readStatePath } from "./query.ts";
34
34
  import { selectedBoundaryFailure, selectRetainedCheckpoint, waitForRecovery } from "./recovery.ts";
35
35
  import type { RehydrationPhase } from "./rehydration.ts";
36
36
  import { TemporalRuntime, type RuntimePublication } from "./runtime.ts";
37
- import { discoverSnapshotData, findAssistantToolBatch, findPassiveStopBoundary, hasPriorConversation, hasUncheckpointedConversation, isNewSession, retainsPhysicalSessionProjection, SNAPSHOT_ENTRY_TYPE } from "./session.ts";
37
+ import { discoverSnapshotData, findAssistantToolBatch, findBranchPolicy, findPassiveStopBoundary, hasPendingFork, hasPriorConversation, hasUncheckpointedConversation, isNewSession, retainsPhysicalSessionProjection, SNAPSHOT_ENTRY_TYPE } from "./session.ts";
38
38
  import { hasCompiledSkillArtifact, hashSkillSource, registeredSkillResolver, SkillReadTracker, type SuccessfulSkillRead } from "./skills.ts";
39
- import { HistoryBoundaryExpiredError, emptySnapshot, migrationFailure, type RetainedBoundaryCheckpoint, type Snapshot } from "./snapshot.ts";
40
- import { emptyState, overlayStates, projectStateForModel, type AtomicScopePatches, type MaterializedState, type ModelState, type ScopedStates, type StateScope } from "./state.ts";
39
+ import { HistoryBoundaryExpiredError, emptySnapshot, migrationFailure, preRuntimeCheckpoint, type InactiveMode, type RetainedBoundaryCheckpoint, type RetainedPiCheckpoint, type Snapshot, type StateFlowMode } from "./snapshot.ts";
40
+ import { emptyState, projectStateForModel, type AtomicScopePatches, type SemanticState, type ModelState, type ScopedSemanticStates, type ScopedStates, type StateScope } from "./state.ts";
41
41
  import { compactStatus, detailedStatus, STATUS_KEY, type StatusDiagnostics } from "./status.ts";
42
42
  import { PublicationBusyError } from "./storage.ts";
43
43
  import { createStateFlowTelegramAdapter, type StateFlowTelegramControlResult, type StateFlowTelegramInspection, type StateFlowTelegramLoader, type StateFlowTelegramScope } from "./telegram.ts";
@@ -49,8 +49,8 @@ export interface StateFlowExtensionOptions {
49
49
  repositoryRoot?: string;
50
50
  onRuntime?: (accessor: { read(offset?: number, scope?: StateScope): ModelState }) => void;
51
51
  telegram?: { load?: StateFlowTelegramLoader };
52
- /** Test/SDK capability override; repository config remains the Pi default. */
53
- passive?: { bootstrap?: boolean; tools?: boolean };
52
+ /** Test/SDK override of the default mode for new sessions; repository config remains the Pi default. */
53
+ mode?: StateFlowMode;
54
54
  }
55
55
 
56
56
  export { formatPatchStateArguments };
@@ -68,15 +68,15 @@ interface InferencePreparation {
68
68
 
69
69
  interface BranchRestoration {
70
70
  readonly controller: AbortController;
71
- /** Memory acceptance is independent of the requested active/passive policy. */
71
+ /** Memory acceptance is independent of the requested session mode. */
72
72
  awaitingAcceptance: boolean;
73
- passiveRequested?: boolean;
73
+ requestedMode?: InactiveMode;
74
74
  operation?: Promise<void>;
75
75
  }
76
76
 
77
77
  type BranchSelection =
78
78
  | { kind: "settled" }
79
- | { kind: "current" }
79
+ | { kind: "current"; mode: InactiveMode }
80
80
  | { kind: "restore"; checkpoint: RetainedBoundaryCheckpoint }
81
81
  | { kind: "fork"; checkpoint: RetainedBoundaryCheckpoint; source: SessionAddress }
82
82
  | { kind: "auto-start" };
@@ -84,29 +84,31 @@ type BranchSelection =
84
84
  export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowExtensionOptions = {}): void {
85
85
  const agentDir = options.agentDir ?? getAgentDir();
86
86
  const loadedConfig = loadStateFlowConfig(agentDir, options.repositoryRoot);
87
- const config = {
88
- ...loadedConfig,
89
- passiveBootstrap: options.passive?.bootstrap ?? loadedConfig.passiveBootstrap,
90
- passiveTools: options.passive?.tools ?? loadedConfig.passiveTools,
91
- };
92
- let snapshot: Snapshot = emptySnapshot();
87
+ const config = options.mode === undefined ? loadedConfig
88
+ : { ...loadedConfig, mode: options.mode, inactiveMode: inactiveModeFor(options.mode) };
89
+ let snapshot: Snapshot = emptySnapshot(config.inactiveMode);
93
90
  let scopeStates: ScopedStates = { global: emptyState(), cwd: emptyState(), session: emptyState() };
91
+ let effectiveState: SemanticState = {};
94
92
  let branchStartsWithoutRuntime = false;
95
93
  let selectedHistoryExpired = false;
96
- let stopPersistenceError: string | undefined;
97
- let stopPersistence: { controller: AbortController; operation?: Promise<StateFlowTelegramControlResult> } | undefined;
94
+ let modePersistenceError: string | undefined;
95
+ /** One pending inactive-mode persistence; later inactive choices coalesce until it publishes. */
96
+ let inactivePersistence: { controller: AbortController; published?: boolean; operation?: Promise<StateFlowTelegramControlResult> } | undefined;
98
97
  let startActivation: { controller: AbortController; operation?: Promise<StateFlowTelegramControlResult> } | undefined;
99
98
  let forkInitialization = false;
100
99
  let branchRestoration: BranchRestoration | undefined;
100
+ let deferredBranch: { reason: unknown; owner: string; cwd: string } | undefined;
101
101
  const restorationOperations = new Set<Promise<void>>();
102
102
  let responseReconciliation: AbortController | undefined;
103
103
  let sharedInspectionLifetime = new AbortController();
104
+ let memoryToolLifetime = new AbortController();
104
105
  let completedRunAccepted = false;
105
106
  let turnAcceptedForBackup = false;
106
107
  let compactionPlan: StateFlowCompactionPlan | undefined;
107
108
  let compactionInFlight = false;
108
109
  let compactionStopped = false;
109
- const compactionMarker = `state-flow-boundary:${randomUUID()}`;
110
+ const compactionPrefix = `state-flow-boundary:${randomUUID()}:`;
111
+ let compactionMarker: string | undefined;
110
112
  let passiveContinuation: PassiveContinuation | undefined;
111
113
  let bootstrapContinuation: PassiveContinuation | undefined;
112
114
  const contextProjection = new ContextProjection();
@@ -118,7 +120,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
118
120
  const repositoryRoot = resolve(options.repositoryRoot ?? config.directory);
119
121
  const diagnosticWriter = new StateFlowDiagnosticWriter(config.logging, stateFlowLogPath(agentDir), repositoryRoot, (message) => notifyActiveContext(message));
120
122
  let backupPending = false;
121
- const backupLifetime = new AbortController();
123
+ let backupLifetime = new AbortController();
122
124
  const backupOperations = new Set<Promise<string | undefined>>();
123
125
  let shuttingDown = false;
124
126
  let pushFailureNotified = false;
@@ -151,7 +153,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
151
153
  return new TemporalRuntime(ctx.cwd, sessionAddress(ctx), repositoryRoot, undefined, config.historyLimit);
152
154
  }
153
155
 
154
- function projectModelState(state: MaterializedState): ModelState {
156
+ function projectModelState(state: SemanticState): SemanticState {
155
157
  return projectStateForModel(state, artifactHints);
156
158
  }
157
159
 
@@ -164,19 +166,28 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
164
166
  options.onRuntime?.({ read: (offset, scope) => {
165
167
  if (scope === "session") assertSelectedBranchAvailable();
166
168
  if (!runtime) throw new Error("State Flow temporal runtime is unavailable");
167
- return projectModelState(runtime.read(offset, scope));
169
+ const { lazy: _lazy, ...defaults } = emptyState();
170
+ return { ...defaults, ...projectModelState(runtime.readView(offset, scope)) };
168
171
  } });
169
172
 
170
173
  function assertPublicationAvailable(): void {
171
174
  assertSelectedBranchAvailable();
172
- if (stopPersistenceError) throw new Error(`Memory writes paused after Stop: ${stopPersistenceError}; use /state-flow-start`);
175
+ if (modePersistenceError) throw new Error(`Memory writes paused after mode change: ${modePersistenceError}; use /state-flow-active`);
176
+ }
177
+
178
+ function isActive(): boolean {
179
+ return snapshot.config.mode === "active";
180
+ }
181
+
182
+ /** An unavailable or pending active selection keeps the configured inactive policy, never an invented one. */
183
+ function inactiveSelection(mode: StateFlowMode): InactiveMode {
184
+ return mode === "active" ? config.inactiveMode : mode;
173
185
  }
174
186
 
175
187
  function appendCheckpoint(): void {
176
- const checkpoint = branchStartsWithoutRuntime ? { disabled: true } : runtime?.retainedCheckpoint(snapshot) ?? { disabled: true };
177
- if ("disabled" in checkpoint && !branchStartsWithoutRuntime) {
178
- throw new Error("State Flow cannot checkpoint an unproven branch as ordinary disabled; restore a valid checkpoint first");
179
- }
188
+ const checkpoint = branchStartsWithoutRuntime ? preRuntimeCheckpoint(snapshot.config.mode)
189
+ : runtime?.view ? runtime.retainedCheckpoint(snapshot) : undefined;
190
+ if (!checkpoint) throw new Error("State Flow cannot checkpoint an unproven branch; restore a valid checkpoint first");
180
191
  pi.appendEntry(SNAPSHOT_ENTRY_TYPE, checkpoint);
181
192
  if ("boundary" in checkpoint) branchStartsWithoutRuntime = false;
182
193
  }
@@ -191,10 +202,10 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
191
202
  ctx.ui.setStatus(STATUS_KEY, compactStatus(snapshot, scopeRevisions(), (color, text) => ctx.ui.theme.fg(color, text)));
192
203
  }
193
204
 
194
- function cancelStopPersistence(): Promise<StateFlowTelegramControlResult> | undefined {
195
- const pending = stopPersistence;
205
+ function cancelInactivePersistence(): Promise<StateFlowTelegramControlResult> | undefined {
206
+ const pending = inactivePersistence;
196
207
  pending?.controller.abort();
197
- stopPersistence = undefined;
208
+ inactivePersistence = undefined;
198
209
  return pending?.operation;
199
210
  }
200
211
 
@@ -222,6 +233,12 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
222
233
  sharedInspectionLifetime = new AbortController();
223
234
  }
224
235
 
236
+ function cancelBackupWork(): void {
237
+ backupLifetime.abort();
238
+ backupLifetime = new AbortController();
239
+ backupPending = false;
240
+ }
241
+
225
242
  function cancelInferencePreparation(): void {
226
243
  inferencePreparation?.controller.abort();
227
244
  inferencePreparation = undefined;
@@ -277,9 +294,9 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
277
294
  artifactReads.setCandidates(artifactInvalidations);
278
295
  }
279
296
 
280
- function missingArtifactRemovals(states: ScopedStates): AtomicScopePatches {
297
+ function missingArtifactRemovals(states: ScopedSemanticStates): AtomicScopePatches {
281
298
  const owners = new Map<string, StateScope[]>();
282
- for (const scope of ["global", "cwd", "session"] as const) for (const path of Object.keys(states[scope].artifacts)) {
299
+ for (const scope of ["global", "cwd", "session"] as const) for (const path of Object.keys(states[scope].artifacts ?? {})) {
283
300
  owners.set(path, [...owners.get(path) ?? [], scope]);
284
301
  }
285
302
  const removals: AtomicScopePatches = {};
@@ -296,33 +313,49 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
296
313
 
297
314
  function installScopeStates(): void {
298
315
  scopeStates = runtime?.view ? runtime.states() : { global: emptyState(), cwd: emptyState(), session: emptyState() };
316
+ effectiveState = runtime?.view ? runtime.readView() : {};
317
+ }
318
+
319
+ /** Active and passive expose both memory tools; Off exposes neither. */
320
+ function memoryToolsAvailable(): boolean {
321
+ return snapshot.config.mode !== "off";
299
322
  }
300
323
 
301
- function passiveToolsAvailable(): boolean {
302
- return snapshot.config.enabled || config.passiveTools;
324
+ /** Passive memory context needs a loaded view; Off and Active never inject it. */
325
+ function passiveMemoryAvailable(): boolean {
326
+ return snapshot.config.mode === "passive" && runtime?.view !== undefined;
303
327
  }
304
328
 
305
329
  async function inspectTelegramState(scope: StateFlowTelegramScope): Promise<StateFlowTelegramInspection> {
306
330
  if (!activeContext || shuttingDown) throw new Error("State Flow is not attached to an active session yet");
307
- if (scope === "session" || scope === "effective") assertSelectedBranchAvailable();
308
- const selected = runtime ??= createRuntime(activeContext);
331
+ const ctx = activeContext;
332
+ const owner = sessionAddress(ctx).key;
333
+ const cwd = ctx.cwd;
334
+ const off = snapshot.config.mode === "off";
335
+ const privateScope = scope === "session" || scope === "effective";
336
+ if (!off && privateScope) assertSelectedBranchAvailable();
337
+ // Off operator reads are disposable: they confer no selected-branch/cache authority.
338
+ const selected = off ? createRuntime(ctx) : runtime ??= createRuntime(ctx);
309
339
  const signal = sharedInspectionLifetime.signal;
310
- if (!selected.view || (scope !== "session" && !stopPersistenceError)) await selected.refreshShared(signal);
340
+ let privateAvailable = true;
341
+ if (off && privateScope) privateAvailable = await selected.refreshCurrentMemory(signal) !== undefined;
342
+ else if (!selected.view || (scope !== "session" && !modePersistenceError)) await selected.refreshShared(signal);
311
343
  signal.throwIfAborted();
312
- if (runtime !== selected) throw new Error("State Flow inspection selection changed; select the scope again");
313
- if (scope === "session" || scope === "effective") assertSelectedBranchAvailable();
344
+ if (activeContext !== ctx || ctx.cwd !== cwd || sessionAddress(ctx).key !== owner || (!off && runtime !== selected)) {
345
+ throw new Error("State Flow inspection selection changed; select the scope again");
346
+ }
347
+ if (!privateAvailable) throw new Error("Current State Flow session memory is unavailable");
348
+ if (!off && privateScope) assertSelectedBranchAvailable();
314
349
  if (!selected.view) throw new Error("State Flow temporal runtime is unavailable");
315
- installScopeStates();
316
- const state = scope === "effective"
317
- ? overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session)
318
- : scopeStates[scope];
319
- return { state: { ...projectModelState(state), lazy: structuredClone(state.lazy) }, revisions: scopeRevisions(), signal };
350
+ if (!off) installScopeStates();
351
+ const state = selected.readView(0, scope === "effective" ? undefined : scope);
352
+ return { state: { ...projectModelState(state), ...(state.lazy === undefined ? {} : { lazy: structuredClone(state.lazy) }) }, revisions: temporalScopeRevisions(selected.view), signal };
320
353
  }
321
354
 
322
355
  function syncStateFlowTools(): void {
323
356
  const active = pi.getActiveTools();
324
357
  const owned = [PATCH_STATE_TOOL_NAME, READ_STATE_TOOL_NAME];
325
- const available = passiveToolsAvailable();
358
+ const available = memoryToolsAvailable();
326
359
  if (owned.every((name) => active.includes(name) === available)) return;
327
360
  pi.setActiveTools(available
328
361
  ? [...new Set([...active, ...owned])]
@@ -374,7 +407,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
374
407
  if (!selected?.view) throw new Error("Temporal State Flow runtime is unavailable; reload before publishing");
375
408
  await selected.withPatchTransaction((transaction) => {
376
409
  signal.throwIfAborted();
377
- if (runtime !== selected || inferencePreparation !== pending || !snapshot.config.enabled || shuttingDown) {
410
+ if (runtime !== selected || inferencePreparation !== pending || !isActive() || shuttingDown) {
378
411
  throw new Error("State Flow inference selection changed while awaiting publication");
379
412
  }
380
413
  assertPublicationAvailable();
@@ -436,6 +469,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
436
469
  cwdScopeKey: cwdScopeKey(cwd),
437
470
  sessionScopeKey: sessionScopeKey(session.key),
438
471
  scopeStates: diagnosticStates,
472
+ effectiveState,
439
473
  recent: diagnosticRecent,
440
474
  historyLimit: config.historyLimit,
441
475
  ...(view === undefined ? {} : { temporal: {
@@ -446,7 +480,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
446
480
  } }),
447
481
  staleArtifacts,
448
482
  ...(durableStateError === undefined ? {} : { durableStateError }),
449
- ...(stopPersistenceError === undefined ? {} : { publicationError: stopPersistenceError }),
483
+ ...(modePersistenceError === undefined ? {} : { publicationError: modePersistenceError }),
450
484
  };
451
485
  }
452
486
 
@@ -459,13 +493,15 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
459
493
  }
460
494
 
461
495
  /** Select the active native branch under one owned restoration lifetime; only current accepted work installs memory. */
462
- function restoreActiveBranch(ctx: ExtensionContext, sessionStartReason?: unknown, notifyRecovery = true, startOwner?: AbortController): Promise<void> {
496
+ function restoreActiveBranch(ctx: ExtensionContext, sessionStartReason?: unknown, notifyRecovery = true, startOwner?: AbortController, requestedMode?: InactiveMode): Promise<void> {
463
497
  contextProjection.reset();
498
+ memoryToolLifetime.abort();
499
+ memoryToolLifetime = new AbortController();
464
500
  cancelBranchRestoration();
465
501
  // Start-owned attachment/fork recovery keeps its owner; only accepted Start cancels Stop.
466
502
  if (!startOwner) {
467
503
  cancelStartActivation();
468
- cancelStopPersistence();
504
+ cancelInactivePersistence();
469
505
  }
470
506
  clearRunTransient();
471
507
  passiveContinuation = undefined;
@@ -474,6 +510,51 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
474
510
  artifactReads.setCandidates([]);
475
511
  telegramStartPending = false;
476
512
  activeContext = ctx;
513
+ if (!startOwner) {
514
+ let mode: StateFlowMode = config.inactiveMode;
515
+ let adoptDefault = false;
516
+ let withoutRuntime = false;
517
+ let persistenceError: string | undefined;
518
+ let pendingFork = false;
519
+ try {
520
+ const branch = ctx.sessionManager.getBranch();
521
+ const discovery = discoverSnapshotData(branch);
522
+ withoutRuntime = discovery.candidates.length === 0 && discovery.errors.length === 0;
523
+ adoptDefault = withoutRuntime && isNewSession(sessionStartReason, branch);
524
+ pendingFork = hasPendingFork(branch, ctx.sessionManager.getSessionId(), PASSIVE_STOP_ENTRY_TYPE);
525
+ const inherited = sessionStartReason === "fork" || pendingFork;
526
+ const policy = findBranchPolicy(branch, inherited ? undefined : ctx.sessionManager.getSessionId(),
527
+ inherited || retainsPhysicalSessionProjection(sessionStartReason) ? PASSIVE_STOP_ENTRY_TYPE : undefined, config.inactiveMode);
528
+ mode = policy?.mode ?? (adoptDefault ? config.mode : config.inactiveMode);
529
+ persistenceError = inherited
530
+ ? findBranchPolicy(branch, ctx.sessionManager.getSessionId(), PASSIVE_STOP_ENTRY_TYPE, config.inactiveMode)?.persistenceError
531
+ : policy?.persistenceError;
532
+ } catch {
533
+ // Off needs only policy; unavailable native evidence grants no memory authority.
534
+ }
535
+ if (mode === "off") {
536
+ cancelBackupWork();
537
+ const owner = sessionAddress(ctx).key;
538
+ const reason = pendingFork || (deferredBranch?.owner === owner && deferredBranch.cwd === ctx.cwd && deferredBranch.reason === "fork"
539
+ && retainsPhysicalSessionProjection(sessionStartReason)) ? "fork" : sessionStartReason;
540
+ if (reason === "fork" && !pendingFork) pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, { owner: ctx.sessionManager.getSessionId(), forkPending: true });
541
+ deferredBranch = { reason, owner, cwd: ctx.cwd };
542
+ runtime = undefined;
543
+ snapshot = emptySnapshot("off");
544
+ branchStartsWithoutRuntime = withoutRuntime;
545
+ selectedHistoryExpired = false;
546
+ modePersistenceError = persistenceError;
547
+ forkInitialization = reason === "fork";
548
+ installScopeStates();
549
+ artifactHints = {};
550
+ if (adoptDefault) appendCheckpoint();
551
+ syncStateFlowTools();
552
+ updateUi(ctx);
553
+ return Promise.resolve();
554
+ }
555
+ if (pendingFork) sessionStartReason = "fork";
556
+ }
557
+ deferredBranch = undefined;
477
558
  runtime = createRuntime(ctx);
478
559
  installScopeStates();
479
560
  branchStartsWithoutRuntime = false;
@@ -484,47 +565,61 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
484
565
  try {
485
566
  const branch = ctx.sessionManager.getBranch();
486
567
  const fence = retainsPhysicalSessionProjection(sessionStartReason)
487
- ? findPassiveStopBoundary(branch, ctx.sessionManager.getSessionId(), PASSIVE_STOP_ENTRY_TYPE)?.persistenceError
568
+ ? findPassiveStopBoundary(branch, ctx.sessionManager.getSessionId(), PASSIVE_STOP_ENTRY_TYPE)
488
569
  : undefined;
489
- stopPersistenceError = fence ?? (startOwner ? stopPersistenceError : undefined);
490
- if (fence) {
491
- // The native Stop owns policy only: read proven memory, never replay or publish a stale selection.
492
- selection = { kind: "current" };
570
+ modePersistenceError = fence?.persistenceError ?? (startOwner ? modePersistenceError : undefined);
571
+ if (fence?.persistenceError) {
572
+ // The native marker owns policy only: read proven memory, never replay or publish a stale selection.
573
+ selection = { kind: "current", mode: fence.mode ?? config.inactiveMode };
493
574
  } else {
494
575
  const discovery = discoverSnapshotData(branch);
495
- const selected = selectRetainedCheckpoint(discovery.candidates);
576
+ const selected = selectRetainedCheckpoint(discovery.candidates, config.inactiveMode);
496
577
  skipped = discovery.errors.length + selected.skipped.length;
497
- branchStartsWithoutRuntime = selected.kind === "disabled" || (discovery.candidates.length === 0 && discovery.errors.length === 0);
578
+ branchStartsWithoutRuntime = selected.kind === "pre-runtime" || (discovery.candidates.length === 0 && discovery.errors.length === 0);
498
579
  if (discovery.candidates.length === 0 && discovery.errors.length > 0) {
499
- snapshot = migrationFailure({}, `Snapshot restoration failed: ${discovery.errors[0]}`);
580
+ snapshot = migrationFailure({}, `Snapshot restoration failed: ${discovery.errors[0]}`, config.inactiveMode);
500
581
  } else if (selected.kind === "boundary" && forkInitialization) {
501
582
  try {
502
583
  // Native parent acquisition stays outside canonical exclusion.
503
584
  const source = forkSource(ctx);
504
- const sourceStopped = findPassiveStopBoundary(branch, source.id, PASSIVE_STOP_ENTRY_TYPE)?.persistenceError !== undefined;
505
- selection = { kind: "fork", source, checkpoint: sourceStopped ? { ...selected.checkpoint, enabled: false } : selected.checkpoint };
585
+ const sourceFence = findPassiveStopBoundary(branch, source.id, PASSIVE_STOP_ENTRY_TYPE);
586
+ selection = { kind: "fork", source, checkpoint: sourceFence?.persistenceError !== undefined
587
+ ? { ...selected.checkpoint, mode: sourceFence.mode ?? config.inactiveMode } : selected.checkpoint };
506
588
  } catch (error) {
507
- snapshot = selectedBoundaryFailure(diagnosticText(error));
589
+ snapshot = selectedBoundaryFailure(diagnosticText(error), inactiveSelection(selected.checkpoint.mode));
508
590
  }
509
591
  } else if (selected.kind === "boundary") {
510
592
  selection = { kind: "restore", checkpoint: selected.checkpoint };
593
+ } else if (selected.kind === "pre-runtime") {
594
+ snapshot = emptySnapshot(selected.mode);
511
595
  } else if (discovery.candidates.length > 0) {
512
- snapshot = selected.kind === "disabled" ? emptySnapshot() : selected.snapshot;
513
- } else if (config.autoStart && isNewSession(sessionStartReason, branch)) {
514
- selection = { kind: "auto-start" };
596
+ snapshot = selected.snapshot;
597
+ } else if (isNewSession(sessionStartReason, branch)) {
598
+ // The repository mode is only a default for genuinely new sessions.
599
+ if (config.mode === "active") selection = { kind: "auto-start" };
600
+ else {
601
+ snapshot = emptySnapshot(config.mode);
602
+ // Adopt the default once without initializing semantic storage.
603
+ appendCheckpoint();
604
+ }
515
605
  } else {
516
- snapshot = emptySnapshot();
606
+ snapshot = emptySnapshot(config.inactiveMode);
517
607
  }
518
608
  }
519
609
  } catch (error) {
520
610
  selection = { kind: "settled" };
521
- snapshot = selectedBoundaryFailure(diagnosticText(error));
611
+ snapshot = selectedBoundaryFailure(diagnosticText(error), config.inactiveMode);
522
612
  }
523
613
  // Pending selection grants neither private reads nor publication until its acceptance installs memory.
524
- if (selection.kind !== "settled") snapshot = migrationFailure({}, "State Flow branch restoration is pending");
614
+ if (selection.kind !== "settled") {
615
+ snapshot = migrationFailure({}, "State Flow branch restoration is pending", selection.kind === "current" ? selection.mode
616
+ : selection.kind === "auto-start" ? config.inactiveMode : inactiveSelection(selection.checkpoint.mode));
617
+ }
618
+ if (requestedMode) snapshot = deactivateEpisode(snapshot, requestedMode);
525
619
  const pending: BranchRestoration = {
526
620
  controller: new AbortController(),
527
621
  awaitingAcceptance: selection.kind !== "settled" && selection.kind !== "current",
622
+ ...(requestedMode === undefined ? {} : { requestedMode }),
528
623
  };
529
624
  branchRestoration = pending;
530
625
  const operation = attachBranch(ctx, pending, selection, sessionStartReason, notifyRecovery, skipped);
@@ -578,22 +673,23 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
578
673
  const current = await candidate.refreshCurrentMemory(signal);
579
674
  assertCurrent();
580
675
  if (current) runtime = selected = candidate;
581
- snapshot = current ?? migrationFailure({}, "Current State Flow session memory is unavailable");
676
+ snapshot = current ? deactivateEpisode(current, pending.requestedMode ?? selection.mode)
677
+ : migrationFailure({}, "Current State Flow session memory is unavailable", pending.requestedMode ?? selection.mode);
582
678
  installScopeStates();
583
679
  } else if (selection.kind === "restore") {
584
680
  await candidate.withRestoreTransaction(selection.checkpoint, (restored, publish) => {
585
681
  assertCurrent();
586
682
  // Canceled preparation or boundary continuation may have no specification. Retain uncompiled native context.
587
- if (restored.config.enabled && restored.meta.specification === undefined
683
+ if (restored.config.mode === "active" && restored.meta.specification === undefined
588
684
  && retainsPhysicalSessionProjection(reason) && hasUncheckpointedConversation(ctx.sessionManager.getBranch())) restored.meta.bootstrap = true;
589
- const next = pending.passiveRequested ? stopEpisode(restored) : restored;
685
+ const next = pending.requestedMode ? deactivateEpisode(restored, pending.requestedMode) : restored;
590
686
  accept(candidate, next, publish(next), appendCheckpoint);
591
687
  }, signal);
592
688
  } else if (selection.kind === "fork") {
593
689
  await candidate.withForkTransaction(selection.source, selection.checkpoint, (child, publish) => {
594
690
  assertCurrent();
595
691
  // Mode is selected independently of creating the child's private memory.
596
- const next = pending.passiveRequested ? stopEpisode(child) : child;
692
+ const next = pending.requestedMode ? deactivateEpisode(child, pending.requestedMode) : child;
597
693
  const publication = publish(next);
598
694
  forkInitialization = false;
599
695
  accept(candidate, next, publication, () => {
@@ -609,7 +705,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
609
705
  const discovery = discoverSnapshotData(branch);
610
706
  if (current || discovery.candidates.length > 0 || discovery.errors.length > 0) throw new Error("Existing session runtime requires retained-boundary restoration");
611
707
  const activated = startEpisode(hasPriorConversation(branch));
612
- const next = pending.passiveRequested ? stopEpisode(activated) : activated;
708
+ const next = pending.requestedMode ? deactivateEpisode(activated, pending.requestedMode) : activated;
613
709
  accept(candidate, next, publish(next), appendCheckpoint);
614
710
  }, signal, true);
615
711
  }
@@ -620,7 +716,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
620
716
  }
621
717
  if (!isCurrent()) return;
622
718
  if (error instanceof HistoryBoundaryExpiredError) selectedHistoryExpired = true;
623
- snapshot = selectedBoundaryFailure(diagnosticText(error));
719
+ snapshot = selectedBoundaryFailure(diagnosticText(error), snapshot.config.mode === "active" ? config.inactiveMode : snapshot.config.mode);
624
720
  installScopeStates();
625
721
  } finally {
626
722
  pending.awaitingAcceptance = false;
@@ -628,25 +724,19 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
628
724
  if (accepted) return;
629
725
  }
630
726
  settleSelection(ctx, reason, notifyRecovery, skipped, selected !== placeholder);
631
- if (snapshot.config.enabled || !(config.passiveBootstrap || config.passiveTools) || runtime?.view) return;
632
- const passive = createRuntime(ctx);
633
- const priorView = selected?.view;
634
- try {
635
- await passive.refreshShared(signal);
636
- } catch (error) {
637
- if (isCurrent() && !signal.aborted && notifyRecovery && !stopPersistenceError) notifyProblem(ctx, `State Flow passive memory is unavailable: ${diagnosticText(error)}`, "warning");
638
- return;
639
- }
727
+ if (snapshot.config.mode !== "passive" || runtime?.view) return;
728
+ const passive = await loadPassiveView(ctx, signal, isCurrent, notifyRecovery);
729
+ if (passive === undefined) return;
640
730
  assertCurrent();
641
731
  // An intervening passive patch or inspection owns its newer cache, even on the same physical branch.
642
- if (selected?.view !== priorView) return;
732
+ if (runtime?.view) return;
643
733
  runtime = selected = passive;
644
734
  installScopeStates();
645
735
  updateUi(ctx);
646
736
  } catch (error) {
647
737
  // Host failures before acceptance leave the selection unavailable, never invented empty memory.
648
738
  if (accepted || !isCurrent()) return;
649
- snapshot = selectedBoundaryFailure(diagnosticText(error));
739
+ snapshot = selectedBoundaryFailure(diagnosticText(error), snapshot.config.mode === "active" ? config.inactiveMode : snapshot.config.mode);
650
740
  installScopeStates();
651
741
  } finally {
652
742
  pending.awaitingAcceptance = false;
@@ -654,29 +744,41 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
654
744
  }
655
745
  }
656
746
 
747
+ /** Load current shared memory for passive projection without initializing or publishing it. */
748
+ async function loadPassiveView(ctx: ExtensionContext, signal: AbortSignal, isCurrent: () => boolean, notify: boolean): Promise<TemporalRuntime | undefined> {
749
+ const passive = createRuntime(ctx);
750
+ try {
751
+ await passive.refreshShared(signal);
752
+ } catch (error) {
753
+ if (isCurrent() && !signal.aborted && notify && !modePersistenceError) notifyProblem(ctx, `State Flow passive memory is unavailable: ${diagnosticText(error)}`, "warning");
754
+ return undefined;
755
+ }
756
+ return passive;
757
+ }
758
+
759
+ function selectedContinuation(ctx: ExtensionContext, reason: unknown): PassiveContinuation | undefined {
760
+ if (!retainsPhysicalSessionProjection(reason) || !runtime?.view) return undefined;
761
+ const boundary = findPassiveStopBoundary(ctx.sessionManager.getBranch(), ctx.sessionManager.getSessionId(), PASSIVE_STOP_ENTRY_TYPE);
762
+ return boundary === undefined ? undefined : createPassiveContinuation(
763
+ projectModelState(effectiveState), boundary.at, boundary.from, boundary.preserveContext,
764
+ );
765
+ }
766
+
657
767
  function settleSelection(ctx: ExtensionContext, reason: unknown, notifyRecovery: boolean, skipped: number, selectedMemory: boolean): void {
658
- const failure = !snapshot.config.enabled && snapshot.meta.validation?.attempt === 0 ? snapshot.meta.validation.error : undefined;
659
- if (failure !== undefined && notifyRecovery && !stopPersistenceError) {
660
- if (selectedHistoryExpired) notifyProblem(ctx, "State Flow history is outside the retained temporal window; /state-flow-start can use current session memory.", "warning");
768
+ const failure = !isActive() && snapshot.meta.validation?.attempt === 0 ? snapshot.meta.validation.error : undefined;
769
+ if (failure !== undefined && notifyRecovery && !modePersistenceError) {
770
+ if (selectedHistoryExpired) notifyProblem(ctx, "State Flow history is outside the retained temporal window; /state-flow-active can use current session memory.", "warning");
661
771
  else notifyProblem(ctx, `State Flow restore failed: ${failure}`, "error");
662
772
  }
663
- if (selectedMemory && retainsPhysicalSessionProjection(reason) && runtime?.view) {
664
- const boundary = findPassiveStopBoundary(ctx.sessionManager.getBranch(), ctx.sessionManager.getSessionId(), PASSIVE_STOP_ENTRY_TYPE);
665
- if (boundary !== undefined) {
666
- const continuation = createPassiveContinuation(
667
- projectModelState(overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session)),
668
- boundary.at,
669
- boundary.from,
670
- boundary.preserveContext,
671
- );
672
- if (!snapshot.config.enabled) passiveContinuation = continuation;
673
- else if (snapshot.meta.bootstrap) bootstrapContinuation = continuation;
674
- }
773
+ const continuation = selectedMemory ? selectedContinuation(ctx, reason) : undefined;
774
+ if (continuation !== undefined) {
775
+ if (!isActive()) passiveContinuation = continuation;
776
+ else if (snapshot.meta.bootstrap) bootstrapContinuation = continuation;
675
777
  }
676
- if (notifyRecovery && snapshot.config.enabled && skipped > 0) {
778
+ if (notifyRecovery && isActive() && skipped > 0) {
677
779
  notifyProblem(ctx, `State Flow skipped ${skipped} malformed snapshot(s); restored the last valid one.`, "warning");
678
780
  }
679
- if (snapshot.config.enabled) deferInferencePreparation();
781
+ if (isActive()) deferInferencePreparation();
680
782
  syncStateFlowTools();
681
783
  updateUi(ctx);
682
784
  }
@@ -698,7 +800,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
698
800
  async execute(_toolCallId, params, signal) {
699
801
  try {
700
802
  type ReadDetails = { path?: string; paths?: string[]; projection?: string; transitionId?: string };
701
- if (!passiveToolsAvailable()) throw new Error("State Flow tools are disabled by configuration");
803
+ if (!memoryToolsAvailable()) throw new Error("State Flow tools are off for this session");
702
804
  if (signal?.aborted) throw new Error("State Flow read was aborted");
703
805
  if (!runtime?.view) throw new Error("State Flow temporal runtime is unavailable");
704
806
  if (params.path === undefined && params.paths === undefined) throw new Error("read_state requires path or paths");
@@ -750,8 +852,9 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
750
852
  return new Text(separatedOutput(theme.fg("dim", formatPatchStateArguments(context.args))), 0, 0);
751
853
  },
752
854
  async execute(toolCallId, params, signal, _onUpdate, ctx) {
855
+ signal = signal ? AbortSignal.any([signal, memoryToolLifetime.signal]) : memoryToolLifetime.signal;
753
856
  try {
754
- if (!passiveToolsAvailable()) throw new Error("State Flow tools are disabled by configuration");
857
+ if (!memoryToolsAvailable()) throw new Error("State Flow tools are off for this session");
755
858
  assertPublicationAvailable();
756
859
  if (signal?.aborted) throw new Error("State Flow patch was aborted before materialization");
757
860
  if (!isObject(params)) throw new Error("patch_state requires an object");
@@ -773,10 +876,10 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
773
876
  const selected = runtime ??= createRuntime(ctx);
774
877
  const acquiredArtifacts = structuredClone([...artifactReads.successful.values()]);
775
878
  const acquiredSkills = structuredClone([...skillReads.successful.values()]);
776
- const previousEffective = overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session);
879
+ const previousEffective = effectiveState;
777
880
  return await selected.withPatchTransaction((transaction) => {
778
881
  if (runtime !== selected) throw new Error("State Flow session selection changed while awaiting publication");
779
- if (!passiveToolsAvailable()) throw new Error("State Flow tools are disabled by configuration");
882
+ if (!memoryToolsAvailable()) throw new Error("State Flow tools are off for this session");
780
883
  assertPublicationAvailable();
781
884
  for (const acquired of acquiredArtifacts) {
782
885
  const observation = inspectRegisteredArtifactPaths([acquired.path])[0];
@@ -803,7 +906,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
803
906
  }
804
907
  clearAcceptedAcquisitions(new Set(acquiredArtifacts.map(({ path }) => path)));
805
908
  updateUi(ctx);
806
- const updates = contextProjection.acceptPatch(previousEffective, overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session), patches, artifactHints);
909
+ const updates = contextProjection.acceptPatch(previousEffective, effectiveState, patches, artifactHints);
807
910
  const acknowledgement = changed
808
911
  ? `\nState materialized atomically at ${scopes.join("+")} scope${scopes.length === 1 ? "" : "s"}.`
809
912
  : "\nState already current.";
@@ -816,7 +919,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
816
919
  let attempted: unknown;
817
920
  try { attempted = structuredClone(params); } catch { attempted = undefined; }
818
921
  const cause = diagnosticText(error);
819
- recordDiagnostic(cause, /concurrently|advanced/.test(cause) ? "publication-conflict" : "invalid-patch", ctx, {
922
+ if (!signal.aborted && memoryToolsAvailable()) recordDiagnostic(cause, /concurrently|advanced/.test(cause) ? "publication-conflict" : "invalid-patch", ctx, {
820
923
  input: attempted,
821
924
  tool: PATCH_STATE_TOOL_NAME,
822
925
  toolCallId,
@@ -830,8 +933,8 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
830
933
  if (shuttingDown) return Promise.resolve({ ok: false, message: "State Flow is shutting down" });
831
934
  if (startActivation?.operation) return startActivation.operation;
832
935
  telegramStartPending = false;
833
- if (activeContext && !branchRestoration && snapshot.config.enabled) {
834
- return Promise.resolve({ ok: true, message: "State Flow is already enabled", signal: sharedInspectionLifetime.signal });
936
+ if (activeContext && !branchRestoration && isActive()) {
937
+ return Promise.resolve({ ok: true, message: "State Flow is already active", signal: sharedInspectionLifetime.signal });
835
938
  }
836
939
  const pending: NonNullable<typeof startActivation> = { controller: new AbortController() };
837
940
  startActivation = pending;
@@ -854,16 +957,17 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
854
957
  && ctx.sessionManager.getHeader()?.timestamp === timestamp;
855
958
  const superseded = (): StateFlowTelegramControlResult => {
856
959
  pending.abort();
857
- return { ok: false, message: "State Flow Start was superseded", signal: pending.signal };
960
+ return { ok: false, message: "State Flow activation was superseded", signal: pending.signal };
858
961
  };
859
962
  try {
860
- if (!activeContext) await waitForRecovery(restoreActiveBranch(ctx, undefined, false, pending), signal);
963
+ if (deferredBranch?.reason === "fork") await waitForRecovery(restoreActiveBranch(ctx, "fork", false, pending), signal);
964
+ else if (!activeContext) await waitForRecovery(restoreActiveBranch(ctx, undefined, false, pending), signal);
861
965
  else if (branchRestoration?.operation) await waitForRecovery(branchRestoration.operation, signal);
862
966
  if (!isOwner()) return superseded();
863
- if (snapshot.config.enabled) return { ok: true, message: "State Flow is already enabled", signal: sharedInspectionLifetime.signal };
967
+ if (isActive()) return { ok: true, message: "State Flow is already active", signal: sharedInspectionLifetime.signal };
864
968
  if (forkInitialization) {
865
- // Withdraw this join on cancellation without revoking the independently owned Stop.
866
- const stopping = stopPersistence?.operation;
969
+ // Withdraw this join on cancellation without revoking the independently owned mode persistence.
970
+ const stopping = inactivePersistence?.operation;
867
971
  if (stopping) await waitForRecovery(stopping, signal);
868
972
  if (!isOwner()) return superseded();
869
973
  await waitForRecovery(restoreActiveBranch(ctx, "fork", false, pending), signal);
@@ -873,7 +977,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
873
977
  return activateCurrentState(ctx, pending);
874
978
  } catch (error) {
875
979
  if (!isOwner()) return superseded();
876
- const message = conciseDiagnostic(`State Flow Start failed: ${diagnosticText(error)}`);
980
+ const message = conciseDiagnostic(`State Flow activation failed: ${diagnosticText(error)}`);
877
981
  notifyProblem(ctx, message, "error");
878
982
  return { ok: false, message, signal: AbortSignal.any([pending.signal, sharedInspectionLifetime.signal]) };
879
983
  }
@@ -885,36 +989,46 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
885
989
  const cwd = ctx.cwd;
886
990
  const file = ctx.sessionManager.getSessionFile();
887
991
  const timestamp = ctx.sessionManager.getHeader()?.timestamp;
888
- const initiallyEnabled = snapshot.config.enabled;
992
+ const initiallyActive = isActive();
889
993
  let accepted = false;
890
994
  let receipt = AbortSignal.any([pending.signal, sharedInspectionLifetime.signal]);
891
- const isCurrent = () => startActivation?.controller === pending && runtime === selected && !shuttingDown
995
+ const signal = ctx.signal ? AbortSignal.any([pending.signal, ctx.signal]) : pending.signal;
996
+ const isCurrent = () => startActivation?.controller === pending && !signal.aborted && runtime === selected && !shuttingDown
892
997
  && ctx.cwd === cwd && ctx.sessionManager.getSessionId() === owner && ctx.sessionManager.getSessionFile() === file
893
- && ctx.sessionManager.getHeader()?.timestamp === timestamp && snapshot.config.enabled === (accepted || initiallyEnabled);
998
+ && ctx.sessionManager.getHeader()?.timestamp === timestamp && isActive() === (accepted || initiallyActive);
894
999
  const superseded = (): StateFlowTelegramControlResult => {
895
1000
  pending.abort();
896
- return { ok: false, message: "State Flow Start was superseded", signal: pending.signal };
1001
+ return { ok: false, message: "State Flow activation was superseded", signal: pending.signal };
897
1002
  };
898
1003
  try {
899
1004
  const activation = createRuntime(ctx);
900
- const signal = ctx.signal ? AbortSignal.any([pending.signal, ctx.signal]) : pending.signal;
1005
+ const deferred = deferredBranch;
1006
+ if (deferred) {
1007
+ const discovery = discoverSnapshotData(ctx.sessionManager.getBranch());
1008
+ const retained = selectRetainedCheckpoint(discovery.candidates, config.inactiveMode);
1009
+ branchStartsWithoutRuntime = retained.kind === "pre-runtime" || (discovery.candidates.length === 0 && discovery.errors.length === 0);
1010
+ }
1011
+ // Off activation owns one cancellable current-head publication, not an independent read-only restore.
901
1012
  const result = await activation.withStartTransaction((current, publish) => {
902
1013
  signal.throwIfAborted();
903
- if (!isCurrent()) throw new Error("State Flow Start selection changed while awaiting publication");
1014
+ if (!isCurrent()) throw new Error("State Flow activation selection changed while awaiting publication");
904
1015
  if (!current && !branchStartsWithoutRuntime) throw new Error(snapshot.meta.validation?.error ?? "Current State Flow session memory is unavailable");
905
- const continuation = passiveContinuation ?? bootstrapContinuation;
906
- const bootstrap = hasPriorConversation(ctx.sessionManager.getBranch()) || passiveContinuation !== undefined;
1016
+ const boundary = deferred && retainsPhysicalSessionProjection(deferred.reason)
1017
+ ? findPassiveStopBoundary(ctx.sessionManager.getBranch(), owner, PASSIVE_STOP_ENTRY_TYPE) : undefined;
1018
+ const bootstrap = hasPriorConversation(ctx.sessionManager.getBranch()) || passiveContinuation !== undefined || boundary !== undefined;
907
1019
  const activated = current ? resumeEpisode(current, bootstrap) : startEpisode(bootstrap);
908
1020
  const recoveredCurrent = selectedHistoryExpired;
909
1021
  const publication = publish(activated);
910
1022
  accepted = true;
911
- cancelStopPersistence();
1023
+ cancelInactivePersistence();
912
1024
  runtime = selected = activation;
913
1025
  snapshot = activated;
914
1026
  activeContext = ctx;
915
1027
  selectedHistoryExpired = false;
916
- stopPersistenceError = undefined;
1028
+ modePersistenceError = undefined;
917
1029
  installScopeStates();
1030
+ const continuation = passiveContinuation ?? bootstrapContinuation ?? (deferred ? selectedContinuation(ctx, deferred.reason) : undefined);
1031
+ deferredBranch = undefined;
918
1032
  clearRunTransient();
919
1033
  contextProjection.reset();
920
1034
  passiveContinuation = undefined;
@@ -924,32 +1038,49 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
924
1038
  recordPolicyPublication(publication, ctx);
925
1039
  syncStateFlowTools();
926
1040
  updateUi(ctx);
1041
+ if (forkInitialization) {
1042
+ forkInitialization = false;
1043
+ pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, { reset: true, owner });
1044
+ }
927
1045
  appendCheckpoint();
928
1046
  ctx.ui.notify(
929
1047
  recoveredCurrent
930
- ? "State Flow enabled from current session memory; unavailable historical state was not restored."
1048
+ ? "State Flow active from current session memory; unavailable historical state was not restored."
931
1049
  : snapshot.meta.bootstrap
932
- ? "State Flow enabled. The next complete agent run will migrate active context into state."
933
- : "State Flow enabled. The next prompt starts a stateful agent run.",
1050
+ ? "State Flow active. The next complete agent run will migrate active context into state."
1051
+ : "State Flow active. The next prompt starts a stateful agent run.",
934
1052
  "info",
935
1053
  );
936
- return { ok: true, message: "State Flow enabled", signal: receipt };
1054
+ return { ok: true, message: "State Flow active", signal: receipt };
937
1055
  }, signal, branchStartsWithoutRuntime);
938
1056
  return isCurrent() ? result : superseded();
939
1057
  } catch (error) {
940
1058
  if (!isCurrent()) return superseded();
941
- const message = conciseDiagnostic(`${accepted ? "State Flow enabled; lifecycle update failed" : "State Flow Start failed"}: ${diagnosticText(error)}`);
1059
+ const message = conciseDiagnostic(`${accepted ? "State Flow active; lifecycle update failed" : "State Flow activation failed"}: ${diagnosticText(error)}`);
942
1060
  notifyProblem(ctx, message, accepted ? "warning" : "error");
943
1061
  return { ok: accepted, message, signal: receipt };
944
1062
  }
945
1063
  }
946
1064
 
947
- pi.registerCommand("state-flow-start", {
948
- description: "Start State Flow mode",
949
- handler: async (_args, ctx) => {
950
- await startStateFlow(ctx);
951
- },
952
- });
1065
+ const MODE_COMMAND_DESCRIPTIONS: Record<StateFlowMode, string> = {
1066
+ active: "Make State Flow active on the current session branch",
1067
+ passive: "Use passive State Flow memory on the current session branch",
1068
+ off: "Turn State Flow tools and context off on the current session branch",
1069
+ };
1070
+
1071
+ /** Terminal commands and Telegram controls share these lifecycle owners. */
1072
+ function selectMode(ctx: ExtensionContext, mode: StateFlowMode): Promise<StateFlowTelegramControlResult> {
1073
+ return mode === "active" ? startStateFlow(ctx) : deactivateStateFlow(ctx, mode);
1074
+ }
1075
+
1076
+ for (const mode of ["active", "passive", "off"] as const) {
1077
+ pi.registerCommand(`state-flow-${mode}`, {
1078
+ description: MODE_COMMAND_DESCRIPTIONS[mode],
1079
+ handler: async (_args, ctx) => {
1080
+ await selectMode(ctx, mode);
1081
+ },
1082
+ });
1083
+ }
953
1084
 
954
1085
  pi.registerCommand("state-flow-status", {
955
1086
  description: "Show State Flow runtime status",
@@ -958,97 +1089,219 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
958
1089
  },
959
1090
  });
960
1091
 
961
- function stopStateFlow(ctx: ExtensionContext): Promise<StateFlowTelegramControlResult> {
962
- cancelStartActivation();
1092
+ /** Local policy, tools and UI change before any canonical wait. */
1093
+ function applyInactiveMode(ctx: ExtensionContext, mode: InactiveMode): void {
1094
+ snapshot = deactivateEpisode(snapshot, mode);
1095
+ clearRunTransient();
1096
+ syncStateFlowTools();
1097
+ updateUi(ctx);
1098
+ }
1099
+
1100
+ function deactivateStateFlow(ctx: ExtensionContext, mode: InactiveMode): Promise<StateFlowTelegramControlResult> {
963
1101
  if (shuttingDown) return Promise.resolve({ ok: false, message: "State Flow is shutting down" });
964
- if (stopPersistence?.operation) return stopPersistence.operation;
1102
+ if (mode === "off") return selectOff(ctx);
1103
+ cancelStartActivation();
1104
+ telegramStartPending = false;
1105
+ const persisting = inactivePersistence;
1106
+ if (persisting?.operation && !persisting.published) {
1107
+ // Pending inactive choices coalesce: acceptance persists whichever inactive mode is current then.
1108
+ if (snapshot.config.mode !== mode) applyInactiveMode(ctx, mode);
1109
+ return persisting.operation;
1110
+ }
965
1111
  if (!activeContext) void restoreActiveBranch(ctx, undefined, false);
966
1112
  const restoring = branchRestoration;
1113
+ // Read-only recovery also retains the latest policy; its native write fence is updated below.
1114
+ if (restoring) restoring.requestedMode = mode;
967
1115
  if (restoring?.awaitingAcceptance) {
968
- // Stop changes policy without cancelling memory restoration or initialization.
969
- restoring.passiveRequested = true;
970
- snapshot = stopEpisode(snapshot);
971
- clearRunTransient();
972
- syncStateFlowTools();
973
- updateUi(ctx);
974
- return Promise.resolve({ ok: true, message: "State Flow disabled; memory restoration continues" });
1116
+ // Passive keeps independently owned restoration; Off withdraws it through selectOff.
1117
+ applyInactiveMode(ctx, mode);
1118
+ return Promise.resolve({ ok: true, message: `State Flow ${mode}; memory restoration continues` });
1119
+ }
1120
+ if (snapshot.config.mode === mode) {
1121
+ const retained = selectRetainedCheckpoint(discoverSnapshotData(ctx.sessionManager.getBranch()).candidates, config.inactiveMode);
1122
+ const recordedMode = retained.kind === "pre-runtime" ? retained.mode : retained.kind === "boundary" ? retained.checkpoint.mode : undefined;
1123
+ if (modePersistenceError || recordedMode === mode) return Promise.resolve({ ok: true, message: `State Flow is already ${mode}` });
975
1124
  }
976
- const pending: NonNullable<typeof stopPersistence> = { controller: new AbortController() };
977
- stopPersistence = pending;
978
- const operation = persistStoppedState(ctx, pending.controller);
1125
+ cancelInactivePersistence();
1126
+ const pending: NonNullable<typeof inactivePersistence> = { controller: new AbortController() };
1127
+ inactivePersistence = pending;
1128
+ const operation = deferredBranch && mode === "passive" && !branchStartsWithoutRuntime
1129
+ ? acquirePassiveMode(ctx, pending, deferredBranch.reason)
1130
+ : persistInactiveMode(ctx, pending, mode);
979
1131
  pending.operation = operation;
980
- const finished = () => { if (stopPersistence === pending) stopPersistence = undefined; };
1132
+ const finished = () => { if (inactivePersistence === pending) inactivePersistence = undefined; };
981
1133
  void operation.then(finished, finished);
982
1134
  return operation;
983
1135
  }
984
1136
 
985
- async function persistStoppedState(ctx: ExtensionContext, pending: AbortController): Promise<StateFlowTelegramControlResult> {
986
- telegramStartPending = false;
1137
+ /** Off owns cancellation and native policy only; no canonical transaction or semantic handoff. */
1138
+ function selectOff(ctx: ExtensionContext): Promise<StateFlowTelegramControlResult> {
1139
+ const address = sessionAddress(ctx);
1140
+ const owner = ctx.sessionManager.getSessionId();
1141
+ const branch = ctx.sessionManager.getBranch();
987
1142
  const selected = runtime;
1143
+ const sameOwner = selected?.sessionKey === address.key && selected.cwd === ctx.cwd;
1144
+ const forkRecorded = hasPendingFork(branch, owner, PASSIVE_STOP_ENTRY_TYPE);
1145
+ const pendingFork = forkRecorded || (sameOwner && forkInitialization) || (deferredBranch?.owner === address.key && deferredBranch.cwd === ctx.cwd && deferredBranch.reason === "fork");
1146
+ const policy = findBranchPolicy(branch, owner, PASSIVE_STOP_ENTRY_TYPE, config.inactiveMode);
1147
+ const sameDeferred = deferredBranch?.owner === address.key && deferredBranch.cwd === ctx.cwd;
1148
+ const current = sameOwner || sameDeferred ? snapshot : emptySnapshot("off");
1149
+ if (!sameOwner && !sameDeferred) modePersistenceError = policy?.persistenceError;
1150
+ const alreadyOff = current.config.mode === "off" && policy?.mode === "off";
1151
+ const incoming = sameOwner || sameDeferred ? current.meta.bootstrap ? bootstrapContinuation : passiveContinuation : undefined;
1152
+ const unfinished = current.meta.specification !== undefined || (inferencePreparation?.prompt !== undefined && !inferencePreparation.accepted)
1153
+ || (isActive() && hasUncheckpointedConversation(branch));
1154
+ const at = incoming?.startedAt ?? Date.now();
1155
+ const from = incoming ? incoming.activeRunStartedAt : (sameOwner || sameDeferred) && (!ctx.isIdle() || unfinished) ? runAnchorTimestamp : undefined;
1156
+ const preserveContext = modePersistenceError !== undefined || (incoming ? incoming.preserveContext : current.meta.bootstrap === true || (unfinished && from === undefined));
1157
+ const discovery = discoverSnapshotData(branch);
1158
+ const preRuntime = (sameOwner && branchStartsWithoutRuntime) || (discovery.candidates.length === 0 && discovery.errors.length === 0);
1159
+ let checkpoint: RetainedPiCheckpoint | undefined;
1160
+ try {
1161
+ checkpoint = !modePersistenceError && current.meta.validation?.attempt !== 0
1162
+ ? preRuntime ? preRuntimeCheckpoint("off") : sameOwner && selected?.view ? selected.retainedCheckpoint(deactivateEpisode(current, "off")) : undefined
1163
+ : undefined;
1164
+ } catch {
1165
+ // An unusable cached bookmark cannot prevent native Off policy or authorize replacement memory.
1166
+ }
1167
+ cancelStartActivation();
1168
+ cancelBranchRestoration();
1169
+ cancelInactivePersistence();
1170
+ cancelBackupWork();
1171
+ memoryToolLifetime.abort();
1172
+ memoryToolLifetime = new AbortController();
1173
+ clearRunTransient();
1174
+ contextProjection.reset();
1175
+ telegramStartPending = false;
1176
+ bootstrapContinuation = undefined;
1177
+ passiveContinuation = undefined;
1178
+ artifactInvalidations = [];
1179
+ artifactHints = {};
1180
+ artifactReads.setCandidates([]);
1181
+ runtime = undefined;
1182
+ snapshot = emptySnapshot("off");
1183
+ selectedHistoryExpired = false;
1184
+ branchStartsWithoutRuntime = preRuntime;
1185
+ forkInitialization = pendingFork;
1186
+ deferredBranch = { owner: address.key, cwd: ctx.cwd, reason: pendingFork ? "fork" : undefined };
1187
+ activeContext = ctx;
1188
+ installScopeStates();
1189
+ syncStateFlowTools();
1190
+ updateUi(ctx);
1191
+ try {
1192
+ if (!alreadyOff || (pendingFork && !forkRecorded)) {
1193
+ if (!checkpoint || !preRuntime || pendingFork || preserveContext || from !== undefined || modePersistenceError) pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, {
1194
+ owner, at, mode: "off", memoryDeferred: true,
1195
+ ...(from === undefined ? {} : { from }),
1196
+ ...(preserveContext ? { preserveContext: true } : {}),
1197
+ ...(pendingFork ? { forkPending: true } : {}),
1198
+ ...(modePersistenceError === undefined ? {} : { persistenceError: modePersistenceError }),
1199
+ });
1200
+ if (checkpoint) pi.appendEntry(SNAPSHOT_ENTRY_TYPE, checkpoint);
1201
+ }
1202
+ return Promise.resolve({ ok: true, message: alreadyOff ? "State Flow is already off" : "State Flow off" });
1203
+ } catch (error) {
1204
+ const message = conciseDiagnostic(`State Flow off; native mode recording failed: ${diagnosticText(error)}`);
1205
+ notifyProblem(ctx, message, "error");
1206
+ return Promise.resolve({ ok: false, message });
1207
+ }
1208
+ }
1209
+
1210
+ async function acquirePassiveMode(ctx: ExtensionContext, pending: NonNullable<typeof inactivePersistence>, reason: unknown): Promise<StateFlowTelegramControlResult> {
1211
+ await restoreActiveBranch(ctx, reason, true, pending.controller, "passive");
1212
+ if (inactivePersistence !== pending || pending.controller.signal.aborted || shuttingDown) {
1213
+ return { ok: false, message: "State Flow mode change was superseded" };
1214
+ }
1215
+ return persistInactiveMode(ctx, pending, "passive");
1216
+ }
1217
+
1218
+ async function persistInactiveMode(ctx: ExtensionContext, pending: NonNullable<typeof inactivePersistence>, mode: InactiveMode): Promise<StateFlowTelegramControlResult> {
1219
+ let selected = runtime;
988
1220
  const owner = ctx.sessionManager.getSessionId();
989
1221
  const current = snapshot;
1222
+ const wasActive = current.config.mode === "active";
990
1223
  let unfinished = current.meta.specification !== undefined
991
1224
  || (inferencePreparation?.prompt !== undefined && !inferencePreparation.accepted);
992
- snapshot = stopEpisode(current);
993
- clearRunTransient();
994
- syncStateFlowTools();
995
- updateUi(ctx);
996
- if (stopPersistenceError) return { ok: true, message: "State Flow disabled; memory writes remain paused" };
1225
+ applyInactiveMode(ctx, mode);
997
1226
  const stoppedAt = Date.now();
1227
+ if (modePersistenceError) {
1228
+ // Writes stay paused; the native marker records only the newly selected inactive policy.
1229
+ pending.published = true;
1230
+ pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, {
1231
+ at: passiveContinuation?.startedAt ?? stoppedAt,
1232
+ ...(passiveContinuation?.activeRunStartedAt === undefined ? {} : { from: passiveContinuation.activeRunStartedAt }),
1233
+ preserveContext: true, owner, persistenceError: modePersistenceError, mode,
1234
+ });
1235
+ return { ok: true, message: `State Flow ${mode}; memory writes remain paused` };
1236
+ }
998
1237
  const incomingBoundary = current.meta.bootstrap ? bootstrapContinuation : undefined;
999
1238
  const anchor = runAnchorTimestamp;
1000
1239
  const idle = ctx.isIdle();
1001
- const isCurrent = () => stopPersistence?.controller === pending && runtime === selected
1002
- && !shuttingDown && ctx.sessionManager.getSessionId() === owner && !snapshot.config.enabled;
1003
- const superseded = (): StateFlowTelegramControlResult => ({ ok: false, message: "State Flow Stop was superseded" });
1240
+ const isCurrent = () => inactivePersistence === pending && runtime === selected
1241
+ && !shuttingDown && ctx.sessionManager.getSessionId() === owner && !isActive();
1242
+ const superseded = (): StateFlowTelegramControlResult => ({ ok: false, message: "State Flow mode change was superseded" });
1004
1243
  const freezeHandoff = (): PassiveContinuation | undefined => {
1005
1244
  contextProjection.reset();
1006
- const handoff = (current.config.enabled || stopPersistenceError) && selected?.view && current.meta.validation?.attempt !== 0
1245
+ const handoff = (wasActive || modePersistenceError) && selected?.view && current.meta.validation?.attempt !== 0
1007
1246
  ? createPassiveContinuation(
1008
- projectModelState(overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session)),
1247
+ projectModelState(effectiveState),
1009
1248
  incomingBoundary?.startedAt ?? stoppedAt,
1010
1249
  incomingBoundary ? incomingBoundary.activeRunStartedAt : !idle || unfinished ? anchor : undefined,
1011
- stopPersistenceError !== undefined || (incomingBoundary ? incomingBoundary.preserveContext : current.meta.bootstrap === true || (unfinished && anchor === undefined)),
1250
+ modePersistenceError !== undefined || (incomingBoundary ? incomingBoundary.preserveContext : current.meta.bootstrap === true || (unfinished && anchor === undefined)),
1012
1251
  )
1013
1252
  : undefined;
1014
- passiveContinuation = handoff ?? (!current.config.enabled ? passiveContinuation : undefined);
1253
+ // Repeated Passive choices keep the already selected continuation boundary.
1254
+ passiveContinuation = handoff ?? (!wasActive ? passiveContinuation : undefined);
1015
1255
  return handoff;
1016
1256
  };
1017
1257
  const complete = (): StateFlowTelegramControlResult => {
1258
+ pending.published = true;
1018
1259
  const exitHandoff = freezeHandoff();
1019
- if (exitHandoff || stopPersistenceError) pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, {
1260
+ const selectedMode = snapshot.config.mode;
1261
+ if (exitHandoff || modePersistenceError) pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, {
1020
1262
  at: exitHandoff?.startedAt ?? stoppedAt,
1021
1263
  ...(exitHandoff?.activeRunStartedAt === undefined ? {} : { from: exitHandoff.activeRunStartedAt }),
1022
- ...(exitHandoff?.preserveContext || stopPersistenceError ? { preserveContext: true } : {}),
1023
- ...(stopPersistenceError === undefined ? {} : { owner, persistenceError: stopPersistenceError }),
1264
+ ...(exitHandoff?.preserveContext || modePersistenceError ? { preserveContext: true } : {}),
1265
+ ...(modePersistenceError === undefined ? {} : { owner, persistenceError: modePersistenceError, mode: selectedMode }),
1024
1266
  });
1025
- if (!stopPersistenceError) {
1267
+ if (!modePersistenceError) {
1026
1268
  appendCheckpoint();
1027
- return { ok: true, message: "State Flow disabled" };
1269
+ return { ok: true, message: `State Flow ${selectedMode}` };
1028
1270
  }
1029
- const message = conciseDiagnostic(`State Flow disabled; memory writes paused: ${stopPersistenceError}`);
1271
+ const message = conciseDiagnostic(`State Flow ${selectedMode}; memory writes paused: ${modePersistenceError}`);
1030
1272
  notifyProblem(ctx, message, "warning");
1031
1273
  return { ok: true, message };
1032
1274
  };
1033
1275
  let accepted = false;
1034
1276
  try {
1035
- unfinished ||= current.config.enabled && hasUncheckpointedConversation(ctx.sessionManager.getBranch());
1277
+ unfinished ||= wasActive && hasUncheckpointedConversation(ctx.sessionManager.getBranch());
1036
1278
  // Projection changes before any wait; capture the old run's boundary before later native input can replace it.
1037
1279
  freezeHandoff();
1038
1280
  bootstrapContinuation = undefined;
1039
1281
  artifactInvalidations = [];
1040
1282
  artifactReads.setCandidates([]);
1041
1283
  assertPublicationAvailable();
1042
- if (branchStartsWithoutRuntime || !selected?.view) return complete();
1043
- const signal = ctx.signal ? AbortSignal.any([pending.signal, ctx.signal]) : pending.signal;
1284
+ const signal = ctx.signal ? AbortSignal.any([pending.controller.signal, ctx.signal]) : pending.controller.signal;
1285
+ if (branchStartsWithoutRuntime || !selected?.view) {
1286
+ // A pre-runtime choice is native-only; Passive then loads shared memory without initializing storage.
1287
+ const result = complete();
1288
+ if (snapshot.config.mode !== "passive" || runtime?.view) return result;
1289
+ const passive = await loadPassiveView(ctx, signal, isCurrent, true);
1290
+ if (passive && isCurrent() && !signal.aborted && !runtime?.view) {
1291
+ runtime = selected = passive;
1292
+ installScopeStates();
1293
+ updateUi(ctx);
1294
+ }
1295
+ return isCurrent() ? result : superseded();
1296
+ }
1044
1297
  const result = await selected.withLifecycleTransaction((publish) => {
1045
1298
  signal.throwIfAborted();
1046
- if (!isCurrent()) throw new Error("State Flow Stop selection changed while awaiting publication");
1299
+ if (!isCurrent()) throw new Error("State Flow mode selection changed while awaiting publication");
1047
1300
  assertPublicationAvailable();
1048
- const stopped = stopEpisode(snapshot);
1049
- const publication = publish(stopped);
1301
+ const next = structuredClone(snapshot);
1302
+ const publication = publish(next);
1050
1303
  accepted = true;
1051
- snapshot = stopped;
1304
+ snapshot = next;
1052
1305
  installScopeStates();
1053
1306
  recordPolicyPublication(publication, ctx);
1054
1307
  return complete();
@@ -1058,11 +1311,11 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1058
1311
  if (!isCurrent()) return superseded();
1059
1312
  const cause = diagnosticText(error);
1060
1313
  if (accepted) {
1061
- const message = conciseDiagnostic(`State Flow disabled; lifecycle update failed: ${cause}`);
1314
+ const message = conciseDiagnostic(`State Flow ${snapshot.config.mode}; lifecycle update failed: ${cause}`);
1062
1315
  notifyProblem(ctx, message, "warning");
1063
1316
  return { ok: true, message };
1064
1317
  }
1065
- stopPersistenceError = cause.trim() ? cause : "Canonical State Flow persistence failed";
1318
+ modePersistenceError = cause.trim() ? cause : "Canonical State Flow persistence failed";
1066
1319
  bootstrapContinuation = undefined;
1067
1320
  artifactInvalidations = [];
1068
1321
  artifactReads.setCandidates([]);
@@ -1070,18 +1323,11 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1070
1323
  }
1071
1324
  }
1072
1325
 
1073
- pi.registerCommand("state-flow-stop", {
1074
- description: "Stop State Flow on the current session branch",
1075
- handler: async (_args, ctx) => {
1076
- await stopStateFlow(ctx);
1077
- },
1078
- });
1079
-
1080
1326
  const telegram = createStateFlowTelegramAdapter({
1081
1327
  ...(options.telegram?.load === undefined ? {} : { load: options.telegram.load }),
1082
1328
  port: {
1083
1329
  snapshot: () => ({
1084
- enabled: snapshot.config.enabled,
1330
+ mode: snapshot.config.mode,
1085
1331
  step: snapshot.meta.step,
1086
1332
  revisions: scopeRevisions(),
1087
1333
  bootstrap: snapshot.meta.bootstrap === true,
@@ -1089,13 +1335,11 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1089
1335
  }),
1090
1336
  inspect: inspectTelegramState,
1091
1337
  canStartNow: () => activeContext === undefined || activeContext.isIdle(),
1092
- start: () => {
1093
- if (!activeContext) throw new Error("State Flow is not attached to an active session yet");
1094
- return startStateFlow(activeContext);
1095
- },
1096
- stop: async () => {
1338
+ select: async (mode) => {
1097
1339
  if (!activeContext) throw new Error("State Flow is not attached to an active session yet");
1098
- const operation = stopStateFlow(activeContext);
1340
+ const operation = selectMode(activeContext, mode);
1341
+ if (mode === "active") return operation;
1342
+ // Inactive choices revoke older presentation at once; the receipt belongs to the new lifetime.
1099
1343
  const signal = sharedInspectionLifetime.signal;
1100
1344
  try {
1101
1345
  return { ...await operation, signal };
@@ -1117,8 +1361,8 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1117
1361
  pi.on("before_agent_start", (event) => {
1118
1362
  // Native run identity is independent of semantic enablement and survives mode toggles.
1119
1363
  runAnchorTimestamp = undefined;
1120
- if (!snapshot.config.enabled) {
1121
- if (!config.passiveBootstrap || !runtime?.view) return;
1364
+ if (!isActive()) {
1365
+ if (!passiveMemoryAvailable()) return;
1122
1366
  (event.systemPromptOptions.sections ??= {}).state_flow = PASSIVE_MEMORY_PROTOCOL;
1123
1367
  return;
1124
1368
  }
@@ -1133,20 +1377,23 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1133
1377
  });
1134
1378
 
1135
1379
  pi.on("context_with_system", (event) => {
1136
- const protocol = snapshot.config.enabled ? stateFlowProtocol(snapshot.meta.bootstrap === true)
1137
- : config.passiveBootstrap && runtime?.view ? PASSIVE_MEMORY_PROTOCOL : undefined;
1380
+ // Off removes the owned section, including one contributed before an in-flight mode change.
1381
+ const protocol = isActive() ? stateFlowProtocol(snapshot.meta.bootstrap === true)
1382
+ : passiveMemoryAvailable() ? PASSIVE_MEMORY_PROTOCOL : undefined;
1138
1383
  return { messages: projectSystemProtocol(event.messages, protocol) };
1139
1384
  });
1140
1385
 
1141
1386
  function projectContext(messages: AgentMessage[]) {
1387
+ // Off injects no State Flow context, including a retained passive handoff.
1388
+ if (snapshot.config.mode === "off") return;
1142
1389
  if (runtime?.view) refreshArtifactHints();
1143
- if (!snapshot.config.enabled && !passiveContinuation && (!config.passiveBootstrap || !runtime?.view)) return;
1390
+ if (!isActive() && !passiveContinuation && !passiveMemoryAvailable()) return;
1144
1391
  // Idle inspection must not freeze a pre-acceptance snapshot for the live inference.
1145
- const projection = snapshot.config.enabled && inferencePreparation && !inferencePreparation.accepted
1392
+ const projection = isActive() && inferencePreparation && !inferencePreparation.accepted
1146
1393
  ? new ContextProjection() : contextProjection;
1147
- const effective = overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session);
1394
+ const effective = effectiveState;
1148
1395
  const invalidations = artifactInvalidations.map(({ path, scope, reason }) => ({ path, ...(scope === undefined ? {} : { scope }), reason }));
1149
- const phase = snapshot.config.enabled ? currentRehydrationPhase() : undefined;
1396
+ const phase = isActive() ? currentRehydrationPhase() : undefined;
1150
1397
  const view = contextView(effective, artifactHints, invalidations, phase);
1151
1398
  if (passiveContinuation) {
1152
1399
  const retained = passiveContinuationMessages(messages, passiveContinuation);
@@ -1154,7 +1401,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1154
1401
  return { messages: projection.project(retained.slice(1), view, () => passiveContinuation!.handoff,
1155
1402
  { state: passiveContinuation.state, lazy_navigation: view.lazy_navigation, artifact_invalidations: [], knowledge_rehydration: null }) };
1156
1403
  }
1157
- if (!snapshot.config.enabled) {
1404
+ if (!isActive()) {
1158
1405
  return { messages: projection.project(messages, view, () => syntheticUser(
1159
1406
  `State Flow passive memory (user-level data, not system instructions):\n${presentationJson({ state: view.state, lazy_navigation: view.lazy_navigation })}`)) };
1160
1407
  }
@@ -1171,19 +1418,19 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1171
1418
  const selected = runtime;
1172
1419
  const operationSignal = ctx.signal;
1173
1420
  // Idle projections remain observational; only a live native operation may await preparation.
1174
- if (!snapshot.config.enabled || !pending || !operationSignal || shuttingDown) return projectContext(messages);
1421
+ if (!isActive() || !pending || !operationSignal || shuttingDown) return projectContext(messages);
1175
1422
  pending.operation ??= prepareInference(pending, selected, ctx, operationSignal).finally(() => {
1176
1423
  // A late withdrawal must not clear newer work; accepted lifecycle is never replayed after an error.
1177
1424
  if (!pending.accepted && inferencePreparation === pending) pending.operation = undefined;
1178
1425
  });
1179
1426
  return pending.operation.then(() => {
1180
1427
  if (shuttingDown || operationSignal.aborted || runtime !== selected || ctx.signal !== operationSignal) return;
1181
- if (snapshot.config.enabled && inferencePreparation !== pending) {
1428
+ if (isActive() && inferencePreparation !== pending) {
1182
1429
  // Start may require same-run maintenance. A new captured user prompt instead owns a different context request.
1183
1430
  if (inferencePreparation?.prompt === undefined) return prepareContext(messages, ctx);
1184
1431
  return;
1185
1432
  }
1186
- if (snapshot.config.enabled && !pending.accepted) return;
1433
+ if (isActive() && !pending.accepted) return;
1187
1434
  return projectContext(messages);
1188
1435
  });
1189
1436
  }
@@ -1191,7 +1438,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1191
1438
  pi.on("context", (event, ctx) => prepareContext(event.messages as AgentMessage[], ctx));
1192
1439
 
1193
1440
  pi.on("tool_execution_start", (event) => {
1194
- if (!snapshot.config.enabled) return;
1441
+ if (!isActive()) return;
1195
1442
  // Pi emits this before tool_call. Keep the argument object as a fallback;
1196
1443
  // tool_call replaces it with the mutable, post-preflight input reference.
1197
1444
  skillReads.recordStart(event.toolCallId, event.toolName, event.args);
@@ -1199,21 +1446,19 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1199
1446
  });
1200
1447
 
1201
1448
  pi.on("tool_call", (event, ctx) => {
1202
- if (!snapshot.config.enabled) return;
1449
+ if (!isActive()) return;
1203
1450
  const batch = findAssistantToolBatch(ctx.sessionManager, event.toolCallId);
1204
1451
  const patchCalls = batch?.filter((name) => name === PATCH_STATE_TOOL_NAME).length ?? 0;
1205
1452
  if (patchCalls > 0) {
1206
1453
  if (patchCalls !== 1) {
1207
- return {
1208
- block: true,
1209
- reason: "A State Flow barrier response must contain exactly one patch_state call",
1210
- };
1454
+ const reason = "A State Flow barrier response must contain exactly one patch_state call";
1455
+ recordDiagnostic(reason, "barrier-block", ctx, { tool: event.toolName, toolCallId: event.toolCallId, batchToolNames: batch });
1456
+ return { block: true, reason };
1211
1457
  }
1212
1458
  if (event.toolName !== PATCH_STATE_TOOL_NAME) {
1213
- return {
1214
- block: true,
1215
- reason: "Blocked by the patch_state barrier; reconsider this action after State Flow rematerializes context",
1216
- };
1459
+ const reason = "Blocked by the patch_state barrier; reconsider this action after State Flow rematerializes context";
1460
+ recordDiagnostic(reason, "barrier-block", ctx, { tool: event.toolName, toolCallId: event.toolCallId, batchToolNames: batch });
1461
+ return { block: true, reason };
1217
1462
  }
1218
1463
  }
1219
1464
  skillReads.recordCall(event.toolCallId, event.toolName, event.input);
@@ -1221,14 +1466,14 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1221
1466
  });
1222
1467
 
1223
1468
  pi.on("tool_execution_end", (event) => {
1224
- if (!snapshot.config.enabled) return;
1469
+ if (!isActive()) return;
1225
1470
  skillReads.recordEnd(event.toolCallId, event.toolName, event.isError);
1226
1471
  dropCurrentSkillReads();
1227
1472
  artifactReads.recordEnd(event.toolCallId, event.toolName, event.isError);
1228
1473
  });
1229
1474
 
1230
1475
  pi.on("tool_result", (event) => {
1231
- if (!snapshot.config.enabled) return;
1476
+ if (!isActive()) return;
1232
1477
  const read = skillReads.recordResult(event.toolName, event.input, event.isError);
1233
1478
  if (!read) return;
1234
1479
  dropCurrentSkillReads();
@@ -1240,7 +1485,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1240
1485
  pi.on("message_end", (event, ctx): any => {
1241
1486
  // Observe actual user events even while disabled; Start/Stop cannot invent or erase them.
1242
1487
  if (event.message.role === "user" && runAnchorTimestamp === undefined) runAnchorTimestamp = event.message.timestamp;
1243
- if (shuttingDown || !snapshot.config.enabled) return;
1488
+ if (shuttingDown || !isActive()) return;
1244
1489
  if (event.message.role !== "assistant") return;
1245
1490
  cancelResponseReconciliation();
1246
1491
  const message = event.message as unknown as { role: "assistant"; stopReason?: string; content?: unknown };
@@ -1255,7 +1500,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1255
1500
  pi.on("turn_end", async (event, ctx) => {
1256
1501
  if (shuttingDown) return;
1257
1502
  const pending = responseReconciliation;
1258
- if (!snapshot.config.enabled || !pending) {
1503
+ if (!isActive() || !pending) {
1259
1504
  updateUi(ctx);
1260
1505
  return;
1261
1506
  }
@@ -1267,7 +1512,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1267
1512
  if (!selected?.view) throw new Error("Temporal State Flow runtime is unavailable; reload before publishing");
1268
1513
  await selected.withPatchTransaction((transaction) => {
1269
1514
  signal.throwIfAborted();
1270
- if (runtime !== selected || responseReconciliation !== pending || !snapshot.config.enabled) {
1515
+ if (runtime !== selected || responseReconciliation !== pending || !isActive()) {
1271
1516
  throw new Error("State Flow response selection changed while awaiting publication");
1272
1517
  }
1273
1518
  assertPublicationAvailable();
@@ -1306,20 +1551,22 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1306
1551
  });
1307
1552
 
1308
1553
  pi.on("session_before_compact", (event) => {
1309
- if (!compactionPlan) return;
1310
- if (compactionStopped && event.reason === "manual" && event.customInstructions === compactionMarker) return { cancel: true };
1311
- const result = stateFlowCompactionResult(compactionPlan, compactionMarker, event);
1554
+ if (event.reason !== "manual" || !event.customInstructions?.startsWith("state-flow-boundary:")) return;
1555
+ const marker = compactionMarker;
1556
+ if (shuttingDown || compactionStopped || !isActive() || !compactionPlan || !marker || event.customInstructions !== marker) return { cancel: true };
1557
+ const result = stateFlowCompactionResult(compactionPlan, marker, event);
1312
1558
  if (result === undefined || "cancel" in result) return result;
1313
1559
  return { compaction: result };
1314
1560
  });
1315
1561
 
1316
1562
  pi.on("agent_before_settle", async (_event, ctx) => {
1317
- if (shuttingDown || !turnAcceptedForBackup) return;
1563
+ if (shuttingDown || !memoryToolsAvailable() || !turnAcceptedForBackup) return;
1318
1564
  turnAcceptedForBackup = false;
1319
1565
  if (!backupPending) return;
1320
1566
  backupPending = false;
1321
1567
  const operationSignal = ctx.signal;
1322
- const signal = operationSignal ? AbortSignal.any([backupLifetime.signal, operationSignal]) : backupLifetime.signal;
1568
+ const lifetime = backupLifetime.signal;
1569
+ const signal = operationSignal ? AbortSignal.any([lifetime, operationSignal]) : lifetime;
1323
1570
  const selected = runtime;
1324
1571
  try {
1325
1572
  if (existsSync(join(repositoryRoot, ".git"))) {
@@ -1330,9 +1577,9 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1330
1577
  backupOperations.add(operation);
1331
1578
  try { await operation; }
1332
1579
  finally { backupOperations.delete(operation); }
1333
- if (signal.aborted) return;
1580
+ if (signal.aborted || snapshot.config.mode === "off") return;
1334
1581
  startStateFlowBackupPush(repositoryRoot, (error) => {
1335
- if (shuttingDown) return;
1582
+ if (shuttingDown || lifetime.aborted || snapshot.config.mode === "off") return;
1336
1583
  const message = diagnosticText(error);
1337
1584
  const recorded = diagnosticWriter.recordBackupPushFailure(pushSessionId, pushCwd, message);
1338
1585
  if (pushFailureNotified) return;
@@ -1340,7 +1587,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1340
1587
  notifyActiveContext(recorded
1341
1588
  ? `State Flow Git backup push failed; state is saved locally. Details: ${stateFlowLogPath(agentDir)}. A later accepted turn retries.`
1342
1589
  : `State Flow Git backup push failed; local diagnostics unavailable: ${message}`);
1343
- }, () => { pushFailureNotified = false; });
1590
+ }, () => { pushFailureNotified = false; }, lifetime);
1344
1591
  }
1345
1592
  } catch (error) {
1346
1593
  if (signal.aborted || runtime !== selected) return;
@@ -1353,12 +1600,12 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1353
1600
  });
1354
1601
 
1355
1602
  pi.on("agent_settled", async (_event, ctx) => {
1356
- if (telegramStartPending && !snapshot.config.enabled) {
1603
+ if (telegramStartPending && !isActive()) {
1357
1604
  telegramStartPending = false;
1358
1605
  await startStateFlow(ctx);
1359
1606
  return;
1360
1607
  }
1361
- if (!completedRunAccepted || compactionStopped || !snapshot.config.enabled || snapshot.meta.bootstrap
1608
+ if (!completedRunAccepted || compactionStopped || !isActive() || snapshot.meta.bootstrap
1362
1609
  || compactionInFlight || !ctx.isIdle() || ctx.hasPendingMessages() || !runtime?.view
1363
1610
  || !shouldRequestStateFlowCompaction(ctx.getContextUsage())) return;
1364
1611
  completedRunAccepted = false;
@@ -1367,11 +1614,15 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1367
1614
  const plan = planStateFlowCompaction(entries, runtime.causalBasis(), snapshot.meta.step, runAnchorTimestamp);
1368
1615
  if (!plan) return;
1369
1616
  compactionPlan = plan;
1617
+ const marker = compactionMarker = `${compactionPrefix}${randomUUID()}`;
1370
1618
  compactionInFlight = true;
1371
1619
  // Pi dispatches deferred companion prompts after all settled handlers return.
1372
1620
  await new Promise<void>((resolve) => {
1373
- const finished = () => { compactionPlan = undefined; compactionInFlight = false; resolve(); };
1374
- ctx.compact({ customInstructions: compactionMarker, onComplete: finished, onError: finished });
1621
+ const finished = () => {
1622
+ if (compactionMarker === marker) { compactionPlan = undefined; compactionInFlight = false; }
1623
+ resolve();
1624
+ };
1625
+ ctx.compact({ customInstructions: marker, onComplete: finished, onError: finished });
1375
1626
  });
1376
1627
  });
1377
1628
 
@@ -1389,10 +1640,11 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1389
1640
  });
1390
1641
  pi.on("session_shutdown", async (_event, _ctx) => {
1391
1642
  shuttingDown = true;
1643
+ memoryToolLifetime.abort();
1392
1644
  contextProjection.reset();
1393
1645
  const restoring = cancelBranchRestoration();
1394
1646
  const starting = cancelStartActivation();
1395
- const stopping = cancelStopPersistence();
1647
+ const stopping = cancelInactivePersistence();
1396
1648
  backupLifetime.abort();
1397
1649
  cancelInferencePreparation();
1398
1650
  cancelResponseReconciliation();