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