@llblab/pi-kit 0.24.1 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/BACKLOG.md +5 -1
- package/CHANGELOG.md +12 -0
- package/README.md +11 -8
- package/node_modules/@llblab/pi-actors/AGENTS.md +2 -0
- package/node_modules/@llblab/pi-actors/CHANGELOG.md +4 -1
- package/node_modules/@llblab/pi-actors/LICENSE +21 -0
- package/node_modules/@llblab/pi-actors/README.md +1 -1
- package/node_modules/@llblab/pi-actors/docs/coordinator-delivery.md +1 -1
- package/node_modules/@llblab/pi-actors/package.json +4 -3
- package/node_modules/@llblab/pi-claude-usage/AGENTS.md +23 -0
- package/node_modules/@llblab/pi-claude-usage/BACKLOG.md +4 -0
- package/node_modules/@llblab/pi-claude-usage/CHANGELOG.md +21 -0
- package/node_modules/@llblab/pi-claude-usage/LICENSE +22 -0
- package/node_modules/@llblab/pi-claude-usage/README.md +155 -0
- package/node_modules/@llblab/pi-claude-usage/banner.jpg +0 -0
- package/node_modules/@llblab/pi-claude-usage/index.ts +8 -0
- package/node_modules/@llblab/pi-claude-usage/lib/extension.ts +30 -0
- package/node_modules/@llblab/pi-claude-usage/lib/fast.ts +24 -0
- package/node_modules/@llblab/pi-claude-usage/lib/query.ts +146 -0
- package/node_modules/@llblab/pi-claude-usage/lib/status-format.ts +297 -0
- package/node_modules/@llblab/pi-claude-usage/lib/status.ts +366 -0
- package/node_modules/@llblab/pi-claude-usage/lib/telegram.ts +44 -0
- package/node_modules/@llblab/pi-claude-usage/lib/usage-store.ts +221 -0
- package/node_modules/@llblab/pi-claude-usage/lib/usage.ts +128 -0
- package/node_modules/@llblab/pi-claude-usage/package.json +64 -0
- package/node_modules/@llblab/pi-clean-room/AGENTS.md +1 -0
- package/node_modules/@llblab/pi-clean-room/CHANGELOG.md +5 -0
- package/node_modules/@llblab/pi-clean-room/LICENSE +21 -0
- package/node_modules/@llblab/pi-clean-room/README.md +1 -1
- package/node_modules/@llblab/pi-clean-room/package.json +3 -2
- package/node_modules/@llblab/pi-codex-usage/AGENTS.md +9 -6
- package/node_modules/@llblab/pi-codex-usage/BACKLOG.md +2 -1
- package/node_modules/@llblab/pi-codex-usage/CHANGELOG.md +17 -0
- package/node_modules/@llblab/pi-codex-usage/README.md +75 -17
- package/node_modules/@llblab/pi-codex-usage/index.ts +8 -1602
- package/node_modules/@llblab/pi-codex-usage/lib/extension.ts +25 -0
- package/node_modules/@llblab/pi-codex-usage/lib/fast.ts +23 -0
- package/node_modules/@llblab/pi-codex-usage/lib/query.ts +368 -0
- package/node_modules/@llblab/pi-codex-usage/lib/status-format.ts +347 -0
- package/node_modules/@llblab/pi-codex-usage/lib/status.ts +435 -0
- package/node_modules/@llblab/pi-codex-usage/lib/telegram.ts +45 -0
- package/node_modules/@llblab/pi-codex-usage/lib/usage-store.ts +229 -0
- package/node_modules/@llblab/pi-codex-usage/lib/usage.ts +425 -0
- package/node_modules/@llblab/pi-codex-usage/package.json +11 -6
- package/node_modules/@llblab/pi-command-fast/AGENTS.md +7 -0
- package/node_modules/@llblab/pi-command-fast/BACKLOG.md +9 -0
- package/node_modules/@llblab/pi-command-fast/CHANGELOG.md +7 -0
- package/node_modules/@llblab/pi-command-fast/LICENSE +21 -0
- package/node_modules/@llblab/pi-command-fast/README.md +42 -0
- package/node_modules/@llblab/pi-command-fast/dist/command.d.ts +8 -0
- package/node_modules/@llblab/pi-command-fast/dist/command.js +52 -0
- package/node_modules/@llblab/pi-command-fast/dist/index.d.ts +3 -0
- package/node_modules/@llblab/pi-command-fast/dist/index.js +3 -0
- package/node_modules/@llblab/pi-command-fast/dist/models-json.d.ts +10 -0
- package/node_modules/@llblab/pi-command-fast/dist/models-json.js +81 -0
- package/node_modules/@llblab/pi-command-fast/package.json +49 -0
- package/node_modules/@llblab/pi-grow-loop/AGENTS.md +1 -0
- package/node_modules/@llblab/pi-grow-loop/CHANGELOG.md +4 -1
- package/node_modules/@llblab/pi-grow-loop/LICENSE +21 -0
- package/node_modules/@llblab/pi-grow-loop/README.md +1 -1
- package/node_modules/@llblab/pi-grow-loop/package.json +3 -2
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +43 -56
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +17 -3
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +25 -0
- package/node_modules/@llblab/pi-state-flow/LICENSE +21 -0
- package/node_modules/@llblab/pi-state-flow/README.md +18 -15
- package/node_modules/@llblab/pi-state-flow/dist/index.d.ts +2 -2
- package/node_modules/@llblab/pi-state-flow/dist/index.js +2 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.d.ts +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.js +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/config.d.ts +7 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/config.js +16 -7
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +9 -9
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +5 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +2 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/durable.d.ts +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +7 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/episode.d.ts +3 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/episode.js +5 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.d.ts +3 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +503 -235
- package/node_modules/@llblab/pi-state-flow/dist/lib/git.d.ts +2 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +16 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/history.d.ts +11 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/history.js +6 -7
- package/node_modules/@llblab/pi-state-flow/dist/lib/logging.d.ts +4 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/logging.js +1 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/query.d.ts +4 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +13 -13
- package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.d.ts +7 -6
- package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.js +9 -9
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +3 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +17 -12
- package/node_modules/@llblab/pi-state-flow/dist/lib/session.d.ts +13 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/session.js +62 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +23 -8
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +52 -20
- package/node_modules/@llblab/pi-state-flow/dist/lib/state.d.ts +22 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/state.js +30 -10
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +5 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +19 -28
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +17 -15
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +52 -52
- package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.d.ts +8 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.js +34 -18
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.d.ts +5 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +13 -19
- package/node_modules/@llblab/pi-state-flow/dist/package.json +12 -11
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +2 -2
- package/node_modules/@llblab/pi-state-flow/docs/README.md +2 -1
- package/node_modules/@llblab/pi-state-flow/docs/agent-contract-relocation.md +72 -0
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +44 -36
- package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +14 -6
- package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +7 -5
- package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +6 -6
- package/node_modules/@llblab/pi-state-flow/docs/performance.md +1 -1
- package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +13 -12
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +37 -33
- package/node_modules/@llblab/pi-state-flow/index.ts +3 -2
- package/node_modules/@llblab/pi-state-flow/lib/compaction.ts +2 -2
- package/node_modules/@llblab/pi-state-flow/lib/config.ts +20 -10
- package/node_modules/@llblab/pi-state-flow/lib/context.ts +15 -14
- package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +1 -1
- package/node_modules/@llblab/pi-state-flow/lib/durable.ts +8 -6
- package/node_modules/@llblab/pi-state-flow/lib/episode.ts +6 -6
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +484 -232
- package/node_modules/@llblab/pi-state-flow/lib/git.ts +14 -5
- package/node_modules/@llblab/pi-state-flow/lib/history.ts +16 -11
- package/node_modules/@llblab/pi-state-flow/lib/logging.ts +5 -1
- package/node_modules/@llblab/pi-state-flow/lib/query.ts +16 -16
- package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +11 -11
- package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +19 -13
- package/node_modules/@llblab/pi-state-flow/lib/session.ts +57 -3
- package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +57 -22
- package/node_modules/@llblab/pi-state-flow/lib/state.ts +46 -13
- package/node_modules/@llblab/pi-state-flow/lib/status.ts +23 -32
- package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +66 -65
- package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +39 -19
- package/node_modules/@llblab/pi-state-flow/lib/transition.ts +19 -27
- package/node_modules/@llblab/pi-state-flow/package.json +12 -11
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +2 -2
- package/node_modules/jsonc-parser/CHANGELOG.md +76 -0
- package/node_modules/jsonc-parser/LICENSE.md +21 -0
- package/node_modules/jsonc-parser/README.md +364 -0
- package/node_modules/jsonc-parser/SECURITY.md +41 -0
- package/node_modules/jsonc-parser/lib/esm/impl/edit.js +185 -0
- package/node_modules/jsonc-parser/lib/esm/impl/format.js +261 -0
- package/node_modules/jsonc-parser/lib/esm/impl/parser.js +659 -0
- package/node_modules/jsonc-parser/lib/esm/impl/scanner.js +443 -0
- package/node_modules/jsonc-parser/lib/esm/impl/string-intern.js +29 -0
- package/node_modules/jsonc-parser/lib/esm/main.d.ts +351 -0
- package/node_modules/jsonc-parser/lib/esm/main.js +178 -0
- package/node_modules/jsonc-parser/lib/umd/impl/edit.js +201 -0
- package/node_modules/jsonc-parser/lib/umd/impl/format.js +275 -0
- package/node_modules/jsonc-parser/lib/umd/impl/parser.js +682 -0
- package/node_modules/jsonc-parser/lib/umd/impl/scanner.js +456 -0
- package/node_modules/jsonc-parser/lib/umd/impl/string-intern.js +42 -0
- package/node_modules/jsonc-parser/lib/umd/main.d.ts +351 -0
- package/node_modules/jsonc-parser/lib/umd/main.js +194 -0
- package/node_modules/jsonc-parser/package.json +37 -0
- package/package.json +10 -6
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
type ArtifactInvalidationRequest,
|
|
16
16
|
} from "./artifact.ts";
|
|
17
17
|
import { hasCompactionSizedTranscript, planStateFlowCompaction, shouldRequestStateFlowCompaction, stateFlowCompactionResult, type StateFlowCompactionPlan } from "./compaction.ts";
|
|
18
|
-
import { loadStateFlowConfig } from "./config.ts";
|
|
18
|
+
import { inactiveModeFor, loadStateFlowConfig } from "./config.ts";
|
|
19
19
|
import { ContextProjection, contextView, createPassiveContinuation, currentRunTrajectory, passiveContinuationMessages, projectSystemProtocol, runtimeContextHead, syntheticUser, type PassiveContinuation } from "./context.ts";
|
|
20
20
|
import { readNativeSessionHeader } from "./continuation.ts";
|
|
21
21
|
import {
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
sessionScopeKey,
|
|
25
25
|
type SessionAddress,
|
|
26
26
|
} from "./durable.ts";
|
|
27
|
-
import { completeRun, prepareRun, resumeEpisode, startEpisode
|
|
27
|
+
import { completeRun, deactivateEpisode, prepareRun, resumeEpisode, startEpisode } from "./episode.ts";
|
|
28
28
|
import { awaitInFlightBackupPushes, backupCurrentStateFlowFiles, startStateFlowBackupPush } from "./git.ts";
|
|
29
29
|
import { projectRecentTransitionsWithLimit } from "./history.ts";
|
|
30
30
|
import { isObject, presentationJson, sameJson, type JsonObject } from "./json.ts";
|
|
@@ -34,10 +34,10 @@ import { readProjectedState, readStatePath } from "./query.ts";
|
|
|
34
34
|
import { selectedBoundaryFailure, selectRetainedCheckpoint, waitForRecovery } from "./recovery.ts";
|
|
35
35
|
import type { RehydrationPhase } from "./rehydration.ts";
|
|
36
36
|
import { TemporalRuntime, type RuntimePublication } from "./runtime.ts";
|
|
37
|
-
import { discoverSnapshotData, findAssistantToolBatch, findPassiveStopBoundary, hasPriorConversation, hasUncheckpointedConversation, isNewSession, retainsPhysicalSessionProjection, SNAPSHOT_ENTRY_TYPE } from "./session.ts";
|
|
37
|
+
import { discoverSnapshotData, findAssistantToolBatch, findBranchPolicy, findPassiveStopBoundary, hasPendingFork, hasPriorConversation, hasUncheckpointedConversation, isNewSession, retainsPhysicalSessionProjection, SNAPSHOT_ENTRY_TYPE } from "./session.ts";
|
|
38
38
|
import { hasCompiledSkillArtifact, hashSkillSource, registeredSkillResolver, SkillReadTracker, type SuccessfulSkillRead } from "./skills.ts";
|
|
39
|
-
import { HistoryBoundaryExpiredError, emptySnapshot, migrationFailure, type RetainedBoundaryCheckpoint, type Snapshot } from "./snapshot.ts";
|
|
40
|
-
import { emptyState,
|
|
39
|
+
import { HistoryBoundaryExpiredError, emptySnapshot, migrationFailure, preRuntimeCheckpoint, type InactiveMode, type RetainedBoundaryCheckpoint, type RetainedPiCheckpoint, type Snapshot, type StateFlowMode } from "./snapshot.ts";
|
|
40
|
+
import { emptyState, projectStateForModel, type AtomicScopePatches, type SemanticState, type ModelState, type ScopedSemanticStates, type ScopedStates, type StateScope } from "./state.ts";
|
|
41
41
|
import { compactStatus, detailedStatus, STATUS_KEY, type StatusDiagnostics } from "./status.ts";
|
|
42
42
|
import { PublicationBusyError } from "./storage.ts";
|
|
43
43
|
import { createStateFlowTelegramAdapter, type StateFlowTelegramControlResult, type StateFlowTelegramInspection, type StateFlowTelegramLoader, type StateFlowTelegramScope } from "./telegram.ts";
|
|
@@ -49,8 +49,8 @@ export interface StateFlowExtensionOptions {
|
|
|
49
49
|
repositoryRoot?: string;
|
|
50
50
|
onRuntime?: (accessor: { read(offset?: number, scope?: StateScope): ModelState }) => void;
|
|
51
51
|
telegram?: { load?: StateFlowTelegramLoader };
|
|
52
|
-
/** Test/SDK
|
|
53
|
-
|
|
52
|
+
/** Test/SDK override of the default mode for new sessions; repository config remains the Pi default. */
|
|
53
|
+
mode?: StateFlowMode;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
export { formatPatchStateArguments };
|
|
@@ -68,15 +68,15 @@ interface InferencePreparation {
|
|
|
68
68
|
|
|
69
69
|
interface BranchRestoration {
|
|
70
70
|
readonly controller: AbortController;
|
|
71
|
-
/** Memory acceptance is independent of the requested
|
|
71
|
+
/** Memory acceptance is independent of the requested session mode. */
|
|
72
72
|
awaitingAcceptance: boolean;
|
|
73
|
-
|
|
73
|
+
requestedMode?: InactiveMode;
|
|
74
74
|
operation?: Promise<void>;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
type BranchSelection =
|
|
78
78
|
| { kind: "settled" }
|
|
79
|
-
| { kind: "current" }
|
|
79
|
+
| { kind: "current"; mode: InactiveMode }
|
|
80
80
|
| { kind: "restore"; checkpoint: RetainedBoundaryCheckpoint }
|
|
81
81
|
| { kind: "fork"; checkpoint: RetainedBoundaryCheckpoint; source: SessionAddress }
|
|
82
82
|
| { kind: "auto-start" };
|
|
@@ -84,29 +84,31 @@ type BranchSelection =
|
|
|
84
84
|
export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowExtensionOptions = {}): void {
|
|
85
85
|
const agentDir = options.agentDir ?? getAgentDir();
|
|
86
86
|
const loadedConfig = loadStateFlowConfig(agentDir, options.repositoryRoot);
|
|
87
|
-
const config =
|
|
88
|
-
...loadedConfig,
|
|
89
|
-
|
|
90
|
-
passiveTools: options.passive?.tools ?? loadedConfig.passiveTools,
|
|
91
|
-
};
|
|
92
|
-
let snapshot: Snapshot = emptySnapshot();
|
|
87
|
+
const config = options.mode === undefined ? loadedConfig
|
|
88
|
+
: { ...loadedConfig, mode: options.mode, inactiveMode: inactiveModeFor(options.mode) };
|
|
89
|
+
let snapshot: Snapshot = emptySnapshot(config.inactiveMode);
|
|
93
90
|
let scopeStates: ScopedStates = { global: emptyState(), cwd: emptyState(), session: emptyState() };
|
|
91
|
+
let effectiveState: SemanticState = {};
|
|
94
92
|
let branchStartsWithoutRuntime = false;
|
|
95
93
|
let selectedHistoryExpired = false;
|
|
96
|
-
let
|
|
97
|
-
|
|
94
|
+
let modePersistenceError: string | undefined;
|
|
95
|
+
/** One pending inactive-mode persistence; later inactive choices coalesce until it publishes. */
|
|
96
|
+
let inactivePersistence: { controller: AbortController; published?: boolean; operation?: Promise<StateFlowTelegramControlResult> } | undefined;
|
|
98
97
|
let startActivation: { controller: AbortController; operation?: Promise<StateFlowTelegramControlResult> } | undefined;
|
|
99
98
|
let forkInitialization = false;
|
|
100
99
|
let branchRestoration: BranchRestoration | undefined;
|
|
100
|
+
let deferredBranch: { reason: unknown; owner: string; cwd: string } | undefined;
|
|
101
101
|
const restorationOperations = new Set<Promise<void>>();
|
|
102
102
|
let responseReconciliation: AbortController | undefined;
|
|
103
103
|
let sharedInspectionLifetime = new AbortController();
|
|
104
|
+
let memoryToolLifetime = new AbortController();
|
|
104
105
|
let completedRunAccepted = false;
|
|
105
106
|
let turnAcceptedForBackup = false;
|
|
106
107
|
let compactionPlan: StateFlowCompactionPlan | undefined;
|
|
107
108
|
let compactionInFlight = false;
|
|
108
109
|
let compactionStopped = false;
|
|
109
|
-
const
|
|
110
|
+
const compactionPrefix = `state-flow-boundary:${randomUUID()}:`;
|
|
111
|
+
let compactionMarker: string | undefined;
|
|
110
112
|
let passiveContinuation: PassiveContinuation | undefined;
|
|
111
113
|
let bootstrapContinuation: PassiveContinuation | undefined;
|
|
112
114
|
const contextProjection = new ContextProjection();
|
|
@@ -118,7 +120,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
118
120
|
const repositoryRoot = resolve(options.repositoryRoot ?? config.directory);
|
|
119
121
|
const diagnosticWriter = new StateFlowDiagnosticWriter(config.logging, stateFlowLogPath(agentDir), repositoryRoot, (message) => notifyActiveContext(message));
|
|
120
122
|
let backupPending = false;
|
|
121
|
-
|
|
123
|
+
let backupLifetime = new AbortController();
|
|
122
124
|
const backupOperations = new Set<Promise<string | undefined>>();
|
|
123
125
|
let shuttingDown = false;
|
|
124
126
|
let pushFailureNotified = false;
|
|
@@ -151,7 +153,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
151
153
|
return new TemporalRuntime(ctx.cwd, sessionAddress(ctx), repositoryRoot, undefined, config.historyLimit);
|
|
152
154
|
}
|
|
153
155
|
|
|
154
|
-
function projectModelState(state:
|
|
156
|
+
function projectModelState(state: SemanticState): SemanticState {
|
|
155
157
|
return projectStateForModel(state, artifactHints);
|
|
156
158
|
}
|
|
157
159
|
|
|
@@ -164,19 +166,28 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
164
166
|
options.onRuntime?.({ read: (offset, scope) => {
|
|
165
167
|
if (scope === "session") assertSelectedBranchAvailable();
|
|
166
168
|
if (!runtime) throw new Error("State Flow temporal runtime is unavailable");
|
|
167
|
-
|
|
169
|
+
const { lazy: _lazy, ...defaults } = emptyState();
|
|
170
|
+
return { ...defaults, ...projectModelState(runtime.readView(offset, scope)) };
|
|
168
171
|
} });
|
|
169
172
|
|
|
170
173
|
function assertPublicationAvailable(): void {
|
|
171
174
|
assertSelectedBranchAvailable();
|
|
172
|
-
if (
|
|
175
|
+
if (modePersistenceError) throw new Error(`Memory writes paused after mode change: ${modePersistenceError}; use /state-flow-active`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function isActive(): boolean {
|
|
179
|
+
return snapshot.config.mode === "active";
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** An unavailable or pending active selection keeps the configured inactive policy, never an invented one. */
|
|
183
|
+
function inactiveSelection(mode: StateFlowMode): InactiveMode {
|
|
184
|
+
return mode === "active" ? config.inactiveMode : mode;
|
|
173
185
|
}
|
|
174
186
|
|
|
175
187
|
function appendCheckpoint(): void {
|
|
176
|
-
const checkpoint = branchStartsWithoutRuntime ?
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
188
|
+
const checkpoint = branchStartsWithoutRuntime ? preRuntimeCheckpoint(snapshot.config.mode)
|
|
189
|
+
: runtime?.view ? runtime.retainedCheckpoint(snapshot) : undefined;
|
|
190
|
+
if (!checkpoint) throw new Error("State Flow cannot checkpoint an unproven branch; restore a valid checkpoint first");
|
|
180
191
|
pi.appendEntry(SNAPSHOT_ENTRY_TYPE, checkpoint);
|
|
181
192
|
if ("boundary" in checkpoint) branchStartsWithoutRuntime = false;
|
|
182
193
|
}
|
|
@@ -191,10 +202,10 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
191
202
|
ctx.ui.setStatus(STATUS_KEY, compactStatus(snapshot, scopeRevisions(), (color, text) => ctx.ui.theme.fg(color, text)));
|
|
192
203
|
}
|
|
193
204
|
|
|
194
|
-
function
|
|
195
|
-
const pending =
|
|
205
|
+
function cancelInactivePersistence(): Promise<StateFlowTelegramControlResult> | undefined {
|
|
206
|
+
const pending = inactivePersistence;
|
|
196
207
|
pending?.controller.abort();
|
|
197
|
-
|
|
208
|
+
inactivePersistence = undefined;
|
|
198
209
|
return pending?.operation;
|
|
199
210
|
}
|
|
200
211
|
|
|
@@ -222,6 +233,12 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
222
233
|
sharedInspectionLifetime = new AbortController();
|
|
223
234
|
}
|
|
224
235
|
|
|
236
|
+
function cancelBackupWork(): void {
|
|
237
|
+
backupLifetime.abort();
|
|
238
|
+
backupLifetime = new AbortController();
|
|
239
|
+
backupPending = false;
|
|
240
|
+
}
|
|
241
|
+
|
|
225
242
|
function cancelInferencePreparation(): void {
|
|
226
243
|
inferencePreparation?.controller.abort();
|
|
227
244
|
inferencePreparation = undefined;
|
|
@@ -277,9 +294,9 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
277
294
|
artifactReads.setCandidates(artifactInvalidations);
|
|
278
295
|
}
|
|
279
296
|
|
|
280
|
-
function missingArtifactRemovals(states:
|
|
297
|
+
function missingArtifactRemovals(states: ScopedSemanticStates): AtomicScopePatches {
|
|
281
298
|
const owners = new Map<string, StateScope[]>();
|
|
282
|
-
for (const scope of ["global", "cwd", "session"] as const) for (const path of Object.keys(states[scope].artifacts)) {
|
|
299
|
+
for (const scope of ["global", "cwd", "session"] as const) for (const path of Object.keys(states[scope].artifacts ?? {})) {
|
|
283
300
|
owners.set(path, [...owners.get(path) ?? [], scope]);
|
|
284
301
|
}
|
|
285
302
|
const removals: AtomicScopePatches = {};
|
|
@@ -296,33 +313,49 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
296
313
|
|
|
297
314
|
function installScopeStates(): void {
|
|
298
315
|
scopeStates = runtime?.view ? runtime.states() : { global: emptyState(), cwd: emptyState(), session: emptyState() };
|
|
316
|
+
effectiveState = runtime?.view ? runtime.readView() : {};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** Active and passive expose both memory tools; Off exposes neither. */
|
|
320
|
+
function memoryToolsAvailable(): boolean {
|
|
321
|
+
return snapshot.config.mode !== "off";
|
|
299
322
|
}
|
|
300
323
|
|
|
301
|
-
|
|
302
|
-
|
|
324
|
+
/** Passive memory context needs a loaded view; Off and Active never inject it. */
|
|
325
|
+
function passiveMemoryAvailable(): boolean {
|
|
326
|
+
return snapshot.config.mode === "passive" && runtime?.view !== undefined;
|
|
303
327
|
}
|
|
304
328
|
|
|
305
329
|
async function inspectTelegramState(scope: StateFlowTelegramScope): Promise<StateFlowTelegramInspection> {
|
|
306
330
|
if (!activeContext || shuttingDown) throw new Error("State Flow is not attached to an active session yet");
|
|
307
|
-
|
|
308
|
-
const
|
|
331
|
+
const ctx = activeContext;
|
|
332
|
+
const owner = sessionAddress(ctx).key;
|
|
333
|
+
const cwd = ctx.cwd;
|
|
334
|
+
const off = snapshot.config.mode === "off";
|
|
335
|
+
const privateScope = scope === "session" || scope === "effective";
|
|
336
|
+
if (!off && privateScope) assertSelectedBranchAvailable();
|
|
337
|
+
// Off operator reads are disposable: they confer no selected-branch/cache authority.
|
|
338
|
+
const selected = off ? createRuntime(ctx) : runtime ??= createRuntime(ctx);
|
|
309
339
|
const signal = sharedInspectionLifetime.signal;
|
|
310
|
-
|
|
340
|
+
let privateAvailable = true;
|
|
341
|
+
if (off && privateScope) privateAvailable = await selected.refreshCurrentMemory(signal) !== undefined;
|
|
342
|
+
else if (!selected.view || (scope !== "session" && !modePersistenceError)) await selected.refreshShared(signal);
|
|
311
343
|
signal.throwIfAborted();
|
|
312
|
-
if (
|
|
313
|
-
|
|
344
|
+
if (activeContext !== ctx || ctx.cwd !== cwd || sessionAddress(ctx).key !== owner || (!off && runtime !== selected)) {
|
|
345
|
+
throw new Error("State Flow inspection selection changed; select the scope again");
|
|
346
|
+
}
|
|
347
|
+
if (!privateAvailable) throw new Error("Current State Flow session memory is unavailable");
|
|
348
|
+
if (!off && privateScope) assertSelectedBranchAvailable();
|
|
314
349
|
if (!selected.view) throw new Error("State Flow temporal runtime is unavailable");
|
|
315
|
-
installScopeStates();
|
|
316
|
-
const state = scope === "effective"
|
|
317
|
-
|
|
318
|
-
: scopeStates[scope];
|
|
319
|
-
return { state: { ...projectModelState(state), lazy: structuredClone(state.lazy) }, revisions: scopeRevisions(), signal };
|
|
350
|
+
if (!off) installScopeStates();
|
|
351
|
+
const state = selected.readView(0, scope === "effective" ? undefined : scope);
|
|
352
|
+
return { state: { ...projectModelState(state), ...(state.lazy === undefined ? {} : { lazy: structuredClone(state.lazy) }) }, revisions: temporalScopeRevisions(selected.view), signal };
|
|
320
353
|
}
|
|
321
354
|
|
|
322
355
|
function syncStateFlowTools(): void {
|
|
323
356
|
const active = pi.getActiveTools();
|
|
324
357
|
const owned = [PATCH_STATE_TOOL_NAME, READ_STATE_TOOL_NAME];
|
|
325
|
-
const available =
|
|
358
|
+
const available = memoryToolsAvailable();
|
|
326
359
|
if (owned.every((name) => active.includes(name) === available)) return;
|
|
327
360
|
pi.setActiveTools(available
|
|
328
361
|
? [...new Set([...active, ...owned])]
|
|
@@ -374,7 +407,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
374
407
|
if (!selected?.view) throw new Error("Temporal State Flow runtime is unavailable; reload before publishing");
|
|
375
408
|
await selected.withPatchTransaction((transaction) => {
|
|
376
409
|
signal.throwIfAborted();
|
|
377
|
-
if (runtime !== selected || inferencePreparation !== pending || !
|
|
410
|
+
if (runtime !== selected || inferencePreparation !== pending || !isActive() || shuttingDown) {
|
|
378
411
|
throw new Error("State Flow inference selection changed while awaiting publication");
|
|
379
412
|
}
|
|
380
413
|
assertPublicationAvailable();
|
|
@@ -436,6 +469,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
436
469
|
cwdScopeKey: cwdScopeKey(cwd),
|
|
437
470
|
sessionScopeKey: sessionScopeKey(session.key),
|
|
438
471
|
scopeStates: diagnosticStates,
|
|
472
|
+
effectiveState,
|
|
439
473
|
recent: diagnosticRecent,
|
|
440
474
|
historyLimit: config.historyLimit,
|
|
441
475
|
...(view === undefined ? {} : { temporal: {
|
|
@@ -446,7 +480,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
446
480
|
} }),
|
|
447
481
|
staleArtifacts,
|
|
448
482
|
...(durableStateError === undefined ? {} : { durableStateError }),
|
|
449
|
-
...(
|
|
483
|
+
...(modePersistenceError === undefined ? {} : { publicationError: modePersistenceError }),
|
|
450
484
|
};
|
|
451
485
|
}
|
|
452
486
|
|
|
@@ -459,13 +493,15 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
459
493
|
}
|
|
460
494
|
|
|
461
495
|
/** Select the active native branch under one owned restoration lifetime; only current accepted work installs memory. */
|
|
462
|
-
function restoreActiveBranch(ctx: ExtensionContext, sessionStartReason?: unknown, notifyRecovery = true, startOwner?: AbortController): Promise<void> {
|
|
496
|
+
function restoreActiveBranch(ctx: ExtensionContext, sessionStartReason?: unknown, notifyRecovery = true, startOwner?: AbortController, requestedMode?: InactiveMode): Promise<void> {
|
|
463
497
|
contextProjection.reset();
|
|
498
|
+
memoryToolLifetime.abort();
|
|
499
|
+
memoryToolLifetime = new AbortController();
|
|
464
500
|
cancelBranchRestoration();
|
|
465
501
|
// Start-owned attachment/fork recovery keeps its owner; only accepted Start cancels Stop.
|
|
466
502
|
if (!startOwner) {
|
|
467
503
|
cancelStartActivation();
|
|
468
|
-
|
|
504
|
+
cancelInactivePersistence();
|
|
469
505
|
}
|
|
470
506
|
clearRunTransient();
|
|
471
507
|
passiveContinuation = undefined;
|
|
@@ -474,6 +510,51 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
474
510
|
artifactReads.setCandidates([]);
|
|
475
511
|
telegramStartPending = false;
|
|
476
512
|
activeContext = ctx;
|
|
513
|
+
if (!startOwner) {
|
|
514
|
+
let mode: StateFlowMode = config.inactiveMode;
|
|
515
|
+
let adoptDefault = false;
|
|
516
|
+
let withoutRuntime = false;
|
|
517
|
+
let persistenceError: string | undefined;
|
|
518
|
+
let pendingFork = false;
|
|
519
|
+
try {
|
|
520
|
+
const branch = ctx.sessionManager.getBranch();
|
|
521
|
+
const discovery = discoverSnapshotData(branch);
|
|
522
|
+
withoutRuntime = discovery.candidates.length === 0 && discovery.errors.length === 0;
|
|
523
|
+
adoptDefault = withoutRuntime && isNewSession(sessionStartReason, branch);
|
|
524
|
+
pendingFork = hasPendingFork(branch, ctx.sessionManager.getSessionId(), PASSIVE_STOP_ENTRY_TYPE);
|
|
525
|
+
const inherited = sessionStartReason === "fork" || pendingFork;
|
|
526
|
+
const policy = findBranchPolicy(branch, inherited ? undefined : ctx.sessionManager.getSessionId(),
|
|
527
|
+
inherited || retainsPhysicalSessionProjection(sessionStartReason) ? PASSIVE_STOP_ENTRY_TYPE : undefined, config.inactiveMode);
|
|
528
|
+
mode = policy?.mode ?? (adoptDefault ? config.mode : config.inactiveMode);
|
|
529
|
+
persistenceError = inherited
|
|
530
|
+
? findBranchPolicy(branch, ctx.sessionManager.getSessionId(), PASSIVE_STOP_ENTRY_TYPE, config.inactiveMode)?.persistenceError
|
|
531
|
+
: policy?.persistenceError;
|
|
532
|
+
} catch {
|
|
533
|
+
// Off needs only policy; unavailable native evidence grants no memory authority.
|
|
534
|
+
}
|
|
535
|
+
if (mode === "off") {
|
|
536
|
+
cancelBackupWork();
|
|
537
|
+
const owner = sessionAddress(ctx).key;
|
|
538
|
+
const reason = pendingFork || (deferredBranch?.owner === owner && deferredBranch.cwd === ctx.cwd && deferredBranch.reason === "fork"
|
|
539
|
+
&& retainsPhysicalSessionProjection(sessionStartReason)) ? "fork" : sessionStartReason;
|
|
540
|
+
if (reason === "fork" && !pendingFork) pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, { owner: ctx.sessionManager.getSessionId(), forkPending: true });
|
|
541
|
+
deferredBranch = { reason, owner, cwd: ctx.cwd };
|
|
542
|
+
runtime = undefined;
|
|
543
|
+
snapshot = emptySnapshot("off");
|
|
544
|
+
branchStartsWithoutRuntime = withoutRuntime;
|
|
545
|
+
selectedHistoryExpired = false;
|
|
546
|
+
modePersistenceError = persistenceError;
|
|
547
|
+
forkInitialization = reason === "fork";
|
|
548
|
+
installScopeStates();
|
|
549
|
+
artifactHints = {};
|
|
550
|
+
if (adoptDefault) appendCheckpoint();
|
|
551
|
+
syncStateFlowTools();
|
|
552
|
+
updateUi(ctx);
|
|
553
|
+
return Promise.resolve();
|
|
554
|
+
}
|
|
555
|
+
if (pendingFork) sessionStartReason = "fork";
|
|
556
|
+
}
|
|
557
|
+
deferredBranch = undefined;
|
|
477
558
|
runtime = createRuntime(ctx);
|
|
478
559
|
installScopeStates();
|
|
479
560
|
branchStartsWithoutRuntime = false;
|
|
@@ -484,47 +565,61 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
484
565
|
try {
|
|
485
566
|
const branch = ctx.sessionManager.getBranch();
|
|
486
567
|
const fence = retainsPhysicalSessionProjection(sessionStartReason)
|
|
487
|
-
? findPassiveStopBoundary(branch, ctx.sessionManager.getSessionId(), PASSIVE_STOP_ENTRY_TYPE)
|
|
568
|
+
? findPassiveStopBoundary(branch, ctx.sessionManager.getSessionId(), PASSIVE_STOP_ENTRY_TYPE)
|
|
488
569
|
: undefined;
|
|
489
|
-
|
|
490
|
-
if (fence) {
|
|
491
|
-
// The native
|
|
492
|
-
selection = { kind: "current" };
|
|
570
|
+
modePersistenceError = fence?.persistenceError ?? (startOwner ? modePersistenceError : undefined);
|
|
571
|
+
if (fence?.persistenceError) {
|
|
572
|
+
// The native marker owns policy only: read proven memory, never replay or publish a stale selection.
|
|
573
|
+
selection = { kind: "current", mode: fence.mode ?? config.inactiveMode };
|
|
493
574
|
} else {
|
|
494
575
|
const discovery = discoverSnapshotData(branch);
|
|
495
|
-
const selected = selectRetainedCheckpoint(discovery.candidates);
|
|
576
|
+
const selected = selectRetainedCheckpoint(discovery.candidates, config.inactiveMode);
|
|
496
577
|
skipped = discovery.errors.length + selected.skipped.length;
|
|
497
|
-
branchStartsWithoutRuntime = selected.kind === "
|
|
578
|
+
branchStartsWithoutRuntime = selected.kind === "pre-runtime" || (discovery.candidates.length === 0 && discovery.errors.length === 0);
|
|
498
579
|
if (discovery.candidates.length === 0 && discovery.errors.length > 0) {
|
|
499
|
-
snapshot = migrationFailure({}, `Snapshot restoration failed: ${discovery.errors[0]}
|
|
580
|
+
snapshot = migrationFailure({}, `Snapshot restoration failed: ${discovery.errors[0]}`, config.inactiveMode);
|
|
500
581
|
} else if (selected.kind === "boundary" && forkInitialization) {
|
|
501
582
|
try {
|
|
502
583
|
// Native parent acquisition stays outside canonical exclusion.
|
|
503
584
|
const source = forkSource(ctx);
|
|
504
|
-
const
|
|
505
|
-
selection = { kind: "fork", source, checkpoint:
|
|
585
|
+
const sourceFence = findPassiveStopBoundary(branch, source.id, PASSIVE_STOP_ENTRY_TYPE);
|
|
586
|
+
selection = { kind: "fork", source, checkpoint: sourceFence?.persistenceError !== undefined
|
|
587
|
+
? { ...selected.checkpoint, mode: sourceFence.mode ?? config.inactiveMode } : selected.checkpoint };
|
|
506
588
|
} catch (error) {
|
|
507
|
-
snapshot = selectedBoundaryFailure(diagnosticText(error));
|
|
589
|
+
snapshot = selectedBoundaryFailure(diagnosticText(error), inactiveSelection(selected.checkpoint.mode));
|
|
508
590
|
}
|
|
509
591
|
} else if (selected.kind === "boundary") {
|
|
510
592
|
selection = { kind: "restore", checkpoint: selected.checkpoint };
|
|
593
|
+
} else if (selected.kind === "pre-runtime") {
|
|
594
|
+
snapshot = emptySnapshot(selected.mode);
|
|
511
595
|
} else if (discovery.candidates.length > 0) {
|
|
512
|
-
snapshot = selected.
|
|
513
|
-
} else if (
|
|
514
|
-
|
|
596
|
+
snapshot = selected.snapshot;
|
|
597
|
+
} else if (isNewSession(sessionStartReason, branch)) {
|
|
598
|
+
// The repository mode is only a default for genuinely new sessions.
|
|
599
|
+
if (config.mode === "active") selection = { kind: "auto-start" };
|
|
600
|
+
else {
|
|
601
|
+
snapshot = emptySnapshot(config.mode);
|
|
602
|
+
// Adopt the default once without initializing semantic storage.
|
|
603
|
+
appendCheckpoint();
|
|
604
|
+
}
|
|
515
605
|
} else {
|
|
516
|
-
snapshot = emptySnapshot();
|
|
606
|
+
snapshot = emptySnapshot(config.inactiveMode);
|
|
517
607
|
}
|
|
518
608
|
}
|
|
519
609
|
} catch (error) {
|
|
520
610
|
selection = { kind: "settled" };
|
|
521
|
-
snapshot = selectedBoundaryFailure(diagnosticText(error));
|
|
611
|
+
snapshot = selectedBoundaryFailure(diagnosticText(error), config.inactiveMode);
|
|
522
612
|
}
|
|
523
613
|
// Pending selection grants neither private reads nor publication until its acceptance installs memory.
|
|
524
|
-
if (selection.kind !== "settled")
|
|
614
|
+
if (selection.kind !== "settled") {
|
|
615
|
+
snapshot = migrationFailure({}, "State Flow branch restoration is pending", selection.kind === "current" ? selection.mode
|
|
616
|
+
: selection.kind === "auto-start" ? config.inactiveMode : inactiveSelection(selection.checkpoint.mode));
|
|
617
|
+
}
|
|
618
|
+
if (requestedMode) snapshot = deactivateEpisode(snapshot, requestedMode);
|
|
525
619
|
const pending: BranchRestoration = {
|
|
526
620
|
controller: new AbortController(),
|
|
527
621
|
awaitingAcceptance: selection.kind !== "settled" && selection.kind !== "current",
|
|
622
|
+
...(requestedMode === undefined ? {} : { requestedMode }),
|
|
528
623
|
};
|
|
529
624
|
branchRestoration = pending;
|
|
530
625
|
const operation = attachBranch(ctx, pending, selection, sessionStartReason, notifyRecovery, skipped);
|
|
@@ -578,22 +673,23 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
578
673
|
const current = await candidate.refreshCurrentMemory(signal);
|
|
579
674
|
assertCurrent();
|
|
580
675
|
if (current) runtime = selected = candidate;
|
|
581
|
-
snapshot = current
|
|
676
|
+
snapshot = current ? deactivateEpisode(current, pending.requestedMode ?? selection.mode)
|
|
677
|
+
: migrationFailure({}, "Current State Flow session memory is unavailable", pending.requestedMode ?? selection.mode);
|
|
582
678
|
installScopeStates();
|
|
583
679
|
} else if (selection.kind === "restore") {
|
|
584
680
|
await candidate.withRestoreTransaction(selection.checkpoint, (restored, publish) => {
|
|
585
681
|
assertCurrent();
|
|
586
682
|
// Canceled preparation or boundary continuation may have no specification. Retain uncompiled native context.
|
|
587
|
-
if (restored.config.
|
|
683
|
+
if (restored.config.mode === "active" && restored.meta.specification === undefined
|
|
588
684
|
&& retainsPhysicalSessionProjection(reason) && hasUncheckpointedConversation(ctx.sessionManager.getBranch())) restored.meta.bootstrap = true;
|
|
589
|
-
const next = pending.
|
|
685
|
+
const next = pending.requestedMode ? deactivateEpisode(restored, pending.requestedMode) : restored;
|
|
590
686
|
accept(candidate, next, publish(next), appendCheckpoint);
|
|
591
687
|
}, signal);
|
|
592
688
|
} else if (selection.kind === "fork") {
|
|
593
689
|
await candidate.withForkTransaction(selection.source, selection.checkpoint, (child, publish) => {
|
|
594
690
|
assertCurrent();
|
|
595
691
|
// Mode is selected independently of creating the child's private memory.
|
|
596
|
-
const next = pending.
|
|
692
|
+
const next = pending.requestedMode ? deactivateEpisode(child, pending.requestedMode) : child;
|
|
597
693
|
const publication = publish(next);
|
|
598
694
|
forkInitialization = false;
|
|
599
695
|
accept(candidate, next, publication, () => {
|
|
@@ -609,7 +705,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
609
705
|
const discovery = discoverSnapshotData(branch);
|
|
610
706
|
if (current || discovery.candidates.length > 0 || discovery.errors.length > 0) throw new Error("Existing session runtime requires retained-boundary restoration");
|
|
611
707
|
const activated = startEpisode(hasPriorConversation(branch));
|
|
612
|
-
const next = pending.
|
|
708
|
+
const next = pending.requestedMode ? deactivateEpisode(activated, pending.requestedMode) : activated;
|
|
613
709
|
accept(candidate, next, publish(next), appendCheckpoint);
|
|
614
710
|
}, signal, true);
|
|
615
711
|
}
|
|
@@ -620,7 +716,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
620
716
|
}
|
|
621
717
|
if (!isCurrent()) return;
|
|
622
718
|
if (error instanceof HistoryBoundaryExpiredError) selectedHistoryExpired = true;
|
|
623
|
-
snapshot = selectedBoundaryFailure(diagnosticText(error));
|
|
719
|
+
snapshot = selectedBoundaryFailure(diagnosticText(error), snapshot.config.mode === "active" ? config.inactiveMode : snapshot.config.mode);
|
|
624
720
|
installScopeStates();
|
|
625
721
|
} finally {
|
|
626
722
|
pending.awaitingAcceptance = false;
|
|
@@ -628,25 +724,19 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
628
724
|
if (accepted) return;
|
|
629
725
|
}
|
|
630
726
|
settleSelection(ctx, reason, notifyRecovery, skipped, selected !== placeholder);
|
|
631
|
-
if (snapshot.config.
|
|
632
|
-
const passive =
|
|
633
|
-
|
|
634
|
-
try {
|
|
635
|
-
await passive.refreshShared(signal);
|
|
636
|
-
} catch (error) {
|
|
637
|
-
if (isCurrent() && !signal.aborted && notifyRecovery && !stopPersistenceError) notifyProblem(ctx, `State Flow passive memory is unavailable: ${diagnosticText(error)}`, "warning");
|
|
638
|
-
return;
|
|
639
|
-
}
|
|
727
|
+
if (snapshot.config.mode !== "passive" || runtime?.view) return;
|
|
728
|
+
const passive = await loadPassiveView(ctx, signal, isCurrent, notifyRecovery);
|
|
729
|
+
if (passive === undefined) return;
|
|
640
730
|
assertCurrent();
|
|
641
731
|
// An intervening passive patch or inspection owns its newer cache, even on the same physical branch.
|
|
642
|
-
if (
|
|
732
|
+
if (runtime?.view) return;
|
|
643
733
|
runtime = selected = passive;
|
|
644
734
|
installScopeStates();
|
|
645
735
|
updateUi(ctx);
|
|
646
736
|
} catch (error) {
|
|
647
737
|
// Host failures before acceptance leave the selection unavailable, never invented empty memory.
|
|
648
738
|
if (accepted || !isCurrent()) return;
|
|
649
|
-
snapshot = selectedBoundaryFailure(diagnosticText(error));
|
|
739
|
+
snapshot = selectedBoundaryFailure(diagnosticText(error), snapshot.config.mode === "active" ? config.inactiveMode : snapshot.config.mode);
|
|
650
740
|
installScopeStates();
|
|
651
741
|
} finally {
|
|
652
742
|
pending.awaitingAcceptance = false;
|
|
@@ -654,29 +744,41 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
654
744
|
}
|
|
655
745
|
}
|
|
656
746
|
|
|
747
|
+
/** Load current shared memory for passive projection without initializing or publishing it. */
|
|
748
|
+
async function loadPassiveView(ctx: ExtensionContext, signal: AbortSignal, isCurrent: () => boolean, notify: boolean): Promise<TemporalRuntime | undefined> {
|
|
749
|
+
const passive = createRuntime(ctx);
|
|
750
|
+
try {
|
|
751
|
+
await passive.refreshShared(signal);
|
|
752
|
+
} catch (error) {
|
|
753
|
+
if (isCurrent() && !signal.aborted && notify && !modePersistenceError) notifyProblem(ctx, `State Flow passive memory is unavailable: ${diagnosticText(error)}`, "warning");
|
|
754
|
+
return undefined;
|
|
755
|
+
}
|
|
756
|
+
return passive;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function selectedContinuation(ctx: ExtensionContext, reason: unknown): PassiveContinuation | undefined {
|
|
760
|
+
if (!retainsPhysicalSessionProjection(reason) || !runtime?.view) return undefined;
|
|
761
|
+
const boundary = findPassiveStopBoundary(ctx.sessionManager.getBranch(), ctx.sessionManager.getSessionId(), PASSIVE_STOP_ENTRY_TYPE);
|
|
762
|
+
return boundary === undefined ? undefined : createPassiveContinuation(
|
|
763
|
+
projectModelState(effectiveState), boundary.at, boundary.from, boundary.preserveContext,
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
|
|
657
767
|
function settleSelection(ctx: ExtensionContext, reason: unknown, notifyRecovery: boolean, skipped: number, selectedMemory: boolean): void {
|
|
658
|
-
const failure = !
|
|
659
|
-
if (failure !== undefined && notifyRecovery && !
|
|
660
|
-
if (selectedHistoryExpired) notifyProblem(ctx, "State Flow history is outside the retained temporal window; /state-flow-
|
|
768
|
+
const failure = !isActive() && snapshot.meta.validation?.attempt === 0 ? snapshot.meta.validation.error : undefined;
|
|
769
|
+
if (failure !== undefined && notifyRecovery && !modePersistenceError) {
|
|
770
|
+
if (selectedHistoryExpired) notifyProblem(ctx, "State Flow history is outside the retained temporal window; /state-flow-active can use current session memory.", "warning");
|
|
661
771
|
else notifyProblem(ctx, `State Flow restore failed: ${failure}`, "error");
|
|
662
772
|
}
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
if (
|
|
666
|
-
|
|
667
|
-
projectModelState(overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session)),
|
|
668
|
-
boundary.at,
|
|
669
|
-
boundary.from,
|
|
670
|
-
boundary.preserveContext,
|
|
671
|
-
);
|
|
672
|
-
if (!snapshot.config.enabled) passiveContinuation = continuation;
|
|
673
|
-
else if (snapshot.meta.bootstrap) bootstrapContinuation = continuation;
|
|
674
|
-
}
|
|
773
|
+
const continuation = selectedMemory ? selectedContinuation(ctx, reason) : undefined;
|
|
774
|
+
if (continuation !== undefined) {
|
|
775
|
+
if (!isActive()) passiveContinuation = continuation;
|
|
776
|
+
else if (snapshot.meta.bootstrap) bootstrapContinuation = continuation;
|
|
675
777
|
}
|
|
676
|
-
if (notifyRecovery &&
|
|
778
|
+
if (notifyRecovery && isActive() && skipped > 0) {
|
|
677
779
|
notifyProblem(ctx, `State Flow skipped ${skipped} malformed snapshot(s); restored the last valid one.`, "warning");
|
|
678
780
|
}
|
|
679
|
-
if (
|
|
781
|
+
if (isActive()) deferInferencePreparation();
|
|
680
782
|
syncStateFlowTools();
|
|
681
783
|
updateUi(ctx);
|
|
682
784
|
}
|
|
@@ -698,7 +800,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
698
800
|
async execute(_toolCallId, params, signal) {
|
|
699
801
|
try {
|
|
700
802
|
type ReadDetails = { path?: string; paths?: string[]; projection?: string; transitionId?: string };
|
|
701
|
-
if (!
|
|
803
|
+
if (!memoryToolsAvailable()) throw new Error("State Flow tools are off for this session");
|
|
702
804
|
if (signal?.aborted) throw new Error("State Flow read was aborted");
|
|
703
805
|
if (!runtime?.view) throw new Error("State Flow temporal runtime is unavailable");
|
|
704
806
|
if (params.path === undefined && params.paths === undefined) throw new Error("read_state requires path or paths");
|
|
@@ -750,8 +852,9 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
750
852
|
return new Text(separatedOutput(theme.fg("dim", formatPatchStateArguments(context.args))), 0, 0);
|
|
751
853
|
},
|
|
752
854
|
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
855
|
+
signal = signal ? AbortSignal.any([signal, memoryToolLifetime.signal]) : memoryToolLifetime.signal;
|
|
753
856
|
try {
|
|
754
|
-
if (!
|
|
857
|
+
if (!memoryToolsAvailable()) throw new Error("State Flow tools are off for this session");
|
|
755
858
|
assertPublicationAvailable();
|
|
756
859
|
if (signal?.aborted) throw new Error("State Flow patch was aborted before materialization");
|
|
757
860
|
if (!isObject(params)) throw new Error("patch_state requires an object");
|
|
@@ -773,10 +876,10 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
773
876
|
const selected = runtime ??= createRuntime(ctx);
|
|
774
877
|
const acquiredArtifacts = structuredClone([...artifactReads.successful.values()]);
|
|
775
878
|
const acquiredSkills = structuredClone([...skillReads.successful.values()]);
|
|
776
|
-
const previousEffective =
|
|
879
|
+
const previousEffective = effectiveState;
|
|
777
880
|
return await selected.withPatchTransaction((transaction) => {
|
|
778
881
|
if (runtime !== selected) throw new Error("State Flow session selection changed while awaiting publication");
|
|
779
|
-
if (!
|
|
882
|
+
if (!memoryToolsAvailable()) throw new Error("State Flow tools are off for this session");
|
|
780
883
|
assertPublicationAvailable();
|
|
781
884
|
for (const acquired of acquiredArtifacts) {
|
|
782
885
|
const observation = inspectRegisteredArtifactPaths([acquired.path])[0];
|
|
@@ -803,7 +906,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
803
906
|
}
|
|
804
907
|
clearAcceptedAcquisitions(new Set(acquiredArtifacts.map(({ path }) => path)));
|
|
805
908
|
updateUi(ctx);
|
|
806
|
-
const updates = contextProjection.acceptPatch(previousEffective,
|
|
909
|
+
const updates = contextProjection.acceptPatch(previousEffective, effectiveState, patches, artifactHints);
|
|
807
910
|
const acknowledgement = changed
|
|
808
911
|
? `\nState materialized atomically at ${scopes.join("+")} scope${scopes.length === 1 ? "" : "s"}.`
|
|
809
912
|
: "\nState already current.";
|
|
@@ -816,7 +919,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
816
919
|
let attempted: unknown;
|
|
817
920
|
try { attempted = structuredClone(params); } catch { attempted = undefined; }
|
|
818
921
|
const cause = diagnosticText(error);
|
|
819
|
-
recordDiagnostic(cause, /concurrently|advanced/.test(cause) ? "publication-conflict" : "invalid-patch", ctx, {
|
|
922
|
+
if (!signal.aborted && memoryToolsAvailable()) recordDiagnostic(cause, /concurrently|advanced/.test(cause) ? "publication-conflict" : "invalid-patch", ctx, {
|
|
820
923
|
input: attempted,
|
|
821
924
|
tool: PATCH_STATE_TOOL_NAME,
|
|
822
925
|
toolCallId,
|
|
@@ -830,8 +933,8 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
830
933
|
if (shuttingDown) return Promise.resolve({ ok: false, message: "State Flow is shutting down" });
|
|
831
934
|
if (startActivation?.operation) return startActivation.operation;
|
|
832
935
|
telegramStartPending = false;
|
|
833
|
-
if (activeContext && !branchRestoration &&
|
|
834
|
-
return Promise.resolve({ ok: true, message: "State Flow is already
|
|
936
|
+
if (activeContext && !branchRestoration && isActive()) {
|
|
937
|
+
return Promise.resolve({ ok: true, message: "State Flow is already active", signal: sharedInspectionLifetime.signal });
|
|
835
938
|
}
|
|
836
939
|
const pending: NonNullable<typeof startActivation> = { controller: new AbortController() };
|
|
837
940
|
startActivation = pending;
|
|
@@ -854,16 +957,17 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
854
957
|
&& ctx.sessionManager.getHeader()?.timestamp === timestamp;
|
|
855
958
|
const superseded = (): StateFlowTelegramControlResult => {
|
|
856
959
|
pending.abort();
|
|
857
|
-
return { ok: false, message: "State Flow
|
|
960
|
+
return { ok: false, message: "State Flow activation was superseded", signal: pending.signal };
|
|
858
961
|
};
|
|
859
962
|
try {
|
|
860
|
-
if (
|
|
963
|
+
if (deferredBranch?.reason === "fork") await waitForRecovery(restoreActiveBranch(ctx, "fork", false, pending), signal);
|
|
964
|
+
else if (!activeContext) await waitForRecovery(restoreActiveBranch(ctx, undefined, false, pending), signal);
|
|
861
965
|
else if (branchRestoration?.operation) await waitForRecovery(branchRestoration.operation, signal);
|
|
862
966
|
if (!isOwner()) return superseded();
|
|
863
|
-
if (
|
|
967
|
+
if (isActive()) return { ok: true, message: "State Flow is already active", signal: sharedInspectionLifetime.signal };
|
|
864
968
|
if (forkInitialization) {
|
|
865
|
-
// Withdraw this join on cancellation without revoking the independently owned
|
|
866
|
-
const stopping =
|
|
969
|
+
// Withdraw this join on cancellation without revoking the independently owned mode persistence.
|
|
970
|
+
const stopping = inactivePersistence?.operation;
|
|
867
971
|
if (stopping) await waitForRecovery(stopping, signal);
|
|
868
972
|
if (!isOwner()) return superseded();
|
|
869
973
|
await waitForRecovery(restoreActiveBranch(ctx, "fork", false, pending), signal);
|
|
@@ -873,7 +977,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
873
977
|
return activateCurrentState(ctx, pending);
|
|
874
978
|
} catch (error) {
|
|
875
979
|
if (!isOwner()) return superseded();
|
|
876
|
-
const message = conciseDiagnostic(`State Flow
|
|
980
|
+
const message = conciseDiagnostic(`State Flow activation failed: ${diagnosticText(error)}`);
|
|
877
981
|
notifyProblem(ctx, message, "error");
|
|
878
982
|
return { ok: false, message, signal: AbortSignal.any([pending.signal, sharedInspectionLifetime.signal]) };
|
|
879
983
|
}
|
|
@@ -885,36 +989,46 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
885
989
|
const cwd = ctx.cwd;
|
|
886
990
|
const file = ctx.sessionManager.getSessionFile();
|
|
887
991
|
const timestamp = ctx.sessionManager.getHeader()?.timestamp;
|
|
888
|
-
const
|
|
992
|
+
const initiallyActive = isActive();
|
|
889
993
|
let accepted = false;
|
|
890
994
|
let receipt = AbortSignal.any([pending.signal, sharedInspectionLifetime.signal]);
|
|
891
|
-
const
|
|
995
|
+
const signal = ctx.signal ? AbortSignal.any([pending.signal, ctx.signal]) : pending.signal;
|
|
996
|
+
const isCurrent = () => startActivation?.controller === pending && !signal.aborted && runtime === selected && !shuttingDown
|
|
892
997
|
&& ctx.cwd === cwd && ctx.sessionManager.getSessionId() === owner && ctx.sessionManager.getSessionFile() === file
|
|
893
|
-
&& ctx.sessionManager.getHeader()?.timestamp === timestamp &&
|
|
998
|
+
&& ctx.sessionManager.getHeader()?.timestamp === timestamp && isActive() === (accepted || initiallyActive);
|
|
894
999
|
const superseded = (): StateFlowTelegramControlResult => {
|
|
895
1000
|
pending.abort();
|
|
896
|
-
return { ok: false, message: "State Flow
|
|
1001
|
+
return { ok: false, message: "State Flow activation was superseded", signal: pending.signal };
|
|
897
1002
|
};
|
|
898
1003
|
try {
|
|
899
1004
|
const activation = createRuntime(ctx);
|
|
900
|
-
const
|
|
1005
|
+
const deferred = deferredBranch;
|
|
1006
|
+
if (deferred) {
|
|
1007
|
+
const discovery = discoverSnapshotData(ctx.sessionManager.getBranch());
|
|
1008
|
+
const retained = selectRetainedCheckpoint(discovery.candidates, config.inactiveMode);
|
|
1009
|
+
branchStartsWithoutRuntime = retained.kind === "pre-runtime" || (discovery.candidates.length === 0 && discovery.errors.length === 0);
|
|
1010
|
+
}
|
|
1011
|
+
// Off activation owns one cancellable current-head publication, not an independent read-only restore.
|
|
901
1012
|
const result = await activation.withStartTransaction((current, publish) => {
|
|
902
1013
|
signal.throwIfAborted();
|
|
903
|
-
if (!isCurrent()) throw new Error("State Flow
|
|
1014
|
+
if (!isCurrent()) throw new Error("State Flow activation selection changed while awaiting publication");
|
|
904
1015
|
if (!current && !branchStartsWithoutRuntime) throw new Error(snapshot.meta.validation?.error ?? "Current State Flow session memory is unavailable");
|
|
905
|
-
const
|
|
906
|
-
|
|
1016
|
+
const boundary = deferred && retainsPhysicalSessionProjection(deferred.reason)
|
|
1017
|
+
? findPassiveStopBoundary(ctx.sessionManager.getBranch(), owner, PASSIVE_STOP_ENTRY_TYPE) : undefined;
|
|
1018
|
+
const bootstrap = hasPriorConversation(ctx.sessionManager.getBranch()) || passiveContinuation !== undefined || boundary !== undefined;
|
|
907
1019
|
const activated = current ? resumeEpisode(current, bootstrap) : startEpisode(bootstrap);
|
|
908
1020
|
const recoveredCurrent = selectedHistoryExpired;
|
|
909
1021
|
const publication = publish(activated);
|
|
910
1022
|
accepted = true;
|
|
911
|
-
|
|
1023
|
+
cancelInactivePersistence();
|
|
912
1024
|
runtime = selected = activation;
|
|
913
1025
|
snapshot = activated;
|
|
914
1026
|
activeContext = ctx;
|
|
915
1027
|
selectedHistoryExpired = false;
|
|
916
|
-
|
|
1028
|
+
modePersistenceError = undefined;
|
|
917
1029
|
installScopeStates();
|
|
1030
|
+
const continuation = passiveContinuation ?? bootstrapContinuation ?? (deferred ? selectedContinuation(ctx, deferred.reason) : undefined);
|
|
1031
|
+
deferredBranch = undefined;
|
|
918
1032
|
clearRunTransient();
|
|
919
1033
|
contextProjection.reset();
|
|
920
1034
|
passiveContinuation = undefined;
|
|
@@ -924,32 +1038,49 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
924
1038
|
recordPolicyPublication(publication, ctx);
|
|
925
1039
|
syncStateFlowTools();
|
|
926
1040
|
updateUi(ctx);
|
|
1041
|
+
if (forkInitialization) {
|
|
1042
|
+
forkInitialization = false;
|
|
1043
|
+
pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, { reset: true, owner });
|
|
1044
|
+
}
|
|
927
1045
|
appendCheckpoint();
|
|
928
1046
|
ctx.ui.notify(
|
|
929
1047
|
recoveredCurrent
|
|
930
|
-
? "State Flow
|
|
1048
|
+
? "State Flow active from current session memory; unavailable historical state was not restored."
|
|
931
1049
|
: snapshot.meta.bootstrap
|
|
932
|
-
? "State Flow
|
|
933
|
-
: "State Flow
|
|
1050
|
+
? "State Flow active. The next complete agent run will migrate active context into state."
|
|
1051
|
+
: "State Flow active. The next prompt starts a stateful agent run.",
|
|
934
1052
|
"info",
|
|
935
1053
|
);
|
|
936
|
-
return { ok: true, message: "State Flow
|
|
1054
|
+
return { ok: true, message: "State Flow active", signal: receipt };
|
|
937
1055
|
}, signal, branchStartsWithoutRuntime);
|
|
938
1056
|
return isCurrent() ? result : superseded();
|
|
939
1057
|
} catch (error) {
|
|
940
1058
|
if (!isCurrent()) return superseded();
|
|
941
|
-
const message = conciseDiagnostic(`${accepted ? "State Flow
|
|
1059
|
+
const message = conciseDiagnostic(`${accepted ? "State Flow active; lifecycle update failed" : "State Flow activation failed"}: ${diagnosticText(error)}`);
|
|
942
1060
|
notifyProblem(ctx, message, accepted ? "warning" : "error");
|
|
943
1061
|
return { ok: accepted, message, signal: receipt };
|
|
944
1062
|
}
|
|
945
1063
|
}
|
|
946
1064
|
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
1065
|
+
const MODE_COMMAND_DESCRIPTIONS: Record<StateFlowMode, string> = {
|
|
1066
|
+
active: "Make State Flow active on the current session branch",
|
|
1067
|
+
passive: "Use passive State Flow memory on the current session branch",
|
|
1068
|
+
off: "Turn State Flow tools and context off on the current session branch",
|
|
1069
|
+
};
|
|
1070
|
+
|
|
1071
|
+
/** Terminal commands and Telegram controls share these lifecycle owners. */
|
|
1072
|
+
function selectMode(ctx: ExtensionContext, mode: StateFlowMode): Promise<StateFlowTelegramControlResult> {
|
|
1073
|
+
return mode === "active" ? startStateFlow(ctx) : deactivateStateFlow(ctx, mode);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
for (const mode of ["active", "passive", "off"] as const) {
|
|
1077
|
+
pi.registerCommand(`state-flow-${mode}`, {
|
|
1078
|
+
description: MODE_COMMAND_DESCRIPTIONS[mode],
|
|
1079
|
+
handler: async (_args, ctx) => {
|
|
1080
|
+
await selectMode(ctx, mode);
|
|
1081
|
+
},
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
953
1084
|
|
|
954
1085
|
pi.registerCommand("state-flow-status", {
|
|
955
1086
|
description: "Show State Flow runtime status",
|
|
@@ -958,97 +1089,219 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
958
1089
|
},
|
|
959
1090
|
});
|
|
960
1091
|
|
|
961
|
-
|
|
962
|
-
|
|
1092
|
+
/** Local policy, tools and UI change before any canonical wait. */
|
|
1093
|
+
function applyInactiveMode(ctx: ExtensionContext, mode: InactiveMode): void {
|
|
1094
|
+
snapshot = deactivateEpisode(snapshot, mode);
|
|
1095
|
+
clearRunTransient();
|
|
1096
|
+
syncStateFlowTools();
|
|
1097
|
+
updateUi(ctx);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
function deactivateStateFlow(ctx: ExtensionContext, mode: InactiveMode): Promise<StateFlowTelegramControlResult> {
|
|
963
1101
|
if (shuttingDown) return Promise.resolve({ ok: false, message: "State Flow is shutting down" });
|
|
964
|
-
if (
|
|
1102
|
+
if (mode === "off") return selectOff(ctx);
|
|
1103
|
+
cancelStartActivation();
|
|
1104
|
+
telegramStartPending = false;
|
|
1105
|
+
const persisting = inactivePersistence;
|
|
1106
|
+
if (persisting?.operation && !persisting.published) {
|
|
1107
|
+
// Pending inactive choices coalesce: acceptance persists whichever inactive mode is current then.
|
|
1108
|
+
if (snapshot.config.mode !== mode) applyInactiveMode(ctx, mode);
|
|
1109
|
+
return persisting.operation;
|
|
1110
|
+
}
|
|
965
1111
|
if (!activeContext) void restoreActiveBranch(ctx, undefined, false);
|
|
966
1112
|
const restoring = branchRestoration;
|
|
1113
|
+
// Read-only recovery also retains the latest policy; its native write fence is updated below.
|
|
1114
|
+
if (restoring) restoring.requestedMode = mode;
|
|
967
1115
|
if (restoring?.awaitingAcceptance) {
|
|
968
|
-
//
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1116
|
+
// Passive keeps independently owned restoration; Off withdraws it through selectOff.
|
|
1117
|
+
applyInactiveMode(ctx, mode);
|
|
1118
|
+
return Promise.resolve({ ok: true, message: `State Flow ${mode}; memory restoration continues` });
|
|
1119
|
+
}
|
|
1120
|
+
if (snapshot.config.mode === mode) {
|
|
1121
|
+
const retained = selectRetainedCheckpoint(discoverSnapshotData(ctx.sessionManager.getBranch()).candidates, config.inactiveMode);
|
|
1122
|
+
const recordedMode = retained.kind === "pre-runtime" ? retained.mode : retained.kind === "boundary" ? retained.checkpoint.mode : undefined;
|
|
1123
|
+
if (modePersistenceError || recordedMode === mode) return Promise.resolve({ ok: true, message: `State Flow is already ${mode}` });
|
|
975
1124
|
}
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1125
|
+
cancelInactivePersistence();
|
|
1126
|
+
const pending: NonNullable<typeof inactivePersistence> = { controller: new AbortController() };
|
|
1127
|
+
inactivePersistence = pending;
|
|
1128
|
+
const operation = deferredBranch && mode === "passive" && !branchStartsWithoutRuntime
|
|
1129
|
+
? acquirePassiveMode(ctx, pending, deferredBranch.reason)
|
|
1130
|
+
: persistInactiveMode(ctx, pending, mode);
|
|
979
1131
|
pending.operation = operation;
|
|
980
|
-
const finished = () => { if (
|
|
1132
|
+
const finished = () => { if (inactivePersistence === pending) inactivePersistence = undefined; };
|
|
981
1133
|
void operation.then(finished, finished);
|
|
982
1134
|
return operation;
|
|
983
1135
|
}
|
|
984
1136
|
|
|
985
|
-
|
|
986
|
-
|
|
1137
|
+
/** Off owns cancellation and native policy only; no canonical transaction or semantic handoff. */
|
|
1138
|
+
function selectOff(ctx: ExtensionContext): Promise<StateFlowTelegramControlResult> {
|
|
1139
|
+
const address = sessionAddress(ctx);
|
|
1140
|
+
const owner = ctx.sessionManager.getSessionId();
|
|
1141
|
+
const branch = ctx.sessionManager.getBranch();
|
|
987
1142
|
const selected = runtime;
|
|
1143
|
+
const sameOwner = selected?.sessionKey === address.key && selected.cwd === ctx.cwd;
|
|
1144
|
+
const forkRecorded = hasPendingFork(branch, owner, PASSIVE_STOP_ENTRY_TYPE);
|
|
1145
|
+
const pendingFork = forkRecorded || (sameOwner && forkInitialization) || (deferredBranch?.owner === address.key && deferredBranch.cwd === ctx.cwd && deferredBranch.reason === "fork");
|
|
1146
|
+
const policy = findBranchPolicy(branch, owner, PASSIVE_STOP_ENTRY_TYPE, config.inactiveMode);
|
|
1147
|
+
const sameDeferred = deferredBranch?.owner === address.key && deferredBranch.cwd === ctx.cwd;
|
|
1148
|
+
const current = sameOwner || sameDeferred ? snapshot : emptySnapshot("off");
|
|
1149
|
+
if (!sameOwner && !sameDeferred) modePersistenceError = policy?.persistenceError;
|
|
1150
|
+
const alreadyOff = current.config.mode === "off" && policy?.mode === "off";
|
|
1151
|
+
const incoming = sameOwner || sameDeferred ? current.meta.bootstrap ? bootstrapContinuation : passiveContinuation : undefined;
|
|
1152
|
+
const unfinished = current.meta.specification !== undefined || (inferencePreparation?.prompt !== undefined && !inferencePreparation.accepted)
|
|
1153
|
+
|| (isActive() && hasUncheckpointedConversation(branch));
|
|
1154
|
+
const at = incoming?.startedAt ?? Date.now();
|
|
1155
|
+
const from = incoming ? incoming.activeRunStartedAt : (sameOwner || sameDeferred) && (!ctx.isIdle() || unfinished) ? runAnchorTimestamp : undefined;
|
|
1156
|
+
const preserveContext = modePersistenceError !== undefined || (incoming ? incoming.preserveContext : current.meta.bootstrap === true || (unfinished && from === undefined));
|
|
1157
|
+
const discovery = discoverSnapshotData(branch);
|
|
1158
|
+
const preRuntime = (sameOwner && branchStartsWithoutRuntime) || (discovery.candidates.length === 0 && discovery.errors.length === 0);
|
|
1159
|
+
let checkpoint: RetainedPiCheckpoint | undefined;
|
|
1160
|
+
try {
|
|
1161
|
+
checkpoint = !modePersistenceError && current.meta.validation?.attempt !== 0
|
|
1162
|
+
? preRuntime ? preRuntimeCheckpoint("off") : sameOwner && selected?.view ? selected.retainedCheckpoint(deactivateEpisode(current, "off")) : undefined
|
|
1163
|
+
: undefined;
|
|
1164
|
+
} catch {
|
|
1165
|
+
// An unusable cached bookmark cannot prevent native Off policy or authorize replacement memory.
|
|
1166
|
+
}
|
|
1167
|
+
cancelStartActivation();
|
|
1168
|
+
cancelBranchRestoration();
|
|
1169
|
+
cancelInactivePersistence();
|
|
1170
|
+
cancelBackupWork();
|
|
1171
|
+
memoryToolLifetime.abort();
|
|
1172
|
+
memoryToolLifetime = new AbortController();
|
|
1173
|
+
clearRunTransient();
|
|
1174
|
+
contextProjection.reset();
|
|
1175
|
+
telegramStartPending = false;
|
|
1176
|
+
bootstrapContinuation = undefined;
|
|
1177
|
+
passiveContinuation = undefined;
|
|
1178
|
+
artifactInvalidations = [];
|
|
1179
|
+
artifactHints = {};
|
|
1180
|
+
artifactReads.setCandidates([]);
|
|
1181
|
+
runtime = undefined;
|
|
1182
|
+
snapshot = emptySnapshot("off");
|
|
1183
|
+
selectedHistoryExpired = false;
|
|
1184
|
+
branchStartsWithoutRuntime = preRuntime;
|
|
1185
|
+
forkInitialization = pendingFork;
|
|
1186
|
+
deferredBranch = { owner: address.key, cwd: ctx.cwd, reason: pendingFork ? "fork" : undefined };
|
|
1187
|
+
activeContext = ctx;
|
|
1188
|
+
installScopeStates();
|
|
1189
|
+
syncStateFlowTools();
|
|
1190
|
+
updateUi(ctx);
|
|
1191
|
+
try {
|
|
1192
|
+
if (!alreadyOff || (pendingFork && !forkRecorded)) {
|
|
1193
|
+
if (!checkpoint || !preRuntime || pendingFork || preserveContext || from !== undefined || modePersistenceError) pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, {
|
|
1194
|
+
owner, at, mode: "off", memoryDeferred: true,
|
|
1195
|
+
...(from === undefined ? {} : { from }),
|
|
1196
|
+
...(preserveContext ? { preserveContext: true } : {}),
|
|
1197
|
+
...(pendingFork ? { forkPending: true } : {}),
|
|
1198
|
+
...(modePersistenceError === undefined ? {} : { persistenceError: modePersistenceError }),
|
|
1199
|
+
});
|
|
1200
|
+
if (checkpoint) pi.appendEntry(SNAPSHOT_ENTRY_TYPE, checkpoint);
|
|
1201
|
+
}
|
|
1202
|
+
return Promise.resolve({ ok: true, message: alreadyOff ? "State Flow is already off" : "State Flow off" });
|
|
1203
|
+
} catch (error) {
|
|
1204
|
+
const message = conciseDiagnostic(`State Flow off; native mode recording failed: ${diagnosticText(error)}`);
|
|
1205
|
+
notifyProblem(ctx, message, "error");
|
|
1206
|
+
return Promise.resolve({ ok: false, message });
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
async function acquirePassiveMode(ctx: ExtensionContext, pending: NonNullable<typeof inactivePersistence>, reason: unknown): Promise<StateFlowTelegramControlResult> {
|
|
1211
|
+
await restoreActiveBranch(ctx, reason, true, pending.controller, "passive");
|
|
1212
|
+
if (inactivePersistence !== pending || pending.controller.signal.aborted || shuttingDown) {
|
|
1213
|
+
return { ok: false, message: "State Flow mode change was superseded" };
|
|
1214
|
+
}
|
|
1215
|
+
return persistInactiveMode(ctx, pending, "passive");
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
async function persistInactiveMode(ctx: ExtensionContext, pending: NonNullable<typeof inactivePersistence>, mode: InactiveMode): Promise<StateFlowTelegramControlResult> {
|
|
1219
|
+
let selected = runtime;
|
|
988
1220
|
const owner = ctx.sessionManager.getSessionId();
|
|
989
1221
|
const current = snapshot;
|
|
1222
|
+
const wasActive = current.config.mode === "active";
|
|
990
1223
|
let unfinished = current.meta.specification !== undefined
|
|
991
1224
|
|| (inferencePreparation?.prompt !== undefined && !inferencePreparation.accepted);
|
|
992
|
-
|
|
993
|
-
clearRunTransient();
|
|
994
|
-
syncStateFlowTools();
|
|
995
|
-
updateUi(ctx);
|
|
996
|
-
if (stopPersistenceError) return { ok: true, message: "State Flow disabled; memory writes remain paused" };
|
|
1225
|
+
applyInactiveMode(ctx, mode);
|
|
997
1226
|
const stoppedAt = Date.now();
|
|
1227
|
+
if (modePersistenceError) {
|
|
1228
|
+
// Writes stay paused; the native marker records only the newly selected inactive policy.
|
|
1229
|
+
pending.published = true;
|
|
1230
|
+
pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, {
|
|
1231
|
+
at: passiveContinuation?.startedAt ?? stoppedAt,
|
|
1232
|
+
...(passiveContinuation?.activeRunStartedAt === undefined ? {} : { from: passiveContinuation.activeRunStartedAt }),
|
|
1233
|
+
preserveContext: true, owner, persistenceError: modePersistenceError, mode,
|
|
1234
|
+
});
|
|
1235
|
+
return { ok: true, message: `State Flow ${mode}; memory writes remain paused` };
|
|
1236
|
+
}
|
|
998
1237
|
const incomingBoundary = current.meta.bootstrap ? bootstrapContinuation : undefined;
|
|
999
1238
|
const anchor = runAnchorTimestamp;
|
|
1000
1239
|
const idle = ctx.isIdle();
|
|
1001
|
-
const isCurrent = () =>
|
|
1002
|
-
&& !shuttingDown && ctx.sessionManager.getSessionId() === owner && !
|
|
1003
|
-
const superseded = (): StateFlowTelegramControlResult => ({ ok: false, message: "State Flow
|
|
1240
|
+
const isCurrent = () => inactivePersistence === pending && runtime === selected
|
|
1241
|
+
&& !shuttingDown && ctx.sessionManager.getSessionId() === owner && !isActive();
|
|
1242
|
+
const superseded = (): StateFlowTelegramControlResult => ({ ok: false, message: "State Flow mode change was superseded" });
|
|
1004
1243
|
const freezeHandoff = (): PassiveContinuation | undefined => {
|
|
1005
1244
|
contextProjection.reset();
|
|
1006
|
-
const handoff = (
|
|
1245
|
+
const handoff = (wasActive || modePersistenceError) && selected?.view && current.meta.validation?.attempt !== 0
|
|
1007
1246
|
? createPassiveContinuation(
|
|
1008
|
-
projectModelState(
|
|
1247
|
+
projectModelState(effectiveState),
|
|
1009
1248
|
incomingBoundary?.startedAt ?? stoppedAt,
|
|
1010
1249
|
incomingBoundary ? incomingBoundary.activeRunStartedAt : !idle || unfinished ? anchor : undefined,
|
|
1011
|
-
|
|
1250
|
+
modePersistenceError !== undefined || (incomingBoundary ? incomingBoundary.preserveContext : current.meta.bootstrap === true || (unfinished && anchor === undefined)),
|
|
1012
1251
|
)
|
|
1013
1252
|
: undefined;
|
|
1014
|
-
|
|
1253
|
+
// Repeated Passive choices keep the already selected continuation boundary.
|
|
1254
|
+
passiveContinuation = handoff ?? (!wasActive ? passiveContinuation : undefined);
|
|
1015
1255
|
return handoff;
|
|
1016
1256
|
};
|
|
1017
1257
|
const complete = (): StateFlowTelegramControlResult => {
|
|
1258
|
+
pending.published = true;
|
|
1018
1259
|
const exitHandoff = freezeHandoff();
|
|
1019
|
-
|
|
1260
|
+
const selectedMode = snapshot.config.mode;
|
|
1261
|
+
if (exitHandoff || modePersistenceError) pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, {
|
|
1020
1262
|
at: exitHandoff?.startedAt ?? stoppedAt,
|
|
1021
1263
|
...(exitHandoff?.activeRunStartedAt === undefined ? {} : { from: exitHandoff.activeRunStartedAt }),
|
|
1022
|
-
...(exitHandoff?.preserveContext ||
|
|
1023
|
-
...(
|
|
1264
|
+
...(exitHandoff?.preserveContext || modePersistenceError ? { preserveContext: true } : {}),
|
|
1265
|
+
...(modePersistenceError === undefined ? {} : { owner, persistenceError: modePersistenceError, mode: selectedMode }),
|
|
1024
1266
|
});
|
|
1025
|
-
if (!
|
|
1267
|
+
if (!modePersistenceError) {
|
|
1026
1268
|
appendCheckpoint();
|
|
1027
|
-
return { ok: true, message:
|
|
1269
|
+
return { ok: true, message: `State Flow ${selectedMode}` };
|
|
1028
1270
|
}
|
|
1029
|
-
const message = conciseDiagnostic(`State Flow
|
|
1271
|
+
const message = conciseDiagnostic(`State Flow ${selectedMode}; memory writes paused: ${modePersistenceError}`);
|
|
1030
1272
|
notifyProblem(ctx, message, "warning");
|
|
1031
1273
|
return { ok: true, message };
|
|
1032
1274
|
};
|
|
1033
1275
|
let accepted = false;
|
|
1034
1276
|
try {
|
|
1035
|
-
unfinished ||=
|
|
1277
|
+
unfinished ||= wasActive && hasUncheckpointedConversation(ctx.sessionManager.getBranch());
|
|
1036
1278
|
// Projection changes before any wait; capture the old run's boundary before later native input can replace it.
|
|
1037
1279
|
freezeHandoff();
|
|
1038
1280
|
bootstrapContinuation = undefined;
|
|
1039
1281
|
artifactInvalidations = [];
|
|
1040
1282
|
artifactReads.setCandidates([]);
|
|
1041
1283
|
assertPublicationAvailable();
|
|
1042
|
-
|
|
1043
|
-
|
|
1284
|
+
const signal = ctx.signal ? AbortSignal.any([pending.controller.signal, ctx.signal]) : pending.controller.signal;
|
|
1285
|
+
if (branchStartsWithoutRuntime || !selected?.view) {
|
|
1286
|
+
// A pre-runtime choice is native-only; Passive then loads shared memory without initializing storage.
|
|
1287
|
+
const result = complete();
|
|
1288
|
+
if (snapshot.config.mode !== "passive" || runtime?.view) return result;
|
|
1289
|
+
const passive = await loadPassiveView(ctx, signal, isCurrent, true);
|
|
1290
|
+
if (passive && isCurrent() && !signal.aborted && !runtime?.view) {
|
|
1291
|
+
runtime = selected = passive;
|
|
1292
|
+
installScopeStates();
|
|
1293
|
+
updateUi(ctx);
|
|
1294
|
+
}
|
|
1295
|
+
return isCurrent() ? result : superseded();
|
|
1296
|
+
}
|
|
1044
1297
|
const result = await selected.withLifecycleTransaction((publish) => {
|
|
1045
1298
|
signal.throwIfAborted();
|
|
1046
|
-
if (!isCurrent()) throw new Error("State Flow
|
|
1299
|
+
if (!isCurrent()) throw new Error("State Flow mode selection changed while awaiting publication");
|
|
1047
1300
|
assertPublicationAvailable();
|
|
1048
|
-
const
|
|
1049
|
-
const publication = publish(
|
|
1301
|
+
const next = structuredClone(snapshot);
|
|
1302
|
+
const publication = publish(next);
|
|
1050
1303
|
accepted = true;
|
|
1051
|
-
snapshot =
|
|
1304
|
+
snapshot = next;
|
|
1052
1305
|
installScopeStates();
|
|
1053
1306
|
recordPolicyPublication(publication, ctx);
|
|
1054
1307
|
return complete();
|
|
@@ -1058,11 +1311,11 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1058
1311
|
if (!isCurrent()) return superseded();
|
|
1059
1312
|
const cause = diagnosticText(error);
|
|
1060
1313
|
if (accepted) {
|
|
1061
|
-
const message = conciseDiagnostic(`State Flow
|
|
1314
|
+
const message = conciseDiagnostic(`State Flow ${snapshot.config.mode}; lifecycle update failed: ${cause}`);
|
|
1062
1315
|
notifyProblem(ctx, message, "warning");
|
|
1063
1316
|
return { ok: true, message };
|
|
1064
1317
|
}
|
|
1065
|
-
|
|
1318
|
+
modePersistenceError = cause.trim() ? cause : "Canonical State Flow persistence failed";
|
|
1066
1319
|
bootstrapContinuation = undefined;
|
|
1067
1320
|
artifactInvalidations = [];
|
|
1068
1321
|
artifactReads.setCandidates([]);
|
|
@@ -1070,18 +1323,11 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1070
1323
|
}
|
|
1071
1324
|
}
|
|
1072
1325
|
|
|
1073
|
-
pi.registerCommand("state-flow-stop", {
|
|
1074
|
-
description: "Stop State Flow on the current session branch",
|
|
1075
|
-
handler: async (_args, ctx) => {
|
|
1076
|
-
await stopStateFlow(ctx);
|
|
1077
|
-
},
|
|
1078
|
-
});
|
|
1079
|
-
|
|
1080
1326
|
const telegram = createStateFlowTelegramAdapter({
|
|
1081
1327
|
...(options.telegram?.load === undefined ? {} : { load: options.telegram.load }),
|
|
1082
1328
|
port: {
|
|
1083
1329
|
snapshot: () => ({
|
|
1084
|
-
|
|
1330
|
+
mode: snapshot.config.mode,
|
|
1085
1331
|
step: snapshot.meta.step,
|
|
1086
1332
|
revisions: scopeRevisions(),
|
|
1087
1333
|
bootstrap: snapshot.meta.bootstrap === true,
|
|
@@ -1089,13 +1335,11 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1089
1335
|
}),
|
|
1090
1336
|
inspect: inspectTelegramState,
|
|
1091
1337
|
canStartNow: () => activeContext === undefined || activeContext.isIdle(),
|
|
1092
|
-
|
|
1093
|
-
if (!activeContext) throw new Error("State Flow is not attached to an active session yet");
|
|
1094
|
-
return startStateFlow(activeContext);
|
|
1095
|
-
},
|
|
1096
|
-
stop: async () => {
|
|
1338
|
+
select: async (mode) => {
|
|
1097
1339
|
if (!activeContext) throw new Error("State Flow is not attached to an active session yet");
|
|
1098
|
-
const operation =
|
|
1340
|
+
const operation = selectMode(activeContext, mode);
|
|
1341
|
+
if (mode === "active") return operation;
|
|
1342
|
+
// Inactive choices revoke older presentation at once; the receipt belongs to the new lifetime.
|
|
1099
1343
|
const signal = sharedInspectionLifetime.signal;
|
|
1100
1344
|
try {
|
|
1101
1345
|
return { ...await operation, signal };
|
|
@@ -1117,8 +1361,8 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1117
1361
|
pi.on("before_agent_start", (event) => {
|
|
1118
1362
|
// Native run identity is independent of semantic enablement and survives mode toggles.
|
|
1119
1363
|
runAnchorTimestamp = undefined;
|
|
1120
|
-
if (!
|
|
1121
|
-
if (!
|
|
1364
|
+
if (!isActive()) {
|
|
1365
|
+
if (!passiveMemoryAvailable()) return;
|
|
1122
1366
|
(event.systemPromptOptions.sections ??= {}).state_flow = PASSIVE_MEMORY_PROTOCOL;
|
|
1123
1367
|
return;
|
|
1124
1368
|
}
|
|
@@ -1133,20 +1377,23 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1133
1377
|
});
|
|
1134
1378
|
|
|
1135
1379
|
pi.on("context_with_system", (event) => {
|
|
1136
|
-
|
|
1137
|
-
|
|
1380
|
+
// Off removes the owned section, including one contributed before an in-flight mode change.
|
|
1381
|
+
const protocol = isActive() ? stateFlowProtocol(snapshot.meta.bootstrap === true)
|
|
1382
|
+
: passiveMemoryAvailable() ? PASSIVE_MEMORY_PROTOCOL : undefined;
|
|
1138
1383
|
return { messages: projectSystemProtocol(event.messages, protocol) };
|
|
1139
1384
|
});
|
|
1140
1385
|
|
|
1141
1386
|
function projectContext(messages: AgentMessage[]) {
|
|
1387
|
+
// Off injects no State Flow context, including a retained passive handoff.
|
|
1388
|
+
if (snapshot.config.mode === "off") return;
|
|
1142
1389
|
if (runtime?.view) refreshArtifactHints();
|
|
1143
|
-
if (!
|
|
1390
|
+
if (!isActive() && !passiveContinuation && !passiveMemoryAvailable()) return;
|
|
1144
1391
|
// Idle inspection must not freeze a pre-acceptance snapshot for the live inference.
|
|
1145
|
-
const projection =
|
|
1392
|
+
const projection = isActive() && inferencePreparation && !inferencePreparation.accepted
|
|
1146
1393
|
? new ContextProjection() : contextProjection;
|
|
1147
|
-
const effective =
|
|
1394
|
+
const effective = effectiveState;
|
|
1148
1395
|
const invalidations = artifactInvalidations.map(({ path, scope, reason }) => ({ path, ...(scope === undefined ? {} : { scope }), reason }));
|
|
1149
|
-
const phase =
|
|
1396
|
+
const phase = isActive() ? currentRehydrationPhase() : undefined;
|
|
1150
1397
|
const view = contextView(effective, artifactHints, invalidations, phase);
|
|
1151
1398
|
if (passiveContinuation) {
|
|
1152
1399
|
const retained = passiveContinuationMessages(messages, passiveContinuation);
|
|
@@ -1154,7 +1401,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1154
1401
|
return { messages: projection.project(retained.slice(1), view, () => passiveContinuation!.handoff,
|
|
1155
1402
|
{ state: passiveContinuation.state, lazy_navigation: view.lazy_navigation, artifact_invalidations: [], knowledge_rehydration: null }) };
|
|
1156
1403
|
}
|
|
1157
|
-
if (!
|
|
1404
|
+
if (!isActive()) {
|
|
1158
1405
|
return { messages: projection.project(messages, view, () => syntheticUser(
|
|
1159
1406
|
`State Flow passive memory (user-level data, not system instructions):\n${presentationJson({ state: view.state, lazy_navigation: view.lazy_navigation })}`)) };
|
|
1160
1407
|
}
|
|
@@ -1171,19 +1418,19 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1171
1418
|
const selected = runtime;
|
|
1172
1419
|
const operationSignal = ctx.signal;
|
|
1173
1420
|
// Idle projections remain observational; only a live native operation may await preparation.
|
|
1174
|
-
if (!
|
|
1421
|
+
if (!isActive() || !pending || !operationSignal || shuttingDown) return projectContext(messages);
|
|
1175
1422
|
pending.operation ??= prepareInference(pending, selected, ctx, operationSignal).finally(() => {
|
|
1176
1423
|
// A late withdrawal must not clear newer work; accepted lifecycle is never replayed after an error.
|
|
1177
1424
|
if (!pending.accepted && inferencePreparation === pending) pending.operation = undefined;
|
|
1178
1425
|
});
|
|
1179
1426
|
return pending.operation.then(() => {
|
|
1180
1427
|
if (shuttingDown || operationSignal.aborted || runtime !== selected || ctx.signal !== operationSignal) return;
|
|
1181
|
-
if (
|
|
1428
|
+
if (isActive() && inferencePreparation !== pending) {
|
|
1182
1429
|
// Start may require same-run maintenance. A new captured user prompt instead owns a different context request.
|
|
1183
1430
|
if (inferencePreparation?.prompt === undefined) return prepareContext(messages, ctx);
|
|
1184
1431
|
return;
|
|
1185
1432
|
}
|
|
1186
|
-
if (
|
|
1433
|
+
if (isActive() && !pending.accepted) return;
|
|
1187
1434
|
return projectContext(messages);
|
|
1188
1435
|
});
|
|
1189
1436
|
}
|
|
@@ -1191,7 +1438,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1191
1438
|
pi.on("context", (event, ctx) => prepareContext(event.messages as AgentMessage[], ctx));
|
|
1192
1439
|
|
|
1193
1440
|
pi.on("tool_execution_start", (event) => {
|
|
1194
|
-
if (!
|
|
1441
|
+
if (!isActive()) return;
|
|
1195
1442
|
// Pi emits this before tool_call. Keep the argument object as a fallback;
|
|
1196
1443
|
// tool_call replaces it with the mutable, post-preflight input reference.
|
|
1197
1444
|
skillReads.recordStart(event.toolCallId, event.toolName, event.args);
|
|
@@ -1199,21 +1446,19 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1199
1446
|
});
|
|
1200
1447
|
|
|
1201
1448
|
pi.on("tool_call", (event, ctx) => {
|
|
1202
|
-
if (!
|
|
1449
|
+
if (!isActive()) return;
|
|
1203
1450
|
const batch = findAssistantToolBatch(ctx.sessionManager, event.toolCallId);
|
|
1204
1451
|
const patchCalls = batch?.filter((name) => name === PATCH_STATE_TOOL_NAME).length ?? 0;
|
|
1205
1452
|
if (patchCalls > 0) {
|
|
1206
1453
|
if (patchCalls !== 1) {
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
};
|
|
1454
|
+
const reason = "A State Flow barrier response must contain exactly one patch_state call";
|
|
1455
|
+
recordDiagnostic(reason, "barrier-block", ctx, { tool: event.toolName, toolCallId: event.toolCallId, batchToolNames: batch });
|
|
1456
|
+
return { block: true, reason };
|
|
1211
1457
|
}
|
|
1212
1458
|
if (event.toolName !== PATCH_STATE_TOOL_NAME) {
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
};
|
|
1459
|
+
const reason = "Blocked by the patch_state barrier; reconsider this action after State Flow rematerializes context";
|
|
1460
|
+
recordDiagnostic(reason, "barrier-block", ctx, { tool: event.toolName, toolCallId: event.toolCallId, batchToolNames: batch });
|
|
1461
|
+
return { block: true, reason };
|
|
1217
1462
|
}
|
|
1218
1463
|
}
|
|
1219
1464
|
skillReads.recordCall(event.toolCallId, event.toolName, event.input);
|
|
@@ -1221,14 +1466,14 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1221
1466
|
});
|
|
1222
1467
|
|
|
1223
1468
|
pi.on("tool_execution_end", (event) => {
|
|
1224
|
-
if (!
|
|
1469
|
+
if (!isActive()) return;
|
|
1225
1470
|
skillReads.recordEnd(event.toolCallId, event.toolName, event.isError);
|
|
1226
1471
|
dropCurrentSkillReads();
|
|
1227
1472
|
artifactReads.recordEnd(event.toolCallId, event.toolName, event.isError);
|
|
1228
1473
|
});
|
|
1229
1474
|
|
|
1230
1475
|
pi.on("tool_result", (event) => {
|
|
1231
|
-
if (!
|
|
1476
|
+
if (!isActive()) return;
|
|
1232
1477
|
const read = skillReads.recordResult(event.toolName, event.input, event.isError);
|
|
1233
1478
|
if (!read) return;
|
|
1234
1479
|
dropCurrentSkillReads();
|
|
@@ -1240,7 +1485,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1240
1485
|
pi.on("message_end", (event, ctx): any => {
|
|
1241
1486
|
// Observe actual user events even while disabled; Start/Stop cannot invent or erase them.
|
|
1242
1487
|
if (event.message.role === "user" && runAnchorTimestamp === undefined) runAnchorTimestamp = event.message.timestamp;
|
|
1243
|
-
if (shuttingDown || !
|
|
1488
|
+
if (shuttingDown || !isActive()) return;
|
|
1244
1489
|
if (event.message.role !== "assistant") return;
|
|
1245
1490
|
cancelResponseReconciliation();
|
|
1246
1491
|
const message = event.message as unknown as { role: "assistant"; stopReason?: string; content?: unknown };
|
|
@@ -1255,7 +1500,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1255
1500
|
pi.on("turn_end", async (event, ctx) => {
|
|
1256
1501
|
if (shuttingDown) return;
|
|
1257
1502
|
const pending = responseReconciliation;
|
|
1258
|
-
if (!
|
|
1503
|
+
if (!isActive() || !pending) {
|
|
1259
1504
|
updateUi(ctx);
|
|
1260
1505
|
return;
|
|
1261
1506
|
}
|
|
@@ -1267,7 +1512,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1267
1512
|
if (!selected?.view) throw new Error("Temporal State Flow runtime is unavailable; reload before publishing");
|
|
1268
1513
|
await selected.withPatchTransaction((transaction) => {
|
|
1269
1514
|
signal.throwIfAborted();
|
|
1270
|
-
if (runtime !== selected || responseReconciliation !== pending || !
|
|
1515
|
+
if (runtime !== selected || responseReconciliation !== pending || !isActive()) {
|
|
1271
1516
|
throw new Error("State Flow response selection changed while awaiting publication");
|
|
1272
1517
|
}
|
|
1273
1518
|
assertPublicationAvailable();
|
|
@@ -1306,20 +1551,22 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1306
1551
|
});
|
|
1307
1552
|
|
|
1308
1553
|
pi.on("session_before_compact", (event) => {
|
|
1309
|
-
if (!
|
|
1310
|
-
|
|
1311
|
-
|
|
1554
|
+
if (event.reason !== "manual" || !event.customInstructions?.startsWith("state-flow-boundary:")) return;
|
|
1555
|
+
const marker = compactionMarker;
|
|
1556
|
+
if (shuttingDown || compactionStopped || !isActive() || !compactionPlan || !marker || event.customInstructions !== marker) return { cancel: true };
|
|
1557
|
+
const result = stateFlowCompactionResult(compactionPlan, marker, event);
|
|
1312
1558
|
if (result === undefined || "cancel" in result) return result;
|
|
1313
1559
|
return { compaction: result };
|
|
1314
1560
|
});
|
|
1315
1561
|
|
|
1316
1562
|
pi.on("agent_before_settle", async (_event, ctx) => {
|
|
1317
|
-
if (shuttingDown || !turnAcceptedForBackup) return;
|
|
1563
|
+
if (shuttingDown || !memoryToolsAvailable() || !turnAcceptedForBackup) return;
|
|
1318
1564
|
turnAcceptedForBackup = false;
|
|
1319
1565
|
if (!backupPending) return;
|
|
1320
1566
|
backupPending = false;
|
|
1321
1567
|
const operationSignal = ctx.signal;
|
|
1322
|
-
const
|
|
1568
|
+
const lifetime = backupLifetime.signal;
|
|
1569
|
+
const signal = operationSignal ? AbortSignal.any([lifetime, operationSignal]) : lifetime;
|
|
1323
1570
|
const selected = runtime;
|
|
1324
1571
|
try {
|
|
1325
1572
|
if (existsSync(join(repositoryRoot, ".git"))) {
|
|
@@ -1330,9 +1577,9 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1330
1577
|
backupOperations.add(operation);
|
|
1331
1578
|
try { await operation; }
|
|
1332
1579
|
finally { backupOperations.delete(operation); }
|
|
1333
|
-
if (signal.aborted) return;
|
|
1580
|
+
if (signal.aborted || snapshot.config.mode === "off") return;
|
|
1334
1581
|
startStateFlowBackupPush(repositoryRoot, (error) => {
|
|
1335
|
-
if (shuttingDown) return;
|
|
1582
|
+
if (shuttingDown || lifetime.aborted || snapshot.config.mode === "off") return;
|
|
1336
1583
|
const message = diagnosticText(error);
|
|
1337
1584
|
const recorded = diagnosticWriter.recordBackupPushFailure(pushSessionId, pushCwd, message);
|
|
1338
1585
|
if (pushFailureNotified) return;
|
|
@@ -1340,7 +1587,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1340
1587
|
notifyActiveContext(recorded
|
|
1341
1588
|
? `State Flow Git backup push failed; state is saved locally. Details: ${stateFlowLogPath(agentDir)}. A later accepted turn retries.`
|
|
1342
1589
|
: `State Flow Git backup push failed; local diagnostics unavailable: ${message}`);
|
|
1343
|
-
}, () => { pushFailureNotified = false; });
|
|
1590
|
+
}, () => { pushFailureNotified = false; }, lifetime);
|
|
1344
1591
|
}
|
|
1345
1592
|
} catch (error) {
|
|
1346
1593
|
if (signal.aborted || runtime !== selected) return;
|
|
@@ -1353,12 +1600,12 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1353
1600
|
});
|
|
1354
1601
|
|
|
1355
1602
|
pi.on("agent_settled", async (_event, ctx) => {
|
|
1356
|
-
if (telegramStartPending && !
|
|
1603
|
+
if (telegramStartPending && !isActive()) {
|
|
1357
1604
|
telegramStartPending = false;
|
|
1358
1605
|
await startStateFlow(ctx);
|
|
1359
1606
|
return;
|
|
1360
1607
|
}
|
|
1361
|
-
if (!completedRunAccepted || compactionStopped || !
|
|
1608
|
+
if (!completedRunAccepted || compactionStopped || !isActive() || snapshot.meta.bootstrap
|
|
1362
1609
|
|| compactionInFlight || !ctx.isIdle() || ctx.hasPendingMessages() || !runtime?.view
|
|
1363
1610
|
|| !shouldRequestStateFlowCompaction(ctx.getContextUsage())) return;
|
|
1364
1611
|
completedRunAccepted = false;
|
|
@@ -1367,11 +1614,15 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1367
1614
|
const plan = planStateFlowCompaction(entries, runtime.causalBasis(), snapshot.meta.step, runAnchorTimestamp);
|
|
1368
1615
|
if (!plan) return;
|
|
1369
1616
|
compactionPlan = plan;
|
|
1617
|
+
const marker = compactionMarker = `${compactionPrefix}${randomUUID()}`;
|
|
1370
1618
|
compactionInFlight = true;
|
|
1371
1619
|
// Pi dispatches deferred companion prompts after all settled handlers return.
|
|
1372
1620
|
await new Promise<void>((resolve) => {
|
|
1373
|
-
const finished = () => {
|
|
1374
|
-
|
|
1621
|
+
const finished = () => {
|
|
1622
|
+
if (compactionMarker === marker) { compactionPlan = undefined; compactionInFlight = false; }
|
|
1623
|
+
resolve();
|
|
1624
|
+
};
|
|
1625
|
+
ctx.compact({ customInstructions: marker, onComplete: finished, onError: finished });
|
|
1375
1626
|
});
|
|
1376
1627
|
});
|
|
1377
1628
|
|
|
@@ -1389,10 +1640,11 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1389
1640
|
});
|
|
1390
1641
|
pi.on("session_shutdown", async (_event, _ctx) => {
|
|
1391
1642
|
shuttingDown = true;
|
|
1643
|
+
memoryToolLifetime.abort();
|
|
1392
1644
|
contextProjection.reset();
|
|
1393
1645
|
const restoring = cancelBranchRestoration();
|
|
1394
1646
|
const starting = cancelStartActivation();
|
|
1395
|
-
const stopping =
|
|
1647
|
+
const stopping = cancelInactivePersistence();
|
|
1396
1648
|
backupLifetime.abort();
|
|
1397
1649
|
cancelInferencePreparation();
|
|
1398
1650
|
cancelResponseReconciliation();
|