@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.
- package/CHANGELOG.md +5 -0
- package/README.md +2 -2
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +7 -3
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +679 -2
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +17 -0
- package/node_modules/@llblab/pi-state-flow/README.md +30 -14
- package/node_modules/@llblab/pi-state-flow/api/modules.ts +11 -0
- package/node_modules/@llblab/pi-state-flow/dist/api/modules.d.ts +3 -0
- package/node_modules/@llblab/pi-state-flow/dist/api/modules.js +3 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.d.ts +7 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.js +77 -36
- package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.d.ts +2 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.js +26 -27
- package/node_modules/@llblab/pi-state-flow/dist/lib/config.d.ts +3 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/config.js +13 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +27 -21
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +144 -46
- package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +14 -18
- package/node_modules/@llblab/pi-state-flow/dist/lib/durable.d.ts +6 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +60 -50
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +1049 -310
- package/node_modules/@llblab/pi-state-flow/dist/lib/history.d.ts +4 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/history.js +15 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/json.d.ts +2 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/json.js +2 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/memory.d.ts +3 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/memory.js +7 -7
- package/node_modules/@llblab/pi-state-flow/dist/lib/migration.d.ts +29 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/migration.js +186 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/model-interface.d.ts +17 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/model-interface.js +158 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/module-registry.d.ts +21 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/module-registry.js +108 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/operation.js +3 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/ownership.d.ts +4 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/ownership.js +5 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/planes.d.ts +29 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/planes.js +84 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.d.ts +6 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +33 -16
- package/node_modules/@llblab/pi-state-flow/dist/lib/query.d.ts +21 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +53 -13
- package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.js +5 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.d.ts +3 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.js +10 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +84 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +570 -20
- package/node_modules/@llblab/pi-state-flow/dist/lib/selection.d.ts +10 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/selection.js +32 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/semantic-module.d.ts +79 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/semantic-module.js +187 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/session.d.ts +98 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/session.js +398 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/skills.d.ts +7 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/skills.js +55 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +67 -14
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +129 -9
- package/node_modules/@llblab/pi-state-flow/dist/lib/state.d.ts +26 -14
- package/node_modules/@llblab/pi-state-flow/dist/lib/state.js +61 -19
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +14 -9
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +59 -22
- package/node_modules/@llblab/pi-state-flow/dist/lib/storage.d.ts +111 -10
- package/node_modules/@llblab/pi-state-flow/dist/lib/storage.js +728 -11
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +10 -7
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +50 -14
- package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.d.ts +40 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.js +247 -43
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.d.ts +41 -8
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +132 -39
- package/node_modules/@llblab/pi-state-flow/dist/package.json +6 -1
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +28 -16
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +12 -8
- package/node_modules/@llblab/pi-state-flow/docs/README.md +9 -0
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +76 -43
- package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +9 -1
- package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +6 -0
- package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +23 -19
- package/node_modules/@llblab/pi-state-flow/docs/protocol-composition-decision.md +66 -0
- package/node_modules/@llblab/pi-state-flow/docs/public-module-contract.md +73 -0
- package/node_modules/@llblab/pi-state-flow/docs/scope-selection-contract.md +814 -0
- package/node_modules/@llblab/pi-state-flow/docs/semantic-compatibility-decision.md +212 -0
- package/node_modules/@llblab/pi-state-flow/docs/semantic-extensibility-decision.md +75 -0
- package/node_modules/@llblab/pi-state-flow/docs/semantic-handoff-contract.md +174 -0
- package/node_modules/@llblab/pi-state-flow/docs/semantic-memory-evaluation.md +166 -0
- package/node_modules/@llblab/pi-state-flow/docs/semantic-module-contract.md +84 -0
- package/node_modules/@llblab/pi-state-flow/docs/single-scope-interface-contract.md +201 -0
- package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +22 -1
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +32 -18
- package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +68 -31
- package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +21 -22
- package/node_modules/@llblab/pi-state-flow/lib/config.ts +14 -1
- package/node_modules/@llblab/pi-state-flow/lib/context.ts +142 -46
- package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +14 -17
- package/node_modules/@llblab/pi-state-flow/lib/durable.ts +53 -44
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +897 -297
- package/node_modules/@llblab/pi-state-flow/lib/history.ts +16 -7
- package/node_modules/@llblab/pi-state-flow/lib/json.ts +2 -1
- package/node_modules/@llblab/pi-state-flow/lib/memory.ts +10 -9
- package/node_modules/@llblab/pi-state-flow/lib/migration.ts +189 -0
- package/node_modules/@llblab/pi-state-flow/lib/model-interface.ts +136 -0
- package/node_modules/@llblab/pi-state-flow/lib/module-registry.ts +119 -0
- package/node_modules/@llblab/pi-state-flow/lib/operation.ts +3 -2
- package/node_modules/@llblab/pi-state-flow/lib/ownership.ts +6 -6
- package/node_modules/@llblab/pi-state-flow/lib/planes.ts +98 -0
- package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +37 -16
- package/node_modules/@llblab/pi-state-flow/lib/query.ts +59 -18
- package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +5 -2
- package/node_modules/@llblab/pi-state-flow/lib/rehydration.ts +10 -1
- package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +574 -21
- package/node_modules/@llblab/pi-state-flow/lib/selection.ts +39 -0
- package/node_modules/@llblab/pi-state-flow/lib/semantic-module.ts +239 -0
- package/node_modules/@llblab/pi-state-flow/lib/session.ts +387 -3
- package/node_modules/@llblab/pi-state-flow/lib/skills.ts +45 -4
- package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +156 -16
- package/node_modules/@llblab/pi-state-flow/lib/state.ts +69 -28
- package/node_modules/@llblab/pi-state-flow/lib/status.ts +65 -31
- package/node_modules/@llblab/pi-state-flow/lib/storage.ts +770 -18
- package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +58 -20
- package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +267 -50
- package/node_modules/@llblab/pi-state-flow/lib/transition.ts +184 -52
- package/node_modules/@llblab/pi-state-flow/package.json +6 -1
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +28 -16
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +12 -8
- 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,
|
|
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 {
|
|
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 {
|
|
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
|
|
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 {
|
|
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 = {
|
|
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
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
133
|
-
|
|
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
|
-
|
|
142
|
-
|
|
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
|
-
|
|
168
|
-
|
|
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 =
|
|
174
|
-
effectiveState =
|
|
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" &&
|
|
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 =
|
|
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 =
|
|
286
|
+
const selected = createRuntime(ctx);
|
|
196
287
|
const signal = sharedInspectionLifetime.signal;
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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(
|
|
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 &&
|
|
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 =
|
|
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
|
-
|
|
370
|
+
check();
|
|
371
|
+
if (!selected?.selectedView())
|
|
256
372
|
throw new Error("Temporal State Flow runtime is unavailable; reload before publishing");
|
|
257
|
-
await
|
|
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 =
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
},
|
|
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
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
-
|
|
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 =
|
|
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 (
|
|
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(
|
|
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
|
-
//
|
|
459
|
-
appendCheckpoint();
|
|
626
|
+
// The native policy was captured before memory acquisition.
|
|
460
627
|
}
|
|
461
628
|
}
|
|
462
629
|
else {
|
|
463
|
-
snapshot = emptySnapshot(
|
|
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),
|
|
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
|
-
//
|
|
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" ?
|
|
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
|
|
663
|
+
const wanted = nativeScopeSelection(ctx);
|
|
491
664
|
const sameSession = sessionIdentity(ctx);
|
|
492
|
-
const 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
|
-
//
|
|
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.
|
|
707
|
+
const current = await candidate.refreshSelectedCurrent(wanted, assertCurrent, signal);
|
|
535
708
|
assertCurrent();
|
|
536
|
-
|
|
537
|
-
|
|
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
|
-
//
|
|
544
|
-
|
|
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
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
restored.meta
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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
|
-
|
|
565
|
-
|
|
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
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
})
|
|
575
|
-
|
|
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
|
-
|
|
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
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
const
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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() ||
|
|
862
|
+
if (snapshot.config.mode === "off" || isActive() || readableView() || snapshot.meta.validation?.attempt === 0 || !branchStartsWithoutRuntime)
|
|
610
863
|
return;
|
|
611
|
-
//
|
|
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 (
|
|
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.
|
|
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) || !
|
|
907
|
+
if (!retainsPhysicalSessionProjection(reason) || !readableView())
|
|
649
908
|
return undefined;
|
|
650
|
-
const boundary =
|
|
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.
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
710
|
-
details: { path:
|
|
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(
|
|
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(
|
|
716
|
-
details: { ...(params.path === undefined ? { paths } : { path:
|
|
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
|
-
|
|
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.
|
|
729
|
-
promptSnippet: "Atomically patch
|
|
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:
|
|
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 (!
|
|
761
|
-
throw new Error("
|
|
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
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
800
|
-
const changed =
|
|
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
|
-
|
|
805
|
-
|
|
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
|
-
}
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
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
|
-
|
|
831
|
-
|
|
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
|
-
|
|
834
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
-
?
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
999
|
-
|
|
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
|
|
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"
|
|
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
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
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
|
-
?
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
1232
|
-
if (branchStartsWithoutRuntime || !selected?.
|
|
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" ||
|
|
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 && !
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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() ?
|
|
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
|
-
|
|
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 (
|
|
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 (!
|
|
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, () =>
|
|
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
|
|
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
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
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 (
|
|
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
|
-
|
|
1948
|
+
}
|
|
1949
|
+
return projectContext(messages, modelScopes);
|
|
1398
1950
|
});
|
|
1399
1951
|
}
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
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
|
-
|
|
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
|
|
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?.
|
|
1480
|
-
throw new Error("Temporal
|
|
1481
|
-
await selected.
|
|
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 =
|
|
2202
|
+
const stage = stageSelectedScopedTransition(transaction, { transitions: [], response }, [], transaction.causalBasis);
|
|
1490
2203
|
completeRun(nextSnapshot);
|
|
1491
2204
|
let publication;
|
|
1492
|
-
|
|
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?.
|
|
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();
|