@llblab/pi-kit 0.25.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/BACKLOG.md +5 -1
  2. package/CHANGELOG.md +11 -0
  3. package/README.md +10 -8
  4. package/node_modules/@llblab/pi-actors/AGENTS.md +2 -0
  5. package/node_modules/@llblab/pi-actors/CHANGELOG.md +4 -1
  6. package/node_modules/@llblab/pi-actors/LICENSE +21 -0
  7. package/node_modules/@llblab/pi-actors/README.md +1 -1
  8. package/node_modules/@llblab/pi-actors/docs/coordinator-delivery.md +1 -1
  9. package/node_modules/@llblab/pi-actors/package.json +4 -3
  10. package/node_modules/@llblab/pi-claude-usage/AGENTS.md +6 -3
  11. package/node_modules/@llblab/pi-claude-usage/BACKLOG.md +2 -1
  12. package/node_modules/@llblab/pi-claude-usage/CHANGELOG.md +8 -0
  13. package/node_modules/@llblab/pi-claude-usage/README.md +48 -3
  14. package/node_modules/@llblab/pi-claude-usage/index.ts +8 -1159
  15. package/node_modules/@llblab/pi-claude-usage/lib/extension.ts +30 -0
  16. package/node_modules/@llblab/pi-claude-usage/lib/fast.ts +24 -0
  17. package/node_modules/@llblab/pi-claude-usage/lib/query.ts +146 -0
  18. package/node_modules/@llblab/pi-claude-usage/lib/status-format.ts +297 -0
  19. package/node_modules/@llblab/pi-claude-usage/lib/status.ts +366 -0
  20. package/node_modules/@llblab/pi-claude-usage/lib/telegram.ts +44 -0
  21. package/node_modules/@llblab/pi-claude-usage/lib/usage-store.ts +221 -0
  22. package/node_modules/@llblab/pi-claude-usage/lib/usage.ts +128 -0
  23. package/node_modules/@llblab/pi-claude-usage/package.json +9 -5
  24. package/node_modules/@llblab/pi-clean-room/AGENTS.md +1 -0
  25. package/node_modules/@llblab/pi-clean-room/CHANGELOG.md +5 -0
  26. package/node_modules/@llblab/pi-clean-room/LICENSE +21 -0
  27. package/node_modules/@llblab/pi-clean-room/README.md +1 -1
  28. package/node_modules/@llblab/pi-clean-room/package.json +3 -2
  29. package/node_modules/@llblab/pi-codex-usage/AGENTS.md +9 -6
  30. package/node_modules/@llblab/pi-codex-usage/BACKLOG.md +2 -1
  31. package/node_modules/@llblab/pi-codex-usage/CHANGELOG.md +17 -0
  32. package/node_modules/@llblab/pi-codex-usage/README.md +75 -17
  33. package/node_modules/@llblab/pi-codex-usage/index.ts +8 -1602
  34. package/node_modules/@llblab/pi-codex-usage/lib/extension.ts +25 -0
  35. package/node_modules/@llblab/pi-codex-usage/lib/fast.ts +23 -0
  36. package/node_modules/@llblab/pi-codex-usage/lib/query.ts +368 -0
  37. package/node_modules/@llblab/pi-codex-usage/lib/status-format.ts +347 -0
  38. package/node_modules/@llblab/pi-codex-usage/lib/status.ts +435 -0
  39. package/node_modules/@llblab/pi-codex-usage/lib/telegram.ts +45 -0
  40. package/node_modules/@llblab/pi-codex-usage/lib/usage-store.ts +229 -0
  41. package/node_modules/@llblab/pi-codex-usage/lib/usage.ts +425 -0
  42. package/node_modules/@llblab/pi-codex-usage/package.json +11 -6
  43. package/node_modules/@llblab/pi-command-fast/AGENTS.md +7 -0
  44. package/node_modules/@llblab/pi-command-fast/BACKLOG.md +9 -0
  45. package/node_modules/@llblab/pi-command-fast/CHANGELOG.md +7 -0
  46. package/node_modules/@llblab/pi-command-fast/LICENSE +21 -0
  47. package/node_modules/@llblab/pi-command-fast/README.md +42 -0
  48. package/node_modules/@llblab/pi-command-fast/dist/command.d.ts +8 -0
  49. package/node_modules/@llblab/pi-command-fast/dist/command.js +52 -0
  50. package/node_modules/@llblab/pi-command-fast/dist/index.d.ts +3 -0
  51. package/node_modules/@llblab/pi-command-fast/dist/index.js +3 -0
  52. package/node_modules/@llblab/pi-command-fast/dist/models-json.d.ts +10 -0
  53. package/node_modules/@llblab/pi-command-fast/dist/models-json.js +81 -0
  54. package/node_modules/@llblab/pi-command-fast/package.json +49 -0
  55. package/node_modules/@llblab/pi-grow-loop/AGENTS.md +1 -0
  56. package/node_modules/@llblab/pi-grow-loop/CHANGELOG.md +4 -1
  57. package/node_modules/@llblab/pi-grow-loop/LICENSE +21 -0
  58. package/node_modules/@llblab/pi-grow-loop/README.md +1 -1
  59. package/node_modules/@llblab/pi-grow-loop/package.json +3 -2
  60. package/node_modules/@llblab/pi-state-flow/AGENTS.md +7 -6
  61. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +13 -5
  62. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +16 -1
  63. package/node_modules/@llblab/pi-state-flow/LICENSE +21 -0
  64. package/node_modules/@llblab/pi-state-flow/README.md +117 -35
  65. package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.d.ts +24 -1
  66. package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.js +80 -1
  67. package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.d.ts +18 -1
  68. package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.js +41 -1
  69. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +346 -303
  70. package/node_modules/@llblab/pi-state-flow/dist/lib/git.d.ts +31 -2
  71. package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +74 -5
  72. package/node_modules/@llblab/pi-state-flow/dist/lib/operation.d.ts +37 -0
  73. package/node_modules/@llblab/pi-state-flow/dist/lib/operation.js +59 -0
  74. package/node_modules/@llblab/pi-state-flow/dist/lib/ownership.d.ts +31 -0
  75. package/node_modules/@llblab/pi-state-flow/dist/lib/ownership.js +117 -0
  76. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +7 -7
  77. package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +1 -1
  78. package/node_modules/@llblab/pi-state-flow/dist/lib/session.d.ts +10 -1
  79. package/node_modules/@llblab/pi-state-flow/dist/lib/session.js +60 -1
  80. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +6 -0
  81. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +8 -5
  82. package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +2 -0
  83. package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +28 -0
  84. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +6 -1
  85. package/node_modules/@llblab/pi-state-flow/dist/package.json +10 -9
  86. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +14 -6
  87. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +2 -2
  88. package/node_modules/@llblab/pi-state-flow/docs/README.md +19 -9
  89. package/node_modules/@llblab/pi-state-flow/docs/agent-contract-relocation.md +4 -4
  90. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +646 -89
  91. package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +116 -37
  92. package/node_modules/@llblab/pi-state-flow/docs/filesystem-recovery.md +118 -21
  93. package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +70 -8
  94. package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +88 -14
  95. package/node_modules/@llblab/pi-state-flow/docs/performance.md +83 -66
  96. package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +391 -62
  97. package/node_modules/@llblab/pi-state-flow/docs/usage.md +317 -61
  98. package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +85 -1
  99. package/node_modules/@llblab/pi-state-flow/lib/compaction.ts +49 -2
  100. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +342 -301
  101. package/node_modules/@llblab/pi-state-flow/lib/git.ts +77 -5
  102. package/node_modules/@llblab/pi-state-flow/lib/operation.ts +75 -0
  103. package/node_modules/@llblab/pi-state-flow/lib/ownership.ts +120 -0
  104. package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +7 -8
  105. package/node_modules/@llblab/pi-state-flow/lib/query.ts +1 -1
  106. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +1 -1
  107. package/node_modules/@llblab/pi-state-flow/lib/session.ts +52 -1
  108. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +8 -7
  109. package/node_modules/@llblab/pi-state-flow/lib/status.ts +29 -0
  110. package/node_modules/@llblab/pi-state-flow/lib/transition.ts +5 -1
  111. package/node_modules/@llblab/pi-state-flow/package.json +10 -9
  112. package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +14 -6
  113. package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +2 -2
  114. package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -0
  115. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +10 -0
  116. package/node_modules/@llblab/pi-telegram/README.md +1 -1
  117. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +4 -1
  118. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +16 -12
  119. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +1 -1
  120. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +7 -1
  121. package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +12 -3
  122. package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +137 -83
  123. package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +26 -0
  124. package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.d.ts +57 -2
  125. package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.js +109 -4
  126. package/node_modules/@llblab/pi-telegram/dist/lib/model.js +2 -4
  127. package/node_modules/@llblab/pi-telegram/dist/lib/status.d.ts +3 -1
  128. package/node_modules/@llblab/pi-telegram/dist/lib/status.js +31 -1
  129. package/node_modules/@llblab/pi-telegram/dist/lib/sync.js +4 -4
  130. package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +1 -0
  131. package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +17 -4
  132. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.d.ts +2 -2
  133. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.js +48 -12
  134. package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
  135. package/node_modules/@llblab/pi-telegram/docs/architecture.md +6 -5
  136. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +2 -0
  137. package/node_modules/@llblab/pi-telegram/docs/public-api.md +2 -2
  138. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +4 -0
  139. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +17 -10
  140. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +8 -2
  141. package/node_modules/@llblab/pi-telegram/lib/commands.ts +135 -97
  142. package/node_modules/@llblab/pi-telegram/lib/extension.ts +25 -0
  143. package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +140 -4
  144. package/node_modules/@llblab/pi-telegram/lib/model.ts +2 -4
  145. package/node_modules/@llblab/pi-telegram/lib/status.ts +30 -1
  146. package/node_modules/@llblab/pi-telegram/lib/sync.ts +4 -4
  147. package/node_modules/@llblab/pi-telegram/lib/threads.ts +21 -3
  148. package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +46 -13
  149. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  150. package/node_modules/jsonc-parser/CHANGELOG.md +76 -0
  151. package/node_modules/jsonc-parser/LICENSE.md +21 -0
  152. package/node_modules/jsonc-parser/README.md +364 -0
  153. package/node_modules/jsonc-parser/SECURITY.md +41 -0
  154. package/node_modules/jsonc-parser/lib/esm/impl/edit.js +185 -0
  155. package/node_modules/jsonc-parser/lib/esm/impl/format.js +261 -0
  156. package/node_modules/jsonc-parser/lib/esm/impl/parser.js +659 -0
  157. package/node_modules/jsonc-parser/lib/esm/impl/scanner.js +443 -0
  158. package/node_modules/jsonc-parser/lib/esm/impl/string-intern.js +29 -0
  159. package/node_modules/jsonc-parser/lib/esm/main.d.ts +351 -0
  160. package/node_modules/jsonc-parser/lib/esm/main.js +178 -0
  161. package/node_modules/jsonc-parser/lib/umd/impl/edit.js +201 -0
  162. package/node_modules/jsonc-parser/lib/umd/impl/format.js +275 -0
  163. package/node_modules/jsonc-parser/lib/umd/impl/parser.js +682 -0
  164. package/node_modules/jsonc-parser/lib/umd/impl/scanner.js +456 -0
  165. package/node_modules/jsonc-parser/lib/umd/impl/string-intern.js +42 -0
  166. package/node_modules/jsonc-parser/lib/umd/main.d.ts +351 -0
  167. package/node_modules/jsonc-parser/lib/umd/main.js +194 -0
  168. package/node_modules/jsonc-parser/package.json +37 -0
  169. package/package.json +8 -8
@@ -3,18 +3,11 @@ import { StringEnum, Type } from "@earendil-works/pi-ai";
3
3
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
4
4
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
5
5
  import { Text } from "@earendil-works/pi-tui";
6
- import { randomUUID } from "node:crypto";
7
6
  import { existsSync } from "node:fs";
8
7
  import { isAbsolute, join, resolve } from "node:path";
9
- import { ArtifactReadTracker } from "./acquisition.ts";
10
- import {
11
- classifyArtifactCompilationNeed,
12
- inspectRegisteredArtifactPaths,
13
- ORDINARY_ARTIFACT_COMPILER,
14
- sameArtifactSourceFingerprint,
15
- type ArtifactInvalidationRequest,
16
- } from "./artifact.ts";
17
- import { hasCompactionSizedTranscript, planStateFlowCompaction, shouldRequestStateFlowCompaction, stateFlowCompactionResult, type StateFlowCompactionPlan } from "./compaction.ts";
8
+ import { ArtifactAcquisitionState, missingArtifactRemovals } from "./acquisition.ts";
9
+ import { inspectRegisteredArtifactPaths, sameArtifactSourceFingerprint } from "./artifact.ts";
10
+ import { hasCompactionSizedTranscript, planStateFlowCompaction, shouldRequestStateFlowCompaction, StateFlowCompactionRequests } from "./compaction.ts";
18
11
  import { inactiveModeFor, loadStateFlowConfig } from "./config.ts";
19
12
  import { ContextProjection, contextView, createPassiveContinuation, currentRunTrajectory, passiveContinuationMessages, projectSystemProtocol, runtimeContextHead, syntheticUser, type PassiveContinuation } from "./context.ts";
20
13
  import { readNativeSessionHeader } from "./continuation.ts";
@@ -25,19 +18,20 @@ import {
25
18
  type SessionAddress,
26
19
  } from "./durable.ts";
27
20
  import { completeRun, deactivateEpisode, prepareRun, resumeEpisode, startEpisode } from "./episode.ts";
28
- import { awaitInFlightBackupPushes, backupCurrentStateFlowFiles, startStateFlowBackupPush } from "./git.ts";
21
+ import { awaitInFlightBackupPushes, backupCurrentStateFlowFiles, SettledTurnBackup, startStateFlowBackupPush } from "./git.ts";
29
22
  import { projectRecentTransitionsWithLimit } from "./history.ts";
30
- import { isObject, presentationJson, sameJson, type JsonObject } from "./json.ts";
23
+ import { isObject, presentationJson, sameJson } from "./json.ts";
31
24
  import { StateFlowDiagnosticWriter, stateFlowLogPath, type DiagnosticExtras, type StateFlowDiagnosticCategory } from "./logging.ts";
32
25
  import { assistantToolCallCount, conciseDiagnostic, diagnosticText, finalizedAssistantResponse, formatPatchStateArguments, PASSIVE_MEMORY_PROTOCOL, separatedFailure, separatedOutput, stateFlowProtocol } from "./protocol.ts";
26
+ import { OwnedOperationSlot, RenewableLifetime, type OwnedOperation } from "./operation.ts";
33
27
  import { readProjectedState, readStatePath } from "./query.ts";
34
28
  import { selectedBoundaryFailure, selectRetainedCheckpoint, waitForRecovery } from "./recovery.ts";
35
29
  import type { RehydrationPhase } from "./rehydration.ts";
36
30
  import { TemporalRuntime, type RuntimePublication } from "./runtime.ts";
37
- import { discoverSnapshotData, findAssistantToolBatch, findPassiveStopBoundary, hasPriorConversation, hasUncheckpointedConversation, isNewSession, retainsPhysicalSessionProjection, SNAPSHOT_ENTRY_TYPE } from "./session.ts";
31
+ import { discoverSnapshotData, findAssistantToolBatch, findBranchPolicy, findPassiveStopBoundary, hasPendingFork, hasPriorConversation, hasUncheckpointedConversation, isNewSession, retainsPhysicalSessionProjection, SNAPSHOT_ENTRY_TYPE } from "./session.ts";
38
32
  import { hasCompiledSkillArtifact, hashSkillSource, registeredSkillResolver, SkillReadTracker, type SuccessfulSkillRead } from "./skills.ts";
39
- 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";
33
+ import { HistoryBoundaryExpiredError, emptySnapshot, migrationFailure, preRuntimeCheckpoint, type InactiveMode, type RetainedBoundaryCheckpoint, type RetainedPiCheckpoint, type Snapshot, type StateFlowMode } from "./snapshot.ts";
34
+ import { emptyState, projectStateForModel, type AtomicScopePatches, type SemanticState, type ModelState, type ScopedStates, type StateScope } from "./state.ts";
41
35
  import { compactStatus, detailedStatus, STATUS_KEY, type StatusDiagnostics } from "./status.ts";
42
36
  import { PublicationBusyError } from "./storage.ts";
43
37
  import { createStateFlowTelegramAdapter, type StateFlowTelegramControlResult, type StateFlowTelegramInspection, type StateFlowTelegramLoader, type StateFlowTelegramScope } from "./telegram.ts";
@@ -59,14 +53,14 @@ export const PATCH_STATE_TOOL_NAME = "patch_state";
59
53
  export const READ_STATE_TOOL_NAME = "read_state";
60
54
  const PASSIVE_STOP_ENTRY_TYPE = "state-flow-passive-stop";
61
55
 
62
- interface InferencePreparation {
56
+ interface InferencePreparation extends OwnedOperation {
63
57
  readonly controller: AbortController;
64
58
  readonly prompt?: string;
65
59
  accepted?: boolean;
66
60
  operation?: Promise<void>;
67
61
  }
68
62
 
69
- interface BranchRestoration {
63
+ interface BranchRestoration extends OwnedOperation {
70
64
  readonly controller: AbortController;
71
65
  /** Memory acceptance is independent of the requested session mode. */
72
66
  awaitingAcceptance: boolean;
@@ -74,6 +68,15 @@ interface BranchRestoration {
74
68
  operation?: Promise<void>;
75
69
  }
76
70
 
71
+ interface ModeOperation extends OwnedOperation {
72
+ operation?: Promise<StateFlowTelegramControlResult>;
73
+ }
74
+
75
+ interface InactivePersistence extends ModeOperation {
76
+ /** The native mode marker is recorded; later inactive choices start a new persistence. */
77
+ published?: boolean;
78
+ }
79
+
77
80
  type BranchSelection =
78
81
  | { kind: "settled" }
79
82
  | { kind: "current"; mode: InactiveMode }
@@ -93,47 +96,49 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
93
96
  let selectedHistoryExpired = false;
94
97
  let modePersistenceError: string | undefined;
95
98
  /** One pending inactive-mode persistence; later inactive choices coalesce until it publishes. */
96
- let inactivePersistence: { controller: AbortController; published?: boolean; operation?: Promise<StateFlowTelegramControlResult> } | undefined;
97
- let startActivation: { controller: AbortController; operation?: Promise<StateFlowTelegramControlResult> } | undefined;
99
+ const inactivePersistence = new OwnedOperationSlot<InactivePersistence>();
100
+ const startActivation = new OwnedOperationSlot<ModeOperation>();
98
101
  let forkInitialization = false;
99
- let branchRestoration: BranchRestoration | undefined;
100
- const restorationOperations = new Set<Promise<void>>();
101
- let responseReconciliation: AbortController | undefined;
102
- let sharedInspectionLifetime = new AbortController();
102
+ const branchRestoration = new OwnedOperationSlot<BranchRestoration>();
103
+ let deferredBranch: { reason: unknown; owner: string; cwd: string } | undefined;
104
+ const responseReconciliation = new OwnedOperationSlot<OwnedOperation>();
105
+ const sharedInspectionLifetime = new RenewableLifetime();
106
+ const memoryToolLifetime = new RenewableLifetime();
103
107
  let completedRunAccepted = false;
104
- let turnAcceptedForBackup = false;
105
- let compactionPlan: StateFlowCompactionPlan | undefined;
106
- let compactionInFlight = false;
107
- let compactionStopped = false;
108
- const compactionMarker = `state-flow-boundary:${randomUUID()}`;
108
+ const compactionRequests = new StateFlowCompactionRequests();
109
109
  let passiveContinuation: PassiveContinuation | undefined;
110
110
  let bootstrapContinuation: PassiveContinuation | undefined;
111
111
  const contextProjection = new ContextProjection();
112
- let inferencePreparation: InferencePreparation | undefined;
112
+ const inferencePreparation = new OwnedOperationSlot<InferencePreparation>();
113
113
  let runAnchorTimestamp: number | undefined;
114
114
  let runtime: TemporalRuntime | undefined;
115
115
  let activeContext: ExtensionContext | undefined;
116
116
  let rehydrationPhase: RehydrationPhase | undefined;
117
117
  const repositoryRoot = resolve(options.repositoryRoot ?? config.directory);
118
118
  const diagnosticWriter = new StateFlowDiagnosticWriter(config.logging, stateFlowLogPath(agentDir), repositoryRoot, (message) => notifyActiveContext(message));
119
- let backupPending = false;
120
- const backupLifetime = new AbortController();
121
- const backupOperations = new Set<Promise<string | undefined>>();
119
+ const backup = new SettledTurnBackup();
122
120
  let shuttingDown = false;
123
- let pushFailureNotified = false;
124
121
  const skillReads = new SkillReadTracker(hashSkillSource, (path) => activeContext
125
122
  ? registeredSkillResolver(activeContext.cwd, pi.getCommands())(path)
126
123
  : undefined);
127
- const artifactReads = new ArtifactReadTracker();
128
- let artifactInvalidations: ArtifactInvalidationRequest[] = [];
129
- let artifactHints: Record<string, string> = {};
130
- const SOURCE_CHANGED_HINT = "Source changed since this artifact was compiled. Read and recompile it before relying on it.";
124
+ const acquisition = new ArtifactAcquisitionState();
125
+ const artifactReads = acquisition.reads;
131
126
  let telegramStartPending = false;
132
127
 
133
128
  function sessionAddress(ctx: ExtensionContext): SessionAddress {
134
129
  return resolveSessionAddress(ctx.sessionManager.getSessionFile(), ctx.sessionManager.getSessionId(), ctx.sessionManager.getHeader()?.timestamp);
135
130
  }
136
131
 
132
+ /** Native session identity captured before an await; any change supersedes the waiting operation. */
133
+ function sessionIdentity(ctx: ExtensionContext): () => boolean {
134
+ const cwd = ctx.cwd;
135
+ const owner = ctx.sessionManager.getSessionId();
136
+ const file = ctx.sessionManager.getSessionFile();
137
+ const timestamp = ctx.sessionManager.getHeader()?.timestamp;
138
+ return () => ctx.cwd === cwd && ctx.sessionManager.getSessionId() === owner && ctx.sessionManager.getSessionFile() === file
139
+ && ctx.sessionManager.getHeader()?.timestamp === timestamp;
140
+ }
141
+
137
142
  function notifyProblem(ctx: ExtensionContext, message: string, level: "warning" | "error"): void {
138
143
  ctx.ui.notify(conciseDiagnostic(message), level);
139
144
  }
@@ -151,7 +156,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
151
156
  }
152
157
 
153
158
  function projectModelState(state: SemanticState): SemanticState {
154
- return projectStateForModel(state, artifactHints);
159
+ return projectStateForModel(state, acquisition.hints);
155
160
  }
156
161
 
157
162
  function assertSelectedBranchAvailable(): void {
@@ -199,109 +204,32 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
199
204
  ctx.ui.setStatus(STATUS_KEY, compactStatus(snapshot, scopeRevisions(), (color, text) => ctx.ui.theme.fg(color, text)));
200
205
  }
201
206
 
202
- function cancelInactivePersistence(): Promise<StateFlowTelegramControlResult> | undefined {
203
- const pending = inactivePersistence;
204
- pending?.controller.abort();
205
- inactivePersistence = undefined;
206
- return pending?.operation;
207
- }
208
-
209
- function cancelStartActivation(): Promise<StateFlowTelegramControlResult> | undefined {
210
- const pending = startActivation;
211
- pending?.controller.abort();
212
- startActivation = undefined;
213
- return pending?.operation;
214
- }
215
-
216
- function cancelBranchRestoration(): Promise<void> | undefined {
217
- const pending = branchRestoration;
218
- pending?.controller.abort();
219
- branchRestoration = undefined;
220
- return pending?.operation;
221
- }
222
-
223
- function cancelResponseReconciliation(): void {
224
- responseReconciliation?.abort();
225
- responseReconciliation = undefined;
226
- }
227
-
228
207
  function cancelSharedInspections(): void {
229
- sharedInspectionLifetime.abort(new Error("State Flow inspection was cancelled; select the scope again"));
230
- sharedInspectionLifetime = new AbortController();
231
- }
232
-
233
- function cancelInferencePreparation(): void {
234
- inferencePreparation?.controller.abort();
235
- inferencePreparation = undefined;
208
+ sharedInspectionLifetime.renew(new Error("State Flow inspection was cancelled; select the scope again"));
236
209
  }
237
210
 
238
211
  function clearRunTransient(): void {
239
- cancelInferencePreparation();
240
- cancelResponseReconciliation();
212
+ inferencePreparation.cancel();
213
+ responseReconciliation.cancel();
241
214
  cancelSharedInspections();
242
215
  completedRunAccepted = false;
243
- turnAcceptedForBackup = false;
244
- compactionPlan = undefined;
245
- compactionInFlight = false;
216
+ backup.clearTurn();
217
+ compactionRequests.clear();
246
218
  skillReads.clear();
247
219
  artifactReads.clear();
248
220
  }
249
221
 
250
222
  function deferInferencePreparation(prompt?: string): void {
251
- cancelInferencePreparation();
252
- inferencePreparation = { controller: new AbortController(), prompt };
253
- artifactInvalidations = [];
254
- artifactReads.setCandidates([]);
223
+ inferencePreparation.cancel();
224
+ inferencePreparation.claim({ controller: new AbortController(), prompt });
225
+ acquisition.clearInvalidations();
255
226
  }
256
227
 
257
228
  function refreshArtifactHints(): void {
258
- if (!runtime?.view) {
259
- artifactHints = {};
260
- artifactInvalidations = [];
261
- artifactReads.setCandidates([]);
262
- return;
263
- }
264
- const paths = new Set<string>();
265
- for (const scope of ["global", "cwd", "session"] as const) for (const path of Object.keys(scopeStates[scope].artifacts)) paths.add(path);
266
- const observations = new Map(inspectRegisteredArtifactPaths(paths).map((observation) => [observation.path, observation]));
267
- const nextHints: Record<string, string> = {};
268
- const nextInvalidations = new Map<string, ArtifactInvalidationRequest>();
269
- for (const scope of ["global", "cwd", "session"] as const) {
270
- const provenance = runtime.artifactProvenance(scope);
271
- for (const [path, metadata] of Object.entries(scopeStates[scope].artifacts)) {
272
- delete nextHints[path];
273
- nextInvalidations.delete(path);
274
- if (metadata.kind === "skill") continue;
275
- const observed = observations.get(path);
276
- if (observed?.kind !== "present") continue;
277
- const need = classifyArtifactCompilationNeed({ path, scope, sourceFingerprint: observed.fingerprint }, metadata, ORDINARY_ARTIFACT_COMPILER, false, provenance[path]);
278
- if (need.kind !== "requires-compilation") continue;
279
- if (need.reason === "source-changed") nextHints[path] = SOURCE_CHANGED_HINT;
280
- nextInvalidations.set(path, { path, scope, reason: need.reason });
281
- }
282
- }
283
- artifactHints = nextHints;
284
- artifactInvalidations = [...nextInvalidations.values()].sort((left, right) => left.path.localeCompare(right.path));
285
- artifactReads.setCandidates(artifactInvalidations);
229
+ if (runtime?.view) acquisition.refresh(scopeStates, (scope) => runtime!.artifactProvenance(scope));
230
+ else acquisition.reset();
286
231
  }
287
232
 
288
- function missingArtifactRemovals(states: ScopedSemanticStates): AtomicScopePatches {
289
- const owners = new Map<string, StateScope[]>();
290
- for (const scope of ["global", "cwd", "session"] as const) for (const path of Object.keys(states[scope].artifacts ?? {})) {
291
- owners.set(path, [...owners.get(path) ?? [], scope]);
292
- }
293
- const removals: AtomicScopePatches = {};
294
- for (const observation of inspectRegisteredArtifactPaths(owners.keys())) {
295
- if (observation.kind !== "missing") continue;
296
- for (const scope of owners.get(observation.path) ?? []) {
297
- const artifacts = (removals[scope]?.artifacts ?? {}) as JsonObject;
298
- removals[scope] = { ...(removals[scope] ?? {}), artifacts: { ...artifacts, [observation.path]: null } };
299
- }
300
- }
301
- return removals;
302
- }
303
-
304
-
305
233
  function installScopeStates(): void {
306
234
  scopeStates = runtime?.view ? runtime.states() : { global: emptyState(), cwd: emptyState(), session: emptyState() };
307
235
  effectiveState = runtime?.view ? runtime.readView() : {};
@@ -319,17 +247,28 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
319
247
 
320
248
  async function inspectTelegramState(scope: StateFlowTelegramScope): Promise<StateFlowTelegramInspection> {
321
249
  if (!activeContext || shuttingDown) throw new Error("State Flow is not attached to an active session yet");
322
- if (scope === "session" || scope === "effective") assertSelectedBranchAvailable();
323
- const selected = runtime ??= createRuntime(activeContext);
250
+ const ctx = activeContext;
251
+ const owner = sessionAddress(ctx).key;
252
+ const cwd = ctx.cwd;
253
+ const off = snapshot.config.mode === "off";
254
+ const privateScope = scope === "session" || scope === "effective";
255
+ if (!off && privateScope) assertSelectedBranchAvailable();
256
+ // Off operator reads are disposable: they confer no selected-branch/cache authority.
257
+ const selected = off ? createRuntime(ctx) : runtime ??= createRuntime(ctx);
324
258
  const signal = sharedInspectionLifetime.signal;
325
- if (!selected.view || (scope !== "session" && !modePersistenceError)) await selected.refreshShared(signal);
259
+ let privateAvailable = true;
260
+ if (off && privateScope) privateAvailable = await selected.refreshCurrentMemory(signal) !== undefined;
261
+ else if (!selected.view || (scope !== "session" && !modePersistenceError)) await selected.refreshShared(signal);
326
262
  signal.throwIfAborted();
327
- if (runtime !== selected) throw new Error("State Flow inspection selection changed; select the scope again");
328
- if (scope === "session" || scope === "effective") assertSelectedBranchAvailable();
263
+ if (activeContext !== ctx || ctx.cwd !== cwd || sessionAddress(ctx).key !== owner || (!off && runtime !== selected)) {
264
+ throw new Error("State Flow inspection selection changed; select the scope again");
265
+ }
266
+ if (!privateAvailable) throw new Error("Current State Flow session memory is unavailable");
267
+ if (!off && privateScope) assertSelectedBranchAvailable();
329
268
  if (!selected.view) throw new Error("State Flow temporal runtime is unavailable");
330
- installScopeStates();
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 };
269
+ if (!off) installScopeStates();
270
+ const state = selected.readView(0, scope === "effective" ? undefined : scope);
271
+ return { state: { ...projectModelState(state), ...(state.lazy === undefined ? {} : { lazy: structuredClone(state.lazy) }) }, revisions: temporalScopeRevisions(selected.view), signal };
333
272
  }
334
273
 
335
274
  function syncStateFlowTools(): void {
@@ -342,15 +281,11 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
342
281
  : active.filter((name) => !owned.includes(name)));
343
282
  }
344
283
 
345
- function recordPublication(_publication: RuntimePublication, _ctx: ExtensionContext): void {
284
+ /** Accepted canonical publication establishes runtime authority and makes the settled-turn Git backup due. */
285
+ function recordPublication(): void {
346
286
  // A passive view is not runtime authority; only accepted canonical publication establishes it.
347
287
  branchStartsWithoutRuntime = false;
348
- backupPending = true;
349
- }
350
-
351
- /** Record accepted lifecycle persistence; Git backup is scheduled only after an accepted turn settles. */
352
- function recordPolicyPublication(publication: RuntimePublication | undefined, ctx: ExtensionContext): void {
353
- if (publication) recordPublication(publication, ctx);
288
+ backup.markPublished();
354
289
  }
355
290
 
356
291
  function skillReadIsCurrent(read: SuccessfulSkillRead): boolean {
@@ -374,11 +309,9 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
374
309
  return `State Flow acquisition: this registered Skill belongs at ${target}. If durable compiled guidance is useful, include a non-empty description, kind:"skill", and compilation object there. Unrelated semantic patches do not need to include it.`;
375
310
  }
376
311
 
377
- function clearAcceptedAcquisitions(acquiredArtifactPaths = new Set(artifactReads.successful.keys())): void {
378
- artifactInvalidations = artifactInvalidations.filter(({ path }) => !acquiredArtifactPaths.has(path));
379
- artifactReads.setCandidates(artifactInvalidations);
312
+ function clearAcceptedAcquisitions(acquiredArtifactPaths?: ReadonlySet<string>): void {
313
+ acquisition.acceptAcquired(acquiredArtifactPaths);
380
314
  dropCurrentSkillReads();
381
- artifactReads.clear();
382
315
  }
383
316
 
384
317
  async function prepareInference(pending: InferencePreparation, selected: TemporalRuntime | undefined, ctx: ExtensionContext, operationSignal: AbortSignal): Promise<void> {
@@ -387,7 +320,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
387
320
  if (!selected?.view) throw new Error("Temporal State Flow runtime is unavailable; reload before publishing");
388
321
  await selected.withPatchTransaction((transaction) => {
389
322
  signal.throwIfAborted();
390
- if (runtime !== selected || inferencePreparation !== pending || !isActive() || shuttingDown) {
323
+ if (runtime !== selected || !inferencePreparation.owns(pending) || !isActive() || shuttingDown) {
391
324
  throw new Error("State Flow inference selection changed while awaiting publication");
392
325
  }
393
326
  assertPublicationAvailable();
@@ -410,13 +343,13 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
410
343
  snapshot = nextSnapshot;
411
344
  installScopeStates();
412
345
  if (rotatesRun && rehydrationPhase !== "new-bootstrap" && rehydrationPhase !== "resume-bootstrap") rehydrationPhase = "step";
413
- if (publication?.changed) recordPublication(publication, ctx);
346
+ if (publication?.changed) recordPublication();
414
347
  if (pending.prompt !== undefined || publication?.changed) appendCheckpoint();
415
348
  if (Object.keys(removals).length > 0) clearAcceptedAcquisitions();
416
349
  updateUi(ctx);
417
350
  }, signal);
418
351
  } catch (error) {
419
- if (signal.aborted || runtime !== selected || inferencePreparation !== pending || shuttingDown) return;
352
+ if (signal.aborted || runtime !== selected || !inferencePreparation.owns(pending) || shuttingDown) return;
420
353
  // Pi reports context-hook exceptions and continues. Abort through its public port rather than infer from stale preparation.
421
354
  ctx.abort();
422
355
  const cause = diagnosticText(error);
@@ -428,27 +361,22 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
428
361
  }
429
362
 
430
363
 
431
- function currentRehydrationPhase(): RehydrationPhase | undefined {
432
- return rehydrationPhase;
433
- }
434
-
435
364
  function statusDiagnostics(ctx: ExtensionContext): StatusDiagnostics {
436
365
  const cwd = ctx.cwd;
437
- const diagnosticStates = scopeStates;
438
366
  const view = runtime?.view;
439
367
  const diagnosticRecent = runtime?.recent() ?? [];
440
368
  const durableStateError = snapshot.meta.validation?.attempt === 0 ? snapshot.meta.validation.error
441
369
  : view ? undefined : "no temporal runtime is selected on this branch";
442
370
 
443
371
  const staleArtifacts: StatusDiagnostics["staleArtifacts"] = durableStateError === undefined
444
- ? artifactInvalidations.map(({ path, scope, reason }) => ({ scope: scope ?? "global", path, reason }))
372
+ ? acquisition.invalidations.map(({ path, scope, reason }) => ({ scope: scope ?? "global", path, reason }))
445
373
  : [];
446
374
  const session = sessionAddress(ctx);
447
375
  return {
448
376
  repositoryRoot,
449
377
  cwdScopeKey: cwdScopeKey(cwd),
450
378
  sessionScopeKey: sessionScopeKey(session.key),
451
- scopeStates: diagnosticStates,
379
+ scopeStates,
452
380
  effectiveState,
453
381
  recent: diagnosticRecent,
454
382
  historyLimit: config.historyLimit,
@@ -473,21 +401,66 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
473
401
  }
474
402
 
475
403
  /** Select the active native branch under one owned restoration lifetime; only current accepted work installs memory. */
476
- function restoreActiveBranch(ctx: ExtensionContext, sessionStartReason?: unknown, notifyRecovery = true, startOwner?: AbortController): Promise<void> {
404
+ function restoreActiveBranch(ctx: ExtensionContext, sessionStartReason?: unknown, notifyRecovery = true, startOwner?: AbortController, requestedMode?: InactiveMode): Promise<void> {
477
405
  contextProjection.reset();
478
- cancelBranchRestoration();
406
+ memoryToolLifetime.renew();
407
+ branchRestoration.cancel();
479
408
  // Start-owned attachment/fork recovery keeps its owner; only accepted Start cancels Stop.
480
409
  if (!startOwner) {
481
- cancelStartActivation();
482
- cancelInactivePersistence();
410
+ startActivation.cancel();
411
+ inactivePersistence.cancel();
483
412
  }
484
413
  clearRunTransient();
485
414
  passiveContinuation = undefined;
486
415
  bootstrapContinuation = undefined;
487
- artifactInvalidations = [];
488
- artifactReads.setCandidates([]);
416
+ acquisition.clearInvalidations();
489
417
  telegramStartPending = false;
490
418
  activeContext = ctx;
419
+ if (!startOwner) {
420
+ let mode: StateFlowMode = config.inactiveMode;
421
+ let adoptDefault = false;
422
+ let withoutRuntime = false;
423
+ let persistenceError: string | undefined;
424
+ let pendingFork = false;
425
+ try {
426
+ const branch = ctx.sessionManager.getBranch();
427
+ const discovery = discoverSnapshotData(branch);
428
+ withoutRuntime = discovery.candidates.length === 0 && discovery.errors.length === 0;
429
+ adoptDefault = withoutRuntime && isNewSession(sessionStartReason, branch);
430
+ pendingFork = hasPendingFork(branch, ctx.sessionManager.getSessionId(), PASSIVE_STOP_ENTRY_TYPE);
431
+ const inherited = sessionStartReason === "fork" || pendingFork;
432
+ const policy = findBranchPolicy(branch, inherited ? undefined : ctx.sessionManager.getSessionId(),
433
+ inherited || retainsPhysicalSessionProjection(sessionStartReason) ? PASSIVE_STOP_ENTRY_TYPE : undefined, config.inactiveMode);
434
+ mode = policy?.mode ?? (adoptDefault ? config.mode : config.inactiveMode);
435
+ persistenceError = inherited
436
+ ? findBranchPolicy(branch, ctx.sessionManager.getSessionId(), PASSIVE_STOP_ENTRY_TYPE, config.inactiveMode)?.persistenceError
437
+ : policy?.persistenceError;
438
+ } catch {
439
+ // Off needs only policy; unavailable native evidence grants no memory authority.
440
+ }
441
+ if (mode === "off") {
442
+ backup.cancel();
443
+ const owner = sessionAddress(ctx).key;
444
+ const reason = pendingFork || (deferredBranch?.owner === owner && deferredBranch.cwd === ctx.cwd && deferredBranch.reason === "fork"
445
+ && retainsPhysicalSessionProjection(sessionStartReason)) ? "fork" : sessionStartReason;
446
+ if (reason === "fork" && !pendingFork) pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, { owner: ctx.sessionManager.getSessionId(), forkPending: true });
447
+ deferredBranch = { reason, owner, cwd: ctx.cwd };
448
+ runtime = undefined;
449
+ snapshot = emptySnapshot("off");
450
+ branchStartsWithoutRuntime = withoutRuntime;
451
+ selectedHistoryExpired = false;
452
+ modePersistenceError = persistenceError;
453
+ forkInitialization = reason === "fork";
454
+ installScopeStates();
455
+ acquisition.reset();
456
+ if (adoptDefault) appendCheckpoint();
457
+ syncStateFlowTools();
458
+ updateUi(ctx);
459
+ return Promise.resolve();
460
+ }
461
+ if (pendingFork) sessionStartReason = "fork";
462
+ }
463
+ deferredBranch = undefined;
491
464
  runtime = createRuntime(ctx);
492
465
  installScopeStates();
493
466
  branchStartsWithoutRuntime = false;
@@ -548,17 +521,15 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
548
521
  snapshot = migrationFailure({}, "State Flow branch restoration is pending", selection.kind === "current" ? selection.mode
549
522
  : selection.kind === "auto-start" ? config.inactiveMode : inactiveSelection(selection.checkpoint.mode));
550
523
  }
524
+ if (requestedMode) snapshot = deactivateEpisode(snapshot, requestedMode);
551
525
  const pending: BranchRestoration = {
552
526
  controller: new AbortController(),
553
527
  awaitingAcceptance: selection.kind !== "settled" && selection.kind !== "current",
528
+ ...(requestedMode === undefined ? {} : { requestedMode }),
554
529
  };
555
- branchRestoration = pending;
556
- const operation = attachBranch(ctx, pending, selection, sessionStartReason, notifyRecovery, skipped);
557
- pending.operation = operation;
558
- restorationOperations.add(operation);
559
- const finished = () => { restorationOperations.delete(operation); };
560
- void operation.then(finished, finished);
561
- return operation;
530
+ branchRestoration.claim(pending);
531
+ // attachBranch releases its own ownership; settlement only drains it for shutdown.
532
+ return branchRestoration.track(pending, attachBranch(ctx, pending, selection, sessionStartReason, notifyRecovery, skipped), false);
562
533
  }
563
534
 
564
535
  async function attachBranch(
@@ -568,26 +539,23 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
568
539
  const placeholder = runtime;
569
540
  let selected = runtime;
570
541
  const owner = ctx.sessionManager.getSessionId();
571
- const cwd = ctx.cwd;
572
- const file = ctx.sessionManager.getSessionFile();
573
- const timestamp = ctx.sessionManager.getHeader()?.timestamp;
542
+ const sameSession = sessionIdentity(ctx);
574
543
  const signal = AbortSignal.any([pending.controller.signal, ...(ctx.signal ? [ctx.signal] : [])]);
575
- const isCurrent = () => branchRestoration === pending && !pending.controller.signal.aborted && !shuttingDown && runtime === selected
576
- && ctx.cwd === cwd && ctx.sessionManager.getSessionId() === owner && ctx.sessionManager.getSessionFile() === file
577
- && ctx.sessionManager.getHeader()?.timestamp === timestamp;
544
+ const isCurrent = () => branchRestoration.owns(pending) && !pending.controller.signal.aborted && !shuttingDown && runtime === selected
545
+ && sameSession();
578
546
  const assertCurrent = () => {
579
547
  signal.throwIfAborted();
580
548
  if (!isCurrent()) throw new Error("State Flow branch selection changed while awaiting publication");
581
549
  };
582
550
  let accepted = false;
583
551
  // Install accepted memory before native writes; later ancillary failures cannot revert or replay it.
584
- const accept = (candidate: TemporalRuntime, next: Snapshot, publication: RuntimePublication, nativeWrites: () => void): void => {
552
+ const accept = (candidate: TemporalRuntime, next: Snapshot, nativeWrites: () => void): void => {
585
553
  accepted = true;
586
554
  pending.awaitingAcceptance = false;
587
555
  runtime = selected = candidate;
588
556
  snapshot = next;
589
557
  installScopeStates();
590
- recordPolicyPublication(publication, ctx);
558
+ recordPublication();
591
559
  let failure: unknown;
592
560
  try { nativeWrites(); } catch (error) { failure = error; }
593
561
  settleSelection(ctx, reason, notifyRecovery, skipped, true);
@@ -614,16 +582,17 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
614
582
  if (restored.config.mode === "active" && restored.meta.specification === undefined
615
583
  && retainsPhysicalSessionProjection(reason) && hasUncheckpointedConversation(ctx.sessionManager.getBranch())) restored.meta.bootstrap = true;
616
584
  const next = pending.requestedMode ? deactivateEpisode(restored, pending.requestedMode) : restored;
617
- accept(candidate, next, publish(next), appendCheckpoint);
585
+ publish(next);
586
+ accept(candidate, next, appendCheckpoint);
618
587
  }, signal);
619
588
  } else if (selection.kind === "fork") {
620
589
  await candidate.withForkTransaction(selection.source, selection.checkpoint, (child, publish) => {
621
590
  assertCurrent();
622
591
  // Mode is selected independently of creating the child's private memory.
623
592
  const next = pending.requestedMode ? deactivateEpisode(child, pending.requestedMode) : child;
624
- const publication = publish(next);
593
+ publish(next);
625
594
  forkInitialization = false;
626
- accept(candidate, next, publication, () => {
595
+ accept(candidate, next, () => {
627
596
  pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, { reset: true, owner });
628
597
  appendCheckpoint();
629
598
  });
@@ -637,7 +606,8 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
637
606
  if (current || discovery.candidates.length > 0 || discovery.errors.length > 0) throw new Error("Existing session runtime requires retained-boundary restoration");
638
607
  const activated = startEpisode(hasPriorConversation(branch));
639
608
  const next = pending.requestedMode ? deactivateEpisode(activated, pending.requestedMode) : activated;
640
- accept(candidate, next, publish(next), appendCheckpoint);
609
+ publish(next);
610
+ accept(candidate, next, appendCheckpoint);
641
611
  }, signal, true);
642
612
  }
643
613
  } catch (error) {
@@ -671,7 +641,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
671
641
  installScopeStates();
672
642
  } finally {
673
643
  pending.awaitingAcceptance = false;
674
- if (branchRestoration === pending) branchRestoration = undefined;
644
+ branchRestoration.release(pending);
675
645
  }
676
646
  }
677
647
 
@@ -687,25 +657,24 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
687
657
  return passive;
688
658
  }
689
659
 
660
+ function selectedContinuation(ctx: ExtensionContext, reason: unknown): PassiveContinuation | undefined {
661
+ if (!retainsPhysicalSessionProjection(reason) || !runtime?.view) return undefined;
662
+ const boundary = findPassiveStopBoundary(ctx.sessionManager.getBranch(), ctx.sessionManager.getSessionId(), PASSIVE_STOP_ENTRY_TYPE);
663
+ return boundary === undefined ? undefined : createPassiveContinuation(
664
+ projectModelState(effectiveState), boundary.at, boundary.from, boundary.preserveContext,
665
+ );
666
+ }
667
+
690
668
  function settleSelection(ctx: ExtensionContext, reason: unknown, notifyRecovery: boolean, skipped: number, selectedMemory: boolean): void {
691
669
  const failure = !isActive() && snapshot.meta.validation?.attempt === 0 ? snapshot.meta.validation.error : undefined;
692
670
  if (failure !== undefined && notifyRecovery && !modePersistenceError) {
693
671
  if (selectedHistoryExpired) notifyProblem(ctx, "State Flow history is outside the retained temporal window; /state-flow-active can use current session memory.", "warning");
694
672
  else notifyProblem(ctx, `State Flow restore failed: ${failure}`, "error");
695
673
  }
696
- if (selectedMemory && retainsPhysicalSessionProjection(reason) && runtime?.view) {
697
- const boundary = findPassiveStopBoundary(ctx.sessionManager.getBranch(), ctx.sessionManager.getSessionId(), PASSIVE_STOP_ENTRY_TYPE);
698
- if (boundary !== undefined) {
699
- const continuation = createPassiveContinuation(
700
- projectModelState(effectiveState),
701
- boundary.at,
702
- boundary.from,
703
- boundary.preserveContext,
704
- );
705
- // Off retains the handoff boundary for a later Passive or Active choice but never projects it.
706
- if (!isActive()) passiveContinuation = continuation;
707
- else if (snapshot.meta.bootstrap) bootstrapContinuation = continuation;
708
- }
674
+ const continuation = selectedMemory ? selectedContinuation(ctx, reason) : undefined;
675
+ if (continuation !== undefined) {
676
+ if (!isActive()) passiveContinuation = continuation;
677
+ else if (snapshot.meta.bootstrap) bootstrapContinuation = continuation;
709
678
  }
710
679
  if (notifyRecovery && isActive() && skipped > 0) {
711
680
  notifyProblem(ctx, `State Flow skipped ${skipped} malformed snapshot(s); restored the last valid one.`, "warning");
@@ -722,7 +691,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
722
691
  pi.registerTool({
723
692
  name: READ_STATE_TOOL_NAME,
724
693
  label: "Read State",
725
- description: "Read exact State Flow values or historical semantic patches with one path or an ordered path list. Unscoped semantic paths read the effective overlay; effective makes that overlay explicit, while global, cwd, and session select ownership. Array selectors support zero-based indices and half-open [start..end] ranges. Projection value returns semantic data; keys returns minimal structure; patch intersects the selected path at its boundary.",
694
+ description: "Read exact State Flow values or historical semantic patches with one path or an ordered path list. Unscoped semantic paths read the effective overlay; effective makes that overlay explicit, while global, cwd, and session select ownership. Array selectors support zero-based indices and half-open [start..end] ranges. Projection value returns semantic data; keys returns minimal structure; patch intersects the selected path at its boundary. Path object keys must be ASCII identifiers ([A-Za-z_$][A-Za-z0-9_$-]*); non-ASCII keys cannot be addressed.",
726
695
  promptSnippet: "Read exact state values, structures, or historical semantic patches",
727
696
  parameters: Type.Object({
728
697
  path: Type.Optional(Type.String({ description: "One semantic path; unscoped paths use effective, explicit roots may use effective/global/cwd/session and historical [N], and arrays support half-open [start..end] ranges" })),
@@ -764,7 +733,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
764
733
  pi.registerTool({
765
734
  name: PATCH_STATE_TOOL_NAME,
766
735
  label: "Patch State",
767
- description: "The sole State Flow semantic mutation protocol. Supply one or more global, cwd, or session patches; all supplied scopes commit atomically. This call must be the only State Flow barrier in its assistant response.",
736
+ description: "The sole State Flow semantic mutation protocol. Supply one or more global, cwd, or session patches; all supplied scopes commit atomically. This call must be the only State Flow barrier in its assistant response. Deleting an intent also deletes same-scope working/lazy keys its {\"$ref\"} values own unless another intent refs them. Name keys in ASCII ([A-Za-z_$][A-Za-z0-9_$-]*) so read paths and refs resolve; values may use any language.",
768
737
  promptSnippet: "Atomically patch one or more global/cwd/session scopes",
769
738
  promptGuidelines: [
770
739
  "Use patch_state only for material durable semantic changes; ordinary answers need no finalization call.",
@@ -784,6 +753,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
784
753
  return new Text(separatedOutput(theme.fg("dim", formatPatchStateArguments(context.args))), 0, 0);
785
754
  },
786
755
  async execute(toolCallId, params, signal, _onUpdate, ctx) {
756
+ signal = signal ? AbortSignal.any([signal, memoryToolLifetime.signal]) : memoryToolLifetime.signal;
787
757
  try {
788
758
  if (!memoryToolsAvailable()) throw new Error("State Flow tools are off for this session");
789
759
  assertPublicationAvailable();
@@ -832,12 +802,12 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
832
802
  snapshot = nextSnapshot;
833
803
  installScopeStates();
834
804
  if (publication?.changed) {
835
- recordPublication(publication, ctx);
805
+ recordPublication();
836
806
  appendCheckpoint();
837
807
  }
838
808
  clearAcceptedAcquisitions(new Set(acquiredArtifacts.map(({ path }) => path)));
839
809
  updateUi(ctx);
840
- const updates = contextProjection.acceptPatch(previousEffective, effectiveState, patches, artifactHints);
810
+ const updates = contextProjection.acceptPatch(previousEffective, effectiveState, patches, acquisition.hints);
841
811
  const acknowledgement = changed
842
812
  ? `\nState materialized atomically at ${scopes.join("+")} scope${scopes.length === 1 ? "" : "s"}.`
843
813
  : "\nState already current.";
@@ -850,7 +820,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
850
820
  let attempted: unknown;
851
821
  try { attempted = structuredClone(params); } catch { attempted = undefined; }
852
822
  const cause = diagnosticText(error);
853
- recordDiagnostic(cause, /concurrently|advanced/.test(cause) ? "publication-conflict" : "invalid-patch", ctx, {
823
+ if (!signal.aborted && memoryToolsAvailable()) recordDiagnostic(cause, /concurrently|advanced/.test(cause) ? "publication-conflict" : "invalid-patch", ctx, {
854
824
  input: attempted,
855
825
  tool: PATCH_STATE_TOOL_NAME,
856
826
  toolCallId,
@@ -862,42 +832,33 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
862
832
 
863
833
  function startStateFlow(ctx: ExtensionContext): Promise<StateFlowTelegramControlResult> {
864
834
  if (shuttingDown) return Promise.resolve({ ok: false, message: "State Flow is shutting down" });
865
- if (startActivation?.operation) return startActivation.operation;
835
+ if (startActivation.current?.operation) return startActivation.current.operation;
866
836
  telegramStartPending = false;
867
- if (activeContext && !branchRestoration && isActive()) {
837
+ if (activeContext && !branchRestoration.current && isActive()) {
868
838
  return Promise.resolve({ ok: true, message: "State Flow is already active", signal: sharedInspectionLifetime.signal });
869
839
  }
870
- const pending: NonNullable<typeof startActivation> = { controller: new AbortController() };
871
- startActivation = pending;
872
- const operation = runStart(ctx, pending.controller);
873
- pending.operation = operation;
874
- const finished = () => { if (startActivation === pending) startActivation = undefined; };
875
- void operation.then(finished, finished);
876
- return operation;
840
+ const pending = startActivation.claim({ controller: new AbortController() });
841
+ return startActivation.track(pending, runStart(ctx, pending.controller));
877
842
  }
878
843
 
879
844
  /** Await restoration-owned attachment and exact-source fork recovery before current-head activation. */
880
845
  async function runStart(ctx: ExtensionContext, pending: AbortController): Promise<StateFlowTelegramControlResult> {
881
- const owner = ctx.sessionManager.getSessionId();
882
- const cwd = ctx.cwd;
883
- const file = ctx.sessionManager.getSessionFile();
884
- const timestamp = ctx.sessionManager.getHeader()?.timestamp;
846
+ const sameSession = sessionIdentity(ctx);
885
847
  const signal = ctx.signal ? AbortSignal.any([pending.signal, ctx.signal]) : pending.signal;
886
- const isOwner = () => startActivation?.controller === pending && !signal.aborted && !shuttingDown
887
- && ctx.cwd === cwd && ctx.sessionManager.getSessionId() === owner && ctx.sessionManager.getSessionFile() === file
888
- && ctx.sessionManager.getHeader()?.timestamp === timestamp;
848
+ const isOwner = () => startActivation.current?.controller === pending && !signal.aborted && !shuttingDown && sameSession();
889
849
  const superseded = (): StateFlowTelegramControlResult => {
890
850
  pending.abort();
891
851
  return { ok: false, message: "State Flow activation was superseded", signal: pending.signal };
892
852
  };
893
853
  try {
894
- if (!activeContext) await waitForRecovery(restoreActiveBranch(ctx, undefined, false, pending), signal);
895
- else if (branchRestoration?.operation) await waitForRecovery(branchRestoration.operation, signal);
854
+ if (deferredBranch?.reason === "fork") await waitForRecovery(restoreActiveBranch(ctx, "fork", false, pending), signal);
855
+ else if (!activeContext) await waitForRecovery(restoreActiveBranch(ctx, undefined, false, pending), signal);
856
+ else if (branchRestoration.current?.operation) await waitForRecovery(branchRestoration.current.operation, signal);
896
857
  if (!isOwner()) return superseded();
897
858
  if (isActive()) return { ok: true, message: "State Flow is already active", signal: sharedInspectionLifetime.signal };
898
859
  if (forkInitialization) {
899
860
  // Withdraw this join on cancellation without revoking the independently owned mode persistence.
900
- const stopping = inactivePersistence?.operation;
861
+ const stopping = inactivePersistence.current?.operation;
901
862
  if (stopping) await waitForRecovery(stopping, signal);
902
863
  if (!isOwner()) return superseded();
903
864
  await waitForRecovery(restoreActiveBranch(ctx, "fork", false, pending), signal);
@@ -916,48 +877,59 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
916
877
  async function activateCurrentState(ctx: ExtensionContext, pending: AbortController): Promise<StateFlowTelegramControlResult> {
917
878
  let selected = runtime;
918
879
  const owner = ctx.sessionManager.getSessionId();
919
- const cwd = ctx.cwd;
920
- const file = ctx.sessionManager.getSessionFile();
921
- const timestamp = ctx.sessionManager.getHeader()?.timestamp;
880
+ const sameSession = sessionIdentity(ctx);
922
881
  const initiallyActive = isActive();
923
882
  let accepted = false;
924
883
  let receipt = AbortSignal.any([pending.signal, sharedInspectionLifetime.signal]);
925
- const isCurrent = () => startActivation?.controller === pending && runtime === selected && !shuttingDown
926
- && ctx.cwd === cwd && ctx.sessionManager.getSessionId() === owner && ctx.sessionManager.getSessionFile() === file
927
- && ctx.sessionManager.getHeader()?.timestamp === timestamp && isActive() === (accepted || initiallyActive);
884
+ const signal = ctx.signal ? AbortSignal.any([pending.signal, ctx.signal]) : pending.signal;
885
+ const isCurrent = () => startActivation.current?.controller === pending && !signal.aborted && runtime === selected && !shuttingDown
886
+ && sameSession() && isActive() === (accepted || initiallyActive);
928
887
  const superseded = (): StateFlowTelegramControlResult => {
929
888
  pending.abort();
930
889
  return { ok: false, message: "State Flow activation was superseded", signal: pending.signal };
931
890
  };
932
891
  try {
933
892
  const activation = createRuntime(ctx);
934
- const signal = ctx.signal ? AbortSignal.any([pending.signal, ctx.signal]) : pending.signal;
893
+ const deferred = deferredBranch;
894
+ if (deferred) {
895
+ const discovery = discoverSnapshotData(ctx.sessionManager.getBranch());
896
+ const retained = selectRetainedCheckpoint(discovery.candidates, config.inactiveMode);
897
+ branchStartsWithoutRuntime = retained.kind === "pre-runtime" || (discovery.candidates.length === 0 && discovery.errors.length === 0);
898
+ }
899
+ // Off activation owns one cancellable current-head publication, not an independent read-only restore.
935
900
  const result = await activation.withStartTransaction((current, publish) => {
936
901
  signal.throwIfAborted();
937
902
  if (!isCurrent()) throw new Error("State Flow activation selection changed while awaiting publication");
938
903
  if (!current && !branchStartsWithoutRuntime) throw new Error(snapshot.meta.validation?.error ?? "Current State Flow session memory is unavailable");
939
- const continuation = passiveContinuation ?? bootstrapContinuation;
940
- const bootstrap = hasPriorConversation(ctx.sessionManager.getBranch()) || passiveContinuation !== undefined;
904
+ const boundary = deferred && retainsPhysicalSessionProjection(deferred.reason)
905
+ ? findPassiveStopBoundary(ctx.sessionManager.getBranch(), owner, PASSIVE_STOP_ENTRY_TYPE) : undefined;
906
+ const bootstrap = hasPriorConversation(ctx.sessionManager.getBranch()) || passiveContinuation !== undefined || boundary !== undefined;
941
907
  const activated = current ? resumeEpisode(current, bootstrap) : startEpisode(bootstrap);
942
908
  const recoveredCurrent = selectedHistoryExpired;
943
- const publication = publish(activated);
909
+ publish(activated);
944
910
  accepted = true;
945
- cancelInactivePersistence();
911
+ inactivePersistence.cancel();
946
912
  runtime = selected = activation;
947
913
  snapshot = activated;
948
914
  activeContext = ctx;
949
915
  selectedHistoryExpired = false;
950
916
  modePersistenceError = undefined;
951
917
  installScopeStates();
918
+ const continuation = passiveContinuation ?? bootstrapContinuation ?? (deferred ? selectedContinuation(ctx, deferred.reason) : undefined);
919
+ deferredBranch = undefined;
952
920
  clearRunTransient();
953
921
  contextProjection.reset();
954
922
  passiveContinuation = undefined;
955
923
  bootstrapContinuation = snapshot.meta.bootstrap ? continuation : undefined;
956
924
  deferInferencePreparation();
957
925
  receipt = AbortSignal.any([pending.signal, sharedInspectionLifetime.signal]);
958
- recordPolicyPublication(publication, ctx);
926
+ recordPublication();
959
927
  syncStateFlowTools();
960
928
  updateUi(ctx);
929
+ if (forkInitialization) {
930
+ forkInitialization = false;
931
+ pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, { reset: true, owner });
932
+ }
961
933
  appendCheckpoint();
962
934
  ctx.ui.notify(
963
935
  recoveredCurrent
@@ -1014,21 +986,22 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1014
986
  }
1015
987
 
1016
988
  function deactivateStateFlow(ctx: ExtensionContext, mode: InactiveMode): Promise<StateFlowTelegramControlResult> {
1017
- cancelStartActivation();
1018
989
  if (shuttingDown) return Promise.resolve({ ok: false, message: "State Flow is shutting down" });
990
+ if (mode === "off") return selectOff(ctx);
991
+ startActivation.cancel();
1019
992
  telegramStartPending = false;
1020
- const persisting = inactivePersistence;
993
+ const persisting = inactivePersistence.current;
1021
994
  if (persisting?.operation && !persisting.published) {
1022
995
  // Pending inactive choices coalesce: acceptance persists whichever inactive mode is current then.
1023
996
  if (snapshot.config.mode !== mode) applyInactiveMode(ctx, mode);
1024
997
  return persisting.operation;
1025
998
  }
1026
999
  if (!activeContext) void restoreActiveBranch(ctx, undefined, false);
1027
- const restoring = branchRestoration;
1000
+ const restoring = branchRestoration.current;
1028
1001
  // Read-only recovery also retains the latest policy; its native write fence is updated below.
1029
1002
  if (restoring) restoring.requestedMode = mode;
1030
1003
  if (restoring?.awaitingAcceptance) {
1031
- // A mode change never cancels memory restoration or initialization.
1004
+ // Passive keeps independently owned restoration; Off withdraws it through selectOff.
1032
1005
  applyInactiveMode(ctx, mode);
1033
1006
  return Promise.resolve({ ok: true, message: `State Flow ${mode}; memory restoration continues` });
1034
1007
  }
@@ -1037,23 +1010,98 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1037
1010
  const recordedMode = retained.kind === "pre-runtime" ? retained.mode : retained.kind === "boundary" ? retained.checkpoint.mode : undefined;
1038
1011
  if (modePersistenceError || recordedMode === mode) return Promise.resolve({ ok: true, message: `State Flow is already ${mode}` });
1039
1012
  }
1040
- cancelInactivePersistence();
1041
- const pending: NonNullable<typeof inactivePersistence> = { controller: new AbortController() };
1042
- inactivePersistence = pending;
1043
- const operation = persistInactiveMode(ctx, pending, mode);
1044
- pending.operation = operation;
1045
- const finished = () => { if (inactivePersistence === pending) inactivePersistence = undefined; };
1046
- void operation.then(finished, finished);
1047
- return operation;
1013
+ inactivePersistence.cancel();
1014
+ const pending = inactivePersistence.claim({ controller: new AbortController() });
1015
+ return inactivePersistence.track(pending, deferredBranch && mode === "passive" && !branchStartsWithoutRuntime
1016
+ ? acquirePassiveMode(ctx, pending, deferredBranch.reason)
1017
+ : persistInactiveMode(ctx, pending, mode));
1048
1018
  }
1049
1019
 
1050
- async function persistInactiveMode(ctx: ExtensionContext, pending: NonNullable<typeof inactivePersistence>, mode: InactiveMode): Promise<StateFlowTelegramControlResult> {
1020
+ /** Off owns cancellation and native policy only; no canonical transaction or semantic handoff. */
1021
+ function selectOff(ctx: ExtensionContext): Promise<StateFlowTelegramControlResult> {
1022
+ const address = sessionAddress(ctx);
1023
+ const owner = ctx.sessionManager.getSessionId();
1024
+ const branch = ctx.sessionManager.getBranch();
1025
+ const selected = runtime;
1026
+ const sameOwner = selected?.sessionKey === address.key && selected.cwd === ctx.cwd;
1027
+ const forkRecorded = hasPendingFork(branch, owner, PASSIVE_STOP_ENTRY_TYPE);
1028
+ const pendingFork = forkRecorded || (sameOwner && forkInitialization) || (deferredBranch?.owner === address.key && deferredBranch.cwd === ctx.cwd && deferredBranch.reason === "fork");
1029
+ const policy = findBranchPolicy(branch, owner, PASSIVE_STOP_ENTRY_TYPE, config.inactiveMode);
1030
+ const sameDeferred = deferredBranch?.owner === address.key && deferredBranch.cwd === ctx.cwd;
1031
+ const current = sameOwner || sameDeferred ? snapshot : emptySnapshot("off");
1032
+ if (!sameOwner && !sameDeferred) modePersistenceError = policy?.persistenceError;
1033
+ const alreadyOff = current.config.mode === "off" && policy?.mode === "off";
1034
+ const incoming = sameOwner || sameDeferred ? current.meta.bootstrap ? bootstrapContinuation : passiveContinuation : undefined;
1035
+ const unfinished = current.meta.specification !== undefined || (inferencePreparation.current?.prompt !== undefined && !inferencePreparation.current.accepted)
1036
+ || (isActive() && hasUncheckpointedConversation(branch));
1037
+ const at = incoming?.startedAt ?? Date.now();
1038
+ const from = incoming ? incoming.activeRunStartedAt : (sameOwner || sameDeferred) && (!ctx.isIdle() || unfinished) ? runAnchorTimestamp : undefined;
1039
+ const preserveContext = modePersistenceError !== undefined || (incoming ? incoming.preserveContext : current.meta.bootstrap === true || (unfinished && from === undefined));
1040
+ const discovery = discoverSnapshotData(branch);
1041
+ const preRuntime = (sameOwner && branchStartsWithoutRuntime) || (discovery.candidates.length === 0 && discovery.errors.length === 0);
1042
+ let checkpoint: RetainedPiCheckpoint | undefined;
1043
+ try {
1044
+ checkpoint = !modePersistenceError && current.meta.validation?.attempt !== 0
1045
+ ? preRuntime ? preRuntimeCheckpoint("off") : sameOwner && selected?.view ? selected.retainedCheckpoint(deactivateEpisode(current, "off")) : undefined
1046
+ : undefined;
1047
+ } catch {
1048
+ // An unusable cached bookmark cannot prevent native Off policy or authorize replacement memory.
1049
+ }
1050
+ startActivation.cancel();
1051
+ branchRestoration.cancel();
1052
+ inactivePersistence.cancel();
1053
+ backup.cancel();
1054
+ memoryToolLifetime.renew();
1055
+ clearRunTransient();
1056
+ contextProjection.reset();
1057
+ telegramStartPending = false;
1058
+ bootstrapContinuation = undefined;
1059
+ passiveContinuation = undefined;
1060
+ acquisition.reset();
1061
+ runtime = undefined;
1062
+ snapshot = emptySnapshot("off");
1063
+ selectedHistoryExpired = false;
1064
+ branchStartsWithoutRuntime = preRuntime;
1065
+ forkInitialization = pendingFork;
1066
+ deferredBranch = { owner: address.key, cwd: ctx.cwd, reason: pendingFork ? "fork" : undefined };
1067
+ activeContext = ctx;
1068
+ installScopeStates();
1069
+ syncStateFlowTools();
1070
+ updateUi(ctx);
1071
+ try {
1072
+ if (!alreadyOff || (pendingFork && !forkRecorded)) {
1073
+ if (!checkpoint || !preRuntime || pendingFork || preserveContext || from !== undefined || modePersistenceError) pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, {
1074
+ owner, at, mode: "off", memoryDeferred: true,
1075
+ ...(from === undefined ? {} : { from }),
1076
+ ...(preserveContext ? { preserveContext: true } : {}),
1077
+ ...(pendingFork ? { forkPending: true } : {}),
1078
+ ...(modePersistenceError === undefined ? {} : { persistenceError: modePersistenceError }),
1079
+ });
1080
+ if (checkpoint) pi.appendEntry(SNAPSHOT_ENTRY_TYPE, checkpoint);
1081
+ }
1082
+ return Promise.resolve({ ok: true, message: alreadyOff ? "State Flow is already off" : "State Flow off" });
1083
+ } catch (error) {
1084
+ const message = conciseDiagnostic(`State Flow off; native mode recording failed: ${diagnosticText(error)}`);
1085
+ notifyProblem(ctx, message, "error");
1086
+ return Promise.resolve({ ok: false, message });
1087
+ }
1088
+ }
1089
+
1090
+ async function acquirePassiveMode(ctx: ExtensionContext, pending: InactivePersistence, reason: unknown): Promise<StateFlowTelegramControlResult> {
1091
+ await restoreActiveBranch(ctx, reason, true, pending.controller, "passive");
1092
+ if (!inactivePersistence.owns(pending) || pending.controller.signal.aborted || shuttingDown) {
1093
+ return { ok: false, message: "State Flow mode change was superseded" };
1094
+ }
1095
+ return persistInactiveMode(ctx, pending, "passive");
1096
+ }
1097
+
1098
+ async function persistInactiveMode(ctx: ExtensionContext, pending: InactivePersistence, mode: InactiveMode): Promise<StateFlowTelegramControlResult> {
1051
1099
  let selected = runtime;
1052
1100
  const owner = ctx.sessionManager.getSessionId();
1053
1101
  const current = snapshot;
1054
1102
  const wasActive = current.config.mode === "active";
1055
1103
  let unfinished = current.meta.specification !== undefined
1056
- || (inferencePreparation?.prompt !== undefined && !inferencePreparation.accepted);
1104
+ || (inferencePreparation.current?.prompt !== undefined && !inferencePreparation.current.accepted);
1057
1105
  applyInactiveMode(ctx, mode);
1058
1106
  const stoppedAt = Date.now();
1059
1107
  if (modePersistenceError) {
@@ -1069,7 +1117,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1069
1117
  const incomingBoundary = current.meta.bootstrap ? bootstrapContinuation : undefined;
1070
1118
  const anchor = runAnchorTimestamp;
1071
1119
  const idle = ctx.isIdle();
1072
- const isCurrent = () => inactivePersistence === pending && runtime === selected
1120
+ const isCurrent = () => inactivePersistence.owns(pending) && runtime === selected
1073
1121
  && !shuttingDown && ctx.sessionManager.getSessionId() === owner && !isActive();
1074
1122
  const superseded = (): StateFlowTelegramControlResult => ({ ok: false, message: "State Flow mode change was superseded" });
1075
1123
  const freezeHandoff = (): PassiveContinuation | undefined => {
@@ -1082,7 +1130,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1082
1130
  modePersistenceError !== undefined || (incomingBoundary ? incomingBoundary.preserveContext : current.meta.bootstrap === true || (unfinished && anchor === undefined)),
1083
1131
  )
1084
1132
  : undefined;
1085
- // Off keeps the frozen boundary for a later Passive/Active choice but never projects it.
1133
+ // Repeated Passive choices keep the already selected continuation boundary.
1086
1134
  passiveContinuation = handoff ?? (!wasActive ? passiveContinuation : undefined);
1087
1135
  return handoff;
1088
1136
  };
@@ -1110,8 +1158,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1110
1158
  // Projection changes before any wait; capture the old run's boundary before later native input can replace it.
1111
1159
  freezeHandoff();
1112
1160
  bootstrapContinuation = undefined;
1113
- artifactInvalidations = [];
1114
- artifactReads.setCandidates([]);
1161
+ acquisition.clearInvalidations();
1115
1162
  assertPublicationAvailable();
1116
1163
  const signal = ctx.signal ? AbortSignal.any([pending.controller.signal, ctx.signal]) : pending.controller.signal;
1117
1164
  if (branchStartsWithoutRuntime || !selected?.view) {
@@ -1131,11 +1178,11 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1131
1178
  if (!isCurrent()) throw new Error("State Flow mode selection changed while awaiting publication");
1132
1179
  assertPublicationAvailable();
1133
1180
  const next = structuredClone(snapshot);
1134
- const publication = publish(next);
1181
+ publish(next);
1135
1182
  accepted = true;
1136
1183
  snapshot = next;
1137
1184
  installScopeStates();
1138
- recordPolicyPublication(publication, ctx);
1185
+ recordPublication();
1139
1186
  return complete();
1140
1187
  }, signal);
1141
1188
  return isCurrent() ? result : superseded();
@@ -1149,8 +1196,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1149
1196
  }
1150
1197
  modePersistenceError = cause.trim() ? cause : "Canonical State Flow persistence failed";
1151
1198
  bootstrapContinuation = undefined;
1152
- artifactInvalidations = [];
1153
- artifactReads.setCandidates([]);
1199
+ acquisition.clearInvalidations();
1154
1200
  return complete();
1155
1201
  }
1156
1202
  }
@@ -1184,7 +1230,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1184
1230
  },
1185
1231
  cancelStart: () => {
1186
1232
  telegramStartPending = false;
1187
- cancelStartActivation();
1233
+ startActivation.cancel();
1188
1234
  },
1189
1235
  },
1190
1236
  });
@@ -1201,7 +1247,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1201
1247
  contextProjection.reset();
1202
1248
  skillReads.clear();
1203
1249
  artifactReads.clear();
1204
- cancelResponseReconciliation();
1250
+ responseReconciliation.cancel();
1205
1251
  completedRunAccepted = false;
1206
1252
  // This native hook has no operation signal. Capture only; the first active context owns acceptance.
1207
1253
  deferInferencePreparation(event.prompt);
@@ -1221,12 +1267,12 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1221
1267
  if (runtime?.view) refreshArtifactHints();
1222
1268
  if (!isActive() && !passiveContinuation && !passiveMemoryAvailable()) return;
1223
1269
  // Idle inspection must not freeze a pre-acceptance snapshot for the live inference.
1224
- const projection = isActive() && inferencePreparation && !inferencePreparation.accepted
1270
+ const projection = isActive() && inferencePreparation.current && !inferencePreparation.current.accepted
1225
1271
  ? new ContextProjection() : contextProjection;
1226
1272
  const effective = effectiveState;
1227
- const invalidations = artifactInvalidations.map(({ path, scope, reason }) => ({ path, ...(scope === undefined ? {} : { scope }), reason }));
1228
- const phase = isActive() ? currentRehydrationPhase() : undefined;
1229
- const view = contextView(effective, artifactHints, invalidations, phase);
1273
+ const invalidations = acquisition.invalidations.map(({ path, scope, reason }) => ({ path, ...(scope === undefined ? {} : { scope }), reason }));
1274
+ const phase = isActive() ? rehydrationPhase : undefined;
1275
+ const view = contextView(effective, acquisition.hints, invalidations, phase);
1230
1276
  if (passiveContinuation) {
1231
1277
  const retained = passiveContinuationMessages(messages, passiveContinuation);
1232
1278
  if (!runtime?.view) return { messages: retained };
@@ -1246,20 +1292,20 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1246
1292
  }
1247
1293
 
1248
1294
  function prepareContext(messages: AgentMessage[], ctx: ExtensionContext): ReturnType<typeof projectContext> | Promise<ReturnType<typeof projectContext>> {
1249
- const pending = inferencePreparation;
1295
+ const pending = inferencePreparation.current;
1250
1296
  const selected = runtime;
1251
1297
  const operationSignal = ctx.signal;
1252
1298
  // Idle projections remain observational; only a live native operation may await preparation.
1253
1299
  if (!isActive() || !pending || !operationSignal || shuttingDown) return projectContext(messages);
1254
1300
  pending.operation ??= prepareInference(pending, selected, ctx, operationSignal).finally(() => {
1255
1301
  // A late withdrawal must not clear newer work; accepted lifecycle is never replayed after an error.
1256
- if (!pending.accepted && inferencePreparation === pending) pending.operation = undefined;
1302
+ if (!pending.accepted && inferencePreparation.owns(pending)) pending.operation = undefined;
1257
1303
  });
1258
1304
  return pending.operation.then(() => {
1259
1305
  if (shuttingDown || operationSignal.aborted || runtime !== selected || ctx.signal !== operationSignal) return;
1260
- if (isActive() && inferencePreparation !== pending) {
1306
+ if (isActive() && !inferencePreparation.owns(pending)) {
1261
1307
  // Start may require same-run maintenance. A new captured user prompt instead owns a different context request.
1262
- if (inferencePreparation?.prompt === undefined) return prepareContext(messages, ctx);
1308
+ if (inferencePreparation.current?.prompt === undefined) return prepareContext(messages, ctx);
1263
1309
  return;
1264
1310
  }
1265
1311
  if (isActive() && !pending.accepted) return;
@@ -1319,32 +1365,32 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1319
1365
  if (event.message.role === "user" && runAnchorTimestamp === undefined) runAnchorTimestamp = event.message.timestamp;
1320
1366
  if (shuttingDown || !isActive()) return;
1321
1367
  if (event.message.role !== "assistant") return;
1322
- cancelResponseReconciliation();
1368
+ responseReconciliation.cancel();
1323
1369
  const message = event.message as unknown as { role: "assistant"; stopReason?: string; content?: unknown };
1324
1370
  if (message.stopReason === "aborted" || assistantToolCallCount(message.content) > 0 || message.stopReason === "toolUse") return;
1325
1371
  if (message.stopReason === "length" || message.stopReason === "error") {
1326
1372
  recordDiagnostic(`Assistant response ended with ${message.stopReason}`, "finalization", ctx, { content: message.content });
1327
1373
  return;
1328
1374
  }
1329
- responseReconciliation = new AbortController();
1375
+ responseReconciliation.claim({ controller: new AbortController() });
1330
1376
  });
1331
1377
 
1332
1378
  pi.on("turn_end", async (event, ctx) => {
1333
1379
  if (shuttingDown) return;
1334
- const pending = responseReconciliation;
1380
+ const pending = responseReconciliation.current;
1335
1381
  if (!isActive() || !pending) {
1336
1382
  updateUi(ctx);
1337
1383
  return;
1338
1384
  }
1339
1385
  const selected = runtime;
1340
- const signal = ctx.signal ? AbortSignal.any([pending.signal, ctx.signal]) : pending.signal;
1386
+ const signal = ctx.signal ? AbortSignal.any([pending.controller.signal, ctx.signal]) : pending.controller.signal;
1341
1387
  let responseCommitted = false;
1342
1388
  try {
1343
1389
  const response = finalizedAssistantResponse(event.message);
1344
1390
  if (!selected?.view) throw new Error("Temporal State Flow runtime is unavailable; reload before publishing");
1345
1391
  await selected.withPatchTransaction((transaction) => {
1346
1392
  signal.throwIfAborted();
1347
- if (runtime !== selected || responseReconciliation !== pending || !isActive()) {
1393
+ if (runtime !== selected || !responseReconciliation.owns(pending) || !isActive()) {
1348
1394
  throw new Error("State Flow response selection changed while awaiting publication");
1349
1395
  }
1350
1396
  assertPublicationAvailable();
@@ -1360,64 +1406,56 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1360
1406
  installScopeStates();
1361
1407
  responseCommitted = true;
1362
1408
  contextProjection.reset();
1363
- if (publication?.changed) recordPublication(publication, ctx);
1409
+ if (publication?.changed) recordPublication();
1364
1410
  appendCheckpoint();
1365
1411
  clearAcceptedAcquisitions();
1366
1412
  bootstrapContinuation = undefined;
1367
1413
  rehydrationPhase = "step";
1368
1414
  completedRunAccepted = !wasBootstrap;
1369
- turnAcceptedForBackup = true;
1415
+ backup.markTurnAccepted();
1370
1416
  updateUi(ctx);
1371
1417
  }, signal);
1372
1418
  } catch (error) {
1373
1419
  // Superseded completion owns neither the new lifecycle nor its diagnostics/UI.
1374
- if (signal.aborted || runtime !== selected || responseReconciliation !== pending) return;
1420
+ if (signal.aborted || runtime !== selected || !responseReconciliation.owns(pending)) return;
1375
1421
  const cause = diagnosticText(error);
1376
1422
  recordDiagnostic(cause, "finalization", ctx);
1377
1423
  notifyProblem(ctx, responseCommitted
1378
1424
  ? `State Flow response saved; lifecycle update failed: ${cause}`
1379
1425
  : `State Flow response reconciliation failed: ${cause}`, "error");
1380
1426
  } finally {
1381
- if (responseReconciliation === pending) responseReconciliation = undefined;
1427
+ responseReconciliation.release(pending);
1382
1428
  }
1383
1429
  });
1384
1430
 
1385
1431
  pi.on("session_before_compact", (event) => {
1386
- if (!compactionPlan) return;
1387
- if (compactionStopped && event.reason === "manual" && event.customInstructions === compactionMarker) return { cancel: true };
1388
- const result = stateFlowCompactionResult(compactionPlan, compactionMarker, event);
1432
+ const result = compactionRequests.resolve(event, !shuttingDown && isActive());
1389
1433
  if (result === undefined || "cancel" in result) return result;
1390
1434
  return { compaction: result };
1391
1435
  });
1392
1436
 
1393
1437
  pi.on("agent_before_settle", async (_event, ctx) => {
1394
- if (shuttingDown || !turnAcceptedForBackup) return;
1395
- turnAcceptedForBackup = false;
1396
- if (!backupPending) return;
1397
- backupPending = false;
1438
+ if (shuttingDown || !memoryToolsAvailable() || !backup.takeSettledTurn()) return;
1398
1439
  const operationSignal = ctx.signal;
1399
- const signal = operationSignal ? AbortSignal.any([backupLifetime.signal, operationSignal]) : backupLifetime.signal;
1440
+ const lifetime = backup.lifetime;
1441
+ const signal = operationSignal ? AbortSignal.any([lifetime, operationSignal]) : lifetime;
1400
1442
  const selected = runtime;
1401
1443
  try {
1402
1444
  if (existsSync(join(repositoryRoot, ".git"))) {
1403
1445
  const pushSessionId = sessionAddress(ctx).id;
1404
1446
  const pushCwd = ctx.cwd;
1405
1447
  // Pi 0.87 settles after its agent signal ends; waiting there would strand native Abort.
1406
- const operation = backupCurrentStateFlowFiles(repositoryRoot, signal, operationSignal !== undefined);
1407
- backupOperations.add(operation);
1408
- try { await operation; }
1409
- finally { backupOperations.delete(operation); }
1410
- if (signal.aborted) return;
1448
+ await backup.track(backupCurrentStateFlowFiles(repositoryRoot, signal, operationSignal !== undefined));
1449
+ if (signal.aborted || snapshot.config.mode === "off") return;
1411
1450
  startStateFlowBackupPush(repositoryRoot, (error) => {
1412
- if (shuttingDown) return;
1451
+ if (shuttingDown || lifetime.aborted || snapshot.config.mode === "off") return;
1413
1452
  const message = diagnosticText(error);
1414
1453
  const recorded = diagnosticWriter.recordBackupPushFailure(pushSessionId, pushCwd, message);
1415
- if (pushFailureNotified) return;
1416
- pushFailureNotified = true;
1454
+ if (!backup.claimPushFailureNotice()) return;
1417
1455
  notifyActiveContext(recorded
1418
1456
  ? `State Flow Git backup push failed; state is saved locally. Details: ${stateFlowLogPath(agentDir)}. A later accepted turn retries.`
1419
1457
  : `State Flow Git backup push failed; local diagnostics unavailable: ${message}`);
1420
- }, () => { pushFailureNotified = false; });
1458
+ }, () => backup.resetPushFailureNotice(), lifetime);
1421
1459
  }
1422
1460
  } catch (error) {
1423
1461
  if (signal.aborted || runtime !== selected) return;
@@ -1435,20 +1473,22 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1435
1473
  await startStateFlow(ctx);
1436
1474
  return;
1437
1475
  }
1438
- if (!completedRunAccepted || compactionStopped || !isActive() || snapshot.meta.bootstrap
1439
- || compactionInFlight || !ctx.isIdle() || ctx.hasPendingMessages() || !runtime?.view
1476
+ if (!completedRunAccepted || compactionRequests.stopped || !isActive() || snapshot.meta.bootstrap
1477
+ || compactionRequests.inFlight || !ctx.isIdle() || ctx.hasPendingMessages() || !runtime?.view
1440
1478
  || !shouldRequestStateFlowCompaction(ctx.getContextUsage())) return;
1441
1479
  completedRunAccepted = false;
1442
1480
  const entries = ctx.sessionManager.buildContextEntries();
1443
1481
  if (!hasCompactionSizedTranscript(entries)) return;
1444
1482
  const plan = planStateFlowCompaction(entries, runtime.causalBasis(), snapshot.meta.step, runAnchorTimestamp);
1445
1483
  if (!plan) return;
1446
- compactionPlan = plan;
1447
- compactionInFlight = true;
1484
+ const marker = compactionRequests.begin(plan);
1448
1485
  // Pi dispatches deferred companion prompts after all settled handlers return.
1449
1486
  await new Promise<void>((resolve) => {
1450
- const finished = () => { compactionPlan = undefined; compactionInFlight = false; resolve(); };
1451
- ctx.compact({ customInstructions: compactionMarker, onComplete: finished, onError: finished });
1487
+ const finished = () => {
1488
+ compactionRequests.finish(marker);
1489
+ resolve();
1490
+ };
1491
+ ctx.compact({ customInstructions: marker, onComplete: finished, onError: finished });
1452
1492
  });
1453
1493
  });
1454
1494
 
@@ -1466,20 +1506,21 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1466
1506
  });
1467
1507
  pi.on("session_shutdown", async (_event, _ctx) => {
1468
1508
  shuttingDown = true;
1509
+ memoryToolLifetime.end();
1469
1510
  contextProjection.reset();
1470
- const restoring = cancelBranchRestoration();
1471
- const starting = cancelStartActivation();
1472
- const stopping = cancelInactivePersistence();
1473
- backupLifetime.abort();
1474
- cancelInferencePreparation();
1475
- cancelResponseReconciliation();
1511
+ const restoring = branchRestoration.cancel();
1512
+ const starting = startActivation.cancel();
1513
+ const stopping = inactivePersistence.cancel();
1514
+ backup.abort();
1515
+ inferencePreparation.cancel();
1516
+ responseReconciliation.cancel();
1476
1517
  cancelSharedInspections();
1477
1518
  telegramStartPending = false;
1478
- compactionStopped = true;
1519
+ compactionRequests.stop();
1479
1520
  completedRunAccepted = false;
1480
1521
  activeContext = undefined;
1481
1522
  telegram.dispose();
1482
- await Promise.allSettled([...backupOperations, ...restorationOperations, ...[restoring, stopping, starting].filter((operation) => operation !== undefined)]);
1523
+ await Promise.allSettled([...backup.operations, ...branchRestoration.inflight, ...[restoring, stopping, starting].filter((operation) => operation !== undefined)]);
1483
1524
  await awaitInFlightBackupPushes(repositoryRoot);
1484
1525
  });
1485
1526
  }