@llblab/pi-kit 0.37.1 → 0.38.0

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