@llblab/pi-kit 0.24.0 → 0.25.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 (112) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +10 -0
  3. package/README.md +6 -5
  4. package/node_modules/@llblab/pi-claude-usage/AGENTS.md +20 -0
  5. package/node_modules/@llblab/pi-claude-usage/BACKLOG.md +3 -0
  6. package/node_modules/@llblab/pi-claude-usage/CHANGELOG.md +13 -0
  7. package/node_modules/@llblab/pi-claude-usage/LICENSE +22 -0
  8. package/node_modules/@llblab/pi-claude-usage/README.md +110 -0
  9. package/node_modules/@llblab/pi-claude-usage/banner.jpg +0 -0
  10. package/node_modules/@llblab/pi-claude-usage/index.ts +1159 -0
  11. package/node_modules/@llblab/pi-claude-usage/package.json +60 -0
  12. package/node_modules/@llblab/pi-state-flow/AGENTS.md +42 -56
  13. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +16 -3
  14. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +19 -0
  15. package/node_modules/@llblab/pi-state-flow/README.md +15 -12
  16. package/node_modules/@llblab/pi-state-flow/dist/index.d.ts +2 -2
  17. package/node_modules/@llblab/pi-state-flow/dist/index.js +2 -2
  18. package/node_modules/@llblab/pi-state-flow/dist/lib/config.d.ts +7 -3
  19. package/node_modules/@llblab/pi-state-flow/dist/lib/config.js +16 -7
  20. package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +9 -9
  21. package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +5 -4
  22. package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +2 -2
  23. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.d.ts +1 -1
  24. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +7 -4
  25. package/node_modules/@llblab/pi-state-flow/dist/lib/episode.d.ts +3 -3
  26. package/node_modules/@llblab/pi-state-flow/dist/lib/episode.js +5 -5
  27. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.d.ts +3 -5
  28. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +275 -199
  29. package/node_modules/@llblab/pi-state-flow/dist/lib/history.d.ts +11 -4
  30. package/node_modules/@llblab/pi-state-flow/dist/lib/history.js +6 -7
  31. package/node_modules/@llblab/pi-state-flow/dist/lib/logging.d.ts +4 -1
  32. package/node_modules/@llblab/pi-state-flow/dist/lib/logging.js +1 -0
  33. package/node_modules/@llblab/pi-state-flow/dist/lib/query.d.ts +4 -5
  34. package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +13 -13
  35. package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.d.ts +7 -6
  36. package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.js +9 -9
  37. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +3 -2
  38. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +17 -12
  39. package/node_modules/@llblab/pi-state-flow/dist/lib/session.d.ts +4 -1
  40. package/node_modules/@llblab/pi-state-flow/dist/lib/session.js +2 -1
  41. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +17 -8
  42. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +49 -20
  43. package/node_modules/@llblab/pi-state-flow/dist/lib/state.d.ts +22 -3
  44. package/node_modules/@llblab/pi-state-flow/dist/lib/state.js +30 -10
  45. package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +5 -3
  46. package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +19 -28
  47. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +17 -15
  48. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +52 -52
  49. package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.d.ts +8 -4
  50. package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.js +34 -18
  51. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.d.ts +5 -5
  52. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +13 -19
  53. package/node_modules/@llblab/pi-state-flow/dist/package.json +3 -3
  54. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +2 -2
  55. package/node_modules/@llblab/pi-state-flow/docs/README.md +2 -1
  56. package/node_modules/@llblab/pi-state-flow/docs/agent-contract-relocation.md +72 -0
  57. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +36 -32
  58. package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +12 -4
  59. package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +5 -5
  60. package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +6 -6
  61. package/node_modules/@llblab/pi-state-flow/docs/performance.md +1 -1
  62. package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +13 -12
  63. package/node_modules/@llblab/pi-state-flow/docs/usage.md +32 -29
  64. package/node_modules/@llblab/pi-state-flow/index.ts +3 -2
  65. package/node_modules/@llblab/pi-state-flow/lib/config.ts +20 -10
  66. package/node_modules/@llblab/pi-state-flow/lib/context.ts +15 -14
  67. package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +1 -1
  68. package/node_modules/@llblab/pi-state-flow/lib/durable.ts +8 -6
  69. package/node_modules/@llblab/pi-state-flow/lib/episode.ts +6 -6
  70. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +274 -197
  71. package/node_modules/@llblab/pi-state-flow/lib/history.ts +16 -11
  72. package/node_modules/@llblab/pi-state-flow/lib/logging.ts +5 -1
  73. package/node_modules/@llblab/pi-state-flow/lib/query.ts +16 -16
  74. package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +11 -11
  75. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +19 -13
  76. package/node_modules/@llblab/pi-state-flow/lib/session.ts +6 -3
  77. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +55 -22
  78. package/node_modules/@llblab/pi-state-flow/lib/state.ts +46 -13
  79. package/node_modules/@llblab/pi-state-flow/lib/status.ts +23 -32
  80. package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +66 -65
  81. package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +39 -19
  82. package/node_modules/@llblab/pi-state-flow/lib/transition.ts +19 -27
  83. package/node_modules/@llblab/pi-state-flow/package.json +3 -3
  84. package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +2 -2
  85. package/node_modules/@llblab/pi-telegram/AGENTS.md +1 -1
  86. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +5 -0
  87. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +2 -0
  88. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +55 -2
  89. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.d.ts +21 -0
  90. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +144 -1
  91. package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +9 -0
  92. package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +19 -0
  93. package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +13 -0
  94. package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +29 -6
  95. package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +16 -0
  96. package/node_modules/@llblab/pi-telegram/dist/lib/locks.js +5 -1
  97. package/node_modules/@llblab/pi-telegram/dist/lib/polling.js +6 -2
  98. package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +7 -0
  99. package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +13 -5
  100. package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
  101. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +2 -2
  102. package/node_modules/@llblab/pi-telegram/docs/public-api.md +1 -1
  103. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +79 -1
  104. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +197 -0
  105. package/node_modules/@llblab/pi-telegram/lib/bus.ts +33 -0
  106. package/node_modules/@llblab/pi-telegram/lib/commands.ts +38 -6
  107. package/node_modules/@llblab/pi-telegram/lib/extension.ts +15 -0
  108. package/node_modules/@llblab/pi-telegram/lib/locks.ts +6 -1
  109. package/node_modules/@llblab/pi-telegram/lib/polling.ts +10 -2
  110. package/node_modules/@llblab/pi-telegram/lib/threads.ts +19 -5
  111. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  112. package/package.json +7 -3
@@ -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";
@@ -36,8 +36,8 @@ import type { RehydrationPhase } from "./rehydration.ts";
36
36
  import { TemporalRuntime, type RuntimePublication } from "./runtime.ts";
37
37
  import { discoverSnapshotData, findAssistantToolBatch, findPassiveStopBoundary, 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 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,17 +84,16 @@ 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;
@@ -151,7 +150,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
151
150
  return new TemporalRuntime(ctx.cwd, sessionAddress(ctx), repositoryRoot, undefined, config.historyLimit);
152
151
  }
153
152
 
154
- function projectModelState(state: MaterializedState): ModelState {
153
+ function projectModelState(state: SemanticState): SemanticState {
155
154
  return projectStateForModel(state, artifactHints);
156
155
  }
157
156
 
@@ -164,19 +163,28 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
164
163
  options.onRuntime?.({ read: (offset, scope) => {
165
164
  if (scope === "session") assertSelectedBranchAvailable();
166
165
  if (!runtime) throw new Error("State Flow temporal runtime is unavailable");
167
- return projectModelState(runtime.read(offset, scope));
166
+ const { lazy: _lazy, ...defaults } = emptyState();
167
+ return { ...defaults, ...projectModelState(runtime.readView(offset, scope)) };
168
168
  } });
169
169
 
170
170
  function assertPublicationAvailable(): void {
171
171
  assertSelectedBranchAvailable();
172
- if (stopPersistenceError) throw new Error(`Memory writes paused after Stop: ${stopPersistenceError}; use /state-flow-start`);
172
+ if (modePersistenceError) throw new Error(`Memory writes paused after mode change: ${modePersistenceError}; use /state-flow-active`);
173
+ }
174
+
175
+ function isActive(): boolean {
176
+ return snapshot.config.mode === "active";
177
+ }
178
+
179
+ /** An unavailable or pending active selection keeps the configured inactive policy, never an invented one. */
180
+ function inactiveSelection(mode: StateFlowMode): InactiveMode {
181
+ return mode === "active" ? config.inactiveMode : mode;
173
182
  }
174
183
 
175
184
  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
- }
185
+ const checkpoint = branchStartsWithoutRuntime ? preRuntimeCheckpoint(snapshot.config.mode)
186
+ : runtime?.view ? runtime.retainedCheckpoint(snapshot) : undefined;
187
+ if (!checkpoint) throw new Error("State Flow cannot checkpoint an unproven branch; restore a valid checkpoint first");
180
188
  pi.appendEntry(SNAPSHOT_ENTRY_TYPE, checkpoint);
181
189
  if ("boundary" in checkpoint) branchStartsWithoutRuntime = false;
182
190
  }
@@ -191,10 +199,10 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
191
199
  ctx.ui.setStatus(STATUS_KEY, compactStatus(snapshot, scopeRevisions(), (color, text) => ctx.ui.theme.fg(color, text)));
192
200
  }
193
201
 
194
- function cancelStopPersistence(): Promise<StateFlowTelegramControlResult> | undefined {
195
- const pending = stopPersistence;
202
+ function cancelInactivePersistence(): Promise<StateFlowTelegramControlResult> | undefined {
203
+ const pending = inactivePersistence;
196
204
  pending?.controller.abort();
197
- stopPersistence = undefined;
205
+ inactivePersistence = undefined;
198
206
  return pending?.operation;
199
207
  }
200
208
 
@@ -277,9 +285,9 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
277
285
  artifactReads.setCandidates(artifactInvalidations);
278
286
  }
279
287
 
280
- function missingArtifactRemovals(states: ScopedStates): AtomicScopePatches {
288
+ function missingArtifactRemovals(states: ScopedSemanticStates): AtomicScopePatches {
281
289
  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)) {
290
+ for (const scope of ["global", "cwd", "session"] as const) for (const path of Object.keys(states[scope].artifacts ?? {})) {
283
291
  owners.set(path, [...owners.get(path) ?? [], scope]);
284
292
  }
285
293
  const removals: AtomicScopePatches = {};
@@ -296,10 +304,17 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
296
304
 
297
305
  function installScopeStates(): void {
298
306
  scopeStates = runtime?.view ? runtime.states() : { global: emptyState(), cwd: emptyState(), session: emptyState() };
307
+ effectiveState = runtime?.view ? runtime.readView() : {};
308
+ }
309
+
310
+ /** Active and passive expose both memory tools; Off exposes neither. */
311
+ function memoryToolsAvailable(): boolean {
312
+ return snapshot.config.mode !== "off";
299
313
  }
300
314
 
301
- function passiveToolsAvailable(): boolean {
302
- return snapshot.config.enabled || config.passiveTools;
315
+ /** Passive memory context needs a loaded view; Off and Active never inject it. */
316
+ function passiveMemoryAvailable(): boolean {
317
+ return snapshot.config.mode === "passive" && runtime?.view !== undefined;
303
318
  }
304
319
 
305
320
  async function inspectTelegramState(scope: StateFlowTelegramScope): Promise<StateFlowTelegramInspection> {
@@ -307,22 +322,20 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
307
322
  if (scope === "session" || scope === "effective") assertSelectedBranchAvailable();
308
323
  const selected = runtime ??= createRuntime(activeContext);
309
324
  const signal = sharedInspectionLifetime.signal;
310
- if (!selected.view || (scope !== "session" && !stopPersistenceError)) await selected.refreshShared(signal);
325
+ if (!selected.view || (scope !== "session" && !modePersistenceError)) await selected.refreshShared(signal);
311
326
  signal.throwIfAborted();
312
327
  if (runtime !== selected) throw new Error("State Flow inspection selection changed; select the scope again");
313
328
  if (scope === "session" || scope === "effective") assertSelectedBranchAvailable();
314
329
  if (!selected.view) throw new Error("State Flow temporal runtime is unavailable");
315
330
  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 };
331
+ const state = scope === "effective" ? effectiveState : selected.readView(0, scope);
332
+ return { state: { ...projectModelState(state), ...(state.lazy === undefined ? {} : { lazy: structuredClone(state.lazy) }) }, revisions: scopeRevisions(), signal };
320
333
  }
321
334
 
322
335
  function syncStateFlowTools(): void {
323
336
  const active = pi.getActiveTools();
324
337
  const owned = [PATCH_STATE_TOOL_NAME, READ_STATE_TOOL_NAME];
325
- const available = passiveToolsAvailable();
338
+ const available = memoryToolsAvailable();
326
339
  if (owned.every((name) => active.includes(name) === available)) return;
327
340
  pi.setActiveTools(available
328
341
  ? [...new Set([...active, ...owned])]
@@ -374,7 +387,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
374
387
  if (!selected?.view) throw new Error("Temporal State Flow runtime is unavailable; reload before publishing");
375
388
  await selected.withPatchTransaction((transaction) => {
376
389
  signal.throwIfAborted();
377
- if (runtime !== selected || inferencePreparation !== pending || !snapshot.config.enabled || shuttingDown) {
390
+ if (runtime !== selected || inferencePreparation !== pending || !isActive() || shuttingDown) {
378
391
  throw new Error("State Flow inference selection changed while awaiting publication");
379
392
  }
380
393
  assertPublicationAvailable();
@@ -436,6 +449,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
436
449
  cwdScopeKey: cwdScopeKey(cwd),
437
450
  sessionScopeKey: sessionScopeKey(session.key),
438
451
  scopeStates: diagnosticStates,
452
+ effectiveState,
439
453
  recent: diagnosticRecent,
440
454
  historyLimit: config.historyLimit,
441
455
  ...(view === undefined ? {} : { temporal: {
@@ -446,7 +460,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
446
460
  } }),
447
461
  staleArtifacts,
448
462
  ...(durableStateError === undefined ? {} : { durableStateError }),
449
- ...(stopPersistenceError === undefined ? {} : { publicationError: stopPersistenceError }),
463
+ ...(modePersistenceError === undefined ? {} : { publicationError: modePersistenceError }),
450
464
  };
451
465
  }
452
466
 
@@ -465,7 +479,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
465
479
  // Start-owned attachment/fork recovery keeps its owner; only accepted Start cancels Stop.
466
480
  if (!startOwner) {
467
481
  cancelStartActivation();
468
- cancelStopPersistence();
482
+ cancelInactivePersistence();
469
483
  }
470
484
  clearRunTransient();
471
485
  passiveContinuation = undefined;
@@ -484,44 +498,56 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
484
498
  try {
485
499
  const branch = ctx.sessionManager.getBranch();
486
500
  const fence = retainsPhysicalSessionProjection(sessionStartReason)
487
- ? findPassiveStopBoundary(branch, ctx.sessionManager.getSessionId(), PASSIVE_STOP_ENTRY_TYPE)?.persistenceError
501
+ ? findPassiveStopBoundary(branch, ctx.sessionManager.getSessionId(), PASSIVE_STOP_ENTRY_TYPE)
488
502
  : 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" };
503
+ modePersistenceError = fence?.persistenceError ?? (startOwner ? modePersistenceError : undefined);
504
+ if (fence?.persistenceError) {
505
+ // The native marker owns policy only: read proven memory, never replay or publish a stale selection.
506
+ selection = { kind: "current", mode: fence.mode ?? config.inactiveMode };
493
507
  } else {
494
508
  const discovery = discoverSnapshotData(branch);
495
- const selected = selectRetainedCheckpoint(discovery.candidates);
509
+ const selected = selectRetainedCheckpoint(discovery.candidates, config.inactiveMode);
496
510
  skipped = discovery.errors.length + selected.skipped.length;
497
- branchStartsWithoutRuntime = selected.kind === "disabled" || (discovery.candidates.length === 0 && discovery.errors.length === 0);
511
+ branchStartsWithoutRuntime = selected.kind === "pre-runtime" || (discovery.candidates.length === 0 && discovery.errors.length === 0);
498
512
  if (discovery.candidates.length === 0 && discovery.errors.length > 0) {
499
- snapshot = migrationFailure({}, `Snapshot restoration failed: ${discovery.errors[0]}`);
513
+ snapshot = migrationFailure({}, `Snapshot restoration failed: ${discovery.errors[0]}`, config.inactiveMode);
500
514
  } else if (selected.kind === "boundary" && forkInitialization) {
501
515
  try {
502
516
  // Native parent acquisition stays outside canonical exclusion.
503
517
  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 };
518
+ const sourceFence = findPassiveStopBoundary(branch, source.id, PASSIVE_STOP_ENTRY_TYPE);
519
+ selection = { kind: "fork", source, checkpoint: sourceFence?.persistenceError !== undefined
520
+ ? { ...selected.checkpoint, mode: sourceFence.mode ?? config.inactiveMode } : selected.checkpoint };
506
521
  } catch (error) {
507
- snapshot = selectedBoundaryFailure(diagnosticText(error));
522
+ snapshot = selectedBoundaryFailure(diagnosticText(error), inactiveSelection(selected.checkpoint.mode));
508
523
  }
509
524
  } else if (selected.kind === "boundary") {
510
525
  selection = { kind: "restore", checkpoint: selected.checkpoint };
526
+ } else if (selected.kind === "pre-runtime") {
527
+ snapshot = emptySnapshot(selected.mode);
511
528
  } 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" };
529
+ snapshot = selected.snapshot;
530
+ } else if (isNewSession(sessionStartReason, branch)) {
531
+ // The repository mode is only a default for genuinely new sessions.
532
+ if (config.mode === "active") selection = { kind: "auto-start" };
533
+ else {
534
+ snapshot = emptySnapshot(config.mode);
535
+ // Adopt the default once without initializing semantic storage.
536
+ appendCheckpoint();
537
+ }
515
538
  } else {
516
- snapshot = emptySnapshot();
539
+ snapshot = emptySnapshot(config.inactiveMode);
517
540
  }
518
541
  }
519
542
  } catch (error) {
520
543
  selection = { kind: "settled" };
521
- snapshot = selectedBoundaryFailure(diagnosticText(error));
544
+ snapshot = selectedBoundaryFailure(diagnosticText(error), config.inactiveMode);
522
545
  }
523
546
  // 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");
547
+ if (selection.kind !== "settled") {
548
+ snapshot = migrationFailure({}, "State Flow branch restoration is pending", selection.kind === "current" ? selection.mode
549
+ : selection.kind === "auto-start" ? config.inactiveMode : inactiveSelection(selection.checkpoint.mode));
550
+ }
525
551
  const pending: BranchRestoration = {
526
552
  controller: new AbortController(),
527
553
  awaitingAcceptance: selection.kind !== "settled" && selection.kind !== "current",
@@ -578,22 +604,23 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
578
604
  const current = await candidate.refreshCurrentMemory(signal);
579
605
  assertCurrent();
580
606
  if (current) runtime = selected = candidate;
581
- snapshot = current ?? migrationFailure({}, "Current State Flow session memory is unavailable");
607
+ snapshot = current ? deactivateEpisode(current, pending.requestedMode ?? selection.mode)
608
+ : migrationFailure({}, "Current State Flow session memory is unavailable", pending.requestedMode ?? selection.mode);
582
609
  installScopeStates();
583
610
  } else if (selection.kind === "restore") {
584
611
  await candidate.withRestoreTransaction(selection.checkpoint, (restored, publish) => {
585
612
  assertCurrent();
586
613
  // Canceled preparation or boundary continuation may have no specification. Retain uncompiled native context.
587
- if (restored.config.enabled && restored.meta.specification === undefined
614
+ if (restored.config.mode === "active" && restored.meta.specification === undefined
588
615
  && retainsPhysicalSessionProjection(reason) && hasUncheckpointedConversation(ctx.sessionManager.getBranch())) restored.meta.bootstrap = true;
589
- const next = pending.passiveRequested ? stopEpisode(restored) : restored;
616
+ const next = pending.requestedMode ? deactivateEpisode(restored, pending.requestedMode) : restored;
590
617
  accept(candidate, next, publish(next), appendCheckpoint);
591
618
  }, signal);
592
619
  } else if (selection.kind === "fork") {
593
620
  await candidate.withForkTransaction(selection.source, selection.checkpoint, (child, publish) => {
594
621
  assertCurrent();
595
622
  // Mode is selected independently of creating the child's private memory.
596
- const next = pending.passiveRequested ? stopEpisode(child) : child;
623
+ const next = pending.requestedMode ? deactivateEpisode(child, pending.requestedMode) : child;
597
624
  const publication = publish(next);
598
625
  forkInitialization = false;
599
626
  accept(candidate, next, publication, () => {
@@ -609,7 +636,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
609
636
  const discovery = discoverSnapshotData(branch);
610
637
  if (current || discovery.candidates.length > 0 || discovery.errors.length > 0) throw new Error("Existing session runtime requires retained-boundary restoration");
611
638
  const activated = startEpisode(hasPriorConversation(branch));
612
- const next = pending.passiveRequested ? stopEpisode(activated) : activated;
639
+ const next = pending.requestedMode ? deactivateEpisode(activated, pending.requestedMode) : activated;
613
640
  accept(candidate, next, publish(next), appendCheckpoint);
614
641
  }, signal, true);
615
642
  }
@@ -620,7 +647,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
620
647
  }
621
648
  if (!isCurrent()) return;
622
649
  if (error instanceof HistoryBoundaryExpiredError) selectedHistoryExpired = true;
623
- snapshot = selectedBoundaryFailure(diagnosticText(error));
650
+ snapshot = selectedBoundaryFailure(diagnosticText(error), snapshot.config.mode === "active" ? config.inactiveMode : snapshot.config.mode);
624
651
  installScopeStates();
625
652
  } finally {
626
653
  pending.awaitingAcceptance = false;
@@ -628,25 +655,19 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
628
655
  if (accepted) return;
629
656
  }
630
657
  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
- }
658
+ if (snapshot.config.mode !== "passive" || runtime?.view) return;
659
+ const passive = await loadPassiveView(ctx, signal, isCurrent, notifyRecovery);
660
+ if (passive === undefined) return;
640
661
  assertCurrent();
641
662
  // An intervening passive patch or inspection owns its newer cache, even on the same physical branch.
642
- if (selected?.view !== priorView) return;
663
+ if (runtime?.view) return;
643
664
  runtime = selected = passive;
644
665
  installScopeStates();
645
666
  updateUi(ctx);
646
667
  } catch (error) {
647
668
  // Host failures before acceptance leave the selection unavailable, never invented empty memory.
648
669
  if (accepted || !isCurrent()) return;
649
- snapshot = selectedBoundaryFailure(diagnosticText(error));
670
+ snapshot = selectedBoundaryFailure(diagnosticText(error), snapshot.config.mode === "active" ? config.inactiveMode : snapshot.config.mode);
650
671
  installScopeStates();
651
672
  } finally {
652
673
  pending.awaitingAcceptance = false;
@@ -654,29 +675,42 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
654
675
  }
655
676
  }
656
677
 
678
+ /** Load current shared memory for passive projection without initializing or publishing it. */
679
+ async function loadPassiveView(ctx: ExtensionContext, signal: AbortSignal, isCurrent: () => boolean, notify: boolean): Promise<TemporalRuntime | undefined> {
680
+ const passive = createRuntime(ctx);
681
+ try {
682
+ await passive.refreshShared(signal);
683
+ } catch (error) {
684
+ if (isCurrent() && !signal.aborted && notify && !modePersistenceError) notifyProblem(ctx, `State Flow passive memory is unavailable: ${diagnosticText(error)}`, "warning");
685
+ return undefined;
686
+ }
687
+ return passive;
688
+ }
689
+
657
690
  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");
691
+ const failure = !isActive() && snapshot.meta.validation?.attempt === 0 ? snapshot.meta.validation.error : undefined;
692
+ if (failure !== undefined && notifyRecovery && !modePersistenceError) {
693
+ if (selectedHistoryExpired) notifyProblem(ctx, "State Flow history is outside the retained temporal window; /state-flow-active can use current session memory.", "warning");
661
694
  else notifyProblem(ctx, `State Flow restore failed: ${failure}`, "error");
662
695
  }
663
696
  if (selectedMemory && retainsPhysicalSessionProjection(reason) && runtime?.view) {
664
697
  const boundary = findPassiveStopBoundary(ctx.sessionManager.getBranch(), ctx.sessionManager.getSessionId(), PASSIVE_STOP_ENTRY_TYPE);
665
698
  if (boundary !== undefined) {
666
699
  const continuation = createPassiveContinuation(
667
- projectModelState(overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session)),
700
+ projectModelState(effectiveState),
668
701
  boundary.at,
669
702
  boundary.from,
670
703
  boundary.preserveContext,
671
704
  );
672
- if (!snapshot.config.enabled) passiveContinuation = continuation;
705
+ // Off retains the handoff boundary for a later Passive or Active choice but never projects it.
706
+ if (!isActive()) passiveContinuation = continuation;
673
707
  else if (snapshot.meta.bootstrap) bootstrapContinuation = continuation;
674
708
  }
675
709
  }
676
- if (notifyRecovery && snapshot.config.enabled && skipped > 0) {
710
+ if (notifyRecovery && isActive() && skipped > 0) {
677
711
  notifyProblem(ctx, `State Flow skipped ${skipped} malformed snapshot(s); restored the last valid one.`, "warning");
678
712
  }
679
- if (snapshot.config.enabled) deferInferencePreparation();
713
+ if (isActive()) deferInferencePreparation();
680
714
  syncStateFlowTools();
681
715
  updateUi(ctx);
682
716
  }
@@ -698,7 +732,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
698
732
  async execute(_toolCallId, params, signal) {
699
733
  try {
700
734
  type ReadDetails = { path?: string; paths?: string[]; projection?: string; transitionId?: string };
701
- if (!passiveToolsAvailable()) throw new Error("State Flow tools are disabled by configuration");
735
+ if (!memoryToolsAvailable()) throw new Error("State Flow tools are off for this session");
702
736
  if (signal?.aborted) throw new Error("State Flow read was aborted");
703
737
  if (!runtime?.view) throw new Error("State Flow temporal runtime is unavailable");
704
738
  if (params.path === undefined && params.paths === undefined) throw new Error("read_state requires path or paths");
@@ -751,7 +785,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
751
785
  },
752
786
  async execute(toolCallId, params, signal, _onUpdate, ctx) {
753
787
  try {
754
- if (!passiveToolsAvailable()) throw new Error("State Flow tools are disabled by configuration");
788
+ if (!memoryToolsAvailable()) throw new Error("State Flow tools are off for this session");
755
789
  assertPublicationAvailable();
756
790
  if (signal?.aborted) throw new Error("State Flow patch was aborted before materialization");
757
791
  if (!isObject(params)) throw new Error("patch_state requires an object");
@@ -773,10 +807,10 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
773
807
  const selected = runtime ??= createRuntime(ctx);
774
808
  const acquiredArtifacts = structuredClone([...artifactReads.successful.values()]);
775
809
  const acquiredSkills = structuredClone([...skillReads.successful.values()]);
776
- const previousEffective = overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session);
810
+ const previousEffective = effectiveState;
777
811
  return await selected.withPatchTransaction((transaction) => {
778
812
  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");
813
+ if (!memoryToolsAvailable()) throw new Error("State Flow tools are off for this session");
780
814
  assertPublicationAvailable();
781
815
  for (const acquired of acquiredArtifacts) {
782
816
  const observation = inspectRegisteredArtifactPaths([acquired.path])[0];
@@ -803,7 +837,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
803
837
  }
804
838
  clearAcceptedAcquisitions(new Set(acquiredArtifacts.map(({ path }) => path)));
805
839
  updateUi(ctx);
806
- const updates = contextProjection.acceptPatch(previousEffective, overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session), patches, artifactHints);
840
+ const updates = contextProjection.acceptPatch(previousEffective, effectiveState, patches, artifactHints);
807
841
  const acknowledgement = changed
808
842
  ? `\nState materialized atomically at ${scopes.join("+")} scope${scopes.length === 1 ? "" : "s"}.`
809
843
  : "\nState already current.";
@@ -830,8 +864,8 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
830
864
  if (shuttingDown) return Promise.resolve({ ok: false, message: "State Flow is shutting down" });
831
865
  if (startActivation?.operation) return startActivation.operation;
832
866
  telegramStartPending = false;
833
- if (activeContext && !branchRestoration && snapshot.config.enabled) {
834
- return Promise.resolve({ ok: true, message: "State Flow is already enabled", signal: sharedInspectionLifetime.signal });
867
+ if (activeContext && !branchRestoration && isActive()) {
868
+ return Promise.resolve({ ok: true, message: "State Flow is already active", signal: sharedInspectionLifetime.signal });
835
869
  }
836
870
  const pending: NonNullable<typeof startActivation> = { controller: new AbortController() };
837
871
  startActivation = pending;
@@ -854,16 +888,16 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
854
888
  && ctx.sessionManager.getHeader()?.timestamp === timestamp;
855
889
  const superseded = (): StateFlowTelegramControlResult => {
856
890
  pending.abort();
857
- return { ok: false, message: "State Flow Start was superseded", signal: pending.signal };
891
+ return { ok: false, message: "State Flow activation was superseded", signal: pending.signal };
858
892
  };
859
893
  try {
860
894
  if (!activeContext) await waitForRecovery(restoreActiveBranch(ctx, undefined, false, pending), signal);
861
895
  else if (branchRestoration?.operation) await waitForRecovery(branchRestoration.operation, signal);
862
896
  if (!isOwner()) return superseded();
863
- if (snapshot.config.enabled) return { ok: true, message: "State Flow is already enabled", signal: sharedInspectionLifetime.signal };
897
+ if (isActive()) return { ok: true, message: "State Flow is already active", signal: sharedInspectionLifetime.signal };
864
898
  if (forkInitialization) {
865
- // Withdraw this join on cancellation without revoking the independently owned Stop.
866
- const stopping = stopPersistence?.operation;
899
+ // Withdraw this join on cancellation without revoking the independently owned mode persistence.
900
+ const stopping = inactivePersistence?.operation;
867
901
  if (stopping) await waitForRecovery(stopping, signal);
868
902
  if (!isOwner()) return superseded();
869
903
  await waitForRecovery(restoreActiveBranch(ctx, "fork", false, pending), signal);
@@ -873,7 +907,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
873
907
  return activateCurrentState(ctx, pending);
874
908
  } catch (error) {
875
909
  if (!isOwner()) return superseded();
876
- const message = conciseDiagnostic(`State Flow Start failed: ${diagnosticText(error)}`);
910
+ const message = conciseDiagnostic(`State Flow activation failed: ${diagnosticText(error)}`);
877
911
  notifyProblem(ctx, message, "error");
878
912
  return { ok: false, message, signal: AbortSignal.any([pending.signal, sharedInspectionLifetime.signal]) };
879
913
  }
@@ -885,22 +919,22 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
885
919
  const cwd = ctx.cwd;
886
920
  const file = ctx.sessionManager.getSessionFile();
887
921
  const timestamp = ctx.sessionManager.getHeader()?.timestamp;
888
- const initiallyEnabled = snapshot.config.enabled;
922
+ const initiallyActive = isActive();
889
923
  let accepted = false;
890
924
  let receipt = AbortSignal.any([pending.signal, sharedInspectionLifetime.signal]);
891
925
  const isCurrent = () => startActivation?.controller === pending && runtime === selected && !shuttingDown
892
926
  && ctx.cwd === cwd && ctx.sessionManager.getSessionId() === owner && ctx.sessionManager.getSessionFile() === file
893
- && ctx.sessionManager.getHeader()?.timestamp === timestamp && snapshot.config.enabled === (accepted || initiallyEnabled);
927
+ && ctx.sessionManager.getHeader()?.timestamp === timestamp && isActive() === (accepted || initiallyActive);
894
928
  const superseded = (): StateFlowTelegramControlResult => {
895
929
  pending.abort();
896
- return { ok: false, message: "State Flow Start was superseded", signal: pending.signal };
930
+ return { ok: false, message: "State Flow activation was superseded", signal: pending.signal };
897
931
  };
898
932
  try {
899
933
  const activation = createRuntime(ctx);
900
934
  const signal = ctx.signal ? AbortSignal.any([pending.signal, ctx.signal]) : pending.signal;
901
935
  const result = await activation.withStartTransaction((current, publish) => {
902
936
  signal.throwIfAborted();
903
- if (!isCurrent()) throw new Error("State Flow Start selection changed while awaiting publication");
937
+ if (!isCurrent()) throw new Error("State Flow activation selection changed while awaiting publication");
904
938
  if (!current && !branchStartsWithoutRuntime) throw new Error(snapshot.meta.validation?.error ?? "Current State Flow session memory is unavailable");
905
939
  const continuation = passiveContinuation ?? bootstrapContinuation;
906
940
  const bootstrap = hasPriorConversation(ctx.sessionManager.getBranch()) || passiveContinuation !== undefined;
@@ -908,12 +942,12 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
908
942
  const recoveredCurrent = selectedHistoryExpired;
909
943
  const publication = publish(activated);
910
944
  accepted = true;
911
- cancelStopPersistence();
945
+ cancelInactivePersistence();
912
946
  runtime = selected = activation;
913
947
  snapshot = activated;
914
948
  activeContext = ctx;
915
949
  selectedHistoryExpired = false;
916
- stopPersistenceError = undefined;
950
+ modePersistenceError = undefined;
917
951
  installScopeStates();
918
952
  clearRunTransient();
919
953
  contextProjection.reset();
@@ -927,29 +961,42 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
927
961
  appendCheckpoint();
928
962
  ctx.ui.notify(
929
963
  recoveredCurrent
930
- ? "State Flow enabled from current session memory; unavailable historical state was not restored."
964
+ ? "State Flow active from current session memory; unavailable historical state was not restored."
931
965
  : 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.",
966
+ ? "State Flow active. The next complete agent run will migrate active context into state."
967
+ : "State Flow active. The next prompt starts a stateful agent run.",
934
968
  "info",
935
969
  );
936
- return { ok: true, message: "State Flow enabled", signal: receipt };
970
+ return { ok: true, message: "State Flow active", signal: receipt };
937
971
  }, signal, branchStartsWithoutRuntime);
938
972
  return isCurrent() ? result : superseded();
939
973
  } catch (error) {
940
974
  if (!isCurrent()) return superseded();
941
- const message = conciseDiagnostic(`${accepted ? "State Flow enabled; lifecycle update failed" : "State Flow Start failed"}: ${diagnosticText(error)}`);
975
+ const message = conciseDiagnostic(`${accepted ? "State Flow active; lifecycle update failed" : "State Flow activation failed"}: ${diagnosticText(error)}`);
942
976
  notifyProblem(ctx, message, accepted ? "warning" : "error");
943
977
  return { ok: accepted, message, signal: receipt };
944
978
  }
945
979
  }
946
980
 
947
- pi.registerCommand("state-flow-start", {
948
- description: "Start State Flow mode",
949
- handler: async (_args, ctx) => {
950
- await startStateFlow(ctx);
951
- },
952
- });
981
+ const MODE_COMMAND_DESCRIPTIONS: Record<StateFlowMode, string> = {
982
+ active: "Make State Flow active on the current session branch",
983
+ passive: "Use passive State Flow memory on the current session branch",
984
+ off: "Turn State Flow tools and context off on the current session branch",
985
+ };
986
+
987
+ /** Terminal commands and Telegram controls share these lifecycle owners. */
988
+ function selectMode(ctx: ExtensionContext, mode: StateFlowMode): Promise<StateFlowTelegramControlResult> {
989
+ return mode === "active" ? startStateFlow(ctx) : deactivateStateFlow(ctx, mode);
990
+ }
991
+
992
+ for (const mode of ["active", "passive", "off"] as const) {
993
+ pi.registerCommand(`state-flow-${mode}`, {
994
+ description: MODE_COMMAND_DESCRIPTIONS[mode],
995
+ handler: async (_args, ctx) => {
996
+ await selectMode(ctx, mode);
997
+ },
998
+ });
999
+ }
953
1000
 
954
1001
  pi.registerCommand("state-flow-status", {
955
1002
  description: "Show State Flow runtime status",
@@ -958,97 +1005,135 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
958
1005
  },
959
1006
  });
960
1007
 
961
- function stopStateFlow(ctx: ExtensionContext): Promise<StateFlowTelegramControlResult> {
1008
+ /** Local policy, tools and UI change before any canonical wait. */
1009
+ function applyInactiveMode(ctx: ExtensionContext, mode: InactiveMode): void {
1010
+ snapshot = deactivateEpisode(snapshot, mode);
1011
+ clearRunTransient();
1012
+ syncStateFlowTools();
1013
+ updateUi(ctx);
1014
+ }
1015
+
1016
+ function deactivateStateFlow(ctx: ExtensionContext, mode: InactiveMode): Promise<StateFlowTelegramControlResult> {
962
1017
  cancelStartActivation();
963
1018
  if (shuttingDown) return Promise.resolve({ ok: false, message: "State Flow is shutting down" });
964
- if (stopPersistence?.operation) return stopPersistence.operation;
1019
+ telegramStartPending = false;
1020
+ const persisting = inactivePersistence;
1021
+ if (persisting?.operation && !persisting.published) {
1022
+ // Pending inactive choices coalesce: acceptance persists whichever inactive mode is current then.
1023
+ if (snapshot.config.mode !== mode) applyInactiveMode(ctx, mode);
1024
+ return persisting.operation;
1025
+ }
965
1026
  if (!activeContext) void restoreActiveBranch(ctx, undefined, false);
966
1027
  const restoring = branchRestoration;
1028
+ // Read-only recovery also retains the latest policy; its native write fence is updated below.
1029
+ if (restoring) restoring.requestedMode = mode;
967
1030
  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" });
1031
+ // A mode change never cancels memory restoration or initialization.
1032
+ applyInactiveMode(ctx, mode);
1033
+ return Promise.resolve({ ok: true, message: `State Flow ${mode}; memory restoration continues` });
1034
+ }
1035
+ if (snapshot.config.mode === mode) {
1036
+ const retained = selectRetainedCheckpoint(discoverSnapshotData(ctx.sessionManager.getBranch()).candidates, config.inactiveMode);
1037
+ const recordedMode = retained.kind === "pre-runtime" ? retained.mode : retained.kind === "boundary" ? retained.checkpoint.mode : undefined;
1038
+ if (modePersistenceError || recordedMode === mode) return Promise.resolve({ ok: true, message: `State Flow is already ${mode}` });
975
1039
  }
976
- const pending: NonNullable<typeof stopPersistence> = { controller: new AbortController() };
977
- stopPersistence = pending;
978
- const operation = persistStoppedState(ctx, pending.controller);
1040
+ cancelInactivePersistence();
1041
+ const pending: NonNullable<typeof inactivePersistence> = { controller: new AbortController() };
1042
+ inactivePersistence = pending;
1043
+ const operation = persistInactiveMode(ctx, pending, mode);
979
1044
  pending.operation = operation;
980
- const finished = () => { if (stopPersistence === pending) stopPersistence = undefined; };
1045
+ const finished = () => { if (inactivePersistence === pending) inactivePersistence = undefined; };
981
1046
  void operation.then(finished, finished);
982
1047
  return operation;
983
1048
  }
984
1049
 
985
- async function persistStoppedState(ctx: ExtensionContext, pending: AbortController): Promise<StateFlowTelegramControlResult> {
986
- telegramStartPending = false;
987
- const selected = runtime;
1050
+ async function persistInactiveMode(ctx: ExtensionContext, pending: NonNullable<typeof inactivePersistence>, mode: InactiveMode): Promise<StateFlowTelegramControlResult> {
1051
+ let selected = runtime;
988
1052
  const owner = ctx.sessionManager.getSessionId();
989
1053
  const current = snapshot;
1054
+ const wasActive = current.config.mode === "active";
990
1055
  let unfinished = current.meta.specification !== undefined
991
1056
  || (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" };
1057
+ applyInactiveMode(ctx, mode);
997
1058
  const stoppedAt = Date.now();
1059
+ if (modePersistenceError) {
1060
+ // Writes stay paused; the native marker records only the newly selected inactive policy.
1061
+ pending.published = true;
1062
+ pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, {
1063
+ at: passiveContinuation?.startedAt ?? stoppedAt,
1064
+ ...(passiveContinuation?.activeRunStartedAt === undefined ? {} : { from: passiveContinuation.activeRunStartedAt }),
1065
+ preserveContext: true, owner, persistenceError: modePersistenceError, mode,
1066
+ });
1067
+ return { ok: true, message: `State Flow ${mode}; memory writes remain paused` };
1068
+ }
998
1069
  const incomingBoundary = current.meta.bootstrap ? bootstrapContinuation : undefined;
999
1070
  const anchor = runAnchorTimestamp;
1000
1071
  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" });
1072
+ const isCurrent = () => inactivePersistence === pending && runtime === selected
1073
+ && !shuttingDown && ctx.sessionManager.getSessionId() === owner && !isActive();
1074
+ const superseded = (): StateFlowTelegramControlResult => ({ ok: false, message: "State Flow mode change was superseded" });
1004
1075
  const freezeHandoff = (): PassiveContinuation | undefined => {
1005
1076
  contextProjection.reset();
1006
- const handoff = (current.config.enabled || stopPersistenceError) && selected?.view && current.meta.validation?.attempt !== 0
1077
+ const handoff = (wasActive || modePersistenceError) && selected?.view && current.meta.validation?.attempt !== 0
1007
1078
  ? createPassiveContinuation(
1008
- projectModelState(overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session)),
1079
+ projectModelState(effectiveState),
1009
1080
  incomingBoundary?.startedAt ?? stoppedAt,
1010
1081
  incomingBoundary ? incomingBoundary.activeRunStartedAt : !idle || unfinished ? anchor : undefined,
1011
- stopPersistenceError !== undefined || (incomingBoundary ? incomingBoundary.preserveContext : current.meta.bootstrap === true || (unfinished && anchor === undefined)),
1082
+ modePersistenceError !== undefined || (incomingBoundary ? incomingBoundary.preserveContext : current.meta.bootstrap === true || (unfinished && anchor === undefined)),
1012
1083
  )
1013
1084
  : undefined;
1014
- passiveContinuation = handoff ?? (!current.config.enabled ? passiveContinuation : undefined);
1085
+ // Off keeps the frozen boundary for a later Passive/Active choice but never projects it.
1086
+ passiveContinuation = handoff ?? (!wasActive ? passiveContinuation : undefined);
1015
1087
  return handoff;
1016
1088
  };
1017
1089
  const complete = (): StateFlowTelegramControlResult => {
1090
+ pending.published = true;
1018
1091
  const exitHandoff = freezeHandoff();
1019
- if (exitHandoff || stopPersistenceError) pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, {
1092
+ const selectedMode = snapshot.config.mode;
1093
+ if (exitHandoff || modePersistenceError) pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, {
1020
1094
  at: exitHandoff?.startedAt ?? stoppedAt,
1021
1095
  ...(exitHandoff?.activeRunStartedAt === undefined ? {} : { from: exitHandoff.activeRunStartedAt }),
1022
- ...(exitHandoff?.preserveContext || stopPersistenceError ? { preserveContext: true } : {}),
1023
- ...(stopPersistenceError === undefined ? {} : { owner, persistenceError: stopPersistenceError }),
1096
+ ...(exitHandoff?.preserveContext || modePersistenceError ? { preserveContext: true } : {}),
1097
+ ...(modePersistenceError === undefined ? {} : { owner, persistenceError: modePersistenceError, mode: selectedMode }),
1024
1098
  });
1025
- if (!stopPersistenceError) {
1099
+ if (!modePersistenceError) {
1026
1100
  appendCheckpoint();
1027
- return { ok: true, message: "State Flow disabled" };
1101
+ return { ok: true, message: `State Flow ${selectedMode}` };
1028
1102
  }
1029
- const message = conciseDiagnostic(`State Flow disabled; memory writes paused: ${stopPersistenceError}`);
1103
+ const message = conciseDiagnostic(`State Flow ${selectedMode}; memory writes paused: ${modePersistenceError}`);
1030
1104
  notifyProblem(ctx, message, "warning");
1031
1105
  return { ok: true, message };
1032
1106
  };
1033
1107
  let accepted = false;
1034
1108
  try {
1035
- unfinished ||= current.config.enabled && hasUncheckpointedConversation(ctx.sessionManager.getBranch());
1109
+ unfinished ||= wasActive && hasUncheckpointedConversation(ctx.sessionManager.getBranch());
1036
1110
  // Projection changes before any wait; capture the old run's boundary before later native input can replace it.
1037
1111
  freezeHandoff();
1038
1112
  bootstrapContinuation = undefined;
1039
1113
  artifactInvalidations = [];
1040
1114
  artifactReads.setCandidates([]);
1041
1115
  assertPublicationAvailable();
1042
- if (branchStartsWithoutRuntime || !selected?.view) return complete();
1043
- const signal = ctx.signal ? AbortSignal.any([pending.signal, ctx.signal]) : pending.signal;
1116
+ const signal = ctx.signal ? AbortSignal.any([pending.controller.signal, ctx.signal]) : pending.controller.signal;
1117
+ if (branchStartsWithoutRuntime || !selected?.view) {
1118
+ // A pre-runtime choice is native-only; Passive then loads shared memory without initializing storage.
1119
+ const result = complete();
1120
+ if (snapshot.config.mode !== "passive" || runtime?.view) return result;
1121
+ const passive = await loadPassiveView(ctx, signal, isCurrent, true);
1122
+ if (passive && isCurrent() && !signal.aborted && !runtime?.view) {
1123
+ runtime = selected = passive;
1124
+ installScopeStates();
1125
+ updateUi(ctx);
1126
+ }
1127
+ return isCurrent() ? result : superseded();
1128
+ }
1044
1129
  const result = await selected.withLifecycleTransaction((publish) => {
1045
1130
  signal.throwIfAborted();
1046
- if (!isCurrent()) throw new Error("State Flow Stop selection changed while awaiting publication");
1131
+ if (!isCurrent()) throw new Error("State Flow mode selection changed while awaiting publication");
1047
1132
  assertPublicationAvailable();
1048
- const stopped = stopEpisode(snapshot);
1049
- const publication = publish(stopped);
1133
+ const next = structuredClone(snapshot);
1134
+ const publication = publish(next);
1050
1135
  accepted = true;
1051
- snapshot = stopped;
1136
+ snapshot = next;
1052
1137
  installScopeStates();
1053
1138
  recordPolicyPublication(publication, ctx);
1054
1139
  return complete();
@@ -1058,11 +1143,11 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1058
1143
  if (!isCurrent()) return superseded();
1059
1144
  const cause = diagnosticText(error);
1060
1145
  if (accepted) {
1061
- const message = conciseDiagnostic(`State Flow disabled; lifecycle update failed: ${cause}`);
1146
+ const message = conciseDiagnostic(`State Flow ${snapshot.config.mode}; lifecycle update failed: ${cause}`);
1062
1147
  notifyProblem(ctx, message, "warning");
1063
1148
  return { ok: true, message };
1064
1149
  }
1065
- stopPersistenceError = cause.trim() ? cause : "Canonical State Flow persistence failed";
1150
+ modePersistenceError = cause.trim() ? cause : "Canonical State Flow persistence failed";
1066
1151
  bootstrapContinuation = undefined;
1067
1152
  artifactInvalidations = [];
1068
1153
  artifactReads.setCandidates([]);
@@ -1070,18 +1155,11 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1070
1155
  }
1071
1156
  }
1072
1157
 
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
1158
  const telegram = createStateFlowTelegramAdapter({
1081
1159
  ...(options.telegram?.load === undefined ? {} : { load: options.telegram.load }),
1082
1160
  port: {
1083
1161
  snapshot: () => ({
1084
- enabled: snapshot.config.enabled,
1162
+ mode: snapshot.config.mode,
1085
1163
  step: snapshot.meta.step,
1086
1164
  revisions: scopeRevisions(),
1087
1165
  bootstrap: snapshot.meta.bootstrap === true,
@@ -1089,13 +1167,11 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1089
1167
  }),
1090
1168
  inspect: inspectTelegramState,
1091
1169
  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 () => {
1170
+ select: async (mode) => {
1097
1171
  if (!activeContext) throw new Error("State Flow is not attached to an active session yet");
1098
- const operation = stopStateFlow(activeContext);
1172
+ const operation = selectMode(activeContext, mode);
1173
+ if (mode === "active") return operation;
1174
+ // Inactive choices revoke older presentation at once; the receipt belongs to the new lifetime.
1099
1175
  const signal = sharedInspectionLifetime.signal;
1100
1176
  try {
1101
1177
  return { ...await operation, signal };
@@ -1117,8 +1193,8 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1117
1193
  pi.on("before_agent_start", (event) => {
1118
1194
  // Native run identity is independent of semantic enablement and survives mode toggles.
1119
1195
  runAnchorTimestamp = undefined;
1120
- if (!snapshot.config.enabled) {
1121
- if (!config.passiveBootstrap || !runtime?.view) return;
1196
+ if (!isActive()) {
1197
+ if (!passiveMemoryAvailable()) return;
1122
1198
  (event.systemPromptOptions.sections ??= {}).state_flow = PASSIVE_MEMORY_PROTOCOL;
1123
1199
  return;
1124
1200
  }
@@ -1133,20 +1209,23 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1133
1209
  });
1134
1210
 
1135
1211
  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;
1212
+ // Off removes the owned section, including one contributed before an in-flight mode change.
1213
+ const protocol = isActive() ? stateFlowProtocol(snapshot.meta.bootstrap === true)
1214
+ : passiveMemoryAvailable() ? PASSIVE_MEMORY_PROTOCOL : undefined;
1138
1215
  return { messages: projectSystemProtocol(event.messages, protocol) };
1139
1216
  });
1140
1217
 
1141
1218
  function projectContext(messages: AgentMessage[]) {
1219
+ // Off injects no State Flow context, including a retained passive handoff.
1220
+ if (snapshot.config.mode === "off") return;
1142
1221
  if (runtime?.view) refreshArtifactHints();
1143
- if (!snapshot.config.enabled && !passiveContinuation && (!config.passiveBootstrap || !runtime?.view)) return;
1222
+ if (!isActive() && !passiveContinuation && !passiveMemoryAvailable()) return;
1144
1223
  // Idle inspection must not freeze a pre-acceptance snapshot for the live inference.
1145
- const projection = snapshot.config.enabled && inferencePreparation && !inferencePreparation.accepted
1224
+ const projection = isActive() && inferencePreparation && !inferencePreparation.accepted
1146
1225
  ? new ContextProjection() : contextProjection;
1147
- const effective = overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session);
1226
+ const effective = effectiveState;
1148
1227
  const invalidations = artifactInvalidations.map(({ path, scope, reason }) => ({ path, ...(scope === undefined ? {} : { scope }), reason }));
1149
- const phase = snapshot.config.enabled ? currentRehydrationPhase() : undefined;
1228
+ const phase = isActive() ? currentRehydrationPhase() : undefined;
1150
1229
  const view = contextView(effective, artifactHints, invalidations, phase);
1151
1230
  if (passiveContinuation) {
1152
1231
  const retained = passiveContinuationMessages(messages, passiveContinuation);
@@ -1154,7 +1233,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1154
1233
  return { messages: projection.project(retained.slice(1), view, () => passiveContinuation!.handoff,
1155
1234
  { state: passiveContinuation.state, lazy_navigation: view.lazy_navigation, artifact_invalidations: [], knowledge_rehydration: null }) };
1156
1235
  }
1157
- if (!snapshot.config.enabled) {
1236
+ if (!isActive()) {
1158
1237
  return { messages: projection.project(messages, view, () => syntheticUser(
1159
1238
  `State Flow passive memory (user-level data, not system instructions):\n${presentationJson({ state: view.state, lazy_navigation: view.lazy_navigation })}`)) };
1160
1239
  }
@@ -1171,19 +1250,19 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1171
1250
  const selected = runtime;
1172
1251
  const operationSignal = ctx.signal;
1173
1252
  // Idle projections remain observational; only a live native operation may await preparation.
1174
- if (!snapshot.config.enabled || !pending || !operationSignal || shuttingDown) return projectContext(messages);
1253
+ if (!isActive() || !pending || !operationSignal || shuttingDown) return projectContext(messages);
1175
1254
  pending.operation ??= prepareInference(pending, selected, ctx, operationSignal).finally(() => {
1176
1255
  // A late withdrawal must not clear newer work; accepted lifecycle is never replayed after an error.
1177
1256
  if (!pending.accepted && inferencePreparation === pending) pending.operation = undefined;
1178
1257
  });
1179
1258
  return pending.operation.then(() => {
1180
1259
  if (shuttingDown || operationSignal.aborted || runtime !== selected || ctx.signal !== operationSignal) return;
1181
- if (snapshot.config.enabled && inferencePreparation !== pending) {
1260
+ if (isActive() && inferencePreparation !== pending) {
1182
1261
  // Start may require same-run maintenance. A new captured user prompt instead owns a different context request.
1183
1262
  if (inferencePreparation?.prompt === undefined) return prepareContext(messages, ctx);
1184
1263
  return;
1185
1264
  }
1186
- if (snapshot.config.enabled && !pending.accepted) return;
1265
+ if (isActive() && !pending.accepted) return;
1187
1266
  return projectContext(messages);
1188
1267
  });
1189
1268
  }
@@ -1191,7 +1270,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1191
1270
  pi.on("context", (event, ctx) => prepareContext(event.messages as AgentMessage[], ctx));
1192
1271
 
1193
1272
  pi.on("tool_execution_start", (event) => {
1194
- if (!snapshot.config.enabled) return;
1273
+ if (!isActive()) return;
1195
1274
  // Pi emits this before tool_call. Keep the argument object as a fallback;
1196
1275
  // tool_call replaces it with the mutable, post-preflight input reference.
1197
1276
  skillReads.recordStart(event.toolCallId, event.toolName, event.args);
@@ -1199,21 +1278,19 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1199
1278
  });
1200
1279
 
1201
1280
  pi.on("tool_call", (event, ctx) => {
1202
- if (!snapshot.config.enabled) return;
1281
+ if (!isActive()) return;
1203
1282
  const batch = findAssistantToolBatch(ctx.sessionManager, event.toolCallId);
1204
1283
  const patchCalls = batch?.filter((name) => name === PATCH_STATE_TOOL_NAME).length ?? 0;
1205
1284
  if (patchCalls > 0) {
1206
1285
  if (patchCalls !== 1) {
1207
- return {
1208
- block: true,
1209
- reason: "A State Flow barrier response must contain exactly one patch_state call",
1210
- };
1286
+ const reason = "A State Flow barrier response must contain exactly one patch_state call";
1287
+ recordDiagnostic(reason, "barrier-block", ctx, { tool: event.toolName, toolCallId: event.toolCallId, batchToolNames: batch });
1288
+ return { block: true, reason };
1211
1289
  }
1212
1290
  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
- };
1291
+ const reason = "Blocked by the patch_state barrier; reconsider this action after State Flow rematerializes context";
1292
+ recordDiagnostic(reason, "barrier-block", ctx, { tool: event.toolName, toolCallId: event.toolCallId, batchToolNames: batch });
1293
+ return { block: true, reason };
1217
1294
  }
1218
1295
  }
1219
1296
  skillReads.recordCall(event.toolCallId, event.toolName, event.input);
@@ -1221,14 +1298,14 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1221
1298
  });
1222
1299
 
1223
1300
  pi.on("tool_execution_end", (event) => {
1224
- if (!snapshot.config.enabled) return;
1301
+ if (!isActive()) return;
1225
1302
  skillReads.recordEnd(event.toolCallId, event.toolName, event.isError);
1226
1303
  dropCurrentSkillReads();
1227
1304
  artifactReads.recordEnd(event.toolCallId, event.toolName, event.isError);
1228
1305
  });
1229
1306
 
1230
1307
  pi.on("tool_result", (event) => {
1231
- if (!snapshot.config.enabled) return;
1308
+ if (!isActive()) return;
1232
1309
  const read = skillReads.recordResult(event.toolName, event.input, event.isError);
1233
1310
  if (!read) return;
1234
1311
  dropCurrentSkillReads();
@@ -1240,7 +1317,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1240
1317
  pi.on("message_end", (event, ctx): any => {
1241
1318
  // Observe actual user events even while disabled; Start/Stop cannot invent or erase them.
1242
1319
  if (event.message.role === "user" && runAnchorTimestamp === undefined) runAnchorTimestamp = event.message.timestamp;
1243
- if (shuttingDown || !snapshot.config.enabled) return;
1320
+ if (shuttingDown || !isActive()) return;
1244
1321
  if (event.message.role !== "assistant") return;
1245
1322
  cancelResponseReconciliation();
1246
1323
  const message = event.message as unknown as { role: "assistant"; stopReason?: string; content?: unknown };
@@ -1255,7 +1332,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1255
1332
  pi.on("turn_end", async (event, ctx) => {
1256
1333
  if (shuttingDown) return;
1257
1334
  const pending = responseReconciliation;
1258
- if (!snapshot.config.enabled || !pending) {
1335
+ if (!isActive() || !pending) {
1259
1336
  updateUi(ctx);
1260
1337
  return;
1261
1338
  }
@@ -1267,7 +1344,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1267
1344
  if (!selected?.view) throw new Error("Temporal State Flow runtime is unavailable; reload before publishing");
1268
1345
  await selected.withPatchTransaction((transaction) => {
1269
1346
  signal.throwIfAborted();
1270
- if (runtime !== selected || responseReconciliation !== pending || !snapshot.config.enabled) {
1347
+ if (runtime !== selected || responseReconciliation !== pending || !isActive()) {
1271
1348
  throw new Error("State Flow response selection changed while awaiting publication");
1272
1349
  }
1273
1350
  assertPublicationAvailable();
@@ -1353,12 +1430,12 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1353
1430
  });
1354
1431
 
1355
1432
  pi.on("agent_settled", async (_event, ctx) => {
1356
- if (telegramStartPending && !snapshot.config.enabled) {
1433
+ if (telegramStartPending && !isActive()) {
1357
1434
  telegramStartPending = false;
1358
1435
  await startStateFlow(ctx);
1359
1436
  return;
1360
1437
  }
1361
- if (!completedRunAccepted || compactionStopped || !snapshot.config.enabled || snapshot.meta.bootstrap
1438
+ if (!completedRunAccepted || compactionStopped || !isActive() || snapshot.meta.bootstrap
1362
1439
  || compactionInFlight || !ctx.isIdle() || ctx.hasPendingMessages() || !runtime?.view
1363
1440
  || !shouldRequestStateFlowCompaction(ctx.getContextUsage())) return;
1364
1441
  completedRunAccepted = false;
@@ -1392,7 +1469,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1392
1469
  contextProjection.reset();
1393
1470
  const restoring = cancelBranchRestoration();
1394
1471
  const starting = cancelStartActivation();
1395
- const stopping = cancelStopPersistence();
1472
+ const stopping = cancelInactivePersistence();
1396
1473
  backupLifetime.abort();
1397
1474
  cancelInferencePreparation();
1398
1475
  cancelResponseReconciliation();