@llblab/pi-kit 0.37.1 → 0.38.1

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 (133) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +4 -4
  3. package/node_modules/@llblab/pi-claude-usage/AGENTS.md +1 -1
  4. package/node_modules/@llblab/pi-claude-usage/CHANGELOG.md +4 -0
  5. package/node_modules/@llblab/pi-claude-usage/README.md +3 -3
  6. package/node_modules/@llblab/pi-claude-usage/lib/status.ts +1 -1
  7. package/node_modules/@llblab/pi-claude-usage/lib/usage-store.ts +2 -2
  8. package/node_modules/@llblab/pi-claude-usage/package.json +1 -1
  9. package/node_modules/@llblab/pi-clean-room/CHANGELOG.md +5 -0
  10. package/node_modules/@llblab/pi-clean-room/index.ts +39 -9
  11. package/node_modules/@llblab/pi-clean-room/package.json +1 -1
  12. package/node_modules/@llblab/pi-state-flow/AGENTS.md +7 -3
  13. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +679 -2
  14. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +17 -0
  15. package/node_modules/@llblab/pi-state-flow/README.md +30 -14
  16. package/node_modules/@llblab/pi-state-flow/api/modules.ts +11 -0
  17. package/node_modules/@llblab/pi-state-flow/dist/api/modules.d.ts +3 -0
  18. package/node_modules/@llblab/pi-state-flow/dist/api/modules.js +3 -0
  19. package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.d.ts +7 -4
  20. package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.js +77 -36
  21. package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.d.ts +2 -0
  22. package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.js +26 -27
  23. package/node_modules/@llblab/pi-state-flow/dist/lib/config.d.ts +3 -0
  24. package/node_modules/@llblab/pi-state-flow/dist/lib/config.js +13 -1
  25. package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +27 -21
  26. package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +144 -46
  27. package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +14 -18
  28. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.d.ts +6 -0
  29. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +60 -50
  30. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +1049 -310
  31. package/node_modules/@llblab/pi-state-flow/dist/lib/history.d.ts +4 -4
  32. package/node_modules/@llblab/pi-state-flow/dist/lib/history.js +15 -3
  33. package/node_modules/@llblab/pi-state-flow/dist/lib/json.d.ts +2 -0
  34. package/node_modules/@llblab/pi-state-flow/dist/lib/json.js +2 -1
  35. package/node_modules/@llblab/pi-state-flow/dist/lib/memory.d.ts +3 -1
  36. package/node_modules/@llblab/pi-state-flow/dist/lib/memory.js +7 -7
  37. package/node_modules/@llblab/pi-state-flow/dist/lib/migration.d.ts +29 -0
  38. package/node_modules/@llblab/pi-state-flow/dist/lib/migration.js +186 -0
  39. package/node_modules/@llblab/pi-state-flow/dist/lib/model-interface.d.ts +17 -0
  40. package/node_modules/@llblab/pi-state-flow/dist/lib/model-interface.js +158 -0
  41. package/node_modules/@llblab/pi-state-flow/dist/lib/module-registry.d.ts +21 -0
  42. package/node_modules/@llblab/pi-state-flow/dist/lib/module-registry.js +108 -0
  43. package/node_modules/@llblab/pi-state-flow/dist/lib/operation.js +3 -2
  44. package/node_modules/@llblab/pi-state-flow/dist/lib/ownership.d.ts +4 -4
  45. package/node_modules/@llblab/pi-state-flow/dist/lib/ownership.js +5 -5
  46. package/node_modules/@llblab/pi-state-flow/dist/lib/planes.d.ts +29 -0
  47. package/node_modules/@llblab/pi-state-flow/dist/lib/planes.js +84 -0
  48. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.d.ts +6 -3
  49. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +33 -16
  50. package/node_modules/@llblab/pi-state-flow/dist/lib/query.d.ts +21 -4
  51. package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +53 -13
  52. package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.js +5 -2
  53. package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.d.ts +3 -0
  54. package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.js +10 -1
  55. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +84 -5
  56. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +570 -20
  57. package/node_modules/@llblab/pi-state-flow/dist/lib/selection.d.ts +10 -0
  58. package/node_modules/@llblab/pi-state-flow/dist/lib/selection.js +32 -0
  59. package/node_modules/@llblab/pi-state-flow/dist/lib/semantic-module.d.ts +79 -0
  60. package/node_modules/@llblab/pi-state-flow/dist/lib/semantic-module.js +187 -0
  61. package/node_modules/@llblab/pi-state-flow/dist/lib/session.d.ts +98 -1
  62. package/node_modules/@llblab/pi-state-flow/dist/lib/session.js +398 -3
  63. package/node_modules/@llblab/pi-state-flow/dist/lib/skills.d.ts +7 -2
  64. package/node_modules/@llblab/pi-state-flow/dist/lib/skills.js +55 -3
  65. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +67 -14
  66. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +129 -9
  67. package/node_modules/@llblab/pi-state-flow/dist/lib/state.d.ts +26 -14
  68. package/node_modules/@llblab/pi-state-flow/dist/lib/state.js +61 -19
  69. package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +14 -9
  70. package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +59 -22
  71. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.d.ts +111 -10
  72. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.js +728 -11
  73. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +10 -7
  74. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +50 -14
  75. package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.d.ts +40 -4
  76. package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.js +247 -43
  77. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.d.ts +41 -8
  78. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +132 -39
  79. package/node_modules/@llblab/pi-state-flow/dist/package.json +6 -1
  80. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +28 -16
  81. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +12 -8
  82. package/node_modules/@llblab/pi-state-flow/docs/README.md +9 -0
  83. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +76 -43
  84. package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +9 -1
  85. package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +6 -0
  86. package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +23 -19
  87. package/node_modules/@llblab/pi-state-flow/docs/protocol-composition-decision.md +66 -0
  88. package/node_modules/@llblab/pi-state-flow/docs/public-module-contract.md +73 -0
  89. package/node_modules/@llblab/pi-state-flow/docs/scope-selection-contract.md +814 -0
  90. package/node_modules/@llblab/pi-state-flow/docs/semantic-compatibility-decision.md +212 -0
  91. package/node_modules/@llblab/pi-state-flow/docs/semantic-extensibility-decision.md +75 -0
  92. package/node_modules/@llblab/pi-state-flow/docs/semantic-handoff-contract.md +174 -0
  93. package/node_modules/@llblab/pi-state-flow/docs/semantic-memory-evaluation.md +166 -0
  94. package/node_modules/@llblab/pi-state-flow/docs/semantic-module-contract.md +84 -0
  95. package/node_modules/@llblab/pi-state-flow/docs/single-scope-interface-contract.md +201 -0
  96. package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +22 -1
  97. package/node_modules/@llblab/pi-state-flow/docs/usage.md +32 -18
  98. package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +68 -31
  99. package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +21 -22
  100. package/node_modules/@llblab/pi-state-flow/lib/config.ts +14 -1
  101. package/node_modules/@llblab/pi-state-flow/lib/context.ts +142 -46
  102. package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +14 -17
  103. package/node_modules/@llblab/pi-state-flow/lib/durable.ts +53 -44
  104. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +897 -297
  105. package/node_modules/@llblab/pi-state-flow/lib/history.ts +16 -7
  106. package/node_modules/@llblab/pi-state-flow/lib/json.ts +2 -1
  107. package/node_modules/@llblab/pi-state-flow/lib/memory.ts +10 -9
  108. package/node_modules/@llblab/pi-state-flow/lib/migration.ts +189 -0
  109. package/node_modules/@llblab/pi-state-flow/lib/model-interface.ts +136 -0
  110. package/node_modules/@llblab/pi-state-flow/lib/module-registry.ts +119 -0
  111. package/node_modules/@llblab/pi-state-flow/lib/operation.ts +3 -2
  112. package/node_modules/@llblab/pi-state-flow/lib/ownership.ts +6 -6
  113. package/node_modules/@llblab/pi-state-flow/lib/planes.ts +98 -0
  114. package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +37 -16
  115. package/node_modules/@llblab/pi-state-flow/lib/query.ts +59 -18
  116. package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +5 -2
  117. package/node_modules/@llblab/pi-state-flow/lib/rehydration.ts +10 -1
  118. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +574 -21
  119. package/node_modules/@llblab/pi-state-flow/lib/selection.ts +39 -0
  120. package/node_modules/@llblab/pi-state-flow/lib/semantic-module.ts +239 -0
  121. package/node_modules/@llblab/pi-state-flow/lib/session.ts +387 -3
  122. package/node_modules/@llblab/pi-state-flow/lib/skills.ts +45 -4
  123. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +156 -16
  124. package/node_modules/@llblab/pi-state-flow/lib/state.ts +69 -28
  125. package/node_modules/@llblab/pi-state-flow/lib/status.ts +65 -31
  126. package/node_modules/@llblab/pi-state-flow/lib/storage.ts +770 -18
  127. package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +58 -20
  128. package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +267 -50
  129. package/node_modules/@llblab/pi-state-flow/lib/transition.ts +184 -52
  130. package/node_modules/@llblab/pi-state-flow/package.json +6 -1
  131. package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +28 -16
  132. package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +12 -8
  133. package/package.json +4 -4
@@ -1,7 +1,7 @@
1
1
  import type { AgentMessage } from "@earendil-works/pi-agent-core";
2
2
  import { StringEnum, Type } from "@earendil-works/pi-ai";
3
3
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
4
- import { getAgentDir } from "@earendil-works/pi-coding-agent";
4
+ import { defineTool, getAgentDir } from "@earendil-works/pi-coding-agent";
5
5
  import { Text } from "@earendil-works/pi-tui";
6
6
  import { existsSync } from "node:fs";
7
7
  import { isAbsolute, join, resolve } from "node:path";
@@ -9,7 +9,7 @@ import { ArtifactAcquisitionState, missingArtifactRemovals } from "./acquisition
9
9
  import { inspectRegisteredArtifactPaths, sameArtifactSourceFingerprint } from "./artifact.ts";
10
10
  import { hasCompactionSizedTranscript, planStateFlowCompaction, shouldRequestStateFlowCompaction, StateFlowCompactionRequests } from "./compaction.ts";
11
11
  import { inactiveModeFor, loadStateFlowConfig } from "./config.ts";
12
- import { ContextProjection, contextView, createPassiveContinuation, currentRunTrajectory, passiveContinuationMessages, projectSystemProtocol, runtimeContextHead, syntheticUser, type PassiveContinuation } from "./context.ts";
12
+ import { ContextProjection, contextView, createPassiveContinuation, currentRunTrajectory, passiveContinuationMessages, projectSystemProtocol, runtimeContextHead, passiveContextHead, moduleConversationSource, projectModuleContext, type ScopedModuleContext, type PassiveContinuation } from "./context.ts";
13
13
  import { readNativeSessionHeader } from "./continuation.ts";
14
14
  import {
15
15
  cwdScopeKey,
@@ -22,21 +22,26 @@ import { awaitInFlightBackupPushes, backupCurrentStateFlowFiles, SettledTurnBack
22
22
  import { projectRecentTransitionsWithLimit } from "./history.ts";
23
23
  import { isObject, presentationJson, sameJson } from "./json.ts";
24
24
  import { StateFlowDiagnosticWriter, stateFlowLogPath, type DiagnosticExtras, type StateFlowDiagnosticCategory } from "./logging.ts";
25
- import { assistantToolCallCount, conciseDiagnostic, diagnosticText, finalizedAssistantResponse, formatPatchStateArguments, PASSIVE_MEMORY_PROTOCOL, RESPONSE_PATCH_GUIDELINE, separatedFailure, separatedOutput, stateFlowProtocol } from "./protocol.ts";
25
+ import { normalizeModelPatches, normalizeModelReadPath, projectModelReferences, projectModelArtifactTarget, projectModelMissingDeletion } from "./model-interface.ts";
26
+ import { StateFlowModuleHost } from "./module-registry.ts";
27
+ import { prepareModuleContext, type PreparedModuleContext, type StateFlowModule, type RetainedModuleDescriptor } from "./semantic-module.ts";
28
+ import type { JsonObject } from "./json.ts";
29
+ import { assistantToolCallCount, conciseDiagnostic, diagnosticText, finalizedAssistantResponse, formatPatchStateArguments, passiveMemoryProtocol, RESPONSE_PATCH_GUIDELINE, SCOPE_PARTICIPATION_GUIDELINE, separatedFailure, separatedOutput, stateFlowProtocol } from "./protocol.ts";
26
30
  import { OwnedOperationSlot, RenewableLifetime, type OwnedOperation } from "./operation.ts";
27
31
  import { readProjectedState, readStatePath } from "./query.ts";
28
32
  import { selectedBoundaryFailure, selectRetainedCheckpoint, waitForRecovery } from "./recovery.ts";
29
33
  import type { RehydrationPhase } from "./rehydration.ts";
30
- import { TemporalRuntime, type RuntimePublication } from "./runtime.ts";
31
- import { discoverSnapshotData, findAssistantToolBatch, findBranchPolicy, findPassiveStopBoundary, hasPendingFork, hasPriorConversation, hasUncheckpointedConversation, isNewSession, retainsPhysicalSessionProjection, SNAPSHOT_ENTRY_TYPE } from "./session.ts";
34
+ import { TemporalRuntime, type SelectedRuntimePatchTransaction } from "./runtime.ts";
35
+ import { prepareSelectedBranchPolicy, captureSelectedNativeMarker, NativeToolRequestCorrelation, SelectedPolicyLifetime, type SelectedPolicyRequest, type NativeContextRequest, discoverSnapshotData, findAssistantToolBatch, hasPriorConversation, hasUncheckpointedConversation, isNewSession, retainsPhysicalSessionProjection, SNAPSHOT_ENTRY_TYPE } from "./session.ts";
32
36
  import { hasCompiledSkillArtifact, hashSkillSource, registeredSkillResolver, SkillReadTracker, type SuccessfulSkillRead } from "./skills.ts";
33
- import { emptySnapshot, isActiveRestorationBlocked, 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";
37
+ import { emptySnapshot, isActiveRestorationBlocked, migrationFailure, type InactiveMode, type RetainedBoundaryCheckpoint, type Snapshot, type StateFlowMode } from "./snapshot.ts";
38
+ import { emptyState, projectStateForModel, type SemanticState, type ModelState, type ScopeStateValues, type StateScope } from "./state.ts";
35
39
  import { compactStatus, detailedStatus, STATUS_KEY, type StatusDiagnostics } from "./status.ts";
36
- import { PublicationBusyError } from "./storage.ts";
40
+ import { PublicationBusyError, loadSelectedFileCohort, type SelectedFilePublication } from "./storage.ts";
41
+ import { captureScopeSelection, DEFAULT_SCOPE_SELECTION, parseScopeSelectionArguments, scopeSelectionArgumentCompletions, type ScopeSelection } from "./selection.ts";
37
42
  import { createStateFlowTelegramAdapter, type StateFlowTelegramControlResult, type StateFlowTelegramInspection, type StateFlowTelegramLoader, type StateFlowTelegramScope } from "./telegram.ts";
38
- import { temporalScopeRevisions, type ScopeRevisions } from "./temporal.ts";
39
- import { commitScopedTransition, stageAtomicScopePatches, stageScopedTransition } from "./transition.ts";
43
+ import { readTemporalScopes, temporalScopeRevisions, type ScopeRevisions } from "./temporal.ts";
44
+ import { commitSelectedTransition, stageSelectedScopePatches, stageSelectedScopedTransition, type ModuleReplacement } from "./transition.ts";
40
45
 
41
46
  export interface StateFlowExtensionOptions {
42
47
  agentDir?: string;
@@ -53,11 +58,36 @@ export const PATCH_STATE_TOOL_NAME = "patch_state";
53
58
  export const READ_STATE_TOOL_NAME = "read_state";
54
59
  const PASSIVE_STOP_ENTRY_TYPE = "state-flow-passive-stop";
55
60
 
61
+ const flatPatchParameters = Type.Object({
62
+ intents: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
63
+ strategy: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
64
+ tactics: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
65
+ artifacts: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
66
+ lazy: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
67
+ }, { additionalProperties: false, minProperties: 1 });
68
+ const scopedPatchParameters = Type.Object({
69
+ global: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
70
+ cwd: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
71
+ session: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
72
+ }, { additionalProperties: false, minProperties: 1 });
73
+
56
74
  interface InferencePreparation extends OwnedOperation {
57
75
  readonly controller: AbortController;
58
76
  readonly prompt?: string;
59
77
  accepted?: boolean;
60
78
  operation?: Promise<void>;
79
+ request?: NativeContextRequest;
80
+ modules?: PreparedPublicModules;
81
+ moduleAcceptance?: () => void;
82
+ }
83
+
84
+ interface PreparedPublicModules {
85
+ readonly definitions: readonly StateFlowModule[];
86
+ readonly conversation: readonly AgentMessage[];
87
+ readonly bindings: readonly { module: StateFlowModule; scope: StateScope; content: JsonObject | undefined; descriptor: RetainedModuleDescriptor | undefined; prepared: PreparedModuleContext }[];
88
+ assertCurrent(): void;
89
+ assertOriginal(): void;
90
+ cancel(): void;
61
91
  }
62
92
 
63
93
  interface BranchRestoration extends OwnedOperation {
@@ -81,8 +111,9 @@ type BranchSelection =
81
111
  | { kind: "settled" }
82
112
  | { kind: "current"; mode: InactiveMode }
83
113
  | { kind: "attach"; checkpoint: RetainedBoundaryCheckpoint }
84
- | { kind: "fork"; checkpoint: RetainedBoundaryCheckpoint; source: SessionAddress }
85
- | { kind: "auto-start" };
114
+ | { kind: "fork"; checkpoint: Pick<RetainedBoundaryCheckpoint, "mode" | "bootstrap"> }
115
+ | { kind: "auto-start" }
116
+ | { kind: "pre-runtime"; mode: "active" | "passive" };
86
117
 
87
118
  export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowExtensionOptions = {}): void {
88
119
  const agentDir = options.agentDir ?? getAgentDir();
@@ -90,7 +121,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
90
121
  const config = options.mode === undefined ? loadedConfig
91
122
  : { ...loadedConfig, mode: options.mode, inactiveMode: inactiveModeFor(options.mode) };
92
123
  let snapshot: Snapshot = emptySnapshot(config.inactiveMode);
93
- let scopeStates: ScopedStates = { global: emptyState(), cwd: emptyState(), session: emptyState() };
124
+ let scopeStates: ScopeStateValues = {};
94
125
  let effectiveState: SemanticState = {};
95
126
  let branchStartsWithoutRuntime = false;
96
127
  let modePersistenceError: string | undefined;
@@ -100,9 +131,13 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
100
131
  let forkInitialization = false;
101
132
  const branchRestoration = new OwnedOperationSlot<BranchRestoration>();
102
133
  let deferredBranch: { reason: unknown; owner: string; cwd: string } | undefined;
103
- const responseReconciliation = new OwnedOperationSlot<OwnedOperation>();
134
+ const responseReconciliation = new OwnedOperationSlot<OwnedOperation & { request: SelectedPolicyRequest; operationSignal?: AbortSignal; revalidate: () => void }>();
104
135
  const sharedInspectionLifetime = new RenewableLifetime();
105
136
  const memoryToolLifetime = new RenewableLifetime();
137
+ const requestPolicy = new SelectedPolicyLifetime();
138
+ const nativeRequests = new NativeToolRequestCorrelation();
139
+ const moduleHost = new StateFlowModuleHost();
140
+ let guardAbortedOperation: AbortSignal | undefined;
106
141
  let completedRunAccepted = false;
107
142
  const compactionRequests = new StateFlowCompactionRequests();
108
143
  let passiveContinuation: PassiveContinuation | undefined;
@@ -150,6 +185,18 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
150
185
  }
151
186
  }
152
187
 
188
+ function modeOperationSignal(ctx: ExtensionContext, owned: AbortSignal): AbortSignal {
189
+ const ambient = ctx.signal;
190
+ if (!ambient || guardAbortedOperation === ambient) return owned;
191
+ if (!nativeRequests.ownsOperation(ambient)) return AbortSignal.any([owned, ambient]);
192
+ // User Abort still cancels in-run commands; only this guard's obsolete-model Abort is unrelated.
193
+ const forwarded = new AbortController();
194
+ const abort = () => { if (guardAbortedOperation !== ambient) forwarded.abort(ambient.reason); };
195
+ if (ambient.aborted) abort();
196
+ else ambient.addEventListener("abort", abort, { once: true, signal: owned });
197
+ return AbortSignal.any([owned, forwarded.signal]);
198
+ }
199
+
153
200
  function createRuntime(ctx: ExtensionContext): TemporalRuntime {
154
201
  return new TemporalRuntime(ctx.cwd, sessionAddress(ctx), repositoryRoot, undefined, config.historyLimit);
155
202
  }
@@ -165,7 +212,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
165
212
  }
166
213
 
167
214
  options.onRuntime?.({ read: (offset, scope) => {
168
- if (scope === "session") assertSelectedBranchAvailable();
215
+ assertSelectedBranchAvailable();
169
216
  if (!runtime) throw new Error("State Flow temporal runtime is unavailable");
170
217
  const { lazy: _lazy, ...defaults } = emptyState();
171
218
  return { ...defaults, ...projectModelState(runtime.readView(offset, scope)) };
@@ -181,23 +228,69 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
181
228
  return snapshot.config.mode === "active" && !isActiveRestorationBlocked(snapshot);
182
229
  }
183
230
 
184
- /** Pending acquisition has no accepted Active memory yet. */
185
- function inactiveSelection(mode: StateFlowMode): InactiveMode {
186
- return mode === "active" ? config.inactiveMode : mode;
231
+ function readableView(selected = runtime) {
232
+ return selected?.selectedView() ?? selected?.observedSelectedView();
233
+ }
234
+
235
+ function nativeScopeSelection(ctx = activeContext) {
236
+ const policy = requestPolicy.policy;
237
+ return ctx && policy?.owner === ctx.sessionManager.getSessionId() && policy.cwd === resolve(ctx.cwd)
238
+ ? policy.scopes : config.scopes;
239
+ }
240
+
241
+ function appendNativeMarker(value: Record<string, unknown>, ctx = activeContext!): void {
242
+ pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, captureSelectedNativeMarker({ version: 2,
243
+ owner: ctx.sessionManager.getSessionId(), mode: snapshot.config.mode, scopes: nativeScopeSelection(ctx), ...value }));
244
+ }
245
+
246
+ function assertNativeMemoryScopes(scopes: readonly StateScope[]): void {
247
+ captureScopeSelection(scopes);
248
+ }
249
+
250
+ function branchPolicy(ctx: ExtensionContext, reason?: unknown, memoryRequired = true) {
251
+ const branch = ctx.sessionManager.getBranch();
252
+ const policy = prepareSelectedBranchPolicy(branch, { owner: ctx.sessionManager.getSessionId(), markerEntryType: PASSIVE_STOP_ENTRY_TYPE,
253
+ defaults: { mode: isNewSession(reason, branch) ? config.mode : config.inactiveMode, scopes: config.scopes },
254
+ inactiveMode: config.inactiveMode, fork: reason === "fork" });
255
+ if (memoryRequired) assertNativeMemoryScopes(policy.scopes);
256
+ return policy;
257
+ }
258
+
259
+ /** Failure-only denial evidence, never policy admission: inspect own known-epoch headers without getters or memory I/O. */
260
+ function failedNativePolicyMode(ctx: ExtensionContext, reason?: unknown): StateFlowMode | undefined {
261
+ const own = (value: object, key: string) => Object.getOwnPropertyDescriptor(value, key)?.value;
262
+ try {
263
+ const inherited = reason === "fork" || typeof ctx.sessionManager.getHeader()?.parentSession === "string";
264
+ for (const entry of [...ctx.sessionManager.getBranch()].reverse()) {
265
+ try {
266
+ if (own(entry, "type") !== "custom") continue;
267
+ const type = own(entry, "customType"), data = own(entry, "data");
268
+ if ((type !== SNAPSHOT_ENTRY_TYPE && type !== PASSIVE_STOP_ENTRY_TYPE) || !isObject(data)) continue;
269
+ const epoch = Object.getOwnPropertyDescriptor(data, "version");
270
+ if (epoch && (!("value" in epoch) || epoch.value !== 2)) continue;
271
+ if (type === PASSIVE_STOP_ENTRY_TYPE && own(data, "owner") !== ctx.sessionManager.getSessionId() && !inherited) continue;
272
+ const mode = own(data, "mode");
273
+ if (mode === "active" || mode === "passive" || mode === "off") return mode;
274
+ if (!epoch && typeof own(data, "enabled") === "boolean") return own(data, "enabled") ? "active" : config.inactiveMode;
275
+ } catch { /* Unreadable headers grant no memory participation. */ }
276
+ }
277
+ } catch { /* No failure-only mode evidence. */ }
278
+ return undefined;
187
279
  }
188
280
 
189
281
  function appendCheckpoint(): void {
190
- const checkpoint = branchStartsWithoutRuntime ? preRuntimeCheckpoint(snapshot.config.mode)
191
- : runtime?.view ? runtime.retainedCheckpoint(snapshot) : undefined;
192
- if (!checkpoint) throw new Error("State Flow cannot checkpoint an unproven branch; restore a valid checkpoint first");
193
- pi.appendEntry(SNAPSHOT_ENTRY_TYPE, checkpoint);
194
- if ("boundary" in checkpoint) branchStartsWithoutRuntime = false;
282
+ if (runtime?.selectedView()) {
283
+ pi.appendEntry(SNAPSHOT_ENTRY_TYPE, runtime.retainedSelectedCheckpoint(snapshot.config.mode));
284
+ branchStartsWithoutRuntime = false;
285
+ } else if (branchStartsWithoutRuntime) {
286
+ // A policy capture cannot turn read-only shared observation into accepted memory.
287
+ appendNativeMarker({});
288
+ } else throw new Error("State Flow cannot checkpoint an unproven branch; restore current memory first");
195
289
  }
196
290
 
197
- function scopeRevisions(): ScopeRevisions {
198
- return runtime?.view
199
- ? temporalScopeRevisions(runtime.view)
200
- : { global: 0, cwd: 0, session: 0 };
291
+ function scopeRevisions(): Partial<ScopeRevisions> {
292
+ const view = readableView();
293
+ return view ? temporalScopeRevisions(view) : {};
201
294
  }
202
295
 
203
296
  function updateUi(ctx: ExtensionContext): void {
@@ -226,13 +319,14 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
226
319
  }
227
320
 
228
321
  function refreshArtifactHints(): void {
229
- if (runtime?.view) acquisition.refresh(scopeStates, (scope) => runtime!.artifactProvenance(scope));
322
+ const view = readableView();
323
+ if (view) acquisition.refresh(scopeStates, (scope) => runtime!.artifactProvenance(scope), view.selection);
230
324
  else acquisition.reset();
231
325
  }
232
326
 
233
327
  function installScopeStates(): void {
234
- scopeStates = runtime?.view ? runtime.states() : { global: emptyState(), cwd: emptyState(), session: emptyState() };
235
- effectiveState = runtime?.view ? runtime.readView() : {};
328
+ scopeStates = readableView() ? runtime!.selectedStates().states : {};
329
+ effectiveState = readableView() ? runtime!.readView() : {};
236
330
  }
237
331
 
238
332
  /** Active and passive expose both memory tools; Off exposes neither. */
@@ -242,7 +336,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
242
336
 
243
337
  /** Passive memory context needs a loaded view; Off and Active never inject it. */
244
338
  function passiveMemoryAvailable(): boolean {
245
- return snapshot.config.mode === "passive" && runtime?.view !== undefined;
339
+ return snapshot.config.mode === "passive" && readableView() !== undefined && snapshot.meta.validation?.attempt !== 0;
246
340
  }
247
341
 
248
342
  async function inspectTelegramState(scope: StateFlowTelegramScope): Promise<StateFlowTelegramInspection> {
@@ -250,31 +344,54 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
250
344
  const ctx = activeContext;
251
345
  const owner = sessionAddress(ctx).key;
252
346
  const cwd = ctx.cwd;
347
+ const wanted = captureScopeSelection(nativeScopeSelection(ctx));
348
+ if (scope === "effective" ? wanted.length < 2 : !wanted.includes(scope)) throw new Error("State Flow inspection scope is not available in this selection");
349
+ const inspected = captureScopeSelection(scope === "effective" ? wanted : [scope]);
253
350
  const off = snapshot.config.mode === "off";
254
- const privateScope = scope === "session" || scope === "effective";
351
+ const privateScope = inspected.includes("session");
255
352
  if (!off && privateScope) assertSelectedBranchAvailable();
256
353
  // Off operator reads are disposable: they confer no selected-branch/cache authority.
257
- const selected = off ? createRuntime(ctx) : runtime ??= createRuntime(ctx);
354
+ const selected = createRuntime(ctx);
258
355
  const signal = sharedInspectionLifetime.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);
262
- signal.throwIfAborted();
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();
268
- if (!selected.view) throw new Error("State Flow temporal runtime is unavailable");
269
- if (!off) installScopeStates();
356
+ const check = () => {
357
+ signal.throwIfAborted();
358
+ if (activeContext !== ctx || ctx.cwd !== cwd || sessionAddress(ctx).key !== owner || !sameJson(nativeScopeSelection(ctx), wanted)) throw new Error("State Flow inspection selection changed; select the scope again");
359
+ };
360
+ if (privateScope && (off || !branchStartsWithoutRuntime || scope === "session")) {
361
+ await selected.refreshSelectedCurrent(inspected, check, signal);
362
+ } else await selected.refreshSelectedShared(captureScopeSelection(inspected.filter(owner => owner !== "session")), check, signal);
363
+ check();
364
+ const view = selected.observedSelectedView();
365
+ if (!view) throw new Error("State Flow temporal runtime is unavailable");
270
366
  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 };
367
+ return { state: { ...projectModelState(state), ...(state.lazy === undefined ? {} : { lazy: structuredClone(state.lazy) }) }, revisions: temporalScopeRevisions(view), signal };
272
368
  }
273
369
 
274
- function syncStateFlowTools(): void {
370
+ function syncStateFlowTools(force = false): void {
371
+ if (activeContext && !shuttingDown) {
372
+ const scopes = nativeScopeSelection(activeContext);
373
+ requestPolicy.select({ cwd: activeContext.cwd, owner: activeContext.sessionManager.getSessionId(), mode: snapshot.config.mode, scopes });
374
+ skillReads.select(scopes);
375
+ }
376
+ const available = memoryToolsAvailable();
377
+ if (available && activeContext) {
378
+ const external = moduleHost.captureIfBound()?.modules.filter(module => !module.immutable).map(module => module.id) ?? [];
379
+ const flat = nativeScopeSelection(activeContext).length === 1;
380
+ const desired = [...Object.keys(flatPatchParameters.properties), ...external];
381
+ const sameFlatShape = flat && desired.join("|") === Object.keys(patchStateTool.parameters.properties).join("|");
382
+ const parameters = !flat ? scopedPatchParameters : !external.length ? flatPatchParameters : sameFlatShape ? patchStateTool.parameters
383
+ : Type.Object({ ...flatPatchParameters.properties, ...Object.fromEntries(external.map(id => [id, Type.Optional(Type.Record(Type.String(), Type.Unknown()))])) }, { additionalProperties: false, minProperties: 1 });
384
+ // Cache only the desired shape on our unregistered template: host filtering can hide ToolInfo.
385
+ // Every SDK registration is a fresh copy, preserving old response-wrapper declarations.
386
+ if (force || patchStateTool.parameters !== parameters) {
387
+ patchStateTool.parameters = parameters;
388
+ pi.registerTool({ ...patchStateTool, ...(external.length ? {
389
+ promptGuidelines: [...(patchStateTool.promptGuidelines ?? []), `Registered mutable object planes: ${external.join(", ")}. Availability and matching retained schema/policy are revalidated at acceptance.`],
390
+ } : {}) });
391
+ }
392
+ }
275
393
  const active = pi.getActiveTools();
276
394
  const owned = [PATCH_STATE_TOOL_NAME, READ_STATE_TOOL_NAME];
277
- const available = memoryToolsAvailable();
278
395
  if (owned.every((name) => active.includes(name) === available)) return;
279
396
  pi.setActiveTools(available
280
397
  ? [...new Set([...active, ...owned])]
@@ -289,9 +406,9 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
289
406
  }
290
407
 
291
408
  function skillReadIsCurrent(read: SuccessfulSkillRead): boolean {
292
- return read.hash !== undefined && runtime?.view !== undefined && hasCompiledSkillArtifact(
293
- scopeStates[read.scope].artifacts,
294
- runtime.artifactProvenance(read.scope)[read.path],
409
+ return read.hash !== undefined && readableView()?.selection.includes(read.scope) === true && hasCompiledSkillArtifact(
410
+ scopeStates[read.scope]!.artifacts ?? {},
411
+ runtime!.artifactProvenance(read.scope)[read.path],
295
412
  read.path,
296
413
  read.hash,
297
414
  );
@@ -303,9 +420,9 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
303
420
  }
304
421
  }
305
422
 
306
- function skillAcquisitionHint(read: SuccessfulSkillRead): string | undefined {
423
+ function skillAcquisitionHint(read: SuccessfulSkillRead, wanted: ScopeSelection): string | undefined {
307
424
  if (read.hash === undefined || skillReadIsCurrent(read)) return undefined;
308
- const target = `${read.scope}.artifacts[${JSON.stringify(read.path)}]`;
425
+ const target = projectModelArtifactTarget(read.scope, read.path, wanted);
309
426
  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.`;
310
427
  }
311
428
 
@@ -314,31 +431,58 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
314
431
  dropCurrentSkillReads();
315
432
  }
316
433
 
317
- async function prepareInference(pending: InferencePreparation, selected: TemporalRuntime | undefined, ctx: ExtensionContext, operationSignal: AbortSignal): Promise<void> {
318
- const signal = AbortSignal.any([pending.controller.signal, operationSignal]);
434
+ async function prepareInference(pending: InferencePreparation, selected: TemporalRuntime | undefined, ctx: ExtensionContext, operationSignal: AbortSignal, request?: NativeContextRequest, messages: AgentMessage[] = []): Promise<void> {
435
+ const signal = AbortSignal.any([pending.controller.signal, operationSignal, ...(request ? [request.signal] : [])]);
436
+ const sameSession = sessionIdentity(ctx);
437
+ const check = () => {
438
+ request?.revalidate();
439
+ if (ctx.signal !== operationSignal || !sameSession() || !currentNativeContextOwner(ctx)) throw new Error("State Flow preparation context owner changed");
440
+ };
319
441
  try {
320
- if (!selected?.view) throw new Error("Temporal State Flow runtime is unavailable; reload before publishing");
321
- await selected.withPatchTransaction((transaction) => {
442
+ check();
443
+ if (!selected?.selectedView()) throw new Error("Temporal State Flow runtime is unavailable; reload before publishing");
444
+ pending.modules = await capturePublicModules(messages, ctx, signal);
445
+ pending.modules?.assertOriginal();
446
+ const replacements: ModuleReplacement[] = [];
447
+ try {
448
+ const proposals = await Promise.all((pending.modules?.bindings ?? []).filter(binding => binding.module.produce).map(async binding => {
449
+ const content = await binding.prepared.produce();
450
+ return content === undefined ? undefined : { scope: binding.scope, id: binding.module.id, content };
451
+ }));
452
+ replacements.push(...proposals.filter((item): item is ModuleReplacement => item !== undefined));
453
+ } catch { throw new Error("State Flow registered module production failed"); }
454
+ pending.modules?.assertOriginal();
455
+ await selected.withSelectedPatchTransaction((transaction) => {
322
456
  signal.throwIfAborted();
457
+ check();
323
458
  if (runtime !== selected || !inferencePreparation.owns(pending) || !isActive() || shuttingDown) {
324
459
  throw new Error("State Flow inference selection changed while awaiting publication");
325
460
  }
326
461
  assertPublicationAvailable();
462
+ pending.modules?.assertOriginal();
463
+ for (const binding of pending.modules?.bindings ?? []) {
464
+ if (!sameJson(transaction.states[binding.scope]?.[binding.module.id] ?? null, binding.content ?? null)
465
+ || !sameJson(transaction.modules?.[binding.scope]?.[binding.module.id] ?? null, binding.descriptor ?? null)) throw new Error("State Flow producer content or descriptor basis changed");
466
+ }
327
467
  const nextSnapshot = structuredClone(snapshot);
328
468
  const rotatesRun = pending.prompt !== undefined && nextSnapshot.meta.specification !== undefined;
329
469
  if (pending.prompt !== undefined) prepareRun(nextSnapshot, pending.prompt);
330
470
  // Exact-path metadata validation belongs to the locked current head, including newly adopted registrations.
331
- const removals = missingArtifactRemovals(transaction.states);
332
- let publication: RuntimePublication | undefined;
333
- if (Object.keys(removals).length > 0) {
334
- const stage = stageAtomicScopePatches(transaction.states, removals, [], transaction.causalBasis);
335
- commitScopedTransition(nextSnapshot, transaction.states, stage, (transition, next) => {
336
- publication = transaction.publish(next, transition);
471
+ const removals = missingArtifactRemovals(transaction.states, transaction.selection);
472
+ let publication: SelectedFilePublication | undefined;
473
+ if (Object.keys(removals).length > 0 || replacements.length > 0) {
474
+ const stage = stageSelectedScopePatches(transaction, removals, [], transaction.causalBasis, [], pending.modules?.definitions, replacements);
475
+ commitSelectedTransition(nextSnapshot, transaction, stage, (transition, next) => {
476
+ pending.modules?.assertCurrent();
477
+ publication = transaction.publish(next, transition, stage.provenanceUpdates, stage.moduleAdditions);
337
478
  pending.accepted = true;
479
+ pending.moduleAcceptance = selected.captureSelectedReadBasis();
338
480
  }, transaction.causalBasis, { finalizeRun: false });
339
481
  } else {
482
+ pending.modules?.assertCurrent();
340
483
  publication = transaction.publish(nextSnapshot);
341
484
  pending.accepted = true;
485
+ pending.moduleAcceptance = selected.captureSelectedReadBasis();
342
486
  }
343
487
  snapshot = nextSnapshot;
344
488
  installScopeStates();
@@ -347,11 +491,18 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
347
491
  if (pending.prompt !== undefined || publication?.changed) appendCheckpoint();
348
492
  if (Object.keys(removals).length > 0) clearAcceptedAcquisitions();
349
493
  updateUi(ctx);
350
- }, signal);
494
+ }, () => {
495
+ check();
496
+ if (runtime !== selected || !inferencePreparation.owns(pending) || !isActive()) throw new Error("State Flow preparation superseded");
497
+ pending.modules?.assertCurrent();
498
+ }, signal, [...new Set(pending.modules?.bindings.map(binding => binding.scope) ?? [])]);
351
499
  } catch (error) {
500
+ if (!pending.accepted) pending.modules?.cancel();
352
501
  if (signal.aborted || runtime !== selected || !inferencePreparation.owns(pending) || shuttingDown) return;
502
+ try { check(); } catch { return; }
503
+ if (!contextRequestCurrent(request, ctx, operationSignal)) return;
353
504
  // Pi reports context-hook exceptions and continues. Abort through its public port rather than infer from stale preparation.
354
- ctx.abort();
505
+ abortContext(ctx, operationSignal);
355
506
  const cause = diagnosticText(error);
356
507
  recordDiagnostic(cause, "publication-conflict", ctx);
357
508
  notifyProblem(ctx, pending.accepted
@@ -363,7 +514,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
363
514
 
364
515
  function statusDiagnostics(ctx: ExtensionContext): StatusDiagnostics {
365
516
  const cwd = ctx.cwd;
366
- const view = runtime?.view;
517
+ const view = readableView();
367
518
  const diagnosticRecent = runtime?.recent() ?? [];
368
519
  const durableStateError = snapshot.meta.validation?.attempt === 0 ? snapshot.meta.validation.error
369
520
  : view ? undefined : "no temporal runtime is selected on this branch";
@@ -376,6 +527,8 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
376
527
  repositoryRoot,
377
528
  cwdScopeKey: cwdScopeKey(cwd),
378
529
  sessionScopeKey: sessionScopeKey(session.key),
530
+ selection: view?.selection ?? nativeScopeSelection(ctx),
531
+ requestedSelection: nativeScopeSelection(ctx),
379
532
  scopeStates,
380
533
  effectiveState,
381
534
  recent: diagnosticRecent,
@@ -383,7 +536,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
383
536
  ...(view === undefined ? {} : { temporal: {
384
537
  head: structuredClone(view.lineage.at(-1)!),
385
538
  historyDepth: view.lineage.length - 1,
386
- tailCounts: { global: view.scopes.global.patches.length, cwd: view.scopes.cwd.patches.length, session: view.scopes.session.patches.length },
539
+ tailCounts: Object.fromEntries(view.selection.map(scope => [scope, view.scopes[scope]!.patches.length])),
387
540
  revisions: temporalScopeRevisions(view),
388
541
  } }),
389
542
  staleArtifacts,
@@ -403,6 +556,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
403
556
  /** Select the active native branch under one owned restoration lifetime; only current accepted work installs memory. */
404
557
  function restoreActiveBranch(ctx: ExtensionContext, sessionStartReason?: unknown, notifyRecovery = true, startOwner?: AbortController, requestedMode?: InactiveMode): Promise<void> {
405
558
  contextProjection.reset();
559
+ requestPolicy.invalidate();
406
560
  memoryToolLifetime.renew();
407
561
  branchRestoration.cancel();
408
562
  // Start-owned attachment/fork recovery keeps its owner; only accepted Start cancels Stop.
@@ -416,9 +570,26 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
416
570
  acquisition.clearInvalidations();
417
571
  telegramStartPending = false;
418
572
  activeContext = ctx;
573
+ let nativePolicy;
574
+ try {
575
+ nativePolicy = branchPolicy(ctx, sessionStartReason, false);
576
+ requestPolicy.select({ cwd: ctx.cwd, owner: ctx.sessionManager.getSessionId(), mode: snapshot.config.mode, scopes: nativePolicy.scopes });
577
+ // Freeze admitted policy before accepting any selected memory.
578
+ // Policy-only capture grants no acquisition or semantic-boundary authority.
579
+ if (nativePolicy.requiresCapture) pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, nativePolicy.capture);
580
+ if (nativePolicy.mode !== "off") assertNativeMemoryScopes(nativePolicy.scopes);
581
+ } catch (error) {
582
+ runtime = undefined;
583
+ branchStartsWithoutRuntime = false;
584
+ modePersistenceError = diagnosticText(error);
585
+ snapshot = selectedBoundaryFailure(modePersistenceError, nativePolicy?.mode ?? failedNativePolicyMode(ctx, sessionStartReason) ?? config.inactiveMode);
586
+ installScopeStates();
587
+ syncStateFlowTools();
588
+ updateUi(ctx);
589
+ return Promise.resolve();
590
+ }
419
591
  if (!startOwner) {
420
592
  let mode: StateFlowMode = config.inactiveMode;
421
- let adoptDefault = false;
422
593
  let withoutRuntime = false;
423
594
  let persistenceError: string | undefined;
424
595
  let pendingFork = false;
@@ -426,15 +597,9 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
426
597
  const branch = ctx.sessionManager.getBranch();
427
598
  const discovery = discoverSnapshotData(branch);
428
599
  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;
600
+ pendingFork = nativePolicy.forkPending;
601
+ mode = nativePolicy.mode;
602
+ persistenceError = nativePolicy.persistenceError;
438
603
  } catch {
439
604
  // Off needs only policy; unavailable native evidence grants no memory authority.
440
605
  }
@@ -443,7 +608,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
443
608
  const owner = sessionAddress(ctx).key;
444
609
  const reason = pendingFork || (deferredBranch?.owner === owner && deferredBranch.cwd === ctx.cwd && deferredBranch.reason === "fork"
445
610
  && retainsPhysicalSessionProjection(sessionStartReason)) ? "fork" : sessionStartReason;
446
- if (reason === "fork" && !pendingFork) pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, { owner: ctx.sessionManager.getSessionId(), forkPending: true });
611
+ if (reason === "fork" && !pendingFork) appendNativeMarker({ mode: "off", forkPending: true });
447
612
  deferredBranch = { reason, owner, cwd: ctx.cwd };
448
613
  runtime = undefined;
449
614
  snapshot = emptySnapshot("off");
@@ -452,7 +617,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
452
617
  forkInitialization = reason === "fork";
453
618
  installScopeStates();
454
619
  acquisition.reset();
455
- if (adoptDefault) appendCheckpoint();
620
+
456
621
  syncStateFlowTools();
457
622
  updateUi(ctx);
458
623
  return Promise.resolve();
@@ -463,16 +628,21 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
463
628
  runtime = createRuntime(ctx);
464
629
  installScopeStates();
465
630
  branchStartsWithoutRuntime = false;
466
- forkInitialization = sessionStartReason === "fork";
631
+ forkInitialization = sessionStartReason === "fork" || nativePolicy.forkPending;
467
632
  let selection: BranchSelection = { kind: "settled" };
468
633
  let skipped = 0;
469
634
  try {
470
635
  const branch = ctx.sessionManager.getBranch();
471
- const fence = retainsPhysicalSessionProjection(sessionStartReason)
472
- ? findPassiveStopBoundary(branch, ctx.sessionManager.getSessionId(), PASSIVE_STOP_ENTRY_TYPE)
473
- : undefined;
636
+ const fence = nativePolicy.stop;
474
637
  modePersistenceError = fence?.persistenceError ?? (startOwner ? modePersistenceError : undefined);
475
- if (fence?.persistenceError) {
638
+ if (forkInitialization) {
639
+ // Pending copy authority is independent of whether ancillary Pi checkpoint appends succeeded.
640
+ const selected = selectRetainedCheckpoint(discoverSnapshotData(branch).candidates, config.inactiveMode);
641
+ selection = { kind: "fork", checkpoint: {
642
+ mode: requestedMode ?? (nativePolicy.mode === "off" && startOwner ? "passive" : nativePolicy.mode),
643
+ ...(selected.kind === "boundary" && selected.checkpoint.bootstrap ? { bootstrap: true } : {}),
644
+ } };
645
+ } else if (fence?.persistenceError) {
476
646
  // The native marker owns policy only: read proven memory, never replay or publish a stale selection.
477
647
  selection = { kind: "current", mode: fence.mode ?? config.inactiveMode };
478
648
  } else {
@@ -482,20 +652,11 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
482
652
  branchStartsWithoutRuntime = selected.kind === "pre-runtime" || (discovery.candidates.length === 0 && discovery.errors.length === 0);
483
653
  if (discovery.candidates.length === 0 && discovery.errors.length > 0) {
484
654
  snapshot = migrationFailure({}, `Snapshot restoration failed: ${discovery.errors[0]}`, config.inactiveMode);
485
- } else if (selected.kind === "boundary" && forkInitialization) {
486
- try {
487
- // Native parent acquisition stays outside canonical exclusion.
488
- const source = forkSource(ctx);
489
- const sourceFence = findPassiveStopBoundary(branch, source.id, PASSIVE_STOP_ENTRY_TYPE);
490
- selection = { kind: "fork", source, checkpoint: sourceFence?.persistenceError !== undefined
491
- ? { ...selected.checkpoint, mode: sourceFence.mode ?? config.inactiveMode } : selected.checkpoint };
492
- } catch (error) {
493
- snapshot = selectedBoundaryFailure(diagnosticText(error), requestedMode ?? selected.checkpoint.mode);
494
- }
495
655
  } else if (selected.kind === "boundary") {
496
- selection = { kind: "attach", checkpoint: selected.checkpoint };
656
+ selection = { kind: "attach", checkpoint: { ...selected.checkpoint, mode: nativePolicy.mode } };
497
657
  } else if (selected.kind === "pre-runtime") {
498
- snapshot = emptySnapshot(selected.mode);
658
+ snapshot = emptySnapshot(nativePolicy.mode);
659
+ if (nativePolicy.mode === "active") selection = { kind: "auto-start" };
499
660
  } else if (discovery.candidates.length > 0) {
500
661
  snapshot = selected.snapshot;
501
662
  } else if (isNewSession(sessionStartReason, branch)) {
@@ -503,21 +664,25 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
503
664
  if (config.mode === "active") selection = { kind: "auto-start" };
504
665
  else {
505
666
  snapshot = emptySnapshot(config.mode);
506
- // Adopt the default once without initializing semantic storage.
507
- appendCheckpoint();
667
+ // The native policy was captured before memory acquisition.
508
668
  }
509
669
  } else {
510
- snapshot = emptySnapshot(config.inactiveMode);
670
+ snapshot = emptySnapshot(nativePolicy.mode);
671
+ if (nativePolicy.mode === "active" && branchStartsWithoutRuntime) selection = { kind: "auto-start" };
511
672
  }
512
673
  }
513
674
  } catch (error) {
514
675
  selection = { kind: "settled" };
515
- snapshot = selectedBoundaryFailure(diagnosticText(error), config.inactiveMode);
676
+ snapshot = selectedBoundaryFailure(diagnosticText(error), nativePolicy.mode);
677
+ }
678
+ const preRuntimeMode = requestedMode ?? snapshot.config.mode;
679
+ if (selection.kind === "settled" && branchStartsWithoutRuntime && snapshot.meta.validation?.attempt !== 0 && preRuntimeMode !== "off") {
680
+ selection = { kind: "pre-runtime", mode: preRuntimeMode };
516
681
  }
517
- // Pending selection grants neither private reads nor publication until its acceptance installs memory.
682
+ // Retain selected policy while validation blocks memory authority and Active inference until acceptance.
518
683
  if (selection.kind !== "settled") {
519
684
  snapshot = migrationFailure({}, "State Flow branch restoration is pending", selection.kind === "current" ? selection.mode
520
- : selection.kind === "auto-start" ? config.inactiveMode : inactiveSelection(selection.checkpoint.mode));
685
+ : selection.kind === "auto-start" ? "active" : selection.kind === "pre-runtime" ? selection.mode : selection.checkpoint.mode);
521
686
  }
522
687
  if (requestedMode) snapshot = deactivateEpisode(snapshot, requestedMode);
523
688
  const pending: BranchRestoration = {
@@ -536,11 +701,11 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
536
701
  ): Promise<void> {
537
702
  const placeholder = runtime;
538
703
  let selected = runtime;
539
- const owner = ctx.sessionManager.getSessionId();
704
+ const wanted = nativeScopeSelection(ctx);
540
705
  const sameSession = sessionIdentity(ctx);
541
- const signal = AbortSignal.any([pending.controller.signal, ...(ctx.signal ? [ctx.signal] : [])]);
706
+ const signal = modeOperationSignal(ctx, pending.controller.signal);
542
707
  const isCurrent = () => branchRestoration.owns(pending) && !pending.controller.signal.aborted && !shuttingDown && runtime === selected
543
- && sameSession();
708
+ && sameSession() && sameJson(nativeScopeSelection(ctx), wanted);
544
709
  const assertCurrent = () => {
545
710
  signal.throwIfAborted();
546
711
  if (!isCurrent()) throw new Error("State Flow branch selection changed while awaiting publication");
@@ -548,7 +713,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
548
713
  let accepted = false;
549
714
  const failSelection = (error: unknown): void => {
550
715
  const mode = pending.requestedMode ?? (selection.kind === "auto-start" ? "active"
551
- : selection.kind === "attach" || selection.kind === "fork" ? selection.checkpoint.mode : snapshot.config.mode);
716
+ : selection.kind === "pre-runtime" ? selection.mode : selection.kind === "attach" || selection.kind === "fork" ? selection.checkpoint.mode : snapshot.config.mode);
552
717
  snapshot = selectedBoundaryFailure(diagnosticText(error), mode);
553
718
  installScopeStates();
554
719
  };
@@ -567,61 +732,124 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
567
732
  };
568
733
  try {
569
734
  if (selection.kind !== "settled") {
570
- // Local policy is already passive while the selection waits for exclusion.
735
+ // Selected policy is visible while memory remains unavailable during the exclusion wait.
571
736
  syncStateFlowTools();
572
737
  updateUi(ctx);
573
738
  const candidate = createRuntime(ctx);
574
739
  try {
575
740
  if (selection.kind === "current") {
576
- const current = await candidate.refreshCurrentMemory(signal);
741
+ const current = await candidate.refreshSelectedCurrent(wanted, assertCurrent, signal);
577
742
  assertCurrent();
578
- if (current) runtime = selected = candidate;
579
- snapshot = current ? deactivateEpisode(current, pending.requestedMode ?? selection.mode)
580
- : migrationFailure({}, "Current State Flow session memory is unavailable", pending.requestedMode ?? selection.mode);
743
+ runtime = selected = candidate;
744
+ snapshot = deactivateEpisode(current, pending.requestedMode ?? selection.mode);
581
745
  installScopeStates();
582
746
  } else if (selection.kind === "attach") {
583
- // Memory is the current same-session files; the Pi checkpoint supplies only mode and run lifecycle.
584
- await candidate.withStartTransaction((current, publish) => {
747
+ // A validated shared-only bookmark may precede a new requested Session selection.
748
+ const addsSession = wanted.includes("session") && "scopes" in selection.checkpoint
749
+ && !captureScopeSelection(selection.checkpoint.scopes).includes("session");
750
+ const disposition = addsSession ? await candidate.inspectSelectedLifecycle(wanted, assertCurrent, signal) : undefined;
751
+ assertCurrent();
752
+ if ((pending.requestedMode ?? selection.checkpoint.mode) === "passive" && disposition?.privateAcquisition === "unacquired") {
753
+ // Observation cannot turn the new policy or old bookmark into private acquisition.
754
+ const shared = wanted.filter(scope => scope !== "session");
755
+ if (shared.length) await candidate.refreshSelectedShared(captureScopeSelection(shared), assertCurrent, signal);
585
756
  assertCurrent();
586
- if (!current) throw new Error("Current State Flow session memory is unavailable");
587
- const { checkpoint } = selection;
588
- const restored: Snapshot = { config: { mode: checkpoint.mode }, meta: {
589
- step: current.meta.step,
590
- ...(checkpoint.bootstrap ? { bootstrap: true } : {}),
591
- ...(checkpoint.specification === undefined ? {} : { specification: checkpoint.specification }),
592
- } };
593
- // Canceled preparation or boundary continuation may have no specification. Retain uncompiled native context.
594
- if (restored.config.mode === "active" && restored.meta.specification === undefined
595
- && retainsPhysicalSessionProjection(reason) && hasUncheckpointedConversation(ctx.sessionManager.getBranch())) restored.meta.bootstrap = true;
596
- const next = pending.requestedMode ? deactivateEpisode(restored, pending.requestedMode) : restored;
597
- publish(next);
598
- accept(candidate, next, appendCheckpoint);
599
- }, signal);
757
+ runtime = selected = candidate;
758
+ snapshot = emptySnapshot(pending.requestedMode ?? "passive");
759
+ installScopeStates();
760
+ } else {
761
+ // Memory is the current same-session files; the Pi checkpoint supplies only mode and run lifecycle.
762
+ await candidate.withSelectedCurrentTransaction(wanted, (current, publish) => {
763
+ assertCurrent();
764
+ if (!current) throw new Error("Current State Flow session memory is unavailable");
765
+ const { checkpoint } = selection;
766
+ const restored: Snapshot = { config: { mode: checkpoint.mode }, meta: {
767
+ step: current.meta.step,
768
+ ...(checkpoint.bootstrap ? { bootstrap: true } : {}),
769
+ ...(checkpoint.specification === undefined ? {} : { specification: checkpoint.specification }),
770
+ } };
771
+ // Canceled preparation or boundary continuation may have no specification. Retain uncompiled native context.
772
+ if (restored.config.mode === "active" && restored.meta.specification === undefined
773
+ && retainsPhysicalSessionProjection(reason) && hasUncheckpointedConversation(ctx.sessionManager.getBranch())) restored.meta.bootstrap = true;
774
+ const next = pending.requestedMode ? deactivateEpisode(restored, pending.requestedMode) : restored;
775
+ publish(next);
776
+ accept(candidate, next, appendCheckpoint);
777
+ }, assertCurrent, signal);
778
+ }
600
779
  } else if (selection.kind === "fork") {
601
- // The runtime copies the parent's current memory; the checkpoint supplies only child mode/bootstrap.
602
- await candidate.withForkTransaction(selection.source, selection.checkpoint, (child, publish) => {
780
+ const disposition = await candidate.inspectSelectedLifecycle(wanted, assertCurrent, signal);
781
+ if (disposition && (!wanted.includes("session") || disposition.privateAcquisition === "acquired")) {
782
+ // Acquired child-current wins; a still-deferred shared child keeps its native pending fact.
783
+ await candidate.withSelectedCurrentTransaction(wanted, (current, publish) => {
784
+ assertCurrent();
785
+ const next = { ...current, config: { mode: pending.requestedMode ?? selection.checkpoint.mode } };
786
+ publish(next);
787
+ forkInitialization = false; modePersistenceError = undefined;
788
+ accept(candidate, next, () => { if (disposition.privateAcquisition === "acquired") appendNativeMarker({ reset: true }); appendCheckpoint(); });
789
+ }, assertCurrent, signal);
790
+ } else if (!wanted.includes("session")) {
791
+ // Admit selected shared owners only; no parent header/session data or private acquisition.
792
+ await candidate.withSelectedDeferredForkTransaction(wanted, { mode: pending.requestedMode ?? selection.checkpoint.mode,
793
+ ...(selection.checkpoint.bootstrap ? { bootstrap: true } : {}) }, publish => {
794
+ assertCurrent(); publish();
795
+ forkInitialization = false; modePersistenceError = undefined;
796
+ accept(candidate, candidate.selectedSnapshot(), appendCheckpoint);
797
+ }, signal, true, () => { assertCurrent(); return pending.requestedMode ?? selection.checkpoint.mode; });
798
+ } else {
799
+ if (disposition?.privateAcquisition === "unacquired") throw new Error("Fork child has no deferred private-copy authority");
800
+ const base = disposition ? await loadSelectedFileCohort(ctx.cwd, candidate.sessionId, disposition.selection, candidate.root, candidate.sessionKey, signal) : undefined;
603
801
  assertCurrent();
604
- // Mode is selected independently of creating the child's private memory.
605
- const next = pending.requestedMode ? deactivateEpisode(child, pending.requestedMode) : child;
606
- publish(next);
607
- forkInitialization = false;
608
- accept(candidate, next, () => {
609
- pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, { reset: true, owner });
610
- appendCheckpoint();
611
- });
612
- }, signal);
802
+ if (disposition && !base) throw new Error("Deferred fork child authority disappeared");
803
+ // Direct-parent acquisition follows child classification and remains outside exclusion.
804
+ const source = forkSource(ctx), parentFile = ctx.sessionManager.getHeader()?.parentSession;
805
+ const next: Snapshot = { config: { mode: pending.requestedMode ?? selection.checkpoint.mode }, meta: disposition ? disposition.snapshot.meta : {
806
+ step: 0, ...(selection.checkpoint.bootstrap ? { bootstrap: true } : {}) } };
807
+ await candidate.withSelectedForkCopyTransaction({ ...(base ? { kind: "fork-deferred" as const, base } : { kind: "fork-new" as const }), source,
808
+ selection: wanted, snapshot: next, historyLimit: config.historyLimit,
809
+ author: (_basis, publish) => {
810
+ assertCurrent();
811
+ publish({ ...next, config: { mode: pending.requestedMode ?? selection.checkpoint.mode } });
812
+ } }, publish => {
813
+ assertCurrent(); publish();
814
+ forkInitialization = false; modePersistenceError = undefined;
815
+ accept(candidate, candidate.selectedSnapshot(), () => { appendNativeMarker({ reset: true }); appendCheckpoint(); });
816
+ }, (selectedSource, phase) => {
817
+ assertCurrent();
818
+ if (ctx.sessionManager.getHeader()?.parentSession !== parentFile) throw new Error("Native fork parent changed");
819
+ if (phase === "prepare" && !sameJson(forkSource(ctx), selectedSource)) throw new Error("Native direct parent mismatch");
820
+ }, signal);
821
+ }
613
822
  } else {
614
- await candidate.withStartTransaction((current, publish) => {
823
+ const mode = pending.requestedMode ?? (selection.kind === "pre-runtime" ? selection.mode : "active");
824
+ const disposition = await candidate.inspectSelectedLifecycle(wanted, assertCurrent, signal);
825
+ assertCurrent();
826
+ // A pre-runtime Passive choice may observe shared owners, not acquire an explicitly unacquired Session.
827
+ const observesUnacquired = (pending.requestedMode ?? mode) === "passive" && wanted.includes("session") && disposition?.privateAcquisition === "unacquired";
828
+ if (disposition && !observesUnacquired) {
829
+ await candidate.withSelectedCurrentTransaction(wanted, (current, publish) => {
830
+ assertCurrent();
831
+ const next = { ...current, config: { mode: pending.requestedMode ?? mode } };
832
+ publish(next);
833
+ accept(candidate, next, appendCheckpoint);
834
+ }, assertCurrent, signal);
835
+ } else if (mode === "passive" || observesUnacquired) {
836
+ const shared = wanted.filter(scope => scope !== "session");
837
+ if (shared.length) await candidate.refreshSelectedShared(captureScopeSelection(shared), assertCurrent, signal);
615
838
  assertCurrent();
616
- // Truly new branch authority is rechecked after waiting; existing memory needs its own selection.
617
- const branch = ctx.sessionManager.getBranch();
618
- const discovery = discoverSnapshotData(branch);
619
- if (current || discovery.candidates.length > 0 || discovery.errors.length > 0) throw new Error("Existing session runtime requires retained-boundary restoration");
620
- const activated = startEpisode(hasPriorConversation(branch));
621
- const next = pending.requestedMode ? deactivateEpisode(activated, pending.requestedMode) : activated;
622
- publish(next);
623
- accept(candidate, next, appendCheckpoint);
624
- }, signal, true);
839
+ runtime = selected = candidate;
840
+ snapshot = emptySnapshot(pending.requestedMode ?? mode);
841
+ installScopeStates();
842
+ } else {
843
+ const initial = startEpisode(hasPriorConversation(ctx.sessionManager.getBranch()));
844
+ await candidate.withSelectedFirstPatchTransaction(wanted, initial, transaction => {
845
+ assertCurrent();
846
+ if (!branchStartsWithoutRuntime || forkInitialization) throw new Error("Existing session or pending fork requires current-memory acceptance");
847
+ // Bind the newest independent mode inside the existing atomic origin/lifecycle staging engine.
848
+ const next = pending.requestedMode ? deactivateEpisode(initial, pending.requestedMode) : initial;
849
+ transaction.publish(next);
850
+ accept(candidate, next, appendCheckpoint);
851
+ }, assertCurrent, signal, true);
852
+ }
625
853
  }
626
854
  } catch (error) {
627
855
  if (accepted) {
@@ -636,13 +864,13 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
636
864
  if (accepted) return;
637
865
  }
638
866
  settleSelection(ctx, reason, notifyRecovery, skipped, selected !== placeholder);
639
- if (snapshot.config.mode === "off" || isActive() || runtime?.view) return;
640
- // Unavailable Active memory may expose shared reads, never substitute private memory or model context.
867
+ if (snapshot.config.mode === "off" || isActive() || readableView() || snapshot.meta.validation?.attempt === 0 || !branchStartsWithoutRuntime) return;
868
+ // Only a proven pre-runtime Passive branch may observe shared owners without private acquisition.
641
869
  const passive = await loadPassiveView(ctx, signal, isCurrent, notifyRecovery);
642
870
  if (passive === undefined) return;
643
871
  assertCurrent();
644
872
  // An intervening passive patch or inspection owns its newer cache, even on the same physical branch.
645
- if (runtime?.view) return;
873
+ if (readableView()) return;
646
874
  runtime = selected = passive;
647
875
  installScopeStates();
648
876
  updateUi(ctx);
@@ -658,9 +886,13 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
658
886
 
659
887
  /** Load current shared memory for passive projection without initializing or publishing it. */
660
888
  async function loadPassiveView(ctx: ExtensionContext, signal: AbortSignal, isCurrent: () => boolean, notify: boolean): Promise<TemporalRuntime | undefined> {
889
+ const wanted = nativeScopeSelection(ctx), shared = wanted.filter(scope => scope !== "session");
890
+ if (!shared.length) return undefined;
661
891
  const passive = createRuntime(ctx);
662
892
  try {
663
- await passive.refreshShared(signal);
893
+ await passive.refreshSelectedShared(captureScopeSelection(shared), () => {
894
+ if (!isCurrent() || !sameJson(nativeScopeSelection(ctx), wanted)) throw new Error("State Flow observation selection changed");
895
+ }, signal);
664
896
  } catch (error) {
665
897
  if (isCurrent() && !signal.aborted && notify && !modePersistenceError) notifyProblem(ctx, `State Flow passive memory is unavailable: ${diagnosticText(error)}`, "warning");
666
898
  return undefined;
@@ -669,10 +901,10 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
669
901
  }
670
902
 
671
903
  function selectedContinuation(ctx: ExtensionContext, reason: unknown): PassiveContinuation | undefined {
672
- if (!retainsPhysicalSessionProjection(reason) || !runtime?.view) return undefined;
673
- const boundary = findPassiveStopBoundary(ctx.sessionManager.getBranch(), ctx.sessionManager.getSessionId(), PASSIVE_STOP_ENTRY_TYPE);
904
+ if (!retainsPhysicalSessionProjection(reason) || !readableView()) return undefined;
905
+ const boundary = branchPolicy(ctx).stop;
674
906
  return boundary === undefined ? undefined : createPassiveContinuation(
675
- projectModelState(effectiveState), boundary.at, boundary.from, boundary.preserveContext,
907
+ projectModelState(effectiveState), boundary.at, boundary.from, boundary.preserveContext, nativeScopeSelection(ctx),
676
908
  );
677
909
  }
678
910
 
@@ -698,40 +930,89 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
698
930
  diagnosticWriter.record(sessionAddress(ctx).id, ctx.cwd, error, category, extras);
699
931
  }
700
932
 
933
+ function nativeToolAdmission(id: string, name: string, ctx?: ExtensionContext): SelectedPolicyRequest | undefined {
934
+ if (nativeRequests.has(id)) return nativeRequests.take(id, name);
935
+ // Raw non-model domain invocations have no operation signal. Public SDK tool_call always gates.
936
+ if (ctx?.signal || nativeRequests.active) throw new Error("State Flow tool batch has no current native request");
937
+ return undefined;
938
+ }
939
+
940
+ function currentNativeTool(id: string, name: string, ctx: ExtensionContext): boolean {
941
+ try {
942
+ if (nativeRequests.has(id)) nativeRequests.check(id, name);
943
+ else if (nativeRequests.active || ctx.signal !== undefined) return false;
944
+ const policy = requestPolicy.policy, selected = runtime;
945
+ return policy !== undefined && selected !== undefined && selected.sessionId === policy.owner
946
+ && policy.owner === ctx.sessionManager.getSessionId() && policy.cwd === ctx.cwd && selected.cwd === ctx.cwd
947
+ && selected.sessionKey === sessionAddress(ctx).key;
948
+ } catch { return false; }
949
+ }
950
+
701
951
  pi.registerTool({
702
952
  name: READ_STATE_TOOL_NAME,
703
953
  label: "Read State",
704
- 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.",
954
+ description: "Read exact State Flow values or historical semantic patches with one path or an ordered path list. With one participating owner, semantic paths and structured references are scope-free, effective[N] selects that owner's historical boundary, and patches[N] selects its retained owner patch. With multiple owners, unscoped paths use the effective overlay and owner patch history requires an explicit owner. Optional global/cwd/session roots may address only participating available owners. Array selectors are zero-based indices or half-open [start..end] ranges, never temporal offsets inside a semantic value. Projection value returns 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.",
705
955
  promptSnippet: "Read exact state values, structures, or historical semantic patches",
956
+ promptGuidelines: [SCOPE_PARTICIPATION_GUIDELINE],
706
957
  parameters: Type.Object({
707
- 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" })),
958
+ path: Type.Optional(Type.String({ description: "One semantic path. One owner: flat planes, effective[N] history or retained patches[N]. Multiple owners: effective overlay or explicit participating owner; patch history needs owner.patches[N]. Nested array selectors stay ordinary indices/ranges." })),
708
959
  paths: Type.Optional(Type.Array(Type.String(), { minItems: 1, description: "Ordered query paths evaluated as one all-or-error read" })),
709
960
  projection: Type.Optional(StringEnum(["value", "keys", "patch"] as const, { description: "Value snapshot by default, structural keys with minimal meta, or the selected historical semantic patch" })),
710
961
  }, { additionalProperties: false }),
711
- async execute(_toolCallId, params, signal) {
962
+ async execute(toolCallId, params, signal, _onUpdate, ctx) {
712
963
  try {
964
+ const admission = nativeToolAdmission(toolCallId, READ_STATE_TOOL_NAME, ctx);
965
+ admission?.revalidate();
966
+ if (admission) signal = signal ? AbortSignal.any([signal, admission.signal]) : admission.signal;
713
967
  type ReadDetails = { path?: string; paths?: string[]; projection?: string; transitionId?: string };
714
968
  if (!memoryToolsAvailable()) throw new Error("State Flow tools are off for this session");
715
969
  if (signal?.aborted) throw new Error("State Flow read was aborted");
716
- if (!runtime?.view) throw new Error("State Flow temporal runtime is unavailable");
970
+ assertSelectedBranchAvailable();
971
+ const selected = runtime;
972
+ if (!selected) throw new Error("State Flow temporal runtime is unavailable");
973
+ const receipt = admission ?? requestPolicy.capture();
974
+ const check = () => {
975
+ receipt.revalidate();
976
+ signal?.throwIfAborted();
977
+ if (!ctx || runtime !== selected || selected.sessionId !== receipt.policy.owner || receipt.policy.owner !== ctx.sessionManager.getSessionId() || receipt.policy.cwd !== ctx.cwd
978
+ || selected.cwd !== ctx.cwd || selected.sessionKey !== sessionAddress(ctx).key) throw new Error("State Flow read selection owner changed");
979
+ };
980
+ check();
981
+ const wanted = captureScopeSelection(receipt.policy.scopes);
982
+ params = structuredClone(params);
983
+ check();
717
984
  if (params.path === undefined && params.paths === undefined) throw new Error("read_state requires path or paths");
718
985
  if (params.path !== undefined && params.paths !== undefined) throw new Error("read_state accepts path or paths, not both");
719
986
  {
720
- const paths = params.paths ?? [params.path!];
987
+ const requestedPaths = [...(params.paths ?? [params.path!])];
988
+ const paths = requestedPaths.map(path => normalizeModelReadPath(path, wanted, config.historyLimit));
989
+ check();
990
+ const view = readableView(selected);
991
+ // Wanted policy was checked by normalization; an observational subset is not an exclusion.
992
+ for (const path of paths) {
993
+ const owner = /^(global|cwd|session)(?=\.|\[|$)/.exec(path)?.[1] as StateScope | undefined;
994
+ if (owner && !view?.selection.includes(owner)) throw new Error(`State Flow ${owner}: selected, unavailable`);
995
+ }
996
+ if (!view) throw new Error("State Flow temporal runtime is unavailable");
997
+ check();
721
998
  if (paths.some((path) => /^session(?:\.|\[|$)/.test(path))) assertSelectedBranchAvailable();
722
999
  if (paths.length === 1 && /^(?:global|cwd|session)\.patches(?:\[\d+\])?$/.test(paths[0]!)) {
723
1000
  if (params.projection !== undefined && params.projection !== "value") throw new Error("Scope patch paths support only the value projection");
724
- const result = readStatePath(runtime.view, paths[0]!, config.historyLimit);
1001
+ const result = readStatePath(view, paths[0]!, config.historyLimit);
725
1002
  if (!("patch" in result)) throw new Error("Expected a scope patch path");
1003
+ const output = wanted.length === 1 ? projectModelReferences({ patch: result.patch }, wanted, wanted.flatMap(scope => Object.keys(view.scopes[scope]?.modules ?? {}))) : { patch: result.patch };
1004
+ check();
726
1005
  return {
727
- content: [{ type: "text", text: `\n${JSON.stringify({ patch: result.patch })}` }],
728
- details: { path: paths[0], transitionId: result.boundary.id } as ReadDetails,
1006
+ content: [{ type: "text", text: `\n${JSON.stringify(output)}` }],
1007
+ details: { path: requestedPaths[0], transitionId: result.boundary.id } as ReadDetails,
729
1008
  };
730
1009
  }
731
- const result = readProjectedState(runtime.view, paths, params.projection, config.historyLimit);
1010
+ const result = readProjectedState(view, paths, params.projection, config.historyLimit);
1011
+ const output = wanted.length === 1 ? projectModelReferences(result, wanted, wanted.flatMap(scope => Object.keys(view.scopes[scope]?.modules ?? {}))) : result;
1012
+ check();
732
1013
  return {
733
- content: [{ type: "text", text: `\n${JSON.stringify(result)}` }],
734
- details: { ...(params.path === undefined ? { paths } : { path: params.path }), projection: params.projection ?? "value" } as ReadDetails,
1014
+ content: [{ type: "text", text: `\n${JSON.stringify(output)}` }],
1015
+ details: { ...(params.path === undefined ? { paths: requestedPaths } : { path: requestedPaths[0] }), projection: params.projection ?? "value" } as ReadDetails,
735
1016
  };
736
1017
  }
737
1018
  } catch (error) {
@@ -740,23 +1021,20 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
740
1021
  },
741
1022
  });
742
1023
 
743
- pi.registerTool({
1024
+ const patchStateTool = defineTool({
744
1025
  name: PATCH_STATE_TOOL_NAME,
745
1026
  label: "Patch State",
746
- 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.",
747
- promptSnippet: "Atomically patch one or more global/cwd/session scopes",
1027
+ description: "The sole State Flow semantic mutation protocol. With one participating owner, supply intents/strategy/tactics/artifacts/lazy planes directly, without an owner wrapper. With multiple owners, supply one or more global, cwd, or session patches for participating owners; all supplied owners commit atomically. This call must be the only State Flow barrier in its assistant response. Intent updates run first, then clear owned same-scope strategy/tactics/lazy keys unless a remaining or new intent refs them, then other fields apply. Later writes can recreate cleared paths without old fields. Name keys in ASCII ([A-Za-z_$][A-Za-z0-9_$-]*) so read paths and refs resolve; values may use any language.",
1028
+ promptSnippet: "Atomically patch participating semantic memory in its declared flat or multi-owner form",
748
1029
  promptGuidelines: [
749
1030
  RESPONSE_PATCH_GUIDELINE,
1031
+ SCOPE_PARTICIPATION_GUIDELINE,
750
1032
  "Use patch_state only for material durable semantic changes; ordinary answers need no finalization call.",
751
1033
  "Use patch_state as reconciliation, not append-only notes: place new knowledge at the narrowest valid scope and remove superseded or completed state from touched branches.",
752
1034
  "Call patch_state alone in an assistant response; after its acknowledgement, further reasoning, tools, and later patch_state calls remain allowed.",
753
1035
  ],
754
1036
  executionMode: "sequential",
755
- parameters: Type.Object({
756
- global: Type.Optional(Type.Record(Type.String(), Type.Unknown(), { description: "Optional global semantic patch" })),
757
- cwd: Type.Optional(Type.Record(Type.String(), Type.Unknown(), { description: "Optional project semantic patch" })),
758
- session: Type.Optional(Type.Record(Type.String(), Type.Unknown(), { description: "Optional session semantic patch" })),
759
- }, { additionalProperties: false }),
1037
+ parameters: config.scopes.length === 1 ? flatPatchParameters : scopedPatchParameters,
760
1038
  renderCall(args, theme, context) {
761
1039
  const title = theme.fg("toolTitle", theme.bold(PATCH_STATE_TOOL_NAME));
762
1040
  if (!config.showSuccessfulPatches && !context.isError) return new Text(title, 0, 0);
@@ -768,31 +1046,48 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
768
1046
  },
769
1047
  async execute(toolCallId, params, signal, _onUpdate, ctx) {
770
1048
  signal = signal ? AbortSignal.any([signal, memoryToolLifetime.signal]) : memoryToolLifetime.signal;
1049
+ let admitted = false;
771
1050
  try {
1051
+ const admission = nativeToolAdmission(toolCallId, PATCH_STATE_TOOL_NAME, ctx);
1052
+ admission?.revalidate();
1053
+ if (admission) signal = AbortSignal.any([signal, admission.signal]);
772
1054
  if (!memoryToolsAvailable()) throw new Error("State Flow tools are off for this session");
773
1055
  assertPublicationAvailable();
774
1056
  if (signal?.aborted) throw new Error("State Flow patch was aborted before materialization");
775
- if (!isObject(params)) throw new Error("patch_state requires an object");
1057
+ if (!ctx) throw new Error("State Flow patch selection owner changed");
1058
+ const selected = runtime ??= createRuntime(ctx);
1059
+ const sameSession = sessionIdentity(ctx);
1060
+ const receipt = admission ?? requestPolicy.capture();
1061
+ const wanted = captureScopeSelection(receipt.policy.scopes);
1062
+ const catalog = moduleHost.captureIfBound();
1063
+ signal = AbortSignal.any([signal, receipt.signal, ...(catalog ? [catalog.signal] : [])]);
1064
+ const check = () => {
1065
+ receipt.revalidate();
1066
+ if (runtime !== selected || !sameSession() || selected.sessionId !== receipt.policy.owner || receipt.policy.owner !== ctx.sessionManager.getSessionId()
1067
+ || receipt.policy.cwd !== ctx.cwd || selected.cwd !== ctx.cwd || selected.sessionKey !== sessionAddress(ctx).key) throw new Error("State Flow patch selection owner changed");
1068
+ assertPublicationAvailable();
1069
+ catalog?.assertCurrent();
1070
+ };
1071
+ // Rehash and normalize only after caller admission, still before canonical exclusion.
1072
+ check();
1073
+ const excluded = isObject(params) && Object.keys(params).find(key =>
1074
+ (DEFAULT_SCOPE_SELECTION as readonly string[]).includes(key) && !wanted.includes(key as StateScope));
1075
+ if (excluded) throw new Error(`State Flow ${excluded} patch scope is excluded`);
776
1076
  params = structuredClone(params);
777
- const allowed = new Set(["global", "cwd", "session"]);
778
- for (const key of Object.keys(params)) {
779
- if (!allowed.has(key)) throw new Error(`patch_state does not accept field ${key}`);
780
- }
781
- const patches: AtomicScopePatches = {};
782
- for (const scope of ["global", "cwd", "session"] as const) {
783
- if (!Object.hasOwn(params, scope)) continue;
784
- const patch = params[scope];
785
- if (!isObject(patch)) throw new Error(`patch_state ${scope} must be a semantic patch object`);
786
- if (Object.keys(patch).length === 0) throw new Error(`patch_state ${scope} cannot be empty; omit it when unchanged`);
787
- patches[scope] = patch;
788
- }
1077
+ check();
1078
+ admitted = true;
1079
+ const knownModules = [...new Set([...(catalog?.modules.map(module => module.id) ?? []),
1080
+ ...wanted.flatMap(scope => Object.keys(readableView(selected)?.scopes[scope]?.modules ?? {}))])];
1081
+ const patches = normalizeModelPatches(params, wanted, knownModules);
1082
+ check();
789
1083
  const scopes = Object.keys(patches) as StateScope[];
790
- if (scopes.length === 0) throw new Error("patch_state requires at least one scope patch");
791
- const selected = runtime ??= createRuntime(ctx);
1084
+ const firstAuthoring = selected.selectedView() === undefined;
792
1085
  const acquiredArtifacts = structuredClone([...artifactReads.successful.values()]);
793
1086
  const acquiredSkills = structuredClone([...skillReads.successful.values()]);
794
1087
  const previousEffective = effectiveState;
795
- return await selected.withPatchTransaction((transaction) => {
1088
+ skillReads.validateCompilations(patches);
1089
+ const author = (transaction: SelectedRuntimePatchTransaction) => {
1090
+ check();
796
1091
  if (runtime !== selected) throw new Error("State Flow session selection changed while awaiting publication");
797
1092
  if (!memoryToolsAvailable()) throw new Error("State Flow tools are off for this session");
798
1093
  assertPublicationAvailable();
@@ -803,15 +1098,16 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
803
1098
  throw new Error(`Artifact source changed after acquisition: ${acquired.path}`);
804
1099
  }
805
1100
  }
806
- const stage = stageAtomicScopePatches(transaction.states, patches, acquiredSkills, transaction.causalBasis, acquiredArtifacts);
807
- const changed = (["global", "cwd", "session"] as const).some((scope) =>
1101
+ const stage = stageSelectedScopePatches(transaction, patches, acquiredSkills, transaction.causalBasis, acquiredArtifacts, catalog?.modules);
1102
+ const changed = transaction.selection.some((scope) =>
808
1103
  !sameJson(transaction.states[scope], stage.nextStates[scope])
809
- || Object.entries(stage.provenanceUpdates[scope]).some(([path, evidence]) =>
810
- !Object.hasOwn(transaction.provenance[scope], path) || !sameJson(transaction.provenance[scope][path], evidence)));
811
- const nextSnapshot = structuredClone(snapshot);
812
- let publication: RuntimePublication | undefined;
813
- commitScopedTransition(nextSnapshot, transaction.states, stage, (accepted, next) => {
814
- publication = transaction.publish(next, accepted, stage.provenanceUpdates);
1104
+ || Object.entries(stage.provenanceUpdates[scope] ?? {}).some(([path, evidence]) =>
1105
+ !Object.hasOwn(transaction.provenance[scope] ?? {}, path) || !sameJson(transaction.provenance[scope]?.[path], evidence)));
1106
+ const nextSnapshot = structuredClone(firstAuthoring ? transaction.snapshot : snapshot);
1107
+ let publication: SelectedFilePublication | undefined;
1108
+ commitSelectedTransition(nextSnapshot, transaction, stage, (accepted, next) => {
1109
+ catalog?.assertCurrent();
1110
+ publication = transaction.publish(next, accepted, stage.provenanceUpdates, stage.moduleAdditions);
815
1111
  }, transaction.causalBasis, { finalizeRun: false });
816
1112
  snapshot = nextSnapshot;
817
1113
  installScopeStates();
@@ -821,56 +1117,94 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
821
1117
  }
822
1118
  clearAcceptedAcquisitions(new Set(acquiredArtifacts.map(({ path }) => path)));
823
1119
  updateUi(ctx);
824
- const updates = contextProjection.acceptPatch(previousEffective, effectiveState, patches, acquisition.hints, stage.cascades, isActive());
1120
+ const updates = contextProjection.acceptPatch(previousEffective, effectiveState, patches, acquisition.hints, stage.cascades, isActive(), wanted);
825
1121
  const acknowledgement = changed
826
1122
  ? `\nState materialized atomically at ${scopes.join("+")} scope${scopes.length === 1 ? "" : "s"}.`
827
1123
  : "\nState already current.";
828
1124
  return { content: [
829
- { type: "text" as const, text: acknowledgement + stage.missingDeletions.map(({ path, unavailablePath }) =>
1125
+ { type: "text" as const, text: acknowledgement + stage.missingDeletions.map(deletion => projectModelMissingDeletion(deletion, wanted)).map(({ path, unavailablePath }) =>
830
1126
  `\n\nHint: deletion skipped at ${path}; target absent. First unavailable component: ${unavailablePath}. Check the path if you expected an existing value.`).join("") },
831
1127
  ...(updates ? [{ type: "text" as const, text: `\n${presentationJson({ state_updates: updates })}` }] : []),
832
1128
  ], details: { scopes, step: snapshot.meta.step, changed, missingDeletions: stage.missingDeletions } };
833
- }, signal);
1129
+ };
1130
+ if (!firstAuthoring) return await selected.withSelectedPatchTransaction(author, check, signal);
1131
+ check();
1132
+ const disposition = await selected.inspectSelectedLifecycle(wanted, check, signal);
1133
+ if (disposition && !wanted.includes("session")) {
1134
+ return await selected.withSelectedCurrentTransaction(wanted, (_current, _publish, transaction) => author(Object.freeze({
1135
+ ...transaction, snapshot: { ...transaction.snapshot, config: { mode: receipt.policy.mode } },
1136
+ })), check, signal);
1137
+ }
1138
+ if (disposition) {
1139
+ const acquire = () => {
1140
+ check();
1141
+ if (receipt.policy.mode === "off" || !wanted.includes("session") || disposition.privateAcquisition !== "unacquired"
1142
+ || forkInitialization || branchPolicy(ctx).forkPending) throw new Error("State Flow first mutation requires independent unacquired Session participation");
1143
+ };
1144
+ acquire();
1145
+ const base = await loadSelectedFileCohort(ctx.cwd, selected.sessionId, disposition.selection, selected.root, selected.sessionKey, signal);
1146
+ acquire();
1147
+ if (!base) throw new Error("Unacquired State Flow first mutation source disappeared");
1148
+ let result!: ReturnType<typeof author>;
1149
+ return await selected.withSelectedSessionAcquisitionTransaction({ kind: "unacquired", base, selection: wanted,
1150
+ snapshot: { ...disposition.snapshot, config: { mode: receipt.policy.mode } }, historyLimit: config.historyLimit,
1151
+ author: (basis, publish) => {
1152
+ acquire();
1153
+ result = author(Object.freeze<SelectedRuntimePatchTransaction>({ selection: wanted,
1154
+ states: readTemporalScopes(basis.view, 0, config.historyLimit), snapshot: basis.snapshot,
1155
+ modules: Object.fromEntries(wanted.filter(scope => basis.view.scopes[scope]?.modules !== undefined).map(scope => [scope, structuredClone(basis.view.scopes[scope]!.modules!)])),
1156
+ causalBasis: basis.view.lineage.at(-1)!.id, provenance: basis.provenance,
1157
+ publish: (next, transition, provenance, additions) => { acquire(); return publish(next, transition, provenance, additions); } }));
1158
+ } }, publish => { acquire(); publish(); return result; }, signal, true);
1159
+ }
1160
+ if (!branchStartsWithoutRuntime || forkInitialization || branchPolicy(ctx).forkPending) throw new Error("State Flow first authoring requires native independent origin authority");
1161
+ return await selected.withSelectedFirstPatchTransaction(wanted, snapshot, author, () => {
1162
+ check();
1163
+ if (!branchStartsWithoutRuntime || forkInitialization || branchPolicy(ctx).forkPending) throw new Error("State Flow origin authority changed");
1164
+ }, signal, true);
834
1165
  } catch (error) {
835
- let attempted: unknown;
836
- try { attempted = structuredClone(params); } catch { attempted = undefined; }
837
- const cause = diagnosticText(error);
838
- if (!signal.aborted && memoryToolsAvailable()) recordDiagnostic(cause, /concurrently|advanced/.test(cause) ? "publication-conflict" : "invalid-patch", ctx, {
839
- input: attempted,
840
- tool: PATCH_STATE_TOOL_NAME,
841
- toolCallId,
842
- });
1166
+ if (admitted && !signal.aborted && memoryToolsAvailable()) {
1167
+ let attempted: unknown;
1168
+ try { attempted = structuredClone(params); } catch { attempted = undefined; }
1169
+ const cause = diagnosticText(error);
1170
+ recordDiagnostic(cause, /concurrently|advanced/.test(cause) ? "publication-conflict" : "invalid-patch", ctx, {
1171
+ input: attempted, tool: PATCH_STATE_TOOL_NAME, toolCallId,
1172
+ });
1173
+ }
843
1174
  throw separatedFailure(error);
844
1175
  }
845
1176
  },
846
1177
  });
847
1178
 
848
- function startStateFlow(ctx: ExtensionContext): Promise<StateFlowTelegramControlResult> {
1179
+ pi.registerTool({ ...patchStateTool });
1180
+
1181
+ function startStateFlow(ctx: ExtensionContext, reselect = false): Promise<StateFlowTelegramControlResult> {
849
1182
  if (shuttingDown) return Promise.resolve({ ok: false, message: "State Flow is shutting down" });
850
- if (startActivation.current?.operation) return startActivation.current.operation;
1183
+ if (!reselect && startActivation.current?.operation) return startActivation.current.operation;
851
1184
  telegramStartPending = false;
852
- if (activeContext && !branchRestoration.current && isActive()) {
1185
+ if (!reselect && activeContext && !branchRestoration.current && isActive()) {
853
1186
  return Promise.resolve({ ok: true, message: "State Flow is already active", signal: sharedInspectionLifetime.signal });
854
1187
  }
855
1188
  const pending = startActivation.claim({ controller: new AbortController() });
856
- return startActivation.track(pending, runStart(ctx, pending.controller));
1189
+ return startActivation.track(pending, runStart(ctx, pending.controller, reselect));
857
1190
  }
858
1191
 
859
1192
  /** Await restoration-owned attachment and exact-source fork recovery before current-head activation. */
860
- async function runStart(ctx: ExtensionContext, pending: AbortController): Promise<StateFlowTelegramControlResult> {
1193
+ async function runStart(ctx: ExtensionContext, pending: AbortController, reselect: boolean): Promise<StateFlowTelegramControlResult> {
861
1194
  const sameSession = sessionIdentity(ctx);
862
- const signal = ctx.signal ? AbortSignal.any([pending.signal, ctx.signal]) : pending.signal;
1195
+ const signal = modeOperationSignal(ctx, pending.signal);
863
1196
  const isOwner = () => startActivation.current?.controller === pending && !signal.aborted && !shuttingDown && sameSession();
864
1197
  const superseded = (): StateFlowTelegramControlResult => {
865
1198
  pending.abort();
866
1199
  return { ok: false, message: "State Flow activation was superseded", signal: pending.signal };
867
1200
  };
868
1201
  try {
869
- if (deferredBranch?.reason === "fork") await waitForRecovery(restoreActiveBranch(ctx, "fork", false, pending), signal);
1202
+ if (reselect) await waitForRecovery(restoreActiveBranch(ctx, undefined, false, pending), signal);
1203
+ else if (deferredBranch?.reason === "fork") await waitForRecovery(restoreActiveBranch(ctx, "fork", false, pending), signal);
870
1204
  else if (!activeContext) await waitForRecovery(restoreActiveBranch(ctx, undefined, false, pending), signal);
871
1205
  else if (branchRestoration.current?.operation) await waitForRecovery(branchRestoration.current.operation, signal);
872
1206
  if (!isOwner()) return superseded();
873
- if (isActive()) return { ok: true, message: "State Flow is already active", signal: sharedInspectionLifetime.signal };
1207
+ if (!reselect && isActive()) return { ok: true, message: "State Flow is already active", signal: sharedInspectionLifetime.signal };
874
1208
  if (forkInitialization) {
875
1209
  // Withdraw this join on cancellation without revoking the independently owned mode persistence.
876
1210
  const stopping = inactivePersistence.current?.operation;
@@ -895,7 +1229,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
895
1229
  owns = () => startActivation.current?.controller === pending,
896
1230
  ): Promise<StateFlowTelegramControlResult> {
897
1231
  let selected = runtime;
898
- const owner = ctx.sessionManager.getSessionId();
1232
+ const wanted = nativeScopeSelection(ctx);
899
1233
  const sameSession = sessionIdentity(ctx);
900
1234
  const policy = () => `${snapshot.config.mode}:${isActive()}`;
901
1235
  const initialPolicy = policy();
@@ -903,14 +1237,15 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
903
1237
  const active = mode === "active";
904
1238
  let accepted = false;
905
1239
  let receipt = AbortSignal.any([pending.signal, sharedInspectionLifetime.signal]);
906
- const signal = ctx.signal ? AbortSignal.any([pending.signal, ctx.signal]) : pending.signal;
1240
+ const signal = modeOperationSignal(ctx, pending.signal);
907
1241
  const isCurrent = () => owns() && !signal.aborted && runtime === selected && !shuttingDown
908
- && sameSession() && policy() === (accepted ? acceptedPolicy : initialPolicy);
1242
+ && sameSession() && sameJson(nativeScopeSelection(ctx), wanted) && policy() === (accepted ? acceptedPolicy : initialPolicy);
909
1243
  const superseded = (): StateFlowTelegramControlResult => {
910
1244
  pending.abort();
911
1245
  return { ok: false, message: active ? "State Flow activation was superseded" : "State Flow mode change was superseded", signal: pending.signal };
912
1246
  };
913
1247
  try {
1248
+ assertNativeMemoryScopes(wanted);
914
1249
  const activation = createRuntime(ctx);
915
1250
  const deferred = deferredBranch;
916
1251
  if (deferred) {
@@ -919,15 +1254,20 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
919
1254
  branchStartsWithoutRuntime = retained.kind === "pre-runtime" || (discovery.candidates.length === 0 && discovery.errors.length === 0);
920
1255
  }
921
1256
  // Off activation owns one cancellable current-head publication, not an independent read-only restore.
922
- const result = await activation.withStartTransaction((current, publish) => {
1257
+ const acceptActivation = (current: Snapshot | undefined, publish: (next: Snapshot) => SelectedFilePublication) => {
923
1258
  signal.throwIfAborted();
924
1259
  if (!isCurrent()) throw new Error("State Flow activation selection changed while awaiting publication");
925
1260
  if (!current && (!active || !branchStartsWithoutRuntime)) throw new Error(snapshot.meta.validation?.error ?? "Current State Flow session memory is unavailable");
926
1261
  const boundary = deferred && retainsPhysicalSessionProjection(deferred.reason)
927
- ? findPassiveStopBoundary(ctx.sessionManager.getBranch(), owner, PASSIVE_STOP_ENTRY_TYPE) : undefined;
1262
+ ? branchPolicy(ctx).stop : undefined;
928
1263
  const bootstrap = hasPriorConversation(ctx.sessionManager.getBranch()) || passiveContinuation !== undefined || boundary !== undefined;
929
1264
  const activated = !active ? deactivateEpisode(current!, "passive")
930
1265
  : current ? resumeEpisode(current, bootstrap) : startEpisode(bootstrap);
1266
+ if (active) {
1267
+ // Explicit Start accepts current memory, not the prior run's prompt or repair attempt.
1268
+ delete activated.meta.specification;
1269
+ delete activated.meta.validation;
1270
+ }
931
1271
  const resumedWrites = modePersistenceError !== undefined;
932
1272
  publish(activated);
933
1273
  accepted = true;
@@ -951,7 +1291,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
951
1291
  updateUi(ctx);
952
1292
  if (forkInitialization) {
953
1293
  forkInitialization = false;
954
- pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, { reset: true, owner });
1294
+ appendNativeMarker({ reset: true });
955
1295
  }
956
1296
  appendCheckpoint();
957
1297
  if (!active) {
@@ -962,12 +1302,44 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
962
1302
  ? "State Flow active. The next complete agent run will migrate active context into state."
963
1303
  : "State Flow active. The next prompt starts a stateful agent run.", "info");
964
1304
  return { ok: true, message: "State Flow active", signal: receipt };
965
- }, signal, active && branchStartsWithoutRuntime);
1305
+ };
1306
+ const check = () => { signal.throwIfAborted(); if (!isCurrent()) throw new Error("State Flow activation superseded"); };
1307
+ let result: StateFlowTelegramControlResult;
1308
+ const sameRuntimeOwner = selected?.cwd === ctx.cwd && selected.sessionKey === activation.sessionKey;
1309
+ // Known accepted Session can only take current acceptance, even if its live header is later downgraded.
1310
+ const acceptedSession = sameRuntimeOwner && selected?.selectedView()?.selection.includes("session");
1311
+ const disposition = active && !acceptedSession ? await activation.inspectSelectedLifecycle(wanted, check, signal) : undefined;
1312
+ // Explicit Start owns the grant; header data only selects the existing acquisition route.
1313
+ if (active && wanted.includes("session") && disposition?.privateAcquisition === "unacquired") {
1314
+ if (forkInitialization || branchPolicy(ctx).forkPending) throw new Error("Pending fork cannot use independent private acquisition");
1315
+ // Retain denial-only acquisition knowledge when an existing owner has cleared or excluded its cache.
1316
+ if (sameRuntimeOwner) await selected!.inspectSelectedLifecycle(wanted, check, signal);
1317
+ const base = await loadSelectedFileCohort(ctx.cwd, activation.sessionId, disposition.selection, activation.root, activation.sessionKey, signal);
1318
+ check();
1319
+ if (!base) throw new Error("Unacquired State Flow source authority disappeared");
1320
+ let acquiredResult!: StateFlowTelegramControlResult;
1321
+ result = await activation.withSelectedSessionAcquisitionTransaction({ kind: "unacquired", base, selection: wanted,
1322
+ snapshot: { ...disposition.snapshot, config: { mode: "active" } }, historyLimit: config.historyLimit,
1323
+ author: (basis, publish) => {
1324
+ check();
1325
+ if (forkInitialization || branchPolicy(ctx).forkPending) throw new Error("Pending fork cannot use independent private acquisition");
1326
+ acquiredResult = acceptActivation(basis.snapshot, activated => publish(activated));
1327
+ } }, publish => { check(); publish(); return acquiredResult; }, signal, true);
1328
+ } else if (active && branchStartsWithoutRuntime && !acceptedSession && !disposition) {
1329
+ const next = startEpisode(hasPriorConversation(ctx.sessionManager.getBranch()) || passiveContinuation !== undefined);
1330
+ result = await activation.withSelectedFirstPatchTransaction(wanted, next, transaction => {
1331
+ check();
1332
+ if (!branchStartsWithoutRuntime || forkInitialization) throw new Error("State Flow origin authority changed");
1333
+ return acceptActivation(undefined, activated => transaction.publish(activated));
1334
+ }, check, signal, true);
1335
+ } else result = await activation.withSelectedCurrentTransaction(wanted,
1336
+ (current, publish) => acceptActivation(current, publish), check, signal);
966
1337
  return isCurrent() ? result : superseded();
967
1338
  } catch (error) {
968
1339
  if (!isCurrent()) return superseded();
969
1340
  const message = conciseDiagnostic(`${accepted ? `State Flow ${mode}; lifecycle update failed` : active ? "State Flow activation failed" : "State Flow passive acceptance failed"}: ${diagnosticText(error)}`);
970
- // Failed Passive acceptance falls back to its caller's write-fence report.
1341
+ // An unaccepted activation cannot replace retained lifecycle or poison an independent Stop.
1342
+ // Attachment owns required-proof unavailability; failed Passive acceptance reports through its caller's fence.
971
1343
  if (active || accepted) notifyProblem(ctx, message, accepted ? "warning" : "error");
972
1344
  return { ok: accepted, message, signal: receipt };
973
1345
  }
@@ -980,15 +1352,38 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
980
1352
  };
981
1353
 
982
1354
  /** Terminal commands and Telegram controls share these lifecycle owners. */
983
- function selectMode(ctx: ExtensionContext, mode: StateFlowMode): Promise<StateFlowTelegramControlResult> {
984
- return mode === "active" ? startStateFlow(ctx) : deactivateStateFlow(ctx, mode);
1355
+ function selectMode(ctx: ExtensionContext, mode: StateFlowMode, scopes?: ScopeSelection): Promise<StateFlowTelegramControlResult> {
1356
+ if (!scopes || sameJson(scopes, nativeScopeSelection(ctx))) return mode === "active" ? startStateFlow(ctx) : deactivateStateFlow(ctx, mode);
1357
+ if (shuttingDown) return Promise.resolve({ ok: false, message: "State Flow is shutting down" });
1358
+ if (mode === "off") return selectOff(ctx, scopes);
1359
+ // The native choice is DATA, never a private/fork acquisition or a fence reset.
1360
+ appendNativeMarker({ mode, scopes }, ctx);
1361
+ if (mode === "active") {
1362
+ // A different selection revokes old-scope persistence, not an independent same-scope Stop.
1363
+ inactivePersistence.cancel();
1364
+ return startStateFlow(ctx, true);
1365
+ }
1366
+ const sameSession = sessionIdentity(ctx);
1367
+ const restoring = restoreActiveBranch(ctx, undefined, true, undefined, "passive");
1368
+ const signal = branchRestoration.current?.controller.signal;
1369
+ return restoring.then(() => {
1370
+ if (signal?.aborted || !sameSession() || !sameJson(scopes, nativeScopeSelection(ctx)) || snapshot.config.mode !== "passive") {
1371
+ return { ok: false, message: "State Flow mode change was superseded" };
1372
+ }
1373
+ return { ok: snapshot.meta.validation?.attempt !== 0, message: snapshot.meta.validation?.error ?? "State Flow passive" };
1374
+ });
985
1375
  }
986
1376
 
987
1377
  for (const mode of ["active", "passive", "off"] as const) {
988
1378
  pi.registerCommand(`state-flow-${mode}`, {
989
- description: MODE_COMMAND_DESCRIPTIONS[mode],
990
- handler: async (_args, ctx) => {
991
- await selectMode(ctx, mode);
1379
+ description: `${MODE_COMMAND_DESCRIPTIONS[mode]} [global cwd session]`,
1380
+ getArgumentCompletions: prefix => {
1381
+ const values = scopeSelectionArgumentCompletions(prefix);
1382
+ return values.length ? values.map(value => ({ value, label: value })) : null;
1383
+ },
1384
+ handler: async (args, ctx) => {
1385
+ const scopes = parseScopeSelectionArguments(args);
1386
+ await selectMode(ctx, mode, scopes);
992
1387
  },
993
1388
  });
994
1389
  }
@@ -1033,30 +1428,37 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1033
1428
  if (restoring?.awaitingAcceptance) {
1034
1429
  // Passive keeps independently owned restoration; Off withdraws it through selectOff.
1035
1430
  applyInactiveMode(ctx, mode);
1431
+ // A read-only observer may never write a checkpoint; retain the newer choice now.
1432
+ if (branchPolicy(ctx).mode !== mode) appendNativeMarker({ mode }, ctx);
1036
1433
  return Promise.resolve({ ok: true, message: `State Flow ${mode}; memory restoration continues` });
1037
1434
  }
1038
1435
  if (snapshot.config.mode === mode) {
1039
- const retained = selectRetainedCheckpoint(discoverSnapshotData(ctx.sessionManager.getBranch()).candidates, config.inactiveMode);
1040
- const recordedMode = retained.kind === "pre-runtime" ? retained.mode : retained.kind === "boundary" ? retained.checkpoint.mode : undefined;
1436
+ const recordedMode = branchPolicy(ctx).mode;
1041
1437
  if (!passiveRecoveryNeeded(mode) && (modePersistenceError || recordedMode === mode)) return Promise.resolve({ ok: true, message: `State Flow is already ${mode}` });
1042
1438
  }
1043
1439
  inactivePersistence.cancel();
1044
1440
  const pending = inactivePersistence.claim({ controller: new AbortController() });
1045
- return inactivePersistence.track(pending, deferredBranch && mode === "passive" && !branchStartsWithoutRuntime
1441
+ return inactivePersistence.track(pending, deferredBranch && mode === "passive"
1046
1442
  ? acquirePassiveMode(ctx, pending, deferredBranch.reason)
1047
1443
  : persistInactiveMode(ctx, pending, mode));
1048
1444
  }
1049
1445
 
1050
1446
  /** Off owns cancellation and native policy only; no canonical transaction or semantic handoff. */
1051
- function selectOff(ctx: ExtensionContext): Promise<StateFlowTelegramControlResult> {
1447
+ function selectOff(ctx: ExtensionContext, scopes?: ScopeSelection): Promise<StateFlowTelegramControlResult> {
1052
1448
  const address = sessionAddress(ctx);
1053
1449
  const owner = ctx.sessionManager.getSessionId();
1054
1450
  const branch = ctx.sessionManager.getBranch();
1055
1451
  const selected = runtime;
1056
1452
  const sameOwner = selected?.sessionKey === address.key && selected.cwd === ctx.cwd;
1057
- const forkRecorded = hasPendingFork(branch, owner, PASSIVE_STOP_ENTRY_TYPE);
1058
- const pendingFork = forkRecorded || (sameOwner && forkInitialization) || (deferredBranch?.owner === address.key && deferredBranch.cwd === ctx.cwd && deferredBranch.reason === "fork");
1059
- const policy = findBranchPolicy(branch, owner, PASSIVE_STOP_ENTRY_TYPE, config.inactiveMode);
1453
+ let policy: ReturnType<typeof branchPolicy> | undefined;
1454
+ try {
1455
+ policy = branchPolicy(ctx, undefined, false);
1456
+ requestPolicy.select({ cwd: ctx.cwd, owner, mode: snapshot.config.mode, scopes: scopes ?? policy.scopes });
1457
+ }
1458
+ catch { /* Explicit Off denies memory even when the prior policy cannot be admitted. */ }
1459
+ const forkRecorded = policy?.forkPending ?? false;
1460
+ const pendingFork = forkRecorded || (sameOwner && forkInitialization) || (deferredBranch?.owner === address.key && deferredBranch.cwd === ctx.cwd && deferredBranch.reason === "fork")
1461
+ || (!policy && typeof ctx.sessionManager.getHeader()?.parentSession === "string");
1060
1462
  const sameDeferred = deferredBranch?.owner === address.key && deferredBranch.cwd === ctx.cwd;
1061
1463
  const current = sameOwner || sameDeferred ? snapshot : emptySnapshot("off");
1062
1464
  if (!sameOwner && !sameDeferred) modePersistenceError = policy?.persistenceError;
@@ -1069,10 +1471,10 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1069
1471
  const preserveContext = modePersistenceError !== undefined || (incoming ? incoming.preserveContext : current.meta.bootstrap === true || (unfinished && from === undefined));
1070
1472
  const discovery = discoverSnapshotData(branch);
1071
1473
  const preRuntime = (sameOwner && branchStartsWithoutRuntime) || (discovery.candidates.length === 0 && discovery.errors.length === 0);
1072
- let checkpoint: RetainedPiCheckpoint | undefined;
1474
+ let checkpoint: ReturnType<TemporalRuntime["retainedSelectedCheckpoint"]> | undefined;
1073
1475
  try {
1074
- checkpoint = !modePersistenceError && current.meta.validation?.attempt !== 0
1075
- ? preRuntime ? preRuntimeCheckpoint("off") : sameOwner && selected?.view ? selected.retainedCheckpoint(deactivateEpisode(current, "off")) : undefined
1476
+ checkpoint = !scopes && !modePersistenceError && current.meta.validation?.attempt !== 0
1477
+ ? sameOwner && selected?.selectedView() ? selected.retainedSelectedCheckpoint("off") : undefined
1076
1478
  : undefined;
1077
1479
  } catch {
1078
1480
  // An unusable cached bookmark cannot prevent native Off policy or authorize replacement memory.
@@ -1081,6 +1483,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1081
1483
  branchRestoration.cancel();
1082
1484
  inactivePersistence.cancel();
1083
1485
  backup.cancel();
1486
+ requestPolicy.invalidate();
1084
1487
  memoryToolLifetime.renew();
1085
1488
  clearRunTransient();
1086
1489
  contextProjection.reset();
@@ -1094,13 +1497,15 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1094
1497
  forkInitialization = pendingFork;
1095
1498
  deferredBranch = { owner: address.key, cwd: ctx.cwd, reason: pendingFork ? "fork" : undefined };
1096
1499
  activeContext = ctx;
1500
+ if (scopes) requestPolicy.select({ cwd: ctx.cwd, owner, mode: "off", scopes });
1097
1501
  installScopeStates();
1098
1502
  syncStateFlowTools();
1099
1503
  updateUi(ctx);
1100
1504
  try {
1101
- if (!alreadyOff || (pendingFork && !forkRecorded)) {
1102
- if (!checkpoint || !preRuntime || pendingFork || preserveContext || from !== undefined || modePersistenceError) pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, {
1505
+ if (scopes || !alreadyOff || (pendingFork && !forkRecorded)) {
1506
+ if (!checkpoint || !preRuntime || pendingFork || preserveContext || from !== undefined || modePersistenceError) appendNativeMarker({
1103
1507
  owner, at, mode: "off", memoryDeferred: true,
1508
+ ...(scopes === undefined ? {} : { scopes }),
1104
1509
  ...(from === undefined ? {} : { from }),
1105
1510
  ...(preserveContext ? { preserveContext: true } : {}),
1106
1511
  ...(pendingFork ? { forkPending: true } : {}),
@@ -1128,6 +1533,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1128
1533
  let selected = runtime;
1129
1534
  const owner = ctx.sessionManager.getSessionId();
1130
1535
  const current = snapshot;
1536
+ const wanted = captureScopeSelection(nativeScopeSelection(ctx));
1131
1537
  const wasActive = current.config.mode === "active";
1132
1538
  let unfinished = current.meta.specification !== undefined
1133
1539
  || (inferencePreparation.current?.prompt !== undefined && !inferencePreparation.current.accepted);
@@ -1161,7 +1567,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1161
1567
  if (modePersistenceError) {
1162
1568
  // Writes stay paused; the native marker records only the newly selected inactive policy.
1163
1569
  pending.published = true;
1164
- pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, {
1570
+ appendNativeMarker({
1165
1571
  at: passiveContinuation?.startedAt ?? stoppedAt,
1166
1572
  ...(passiveContinuation?.activeRunStartedAt === undefined ? {} : { from: passiveContinuation.activeRunStartedAt }),
1167
1573
  preserveContext: true, owner, persistenceError: modePersistenceError, mode,
@@ -1176,12 +1582,13 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1176
1582
  const superseded = (): StateFlowTelegramControlResult => ({ ok: false, message: "State Flow mode change was superseded" });
1177
1583
  const freezeHandoff = (): PassiveContinuation | undefined => {
1178
1584
  contextProjection.reset();
1179
- const handoff = (wasActive || modePersistenceError) && selected?.view && current.meta.validation?.attempt !== 0
1585
+ const handoff = (wasActive || modePersistenceError) && readableView(selected) && current.meta.validation?.attempt !== 0
1180
1586
  ? createPassiveContinuation(
1181
1587
  projectModelState(effectiveState),
1182
1588
  incomingBoundary?.startedAt ?? stoppedAt,
1183
1589
  incomingBoundary ? incomingBoundary.activeRunStartedAt : !idle || unfinished ? anchor : undefined,
1184
1590
  modePersistenceError !== undefined || (incomingBoundary ? incomingBoundary.preserveContext : current.meta.bootstrap === true || (unfinished && anchor === undefined)),
1591
+ wanted,
1185
1592
  )
1186
1593
  : undefined;
1187
1594
  // Repeated Passive choices keep the already selected continuation boundary.
@@ -1192,7 +1599,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1192
1599
  pending.published = true;
1193
1600
  const exitHandoff = freezeHandoff();
1194
1601
  const selectedMode = snapshot.config.mode;
1195
- if (exitHandoff || modePersistenceError) pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, {
1602
+ if (exitHandoff || modePersistenceError) appendNativeMarker({
1196
1603
  at: exitHandoff?.startedAt ?? stoppedAt,
1197
1604
  ...(exitHandoff?.activeRunStartedAt === undefined ? {} : { from: exitHandoff.activeRunStartedAt }),
1198
1605
  ...(exitHandoff?.preserveContext || modePersistenceError ? { preserveContext: true } : {}),
@@ -1214,31 +1621,28 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1214
1621
  bootstrapContinuation = undefined;
1215
1622
  acquisition.clearInvalidations();
1216
1623
  assertPublicationAvailable();
1217
- const signal = ctx.signal ? AbortSignal.any([pending.controller.signal, ctx.signal]) : pending.controller.signal;
1218
- if (branchStartsWithoutRuntime || !selected?.view) {
1624
+ const signal = modeOperationSignal(ctx, pending.controller.signal);
1625
+ if (branchStartsWithoutRuntime || !selected?.selectedView()) {
1219
1626
  // A pre-runtime choice is native-only; Passive then loads shared memory without initializing storage.
1220
1627
  const result = complete();
1221
- if (snapshot.config.mode !== "passive" || runtime?.view) return result;
1628
+ if (snapshot.config.mode !== "passive" || readableView()) return result;
1222
1629
  const passive = await loadPassiveView(ctx, signal, isCurrent, true);
1223
- if (passive && isCurrent() && !signal.aborted && !runtime?.view) {
1630
+ if (passive && isCurrent() && !signal.aborted && !readableView()) {
1224
1631
  runtime = selected = passive;
1225
1632
  installScopeStates();
1226
1633
  updateUi(ctx);
1227
1634
  }
1228
1635
  return isCurrent() ? result : superseded();
1229
1636
  }
1230
- const result = await selected.withLifecycleTransaction((publish) => {
1637
+ await selected.persistSelectedPassive(() => {
1231
1638
  signal.throwIfAborted();
1232
1639
  if (!isCurrent()) throw new Error("State Flow mode selection changed while awaiting publication");
1233
1640
  assertPublicationAvailable();
1234
- const next = structuredClone(snapshot);
1235
- publish(next);
1236
- accepted = true;
1237
- snapshot = next;
1238
- installScopeStates();
1239
- recordPublication();
1240
- return complete();
1241
1641
  }, signal);
1642
+ accepted = true;
1643
+ installScopeStates();
1644
+ recordPublication();
1645
+ const result = complete();
1242
1646
  return isCurrent() ? result : superseded();
1243
1647
  } catch (error) {
1244
1648
  if (!isCurrent()) return superseded();
@@ -1260,6 +1664,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1260
1664
  port: {
1261
1665
  snapshot: () => ({
1262
1666
  mode: snapshot.config.mode,
1667
+ scopes: captureScopeSelection(nativeScopeSelection()),
1263
1668
  inferenceBlocked: isActiveRestorationBlocked(snapshot),
1264
1669
  step: snapshot.meta.step,
1265
1670
  revisions: scopeRevisions(),
@@ -1292,12 +1697,31 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1292
1697
  });
1293
1698
  void telegram.ensure();
1294
1699
 
1295
- pi.on("before_agent_start", (event) => {
1700
+ function invalidateModuleModelContext(ctx: ExtensionContext): void {
1701
+ if (!shuttingDown && snapshot.config.mode !== "off" && currentNativeContextOwner(ctx) && moduleHost.captureIfBound()?.modules.length) requestPolicy.invalidate();
1702
+ }
1703
+
1704
+ pi.on("model_select", (event, ctx) => { if (event.previousModel !== event.model) invalidateModuleModelContext(ctx); });
1705
+ pi.on("thinking_level_select", (event, ctx) => { if (event.previousLevel !== event.level) invalidateModuleModelContext(ctx); });
1706
+
1707
+ // turn_start/context/message_end are transport-independent; provider HTTP hooks are optional.
1708
+ pi.on("turn_start", (_event, ctx) => {
1709
+ if (shuttingDown) return;
1710
+ requestPolicy.select({ cwd: ctx.cwd, owner: ctx.sessionManager.getSessionId(), mode: snapshot.config.mode, scopes: nativeScopeSelection(ctx) });
1711
+ const catalog = snapshot.config.mode === "off" ? undefined : moduleHost.captureIfBound();
1712
+ const sameSession = sessionIdentity(ctx);
1713
+ nativeRequests.begin(snapshot.config.mode === "off" ? undefined : requestPolicy.capture(), ctx.signal,
1714
+ () => { catalog?.assertCurrent(); return sameSession(); },
1715
+ catalog ? AbortSignal.any([requestPolicy.signal, catalog.signal]) : requestPolicy.signal);
1716
+ });
1717
+
1718
+ pi.on("before_agent_start", (event, ctx) => {
1719
+ if (shuttingDown || !currentNativeContextOwner(ctx)) return;
1296
1720
  // Native run identity is independent of semantic enablement and survives mode toggles.
1297
1721
  runAnchorTimestamp = undefined;
1298
1722
  if (!isActive()) {
1299
1723
  if (!passiveMemoryAvailable()) return;
1300
- (event.systemPromptOptions.sections ??= {}).state_flow = PASSIVE_MEMORY_PROTOCOL;
1724
+ (event.systemPromptOptions.sections ??= {}).state_flow = passiveMemoryProtocol(nativeScopeSelection(ctx));
1301
1725
  return;
1302
1726
  }
1303
1727
  contextProjection.reset();
@@ -1307,20 +1731,58 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1307
1731
  completedRunAccepted = false;
1308
1732
  // This native hook has no operation signal. Capture only; the first active context owns acceptance.
1309
1733
  deferInferencePreparation(event.prompt);
1310
- (event.systemPromptOptions.sections ??= {}).state_flow = stateFlowProtocol(snapshot.meta.bootstrap === true);
1734
+ (event.systemPromptOptions.sections ??= {}).state_flow = stateFlowProtocol(snapshot.meta.bootstrap === true, nativeScopeSelection(ctx));
1311
1735
  });
1312
1736
 
1313
- pi.on("context_with_system", (event) => {
1737
+ /** Never abort a replacement native operation through an obsolete ambient context. */
1738
+ function abortContext(ctx: ExtensionContext, operationSignal: AbortSignal | undefined): void {
1739
+ if (operationSignal && ctx.signal === operationSignal && !operationSignal.aborted
1740
+ && (!nativeRequests.active || nativeRequests.ownsOperation(operationSignal))) {
1741
+ if (nativeRequests.ownsOperation(operationSignal)) guardAbortedOperation = operationSignal;
1742
+ ctx.abort();
1743
+ }
1744
+ }
1745
+
1746
+ function contextRequest(ctx: ExtensionContext): NativeContextRequest | undefined {
1747
+ if (!ctx.signal || !nativeRequests.active) return undefined;
1748
+ if (!nativeRequests.ownsOperation(ctx.signal)) throw new Error("State Flow context operation is no longer current");
1749
+ return nativeRequests.preparing();
1750
+ }
1751
+
1752
+ function contextRequestCurrent(request: NativeContextRequest | undefined, ctx: ExtensionContext, operationSignal: AbortSignal | undefined): boolean {
1753
+ try { request?.revalidate(); return true; }
1754
+ catch { abortContext(ctx, operationSignal); return false; }
1755
+ }
1756
+
1757
+ /** The post-context membrane must not turn failed Active preparation into raw-history fallback. */
1758
+ function providerContextCurrent(ctx: ExtensionContext): boolean {
1759
+ const operationSignal = ctx.signal;
1760
+ try {
1761
+ const request = contextRequest(ctx);
1762
+ request?.revalidate();
1763
+ if (request && isActive() && inferencePreparation.current && !inferencePreparation.current.accepted) throw new Error("State Flow preparation was not accepted");
1764
+ return true;
1765
+ } catch { abortContext(ctx, operationSignal); return false; }
1766
+ }
1767
+
1768
+ pi.on("context_with_system", (event, ctx) => {
1769
+ if (shuttingDown || !currentNativeContextOwner(ctx) || !providerContextCurrent(ctx)) return;
1314
1770
  // Off removes the owned section, including one contributed before an in-flight mode change.
1315
- const protocol = isActive() ? stateFlowProtocol(snapshot.meta.bootstrap === true)
1316
- : passiveMemoryAvailable() ? PASSIVE_MEMORY_PROTOCOL : undefined;
1771
+ const protocol = isActive() ? stateFlowProtocol(snapshot.meta.bootstrap === true, nativeScopeSelection(ctx))
1772
+ : passiveMemoryAvailable() ? passiveMemoryProtocol(nativeScopeSelection(ctx)) : undefined;
1317
1773
  return { messages: projectSystemProtocol(event.messages, protocol) };
1318
1774
  });
1319
1775
 
1320
- function projectContext(messages: AgentMessage[]) {
1776
+ // Optional transport checkpoint: no payload/header inspection or transformation.
1777
+ pi.on("before_provider_request", (_event, ctx) => {
1778
+ if (shuttingDown || !currentNativeContextOwner(ctx)) return;
1779
+ providerContextCurrent(ctx);
1780
+ });
1781
+
1782
+ function projectContext(messages: AgentMessage[], modelScopes: ScopeSelection) {
1321
1783
  // Off injects no State Flow context, including a retained passive handoff.
1322
- if (snapshot.config.mode === "off") return;
1323
- if (runtime?.view) refreshArtifactHints();
1784
+ if (snapshot.config.mode === "off" || snapshot.meta.validation?.attempt === 0) return;
1785
+ if (readableView()) refreshArtifactHints();
1324
1786
  if (!isActive() && !passiveContinuation && !passiveMemoryAvailable()) return;
1325
1787
  // Idle inspection must not freeze a pre-acceptance snapshot for the live inference.
1326
1788
  const projection = isActive() && inferencePreparation.current && !inferencePreparation.current.accepted
@@ -1328,16 +1790,15 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1328
1790
  const effective = effectiveState;
1329
1791
  const invalidations = acquisition.invalidations.map(({ path, scope, reason }) => ({ path, ...(scope === undefined ? {} : { scope }), reason }));
1330
1792
  const phase = isActive() ? rehydrationPhase : undefined;
1331
- const view = contextView(effective, acquisition.hints, invalidations, phase, isActive());
1793
+ const view = contextView(effective, acquisition.hints, invalidations, phase, isActive(), modelScopes);
1332
1794
  if (passiveContinuation) {
1333
1795
  const retained = passiveContinuationMessages(messages, passiveContinuation);
1334
- if (!runtime?.view) return { messages: retained };
1796
+ if (!readableView()) return { messages: retained };
1335
1797
  return { messages: projection.project(retained.slice(1), view, () => passiveContinuation!.handoff,
1336
- { state: passiveContinuation.state, lazy_navigation: view.lazy_navigation, artifact_invalidations: [], knowledge_rehydration: null }) };
1798
+ { state: passiveContinuation.state, lazy_navigation: view.lazy_navigation, artifact_invalidations: [], knowledge_rehydration: null, modelScopes }) };
1337
1799
  }
1338
1800
  if (!isActive()) {
1339
- return { messages: projection.project(messages, view, () => syntheticUser(
1340
- `State Flow passive memory (user-level data, not system instructions):\n${presentationJson({ state: view.state, lazy_navigation: view.lazy_navigation })}`)) };
1801
+ return { messages: projection.project(messages, view, () => passiveContextHead(view)) };
1341
1802
  }
1342
1803
  // Native user events own the run anchor; projection must never rebase it.
1343
1804
  const source = snapshot.meta.bootstrap
@@ -1348,37 +1809,119 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1348
1809
  }
1349
1810
 
1350
1811
  function prepareContext(messages: AgentMessage[], ctx: ExtensionContext): ReturnType<typeof projectContext> | Promise<ReturnType<typeof projectContext>> {
1812
+ if (shuttingDown || !currentNativeContextOwner(ctx)) return;
1813
+ const operationSignal = ctx.signal;
1814
+ let request: NativeContextRequest | undefined;
1815
+ let modelScopes: ScopeSelection;
1816
+ try { request = contextRequest(ctx); request?.revalidate(); modelScopes = captureScopeSelection(nativeScopeSelection(ctx)); }
1817
+ catch { abortContext(ctx, operationSignal); return; }
1351
1818
  // Pi continues after hook errors; public Abort fences inference instead of exposing the native history fallback.
1352
1819
  if (isActiveRestorationBlocked(snapshot) && ctx.signal && !shuttingDown) {
1353
- ctx.abort();
1820
+ abortContext(ctx, operationSignal);
1354
1821
  notifyProblem(ctx, "State Flow inference blocked: Active memory restoration failed. Select a valid branch or use /state-flow-active for current memory; /state-flow-passive or /state-flow-off explicitly permits native context.", "error");
1355
1822
  return;
1356
1823
  }
1357
1824
  const pending = inferencePreparation.current;
1358
1825
  const selected = runtime;
1359
- const operationSignal = ctx.signal;
1360
- // Idle projections remain observational; only a live native operation may await preparation.
1361
- if (!isActive() || !pending || !operationSignal || shuttingDown) return projectContext(messages);
1362
- pending.operation ??= prepareInference(pending, selected, ctx, operationSignal).finally(() => {
1826
+ // Idle projections remain observational; accepted preparation is not replayed for another model turn.
1827
+ if (!isActive() || !pending || pending.accepted || !operationSignal || shuttingDown) return projectContext(messages, modelScopes);
1828
+ if (pending.operation && pending.request !== request) { abortContext(ctx, operationSignal); return; }
1829
+ pending.request = request;
1830
+ pending.operation ??= prepareInference(pending, selected, ctx, operationSignal, request, messages).finally(() => {
1363
1831
  // A late withdrawal must not clear newer work; accepted lifecycle is never replayed after an error.
1364
1832
  if (!pending.accepted && inferencePreparation.owns(pending)) pending.operation = undefined;
1365
1833
  });
1366
1834
  return pending.operation.then(() => {
1367
- if (shuttingDown || operationSignal.aborted || runtime !== selected || ctx.signal !== operationSignal) return;
1835
+ if (!currentNativeContextOwner(ctx)) return;
1836
+ if (!contextRequestCurrent(request, ctx, operationSignal)) return;
1837
+ if (shuttingDown || operationSignal.aborted || runtime !== selected || ctx.signal !== operationSignal) {
1838
+ if (request) abortContext(ctx, operationSignal);
1839
+ return;
1840
+ }
1368
1841
  if (isActive() && !inferencePreparation.owns(pending)) {
1369
1842
  // Start may require same-run maintenance. A new captured user prompt instead owns a different context request.
1370
1843
  if (inferencePreparation.current?.prompt === undefined) return prepareContext(messages, ctx);
1844
+ if (request) abortContext(ctx, operationSignal);
1371
1845
  return;
1372
1846
  }
1373
- if (isActive() && !pending.accepted) return;
1374
- return projectContext(messages);
1847
+ if (isActive() && !pending.accepted) { if (request) abortContext(ctx, operationSignal); return; }
1848
+ return projectContext(messages, modelScopes);
1375
1849
  });
1376
1850
  }
1377
1851
 
1378
- pi.on("context", (event, ctx) => prepareContext(event.messages as AgentMessage[], ctx));
1852
+ async function capturePublicModules(messages: AgentMessage[], ctx: ExtensionContext, ownedSignal?: AbortSignal): Promise<PreparedPublicModules | undefined> {
1853
+ if (snapshot.config.mode === "off" || !ctx.signal || !nativeRequests.active) return;
1854
+ const selected = runtime, view = readableView(selected), catalog = moduleHost.captureIfBound();
1855
+ if (!selected || !view || !catalog?.modules.length) return;
1856
+ const receipt = requestPolicy.capture(), sameSession = sessionIdentity(ctx), model = ctx.model, request = contextRequest(ctx);
1857
+ const operationSignal = ctx.signal, controller = new AbortController();
1858
+ const signal = AbortSignal.any([operationSignal, receipt.signal, catalog.signal, controller.signal, ...(ownedSignal ? [ownedSignal] : []), ...(request ? [request.signal] : [])]);
1859
+ const basis = selected.captureSelectedReadBasis();
1860
+ const check = () => {
1861
+ signal.throwIfAborted(); receipt.revalidate(); catalog.assertCurrent();
1862
+ if (shuttingDown || runtime !== selected || ctx.signal !== operationSignal || ctx.model !== model || !sameSession() || !currentNativeContextOwner(ctx)) throw new Error("State Flow module context owner changed");
1863
+ };
1864
+ try {
1865
+ check(); basis();
1866
+ const wanted = captureScopeSelection(receipt.policy.scopes);
1867
+ const source = wanted.includes("session") && view.selection.includes("session") ? moduleConversationSource(messages, runAnchorTimestamp) : { conversation: [] };
1868
+ const jobs = wanted.flatMap(scope => {
1869
+ if (!view.selection.includes(scope)) return [];
1870
+ const state = selected.readView(0, scope);
1871
+ return catalog.modules.flatMap(module => {
1872
+ const descriptor = view.scopes[scope]?.modules?.[module.id];
1873
+ if (descriptor && (descriptor.schemaVersion !== module.schemaVersion || descriptor.immutable !== module.immutable)) return [];
1874
+ const content = state[module.id] as JsonObject | undefined;
1875
+ const frame = { mode: receipt.policy.mode as "active" | "passive", scope, ...(scope === "session" ? source : { conversation: [] }) };
1876
+ return [prepareModuleContext(module, content, frame, signal, check).then(prepared => ({ module, scope, content, descriptor, prepared }))];
1877
+ });
1878
+ });
1879
+ const bindings = await Promise.all(jobs);
1880
+ const assertCurrent = () => { check(); for (const item of bindings) item.prepared.assertCurrent(); };
1881
+ const assertOriginal = () => { assertCurrent(); basis(); };
1882
+ assertOriginal();
1883
+ return { bindings, definitions: catalog.modules, conversation: source.conversation, assertCurrent, assertOriginal, cancel: () => controller.abort() };
1884
+ } catch { controller.abort(); throw new Error("State Flow registered module input preparation failed"); }
1885
+ }
1886
+
1887
+ async function preparePublicContext(messages: AgentMessage[], projected: ReturnType<typeof projectContext>, ctx: ExtensionContext) {
1888
+ if (!projected || snapshot.config.mode === "off" || !ctx.signal || !nativeRequests.active) return projected;
1889
+ const operationSignal = ctx.signal, sameSession = sessionIdentity(ctx), selected = runtime;
1890
+ let batch: PreparedPublicModules | undefined;
1891
+ try {
1892
+ const pending = inferencePreparation.current, request = contextRequest(ctx);
1893
+ const accepted = pending?.accepted && pending.request === request ? pending.modules : undefined;
1894
+ batch = accepted ?? await capturePublicModules(messages, ctx);
1895
+ if (!batch || !selected) return projected;
1896
+ if (!accepted) batch.assertOriginal();
1897
+ const current = batch, basis = accepted ? pending!.moduleAcceptance! : selected.captureSelectedReadBasis();
1898
+ const assertCurrent = () => { current.assertCurrent(); basis(); };
1899
+ assertCurrent();
1900
+ const contributions: ScopedModuleContext[] = current.bindings.map(({ module, scope, prepared }) => ({ id: module.id, scope,
1901
+ contributions: prepared.evaluate({ content: selected.readView(0, scope)[module.id] }) }));
1902
+ const result = projectModuleContext(projected.messages, messages, current.conversation, contributions);
1903
+ assertCurrent(); nativeRequests.bindPreparedContext(assertCurrent);
1904
+ return { messages: result };
1905
+ } catch {
1906
+ batch?.cancel(); abortContext(ctx, operationSignal);
1907
+ if (!shuttingDown && sameSession()) notifyProblem(ctx, "State Flow registered module context is unavailable; the current inference was aborted without changing module memory", "error");
1908
+ return undefined;
1909
+ }
1910
+ }
1911
+
1912
+ pi.on("context", (event, ctx) => {
1913
+ if (shuttingDown || !currentNativeContextOwner(ctx)) return;
1914
+ const messages = event.messages as AgentMessage[];
1915
+ const projected = prepareContext(messages, ctx);
1916
+ if (snapshot.config.mode === "off" || !ctx.signal || !nativeRequests.active || !moduleHost.captureIfBound()?.modules.length) return projected;
1917
+ return Promise.resolve(projected).then(result => preparePublicContext(messages, result, ctx));
1918
+ });
1379
1919
 
1380
- pi.on("tool_execution_start", (event) => {
1381
- if (!isActive()) return;
1920
+ pi.on("tool_execution_start", (event, ctx) => {
1921
+ if (event.parentToolCallId) {
1922
+ try { nativeRequests.nested(event.toolCallId, event.toolName, event.parentToolCallId); } catch { return; }
1923
+ }
1924
+ if (!isActive() || !currentNativeTool(event.toolCallId, event.toolName, ctx)) return;
1382
1925
  // Pi emits this before tool_call. Keep the argument object as a fallback;
1383
1926
  // tool_call replaces it with the mutable, post-preflight input reference.
1384
1927
  skillReads.recordStart(event.toolCallId, event.toolName, event.args);
@@ -1386,7 +1929,17 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1386
1929
  });
1387
1930
 
1388
1931
  pi.on("tool_call", (event, ctx) => {
1389
- if (!isActive()) return;
1932
+ const owned = event.toolName === PATCH_STATE_TOOL_NAME || event.toolName === READ_STATE_TOOL_NAME;
1933
+ if (event.parentToolCallId) {
1934
+ if (event.toolName === PATCH_STATE_TOOL_NAME && isActive()) return { block: true, reason: "patch_state requires a direct native assistant barrier, not a nested call" };
1935
+ try { nativeRequests.nested(event.toolCallId, event.toolName, event.parentToolCallId); }
1936
+ catch { if (owned) return { block: true, reason: "State Flow tool batch has no current native request" }; }
1937
+ }
1938
+ if (owned) {
1939
+ try { nativeRequests.check(event.toolCallId, event.toolName); }
1940
+ catch { return { block: true, reason: "State Flow tool batch has no current native request" }; }
1941
+ }
1942
+ if (!isActive() || !currentNativeTool(event.toolCallId, event.toolName, ctx)) return;
1390
1943
  const batch = findAssistantToolBatch(ctx.sessionManager, event.toolCallId);
1391
1944
  const patchCalls = batch?.filter((name) => name === PATCH_STATE_TOOL_NAME).length ?? 0;
1392
1945
  if (patchCalls > 0) {
@@ -1405,28 +1958,50 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1405
1958
  artifactReads.recordCall(event.toolCallId, event.toolName, event.input);
1406
1959
  });
1407
1960
 
1408
- pi.on("tool_execution_end", (event) => {
1409
- if (!isActive()) return;
1961
+ pi.on("tool_execution_end", (event, ctx) => {
1962
+ if (!isActive() || !currentNativeTool(event.toolCallId, event.toolName, ctx)) return;
1410
1963
  skillReads.recordEnd(event.toolCallId, event.toolName, event.isError);
1411
1964
  dropCurrentSkillReads();
1412
1965
  artifactReads.recordEnd(event.toolCallId, event.toolName, event.isError);
1413
1966
  });
1414
1967
 
1415
- pi.on("tool_result", (event) => {
1416
- if (!isActive()) return;
1968
+ pi.on("tool_result", (event, ctx) => {
1969
+ if (!isActive() || !currentNativeTool(event.toolCallId, event.toolName, ctx)) return;
1417
1970
  const read = skillReads.recordResult(event.toolName, event.input, event.isError);
1418
1971
  if (!read) return;
1419
1972
  dropCurrentSkillReads();
1420
- const hint = skillAcquisitionHint(read);
1973
+ const hint = skillAcquisitionHint(read, nativeScopeSelection(ctx));
1421
1974
  if (!hint) return;
1422
1975
  return { content: [...event.content, { type: "text", text: `\n${hint}` }] };
1423
1976
  });
1424
1977
 
1978
+ /** Native contributions and completions may not borrow another physical/native owner. */
1979
+ function currentNativeContextOwner(ctx: ExtensionContext): boolean {
1980
+ try {
1981
+ const policy = requestPolicy.policy, selected = runtime;
1982
+ return policy !== undefined && activeContext !== undefined && policy.owner === ctx.sessionManager.getSessionId()
1983
+ && policy.cwd === ctx.cwd && sessionAddress(activeContext).key === sessionAddress(ctx).key
1984
+ && (selected === undefined || selected.sessionId === policy.owner && selected.cwd === ctx.cwd && selected.sessionKey === sessionAddress(ctx).key)
1985
+ && (!ctx.signal || !nativeRequests.active || nativeRequests.ownsOperation(ctx.signal));
1986
+ } catch { return false; }
1987
+ }
1988
+
1425
1989
  pi.on("message_end", (event, ctx): any => {
1990
+ if (shuttingDown || !currentNativeContextOwner(ctx)) return;
1991
+ if (event.message.role === "assistant") nativeRequests.finalize((Array.isArray(event.message.content) ? event.message.content : [])
1992
+ .filter(block => block.type === "toolCall").map(block => ({ id: block.id, name: block.name })));
1426
1993
  // Observe actual user events even while disabled; Start/Stop cannot invent or erase them.
1427
1994
  if (event.message.role === "user" && runAnchorTimestamp === undefined) runAnchorTimestamp = event.message.timestamp;
1428
1995
  if (shuttingDown || !isActive()) return;
1429
1996
  if (event.message.role !== "assistant") return;
1997
+ let request: SelectedPolicyRequest;
1998
+ try { request = ctx.signal || nativeRequests.active ? nativeRequests.current() : requestPolicy.capture(); } catch { return; }
1999
+ const selected = runtime, sameSession = sessionIdentity(ctx), operationSignal = ctx.signal;
2000
+ const revalidate = () => {
2001
+ request.revalidate();
2002
+ if (runtime !== selected || ctx.signal !== operationSignal || !sameSession() || !currentNativeContextOwner(ctx)) throw new Error("State Flow response selection owner changed");
2003
+ };
2004
+ try { revalidate(); } catch { return; }
1430
2005
  responseReconciliation.cancel();
1431
2006
  const message = event.message as unknown as { role: "assistant"; stopReason?: string; content?: unknown };
1432
2007
  if (message.stopReason === "aborted" || assistantToolCallCount(message.content) > 0 || message.stopReason === "toolUse") return;
@@ -1434,34 +2009,42 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1434
2009
  recordDiagnostic(`Assistant response ended with ${message.stopReason}`, "finalization", ctx, { content: message.content });
1435
2010
  return;
1436
2011
  }
1437
- responseReconciliation.claim({ controller: new AbortController() });
2012
+ responseReconciliation.claim({ controller: new AbortController(), request, operationSignal, revalidate });
1438
2013
  });
1439
2014
 
1440
2015
  pi.on("turn_end", async (event, ctx) => {
1441
- if (shuttingDown) return;
2016
+ if (shuttingDown || !currentNativeContextOwner(ctx)) return;
1442
2017
  const pending = responseReconciliation.current;
2018
+ if (pending && pending.operationSignal !== ctx.signal) return;
2019
+ nativeRequests.end();
1443
2020
  if (!isActive() || !pending) {
1444
2021
  updateUi(ctx);
1445
2022
  return;
1446
2023
  }
1447
- const selected = runtime;
1448
- const signal = ctx.signal ? AbortSignal.any([pending.controller.signal, ctx.signal]) : pending.controller.signal;
2024
+ const selected = runtime, sameSession = sessionIdentity(ctx);
2025
+ const check = () => {
2026
+ pending.revalidate();
2027
+ if (ctx.signal !== pending.operationSignal || !sameSession() || !currentNativeContextOwner(ctx)) throw new Error("State Flow response completion owner changed");
2028
+ };
2029
+ const signal = AbortSignal.any([pending.controller.signal, ...(ctx.signal ? [ctx.signal] : []), pending.request.signal]);
1449
2030
  let responseCommitted = false;
1450
2031
  try {
2032
+ check();
1451
2033
  const response = finalizedAssistantResponse(event.message);
1452
- if (!selected?.view) throw new Error("Temporal State Flow runtime is unavailable; reload before publishing");
1453
- await selected.withPatchTransaction((transaction) => {
2034
+ if (!selected?.selectedView()) throw new Error("Temporal StateFlow runtime is unavailable; reload before publishing");
2035
+ await selected.withSelectedPatchTransaction((transaction) => {
1454
2036
  signal.throwIfAborted();
2037
+ check();
1455
2038
  if (runtime !== selected || !responseReconciliation.owns(pending) || !isActive()) {
1456
2039
  throw new Error("State Flow response selection changed while awaiting publication");
1457
2040
  }
1458
2041
  assertPublicationAvailable();
1459
2042
  const wasBootstrap = snapshot.meta.bootstrap === true;
1460
2043
  const nextSnapshot = structuredClone(snapshot);
1461
- const stage = stageScopedTransition(transaction.states, { transitions: [], response }, [], transaction.causalBasis);
2044
+ const stage = stageSelectedScopedTransition(transaction, { transitions: [], response }, [], transaction.causalBasis);
1462
2045
  completeRun(nextSnapshot);
1463
- let publication: RuntimePublication | undefined;
1464
- commitScopedTransition(nextSnapshot, transaction.states, stage, (accepted, next) => {
2046
+ let publication: SelectedFilePublication | undefined;
2047
+ commitSelectedTransition(nextSnapshot, transaction, stage, (accepted, next) => {
1465
2048
  publication = transaction.publish(next, accepted);
1466
2049
  }, transaction.causalBasis, { finalizeRun: true });
1467
2050
  snapshot = nextSnapshot;
@@ -1476,10 +2059,14 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1476
2059
  completedRunAccepted = !wasBootstrap;
1477
2060
  backup.markTurnAccepted();
1478
2061
  updateUi(ctx);
2062
+ }, () => {
2063
+ check();
2064
+ if (runtime !== selected || !responseReconciliation.owns(pending) || !isActive()) throw new Error("State Flow response superseded");
1479
2065
  }, signal);
1480
2066
  } catch (error) {
1481
2067
  // Superseded completion owns neither the new lifecycle nor its diagnostics/UI.
1482
2068
  if (signal.aborted || runtime !== selected || !responseReconciliation.owns(pending)) return;
2069
+ try { check(); } catch { return; }
1483
2070
  const cause = diagnosticText(error);
1484
2071
  recordDiagnostic(cause, "finalization", ctx);
1485
2072
  notifyProblem(ctx, responseCommitted
@@ -1536,7 +2123,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1536
2123
  return;
1537
2124
  }
1538
2125
  if (!completedRunAccepted || compactionRequests.stopped || !isActive() || snapshot.meta.bootstrap
1539
- || compactionRequests.inFlight || !ctx.isIdle() || ctx.hasPendingMessages() || !runtime?.view
2126
+ || compactionRequests.inFlight || !ctx.isIdle() || ctx.hasPendingMessages() || !runtime?.selectedView()
1540
2127
  || !shouldRequestStateFlowCompaction(ctx.getContextUsage())) return;
1541
2128
  completedRunAccepted = false;
1542
2129
  const entries = ctx.sessionManager.buildContextEntries();
@@ -1557,6 +2144,16 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1557
2144
  pi.on("session_compact", () => { contextProjection.reset(); });
1558
2145
 
1559
2146
  pi.on("session_start", async (event, ctx) => {
2147
+ const sameSession = sessionIdentity(ctx);
2148
+ moduleHost.bind(ctx.sessionManager, () => { if (!sameSession()) throw new Error("State Flow module host native owner changed"); });
2149
+ const refreshModules = (force = false) => {
2150
+ if (shuttingDown) return;
2151
+ const catalog = moduleHost.captureIfBound();
2152
+ if (!catalog || !sameSession()) return;
2153
+ syncStateFlowTools(force || catalog.modules.length > 0);
2154
+ catalog.signal.addEventListener("abort", () => refreshModules(true), { once: true });
2155
+ };
2156
+ refreshModules();
1560
2157
  runAnchorTimestamp = undefined;
1561
2158
  rehydrationPhase = event.reason === "resume" ? "resume-bootstrap" : "new-bootstrap";
1562
2159
  await restoreActiveBranch(ctx, event.reason);
@@ -1568,6 +2165,9 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1568
2165
  });
1569
2166
  pi.on("session_shutdown", async (_event, _ctx) => {
1570
2167
  shuttingDown = true;
2168
+ moduleHost.dispose();
2169
+ requestPolicy.end();
2170
+ nativeRequests.end();
1571
2171
  memoryToolLifetime.end();
1572
2172
  contextProjection.reset();
1573
2173
  const restoring = branchRestoration.cancel();