@llblab/pi-kit 0.25.0 → 0.27.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/BACKLOG.md +5 -1
- package/CHANGELOG.md +11 -0
- package/README.md +10 -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 +6 -3
- package/node_modules/@llblab/pi-claude-usage/BACKLOG.md +2 -1
- package/node_modules/@llblab/pi-claude-usage/CHANGELOG.md +8 -0
- package/node_modules/@llblab/pi-claude-usage/README.md +48 -3
- package/node_modules/@llblab/pi-claude-usage/index.ts +8 -1159
- 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 +9 -5
- 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 +7 -6
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +13 -5
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +16 -1
- package/node_modules/@llblab/pi-state-flow/LICENSE +21 -0
- package/node_modules/@llblab/pi-state-flow/README.md +117 -35
- package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.d.ts +24 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.js +80 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.d.ts +18 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.js +41 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +346 -303
- package/node_modules/@llblab/pi-state-flow/dist/lib/git.d.ts +31 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +74 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/operation.d.ts +37 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/operation.js +59 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/ownership.d.ts +31 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/ownership.js +117 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +7 -7
- package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/session.d.ts +10 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/session.js +60 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +6 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +8 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +2 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +28 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +6 -1
- package/node_modules/@llblab/pi-state-flow/dist/package.json +10 -9
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +14 -6
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +2 -2
- package/node_modules/@llblab/pi-state-flow/docs/README.md +19 -9
- package/node_modules/@llblab/pi-state-flow/docs/agent-contract-relocation.md +4 -4
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +646 -89
- package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +116 -37
- package/node_modules/@llblab/pi-state-flow/docs/filesystem-recovery.md +118 -21
- package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +70 -8
- package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +88 -14
- package/node_modules/@llblab/pi-state-flow/docs/performance.md +83 -66
- package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +391 -62
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +317 -61
- package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +85 -1
- package/node_modules/@llblab/pi-state-flow/lib/compaction.ts +49 -2
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +342 -301
- package/node_modules/@llblab/pi-state-flow/lib/git.ts +77 -5
- package/node_modules/@llblab/pi-state-flow/lib/operation.ts +75 -0
- package/node_modules/@llblab/pi-state-flow/lib/ownership.ts +120 -0
- package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +7 -8
- package/node_modules/@llblab/pi-state-flow/lib/query.ts +1 -1
- package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +1 -1
- package/node_modules/@llblab/pi-state-flow/lib/session.ts +52 -1
- package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +8 -7
- package/node_modules/@llblab/pi-state-flow/lib/status.ts +29 -0
- package/node_modules/@llblab/pi-state-flow/lib/transition.ts +5 -1
- package/node_modules/@llblab/pi-state-flow/package.json +10 -9
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +14 -6
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +2 -2
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -0
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +10 -0
- package/node_modules/@llblab/pi-telegram/README.md +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +4 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +16 -12
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +7 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +12 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +137 -83
- package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +26 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.d.ts +57 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.js +109 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/model.js +2 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/status.d.ts +3 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/status.js +31 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/sync.js +4 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +17 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.d.ts +2 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.js +48 -12
- package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +6 -5
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +2 -0
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +2 -2
- package/node_modules/@llblab/pi-telegram/docs/ui-style.md +4 -0
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +17 -10
- package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +8 -2
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +135 -97
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +25 -0
- package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +140 -4
- package/node_modules/@llblab/pi-telegram/lib/model.ts +2 -4
- package/node_modules/@llblab/pi-telegram/lib/status.ts +30 -1
- package/node_modules/@llblab/pi-telegram/lib/sync.ts +4 -4
- package/node_modules/@llblab/pi-telegram/lib/threads.ts +21 -3
- package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +46 -13
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- 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 +8 -8
|
@@ -3,18 +3,11 @@ import { StringEnum, Type } from "@earendil-works/pi-ai";
|
|
|
3
3
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { Text } from "@earendil-works/pi-tui";
|
|
6
|
-
import { randomUUID } from "node:crypto";
|
|
7
6
|
import { existsSync } from "node:fs";
|
|
8
7
|
import { isAbsolute, join, resolve } from "node:path";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
inspectRegisteredArtifactPaths,
|
|
13
|
-
ORDINARY_ARTIFACT_COMPILER,
|
|
14
|
-
sameArtifactSourceFingerprint,
|
|
15
|
-
type ArtifactInvalidationRequest,
|
|
16
|
-
} from "./artifact.ts";
|
|
17
|
-
import { hasCompactionSizedTranscript, planStateFlowCompaction, shouldRequestStateFlowCompaction, stateFlowCompactionResult, type StateFlowCompactionPlan } from "./compaction.ts";
|
|
8
|
+
import { ArtifactAcquisitionState, missingArtifactRemovals } from "./acquisition.ts";
|
|
9
|
+
import { inspectRegisteredArtifactPaths, sameArtifactSourceFingerprint } from "./artifact.ts";
|
|
10
|
+
import { hasCompactionSizedTranscript, planStateFlowCompaction, shouldRequestStateFlowCompaction, StateFlowCompactionRequests } from "./compaction.ts";
|
|
18
11
|
import { inactiveModeFor, loadStateFlowConfig } from "./config.ts";
|
|
19
12
|
import { ContextProjection, contextView, createPassiveContinuation, currentRunTrajectory, passiveContinuationMessages, projectSystemProtocol, runtimeContextHead, syntheticUser, type PassiveContinuation } from "./context.ts";
|
|
20
13
|
import { readNativeSessionHeader } from "./continuation.ts";
|
|
@@ -25,19 +18,20 @@ import {
|
|
|
25
18
|
type SessionAddress,
|
|
26
19
|
} from "./durable.ts";
|
|
27
20
|
import { completeRun, deactivateEpisode, prepareRun, resumeEpisode, startEpisode } from "./episode.ts";
|
|
28
|
-
import { awaitInFlightBackupPushes, backupCurrentStateFlowFiles, startStateFlowBackupPush } from "./git.ts";
|
|
21
|
+
import { awaitInFlightBackupPushes, backupCurrentStateFlowFiles, SettledTurnBackup, startStateFlowBackupPush } from "./git.ts";
|
|
29
22
|
import { projectRecentTransitionsWithLimit } from "./history.ts";
|
|
30
|
-
import { isObject, presentationJson, sameJson
|
|
23
|
+
import { isObject, presentationJson, sameJson } from "./json.ts";
|
|
31
24
|
import { StateFlowDiagnosticWriter, stateFlowLogPath, type DiagnosticExtras, type StateFlowDiagnosticCategory } from "./logging.ts";
|
|
32
25
|
import { assistantToolCallCount, conciseDiagnostic, diagnosticText, finalizedAssistantResponse, formatPatchStateArguments, PASSIVE_MEMORY_PROTOCOL, separatedFailure, separatedOutput, stateFlowProtocol } from "./protocol.ts";
|
|
26
|
+
import { OwnedOperationSlot, RenewableLifetime, type OwnedOperation } from "./operation.ts";
|
|
33
27
|
import { readProjectedState, readStatePath } from "./query.ts";
|
|
34
28
|
import { selectedBoundaryFailure, selectRetainedCheckpoint, waitForRecovery } from "./recovery.ts";
|
|
35
29
|
import type { RehydrationPhase } from "./rehydration.ts";
|
|
36
30
|
import { TemporalRuntime, type RuntimePublication } from "./runtime.ts";
|
|
37
|
-
import { discoverSnapshotData, findAssistantToolBatch, findPassiveStopBoundary, hasPriorConversation, hasUncheckpointedConversation, isNewSession, retainsPhysicalSessionProjection, SNAPSHOT_ENTRY_TYPE } from "./session.ts";
|
|
31
|
+
import { discoverSnapshotData, findAssistantToolBatch, findBranchPolicy, findPassiveStopBoundary, hasPendingFork, hasPriorConversation, hasUncheckpointedConversation, isNewSession, retainsPhysicalSessionProjection, SNAPSHOT_ENTRY_TYPE } from "./session.ts";
|
|
38
32
|
import { hasCompiledSkillArtifact, hashSkillSource, registeredSkillResolver, SkillReadTracker, type SuccessfulSkillRead } from "./skills.ts";
|
|
39
|
-
import { HistoryBoundaryExpiredError, emptySnapshot, migrationFailure, preRuntimeCheckpoint, type InactiveMode, type RetainedBoundaryCheckpoint, type Snapshot, type StateFlowMode } from "./snapshot.ts";
|
|
40
|
-
import { emptyState, projectStateForModel, type AtomicScopePatches, type SemanticState, type ModelState, type
|
|
33
|
+
import { HistoryBoundaryExpiredError, emptySnapshot, migrationFailure, preRuntimeCheckpoint, type InactiveMode, type RetainedBoundaryCheckpoint, type RetainedPiCheckpoint, type Snapshot, type StateFlowMode } from "./snapshot.ts";
|
|
34
|
+
import { emptyState, projectStateForModel, type AtomicScopePatches, type SemanticState, type ModelState, type ScopedStates, type StateScope } from "./state.ts";
|
|
41
35
|
import { compactStatus, detailedStatus, STATUS_KEY, type StatusDiagnostics } from "./status.ts";
|
|
42
36
|
import { PublicationBusyError } from "./storage.ts";
|
|
43
37
|
import { createStateFlowTelegramAdapter, type StateFlowTelegramControlResult, type StateFlowTelegramInspection, type StateFlowTelegramLoader, type StateFlowTelegramScope } from "./telegram.ts";
|
|
@@ -59,14 +53,14 @@ export const PATCH_STATE_TOOL_NAME = "patch_state";
|
|
|
59
53
|
export const READ_STATE_TOOL_NAME = "read_state";
|
|
60
54
|
const PASSIVE_STOP_ENTRY_TYPE = "state-flow-passive-stop";
|
|
61
55
|
|
|
62
|
-
interface InferencePreparation {
|
|
56
|
+
interface InferencePreparation extends OwnedOperation {
|
|
63
57
|
readonly controller: AbortController;
|
|
64
58
|
readonly prompt?: string;
|
|
65
59
|
accepted?: boolean;
|
|
66
60
|
operation?: Promise<void>;
|
|
67
61
|
}
|
|
68
62
|
|
|
69
|
-
interface BranchRestoration {
|
|
63
|
+
interface BranchRestoration extends OwnedOperation {
|
|
70
64
|
readonly controller: AbortController;
|
|
71
65
|
/** Memory acceptance is independent of the requested session mode. */
|
|
72
66
|
awaitingAcceptance: boolean;
|
|
@@ -74,6 +68,15 @@ interface BranchRestoration {
|
|
|
74
68
|
operation?: Promise<void>;
|
|
75
69
|
}
|
|
76
70
|
|
|
71
|
+
interface ModeOperation extends OwnedOperation {
|
|
72
|
+
operation?: Promise<StateFlowTelegramControlResult>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface InactivePersistence extends ModeOperation {
|
|
76
|
+
/** The native mode marker is recorded; later inactive choices start a new persistence. */
|
|
77
|
+
published?: boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
77
80
|
type BranchSelection =
|
|
78
81
|
| { kind: "settled" }
|
|
79
82
|
| { kind: "current"; mode: InactiveMode }
|
|
@@ -93,47 +96,49 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
93
96
|
let selectedHistoryExpired = false;
|
|
94
97
|
let modePersistenceError: string | undefined;
|
|
95
98
|
/** One pending inactive-mode persistence; later inactive choices coalesce until it publishes. */
|
|
96
|
-
|
|
97
|
-
|
|
99
|
+
const inactivePersistence = new OwnedOperationSlot<InactivePersistence>();
|
|
100
|
+
const startActivation = new OwnedOperationSlot<ModeOperation>();
|
|
98
101
|
let forkInitialization = false;
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
102
|
+
const branchRestoration = new OwnedOperationSlot<BranchRestoration>();
|
|
103
|
+
let deferredBranch: { reason: unknown; owner: string; cwd: string } | undefined;
|
|
104
|
+
const responseReconciliation = new OwnedOperationSlot<OwnedOperation>();
|
|
105
|
+
const sharedInspectionLifetime = new RenewableLifetime();
|
|
106
|
+
const memoryToolLifetime = new RenewableLifetime();
|
|
103
107
|
let completedRunAccepted = false;
|
|
104
|
-
|
|
105
|
-
let compactionPlan: StateFlowCompactionPlan | undefined;
|
|
106
|
-
let compactionInFlight = false;
|
|
107
|
-
let compactionStopped = false;
|
|
108
|
-
const compactionMarker = `state-flow-boundary:${randomUUID()}`;
|
|
108
|
+
const compactionRequests = new StateFlowCompactionRequests();
|
|
109
109
|
let passiveContinuation: PassiveContinuation | undefined;
|
|
110
110
|
let bootstrapContinuation: PassiveContinuation | undefined;
|
|
111
111
|
const contextProjection = new ContextProjection();
|
|
112
|
-
|
|
112
|
+
const inferencePreparation = new OwnedOperationSlot<InferencePreparation>();
|
|
113
113
|
let runAnchorTimestamp: number | undefined;
|
|
114
114
|
let runtime: TemporalRuntime | undefined;
|
|
115
115
|
let activeContext: ExtensionContext | undefined;
|
|
116
116
|
let rehydrationPhase: RehydrationPhase | undefined;
|
|
117
117
|
const repositoryRoot = resolve(options.repositoryRoot ?? config.directory);
|
|
118
118
|
const diagnosticWriter = new StateFlowDiagnosticWriter(config.logging, stateFlowLogPath(agentDir), repositoryRoot, (message) => notifyActiveContext(message));
|
|
119
|
-
|
|
120
|
-
const backupLifetime = new AbortController();
|
|
121
|
-
const backupOperations = new Set<Promise<string | undefined>>();
|
|
119
|
+
const backup = new SettledTurnBackup();
|
|
122
120
|
let shuttingDown = false;
|
|
123
|
-
let pushFailureNotified = false;
|
|
124
121
|
const skillReads = new SkillReadTracker(hashSkillSource, (path) => activeContext
|
|
125
122
|
? registeredSkillResolver(activeContext.cwd, pi.getCommands())(path)
|
|
126
123
|
: undefined);
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
let artifactHints: Record<string, string> = {};
|
|
130
|
-
const SOURCE_CHANGED_HINT = "Source changed since this artifact was compiled. Read and recompile it before relying on it.";
|
|
124
|
+
const acquisition = new ArtifactAcquisitionState();
|
|
125
|
+
const artifactReads = acquisition.reads;
|
|
131
126
|
let telegramStartPending = false;
|
|
132
127
|
|
|
133
128
|
function sessionAddress(ctx: ExtensionContext): SessionAddress {
|
|
134
129
|
return resolveSessionAddress(ctx.sessionManager.getSessionFile(), ctx.sessionManager.getSessionId(), ctx.sessionManager.getHeader()?.timestamp);
|
|
135
130
|
}
|
|
136
131
|
|
|
132
|
+
/** Native session identity captured before an await; any change supersedes the waiting operation. */
|
|
133
|
+
function sessionIdentity(ctx: ExtensionContext): () => boolean {
|
|
134
|
+
const cwd = ctx.cwd;
|
|
135
|
+
const owner = ctx.sessionManager.getSessionId();
|
|
136
|
+
const file = ctx.sessionManager.getSessionFile();
|
|
137
|
+
const timestamp = ctx.sessionManager.getHeader()?.timestamp;
|
|
138
|
+
return () => ctx.cwd === cwd && ctx.sessionManager.getSessionId() === owner && ctx.sessionManager.getSessionFile() === file
|
|
139
|
+
&& ctx.sessionManager.getHeader()?.timestamp === timestamp;
|
|
140
|
+
}
|
|
141
|
+
|
|
137
142
|
function notifyProblem(ctx: ExtensionContext, message: string, level: "warning" | "error"): void {
|
|
138
143
|
ctx.ui.notify(conciseDiagnostic(message), level);
|
|
139
144
|
}
|
|
@@ -151,7 +156,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
151
156
|
}
|
|
152
157
|
|
|
153
158
|
function projectModelState(state: SemanticState): SemanticState {
|
|
154
|
-
return projectStateForModel(state,
|
|
159
|
+
return projectStateForModel(state, acquisition.hints);
|
|
155
160
|
}
|
|
156
161
|
|
|
157
162
|
function assertSelectedBranchAvailable(): void {
|
|
@@ -199,109 +204,32 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
199
204
|
ctx.ui.setStatus(STATUS_KEY, compactStatus(snapshot, scopeRevisions(), (color, text) => ctx.ui.theme.fg(color, text)));
|
|
200
205
|
}
|
|
201
206
|
|
|
202
|
-
function cancelInactivePersistence(): Promise<StateFlowTelegramControlResult> | undefined {
|
|
203
|
-
const pending = inactivePersistence;
|
|
204
|
-
pending?.controller.abort();
|
|
205
|
-
inactivePersistence = undefined;
|
|
206
|
-
return pending?.operation;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function cancelStartActivation(): Promise<StateFlowTelegramControlResult> | undefined {
|
|
210
|
-
const pending = startActivation;
|
|
211
|
-
pending?.controller.abort();
|
|
212
|
-
startActivation = undefined;
|
|
213
|
-
return pending?.operation;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function cancelBranchRestoration(): Promise<void> | undefined {
|
|
217
|
-
const pending = branchRestoration;
|
|
218
|
-
pending?.controller.abort();
|
|
219
|
-
branchRestoration = undefined;
|
|
220
|
-
return pending?.operation;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function cancelResponseReconciliation(): void {
|
|
224
|
-
responseReconciliation?.abort();
|
|
225
|
-
responseReconciliation = undefined;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
207
|
function cancelSharedInspections(): void {
|
|
229
|
-
sharedInspectionLifetime.
|
|
230
|
-
sharedInspectionLifetime = new AbortController();
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function cancelInferencePreparation(): void {
|
|
234
|
-
inferencePreparation?.controller.abort();
|
|
235
|
-
inferencePreparation = undefined;
|
|
208
|
+
sharedInspectionLifetime.renew(new Error("State Flow inspection was cancelled; select the scope again"));
|
|
236
209
|
}
|
|
237
210
|
|
|
238
211
|
function clearRunTransient(): void {
|
|
239
|
-
|
|
240
|
-
|
|
212
|
+
inferencePreparation.cancel();
|
|
213
|
+
responseReconciliation.cancel();
|
|
241
214
|
cancelSharedInspections();
|
|
242
215
|
completedRunAccepted = false;
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
compactionInFlight = false;
|
|
216
|
+
backup.clearTurn();
|
|
217
|
+
compactionRequests.clear();
|
|
246
218
|
skillReads.clear();
|
|
247
219
|
artifactReads.clear();
|
|
248
220
|
}
|
|
249
221
|
|
|
250
222
|
function deferInferencePreparation(prompt?: string): void {
|
|
251
|
-
|
|
252
|
-
inferencePreparation
|
|
253
|
-
|
|
254
|
-
artifactReads.setCandidates([]);
|
|
223
|
+
inferencePreparation.cancel();
|
|
224
|
+
inferencePreparation.claim({ controller: new AbortController(), prompt });
|
|
225
|
+
acquisition.clearInvalidations();
|
|
255
226
|
}
|
|
256
227
|
|
|
257
228
|
function refreshArtifactHints(): void {
|
|
258
|
-
if (
|
|
259
|
-
|
|
260
|
-
artifactInvalidations = [];
|
|
261
|
-
artifactReads.setCandidates([]);
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
const paths = new Set<string>();
|
|
265
|
-
for (const scope of ["global", "cwd", "session"] as const) for (const path of Object.keys(scopeStates[scope].artifacts)) paths.add(path);
|
|
266
|
-
const observations = new Map(inspectRegisteredArtifactPaths(paths).map((observation) => [observation.path, observation]));
|
|
267
|
-
const nextHints: Record<string, string> = {};
|
|
268
|
-
const nextInvalidations = new Map<string, ArtifactInvalidationRequest>();
|
|
269
|
-
for (const scope of ["global", "cwd", "session"] as const) {
|
|
270
|
-
const provenance = runtime.artifactProvenance(scope);
|
|
271
|
-
for (const [path, metadata] of Object.entries(scopeStates[scope].artifacts)) {
|
|
272
|
-
delete nextHints[path];
|
|
273
|
-
nextInvalidations.delete(path);
|
|
274
|
-
if (metadata.kind === "skill") continue;
|
|
275
|
-
const observed = observations.get(path);
|
|
276
|
-
if (observed?.kind !== "present") continue;
|
|
277
|
-
const need = classifyArtifactCompilationNeed({ path, scope, sourceFingerprint: observed.fingerprint }, metadata, ORDINARY_ARTIFACT_COMPILER, false, provenance[path]);
|
|
278
|
-
if (need.kind !== "requires-compilation") continue;
|
|
279
|
-
if (need.reason === "source-changed") nextHints[path] = SOURCE_CHANGED_HINT;
|
|
280
|
-
nextInvalidations.set(path, { path, scope, reason: need.reason });
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
artifactHints = nextHints;
|
|
284
|
-
artifactInvalidations = [...nextInvalidations.values()].sort((left, right) => left.path.localeCompare(right.path));
|
|
285
|
-
artifactReads.setCandidates(artifactInvalidations);
|
|
229
|
+
if (runtime?.view) acquisition.refresh(scopeStates, (scope) => runtime!.artifactProvenance(scope));
|
|
230
|
+
else acquisition.reset();
|
|
286
231
|
}
|
|
287
232
|
|
|
288
|
-
function missingArtifactRemovals(states: ScopedSemanticStates): AtomicScopePatches {
|
|
289
|
-
const owners = new Map<string, StateScope[]>();
|
|
290
|
-
for (const scope of ["global", "cwd", "session"] as const) for (const path of Object.keys(states[scope].artifacts ?? {})) {
|
|
291
|
-
owners.set(path, [...owners.get(path) ?? [], scope]);
|
|
292
|
-
}
|
|
293
|
-
const removals: AtomicScopePatches = {};
|
|
294
|
-
for (const observation of inspectRegisteredArtifactPaths(owners.keys())) {
|
|
295
|
-
if (observation.kind !== "missing") continue;
|
|
296
|
-
for (const scope of owners.get(observation.path) ?? []) {
|
|
297
|
-
const artifacts = (removals[scope]?.artifacts ?? {}) as JsonObject;
|
|
298
|
-
removals[scope] = { ...(removals[scope] ?? {}), artifacts: { ...artifacts, [observation.path]: null } };
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
return removals;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
|
|
305
233
|
function installScopeStates(): void {
|
|
306
234
|
scopeStates = runtime?.view ? runtime.states() : { global: emptyState(), cwd: emptyState(), session: emptyState() };
|
|
307
235
|
effectiveState = runtime?.view ? runtime.readView() : {};
|
|
@@ -319,17 +247,28 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
319
247
|
|
|
320
248
|
async function inspectTelegramState(scope: StateFlowTelegramScope): Promise<StateFlowTelegramInspection> {
|
|
321
249
|
if (!activeContext || shuttingDown) throw new Error("State Flow is not attached to an active session yet");
|
|
322
|
-
|
|
323
|
-
const
|
|
250
|
+
const ctx = activeContext;
|
|
251
|
+
const owner = sessionAddress(ctx).key;
|
|
252
|
+
const cwd = ctx.cwd;
|
|
253
|
+
const off = snapshot.config.mode === "off";
|
|
254
|
+
const privateScope = scope === "session" || scope === "effective";
|
|
255
|
+
if (!off && privateScope) assertSelectedBranchAvailable();
|
|
256
|
+
// Off operator reads are disposable: they confer no selected-branch/cache authority.
|
|
257
|
+
const selected = off ? createRuntime(ctx) : runtime ??= createRuntime(ctx);
|
|
324
258
|
const signal = sharedInspectionLifetime.signal;
|
|
325
|
-
|
|
259
|
+
let privateAvailable = true;
|
|
260
|
+
if (off && privateScope) privateAvailable = await selected.refreshCurrentMemory(signal) !== undefined;
|
|
261
|
+
else if (!selected.view || (scope !== "session" && !modePersistenceError)) await selected.refreshShared(signal);
|
|
326
262
|
signal.throwIfAborted();
|
|
327
|
-
if (
|
|
328
|
-
|
|
263
|
+
if (activeContext !== ctx || ctx.cwd !== cwd || sessionAddress(ctx).key !== owner || (!off && runtime !== selected)) {
|
|
264
|
+
throw new Error("State Flow inspection selection changed; select the scope again");
|
|
265
|
+
}
|
|
266
|
+
if (!privateAvailable) throw new Error("Current State Flow session memory is unavailable");
|
|
267
|
+
if (!off && privateScope) assertSelectedBranchAvailable();
|
|
329
268
|
if (!selected.view) throw new Error("State Flow temporal runtime is unavailable");
|
|
330
|
-
installScopeStates();
|
|
331
|
-
const state = scope === "effective" ?
|
|
332
|
-
return { state: { ...projectModelState(state), ...(state.lazy === undefined ? {} : { lazy: structuredClone(state.lazy) }) }, revisions:
|
|
269
|
+
if (!off) installScopeStates();
|
|
270
|
+
const state = selected.readView(0, scope === "effective" ? undefined : scope);
|
|
271
|
+
return { state: { ...projectModelState(state), ...(state.lazy === undefined ? {} : { lazy: structuredClone(state.lazy) }) }, revisions: temporalScopeRevisions(selected.view), signal };
|
|
333
272
|
}
|
|
334
273
|
|
|
335
274
|
function syncStateFlowTools(): void {
|
|
@@ -342,15 +281,11 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
342
281
|
: active.filter((name) => !owned.includes(name)));
|
|
343
282
|
}
|
|
344
283
|
|
|
345
|
-
|
|
284
|
+
/** Accepted canonical publication establishes runtime authority and makes the settled-turn Git backup due. */
|
|
285
|
+
function recordPublication(): void {
|
|
346
286
|
// A passive view is not runtime authority; only accepted canonical publication establishes it.
|
|
347
287
|
branchStartsWithoutRuntime = false;
|
|
348
|
-
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
/** Record accepted lifecycle persistence; Git backup is scheduled only after an accepted turn settles. */
|
|
352
|
-
function recordPolicyPublication(publication: RuntimePublication | undefined, ctx: ExtensionContext): void {
|
|
353
|
-
if (publication) recordPublication(publication, ctx);
|
|
288
|
+
backup.markPublished();
|
|
354
289
|
}
|
|
355
290
|
|
|
356
291
|
function skillReadIsCurrent(read: SuccessfulSkillRead): boolean {
|
|
@@ -374,11 +309,9 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
374
309
|
return `State Flow acquisition: this registered Skill belongs at ${target}. If durable compiled guidance is useful, include a non-empty description, kind:"skill", and compilation object there. Unrelated semantic patches do not need to include it.`;
|
|
375
310
|
}
|
|
376
311
|
|
|
377
|
-
function clearAcceptedAcquisitions(acquiredArtifactPaths
|
|
378
|
-
|
|
379
|
-
artifactReads.setCandidates(artifactInvalidations);
|
|
312
|
+
function clearAcceptedAcquisitions(acquiredArtifactPaths?: ReadonlySet<string>): void {
|
|
313
|
+
acquisition.acceptAcquired(acquiredArtifactPaths);
|
|
380
314
|
dropCurrentSkillReads();
|
|
381
|
-
artifactReads.clear();
|
|
382
315
|
}
|
|
383
316
|
|
|
384
317
|
async function prepareInference(pending: InferencePreparation, selected: TemporalRuntime | undefined, ctx: ExtensionContext, operationSignal: AbortSignal): Promise<void> {
|
|
@@ -387,7 +320,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
387
320
|
if (!selected?.view) throw new Error("Temporal State Flow runtime is unavailable; reload before publishing");
|
|
388
321
|
await selected.withPatchTransaction((transaction) => {
|
|
389
322
|
signal.throwIfAborted();
|
|
390
|
-
if (runtime !== selected || inferencePreparation
|
|
323
|
+
if (runtime !== selected || !inferencePreparation.owns(pending) || !isActive() || shuttingDown) {
|
|
391
324
|
throw new Error("State Flow inference selection changed while awaiting publication");
|
|
392
325
|
}
|
|
393
326
|
assertPublicationAvailable();
|
|
@@ -410,13 +343,13 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
410
343
|
snapshot = nextSnapshot;
|
|
411
344
|
installScopeStates();
|
|
412
345
|
if (rotatesRun && rehydrationPhase !== "new-bootstrap" && rehydrationPhase !== "resume-bootstrap") rehydrationPhase = "step";
|
|
413
|
-
if (publication?.changed) recordPublication(
|
|
346
|
+
if (publication?.changed) recordPublication();
|
|
414
347
|
if (pending.prompt !== undefined || publication?.changed) appendCheckpoint();
|
|
415
348
|
if (Object.keys(removals).length > 0) clearAcceptedAcquisitions();
|
|
416
349
|
updateUi(ctx);
|
|
417
350
|
}, signal);
|
|
418
351
|
} catch (error) {
|
|
419
|
-
if (signal.aborted || runtime !== selected || inferencePreparation
|
|
352
|
+
if (signal.aborted || runtime !== selected || !inferencePreparation.owns(pending) || shuttingDown) return;
|
|
420
353
|
// Pi reports context-hook exceptions and continues. Abort through its public port rather than infer from stale preparation.
|
|
421
354
|
ctx.abort();
|
|
422
355
|
const cause = diagnosticText(error);
|
|
@@ -428,27 +361,22 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
428
361
|
}
|
|
429
362
|
|
|
430
363
|
|
|
431
|
-
function currentRehydrationPhase(): RehydrationPhase | undefined {
|
|
432
|
-
return rehydrationPhase;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
364
|
function statusDiagnostics(ctx: ExtensionContext): StatusDiagnostics {
|
|
436
365
|
const cwd = ctx.cwd;
|
|
437
|
-
const diagnosticStates = scopeStates;
|
|
438
366
|
const view = runtime?.view;
|
|
439
367
|
const diagnosticRecent = runtime?.recent() ?? [];
|
|
440
368
|
const durableStateError = snapshot.meta.validation?.attempt === 0 ? snapshot.meta.validation.error
|
|
441
369
|
: view ? undefined : "no temporal runtime is selected on this branch";
|
|
442
370
|
|
|
443
371
|
const staleArtifacts: StatusDiagnostics["staleArtifacts"] = durableStateError === undefined
|
|
444
|
-
?
|
|
372
|
+
? acquisition.invalidations.map(({ path, scope, reason }) => ({ scope: scope ?? "global", path, reason }))
|
|
445
373
|
: [];
|
|
446
374
|
const session = sessionAddress(ctx);
|
|
447
375
|
return {
|
|
448
376
|
repositoryRoot,
|
|
449
377
|
cwdScopeKey: cwdScopeKey(cwd),
|
|
450
378
|
sessionScopeKey: sessionScopeKey(session.key),
|
|
451
|
-
scopeStates
|
|
379
|
+
scopeStates,
|
|
452
380
|
effectiveState,
|
|
453
381
|
recent: diagnosticRecent,
|
|
454
382
|
historyLimit: config.historyLimit,
|
|
@@ -473,21 +401,66 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
473
401
|
}
|
|
474
402
|
|
|
475
403
|
/** Select the active native branch under one owned restoration lifetime; only current accepted work installs memory. */
|
|
476
|
-
function restoreActiveBranch(ctx: ExtensionContext, sessionStartReason?: unknown, notifyRecovery = true, startOwner?: AbortController): Promise<void> {
|
|
404
|
+
function restoreActiveBranch(ctx: ExtensionContext, sessionStartReason?: unknown, notifyRecovery = true, startOwner?: AbortController, requestedMode?: InactiveMode): Promise<void> {
|
|
477
405
|
contextProjection.reset();
|
|
478
|
-
|
|
406
|
+
memoryToolLifetime.renew();
|
|
407
|
+
branchRestoration.cancel();
|
|
479
408
|
// Start-owned attachment/fork recovery keeps its owner; only accepted Start cancels Stop.
|
|
480
409
|
if (!startOwner) {
|
|
481
|
-
|
|
482
|
-
|
|
410
|
+
startActivation.cancel();
|
|
411
|
+
inactivePersistence.cancel();
|
|
483
412
|
}
|
|
484
413
|
clearRunTransient();
|
|
485
414
|
passiveContinuation = undefined;
|
|
486
415
|
bootstrapContinuation = undefined;
|
|
487
|
-
|
|
488
|
-
artifactReads.setCandidates([]);
|
|
416
|
+
acquisition.clearInvalidations();
|
|
489
417
|
telegramStartPending = false;
|
|
490
418
|
activeContext = ctx;
|
|
419
|
+
if (!startOwner) {
|
|
420
|
+
let mode: StateFlowMode = config.inactiveMode;
|
|
421
|
+
let adoptDefault = false;
|
|
422
|
+
let withoutRuntime = false;
|
|
423
|
+
let persistenceError: string | undefined;
|
|
424
|
+
let pendingFork = false;
|
|
425
|
+
try {
|
|
426
|
+
const branch = ctx.sessionManager.getBranch();
|
|
427
|
+
const discovery = discoverSnapshotData(branch);
|
|
428
|
+
withoutRuntime = discovery.candidates.length === 0 && discovery.errors.length === 0;
|
|
429
|
+
adoptDefault = withoutRuntime && isNewSession(sessionStartReason, branch);
|
|
430
|
+
pendingFork = hasPendingFork(branch, ctx.sessionManager.getSessionId(), PASSIVE_STOP_ENTRY_TYPE);
|
|
431
|
+
const inherited = sessionStartReason === "fork" || pendingFork;
|
|
432
|
+
const policy = findBranchPolicy(branch, inherited ? undefined : ctx.sessionManager.getSessionId(),
|
|
433
|
+
inherited || retainsPhysicalSessionProjection(sessionStartReason) ? PASSIVE_STOP_ENTRY_TYPE : undefined, config.inactiveMode);
|
|
434
|
+
mode = policy?.mode ?? (adoptDefault ? config.mode : config.inactiveMode);
|
|
435
|
+
persistenceError = inherited
|
|
436
|
+
? findBranchPolicy(branch, ctx.sessionManager.getSessionId(), PASSIVE_STOP_ENTRY_TYPE, config.inactiveMode)?.persistenceError
|
|
437
|
+
: policy?.persistenceError;
|
|
438
|
+
} catch {
|
|
439
|
+
// Off needs only policy; unavailable native evidence grants no memory authority.
|
|
440
|
+
}
|
|
441
|
+
if (mode === "off") {
|
|
442
|
+
backup.cancel();
|
|
443
|
+
const owner = sessionAddress(ctx).key;
|
|
444
|
+
const reason = pendingFork || (deferredBranch?.owner === owner && deferredBranch.cwd === ctx.cwd && deferredBranch.reason === "fork"
|
|
445
|
+
&& retainsPhysicalSessionProjection(sessionStartReason)) ? "fork" : sessionStartReason;
|
|
446
|
+
if (reason === "fork" && !pendingFork) pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, { owner: ctx.sessionManager.getSessionId(), forkPending: true });
|
|
447
|
+
deferredBranch = { reason, owner, cwd: ctx.cwd };
|
|
448
|
+
runtime = undefined;
|
|
449
|
+
snapshot = emptySnapshot("off");
|
|
450
|
+
branchStartsWithoutRuntime = withoutRuntime;
|
|
451
|
+
selectedHistoryExpired = false;
|
|
452
|
+
modePersistenceError = persistenceError;
|
|
453
|
+
forkInitialization = reason === "fork";
|
|
454
|
+
installScopeStates();
|
|
455
|
+
acquisition.reset();
|
|
456
|
+
if (adoptDefault) appendCheckpoint();
|
|
457
|
+
syncStateFlowTools();
|
|
458
|
+
updateUi(ctx);
|
|
459
|
+
return Promise.resolve();
|
|
460
|
+
}
|
|
461
|
+
if (pendingFork) sessionStartReason = "fork";
|
|
462
|
+
}
|
|
463
|
+
deferredBranch = undefined;
|
|
491
464
|
runtime = createRuntime(ctx);
|
|
492
465
|
installScopeStates();
|
|
493
466
|
branchStartsWithoutRuntime = false;
|
|
@@ -548,17 +521,15 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
548
521
|
snapshot = migrationFailure({}, "State Flow branch restoration is pending", selection.kind === "current" ? selection.mode
|
|
549
522
|
: selection.kind === "auto-start" ? config.inactiveMode : inactiveSelection(selection.checkpoint.mode));
|
|
550
523
|
}
|
|
524
|
+
if (requestedMode) snapshot = deactivateEpisode(snapshot, requestedMode);
|
|
551
525
|
const pending: BranchRestoration = {
|
|
552
526
|
controller: new AbortController(),
|
|
553
527
|
awaitingAcceptance: selection.kind !== "settled" && selection.kind !== "current",
|
|
528
|
+
...(requestedMode === undefined ? {} : { requestedMode }),
|
|
554
529
|
};
|
|
555
|
-
branchRestoration
|
|
556
|
-
|
|
557
|
-
pending
|
|
558
|
-
restorationOperations.add(operation);
|
|
559
|
-
const finished = () => { restorationOperations.delete(operation); };
|
|
560
|
-
void operation.then(finished, finished);
|
|
561
|
-
return operation;
|
|
530
|
+
branchRestoration.claim(pending);
|
|
531
|
+
// attachBranch releases its own ownership; settlement only drains it for shutdown.
|
|
532
|
+
return branchRestoration.track(pending, attachBranch(ctx, pending, selection, sessionStartReason, notifyRecovery, skipped), false);
|
|
562
533
|
}
|
|
563
534
|
|
|
564
535
|
async function attachBranch(
|
|
@@ -568,26 +539,23 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
568
539
|
const placeholder = runtime;
|
|
569
540
|
let selected = runtime;
|
|
570
541
|
const owner = ctx.sessionManager.getSessionId();
|
|
571
|
-
const
|
|
572
|
-
const file = ctx.sessionManager.getSessionFile();
|
|
573
|
-
const timestamp = ctx.sessionManager.getHeader()?.timestamp;
|
|
542
|
+
const sameSession = sessionIdentity(ctx);
|
|
574
543
|
const signal = AbortSignal.any([pending.controller.signal, ...(ctx.signal ? [ctx.signal] : [])]);
|
|
575
|
-
const isCurrent = () => branchRestoration
|
|
576
|
-
&&
|
|
577
|
-
&& ctx.sessionManager.getHeader()?.timestamp === timestamp;
|
|
544
|
+
const isCurrent = () => branchRestoration.owns(pending) && !pending.controller.signal.aborted && !shuttingDown && runtime === selected
|
|
545
|
+
&& sameSession();
|
|
578
546
|
const assertCurrent = () => {
|
|
579
547
|
signal.throwIfAborted();
|
|
580
548
|
if (!isCurrent()) throw new Error("State Flow branch selection changed while awaiting publication");
|
|
581
549
|
};
|
|
582
550
|
let accepted = false;
|
|
583
551
|
// Install accepted memory before native writes; later ancillary failures cannot revert or replay it.
|
|
584
|
-
const accept = (candidate: TemporalRuntime, next: Snapshot,
|
|
552
|
+
const accept = (candidate: TemporalRuntime, next: Snapshot, nativeWrites: () => void): void => {
|
|
585
553
|
accepted = true;
|
|
586
554
|
pending.awaitingAcceptance = false;
|
|
587
555
|
runtime = selected = candidate;
|
|
588
556
|
snapshot = next;
|
|
589
557
|
installScopeStates();
|
|
590
|
-
|
|
558
|
+
recordPublication();
|
|
591
559
|
let failure: unknown;
|
|
592
560
|
try { nativeWrites(); } catch (error) { failure = error; }
|
|
593
561
|
settleSelection(ctx, reason, notifyRecovery, skipped, true);
|
|
@@ -614,16 +582,17 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
614
582
|
if (restored.config.mode === "active" && restored.meta.specification === undefined
|
|
615
583
|
&& retainsPhysicalSessionProjection(reason) && hasUncheckpointedConversation(ctx.sessionManager.getBranch())) restored.meta.bootstrap = true;
|
|
616
584
|
const next = pending.requestedMode ? deactivateEpisode(restored, pending.requestedMode) : restored;
|
|
617
|
-
|
|
585
|
+
publish(next);
|
|
586
|
+
accept(candidate, next, appendCheckpoint);
|
|
618
587
|
}, signal);
|
|
619
588
|
} else if (selection.kind === "fork") {
|
|
620
589
|
await candidate.withForkTransaction(selection.source, selection.checkpoint, (child, publish) => {
|
|
621
590
|
assertCurrent();
|
|
622
591
|
// Mode is selected independently of creating the child's private memory.
|
|
623
592
|
const next = pending.requestedMode ? deactivateEpisode(child, pending.requestedMode) : child;
|
|
624
|
-
|
|
593
|
+
publish(next);
|
|
625
594
|
forkInitialization = false;
|
|
626
|
-
accept(candidate, next,
|
|
595
|
+
accept(candidate, next, () => {
|
|
627
596
|
pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, { reset: true, owner });
|
|
628
597
|
appendCheckpoint();
|
|
629
598
|
});
|
|
@@ -637,7 +606,8 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
637
606
|
if (current || discovery.candidates.length > 0 || discovery.errors.length > 0) throw new Error("Existing session runtime requires retained-boundary restoration");
|
|
638
607
|
const activated = startEpisode(hasPriorConversation(branch));
|
|
639
608
|
const next = pending.requestedMode ? deactivateEpisode(activated, pending.requestedMode) : activated;
|
|
640
|
-
|
|
609
|
+
publish(next);
|
|
610
|
+
accept(candidate, next, appendCheckpoint);
|
|
641
611
|
}, signal, true);
|
|
642
612
|
}
|
|
643
613
|
} catch (error) {
|
|
@@ -671,7 +641,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
671
641
|
installScopeStates();
|
|
672
642
|
} finally {
|
|
673
643
|
pending.awaitingAcceptance = false;
|
|
674
|
-
|
|
644
|
+
branchRestoration.release(pending);
|
|
675
645
|
}
|
|
676
646
|
}
|
|
677
647
|
|
|
@@ -687,25 +657,24 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
687
657
|
return passive;
|
|
688
658
|
}
|
|
689
659
|
|
|
660
|
+
function selectedContinuation(ctx: ExtensionContext, reason: unknown): PassiveContinuation | undefined {
|
|
661
|
+
if (!retainsPhysicalSessionProjection(reason) || !runtime?.view) return undefined;
|
|
662
|
+
const boundary = findPassiveStopBoundary(ctx.sessionManager.getBranch(), ctx.sessionManager.getSessionId(), PASSIVE_STOP_ENTRY_TYPE);
|
|
663
|
+
return boundary === undefined ? undefined : createPassiveContinuation(
|
|
664
|
+
projectModelState(effectiveState), boundary.at, boundary.from, boundary.preserveContext,
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
|
|
690
668
|
function settleSelection(ctx: ExtensionContext, reason: unknown, notifyRecovery: boolean, skipped: number, selectedMemory: boolean): void {
|
|
691
669
|
const failure = !isActive() && snapshot.meta.validation?.attempt === 0 ? snapshot.meta.validation.error : undefined;
|
|
692
670
|
if (failure !== undefined && notifyRecovery && !modePersistenceError) {
|
|
693
671
|
if (selectedHistoryExpired) notifyProblem(ctx, "State Flow history is outside the retained temporal window; /state-flow-active can use current session memory.", "warning");
|
|
694
672
|
else notifyProblem(ctx, `State Flow restore failed: ${failure}`, "error");
|
|
695
673
|
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
if (
|
|
699
|
-
|
|
700
|
-
projectModelState(effectiveState),
|
|
701
|
-
boundary.at,
|
|
702
|
-
boundary.from,
|
|
703
|
-
boundary.preserveContext,
|
|
704
|
-
);
|
|
705
|
-
// Off retains the handoff boundary for a later Passive or Active choice but never projects it.
|
|
706
|
-
if (!isActive()) passiveContinuation = continuation;
|
|
707
|
-
else if (snapshot.meta.bootstrap) bootstrapContinuation = continuation;
|
|
708
|
-
}
|
|
674
|
+
const continuation = selectedMemory ? selectedContinuation(ctx, reason) : undefined;
|
|
675
|
+
if (continuation !== undefined) {
|
|
676
|
+
if (!isActive()) passiveContinuation = continuation;
|
|
677
|
+
else if (snapshot.meta.bootstrap) bootstrapContinuation = continuation;
|
|
709
678
|
}
|
|
710
679
|
if (notifyRecovery && isActive() && skipped > 0) {
|
|
711
680
|
notifyProblem(ctx, `State Flow skipped ${skipped} malformed snapshot(s); restored the last valid one.`, "warning");
|
|
@@ -722,7 +691,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
722
691
|
pi.registerTool({
|
|
723
692
|
name: READ_STATE_TOOL_NAME,
|
|
724
693
|
label: "Read State",
|
|
725
|
-
description: "Read exact State Flow values or historical semantic patches with one path or an ordered path list. Unscoped semantic paths read the effective overlay; effective makes that overlay explicit, while global, cwd, and session select ownership. Array selectors support zero-based indices and half-open [start..end] ranges. Projection value returns semantic data; keys returns minimal structure; patch intersects the selected path at its boundary.",
|
|
694
|
+
description: "Read exact State Flow values or historical semantic patches with one path or an ordered path list. Unscoped semantic paths read the effective overlay; effective makes that overlay explicit, while global, cwd, and session select ownership. Array selectors support zero-based indices and half-open [start..end] ranges. Projection value returns semantic data; keys returns minimal structure; patch intersects the selected path at its boundary. Path object keys must be ASCII identifiers ([A-Za-z_$][A-Za-z0-9_$-]*); non-ASCII keys cannot be addressed.",
|
|
726
695
|
promptSnippet: "Read exact state values, structures, or historical semantic patches",
|
|
727
696
|
parameters: Type.Object({
|
|
728
697
|
path: Type.Optional(Type.String({ description: "One semantic path; unscoped paths use effective, explicit roots may use effective/global/cwd/session and historical [N], and arrays support half-open [start..end] ranges" })),
|
|
@@ -764,7 +733,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
764
733
|
pi.registerTool({
|
|
765
734
|
name: PATCH_STATE_TOOL_NAME,
|
|
766
735
|
label: "Patch State",
|
|
767
|
-
description: "The sole State Flow semantic mutation protocol. Supply one or more global, cwd, or session patches; all supplied scopes commit atomically. This call must be the only State Flow barrier in its assistant response.",
|
|
736
|
+
description: "The sole State Flow semantic mutation protocol. Supply one or more global, cwd, or session patches; all supplied scopes commit atomically. This call must be the only State Flow barrier in its assistant response. Deleting an intent also deletes same-scope working/lazy keys its {\"$ref\"} values own unless another intent refs them. Name keys in ASCII ([A-Za-z_$][A-Za-z0-9_$-]*) so read paths and refs resolve; values may use any language.",
|
|
768
737
|
promptSnippet: "Atomically patch one or more global/cwd/session scopes",
|
|
769
738
|
promptGuidelines: [
|
|
770
739
|
"Use patch_state only for material durable semantic changes; ordinary answers need no finalization call.",
|
|
@@ -784,6 +753,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
784
753
|
return new Text(separatedOutput(theme.fg("dim", formatPatchStateArguments(context.args))), 0, 0);
|
|
785
754
|
},
|
|
786
755
|
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
756
|
+
signal = signal ? AbortSignal.any([signal, memoryToolLifetime.signal]) : memoryToolLifetime.signal;
|
|
787
757
|
try {
|
|
788
758
|
if (!memoryToolsAvailable()) throw new Error("State Flow tools are off for this session");
|
|
789
759
|
assertPublicationAvailable();
|
|
@@ -832,12 +802,12 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
832
802
|
snapshot = nextSnapshot;
|
|
833
803
|
installScopeStates();
|
|
834
804
|
if (publication?.changed) {
|
|
835
|
-
recordPublication(
|
|
805
|
+
recordPublication();
|
|
836
806
|
appendCheckpoint();
|
|
837
807
|
}
|
|
838
808
|
clearAcceptedAcquisitions(new Set(acquiredArtifacts.map(({ path }) => path)));
|
|
839
809
|
updateUi(ctx);
|
|
840
|
-
const updates = contextProjection.acceptPatch(previousEffective, effectiveState, patches,
|
|
810
|
+
const updates = contextProjection.acceptPatch(previousEffective, effectiveState, patches, acquisition.hints);
|
|
841
811
|
const acknowledgement = changed
|
|
842
812
|
? `\nState materialized atomically at ${scopes.join("+")} scope${scopes.length === 1 ? "" : "s"}.`
|
|
843
813
|
: "\nState already current.";
|
|
@@ -850,7 +820,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
850
820
|
let attempted: unknown;
|
|
851
821
|
try { attempted = structuredClone(params); } catch { attempted = undefined; }
|
|
852
822
|
const cause = diagnosticText(error);
|
|
853
|
-
recordDiagnostic(cause, /concurrently|advanced/.test(cause) ? "publication-conflict" : "invalid-patch", ctx, {
|
|
823
|
+
if (!signal.aborted && memoryToolsAvailable()) recordDiagnostic(cause, /concurrently|advanced/.test(cause) ? "publication-conflict" : "invalid-patch", ctx, {
|
|
854
824
|
input: attempted,
|
|
855
825
|
tool: PATCH_STATE_TOOL_NAME,
|
|
856
826
|
toolCallId,
|
|
@@ -862,42 +832,33 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
862
832
|
|
|
863
833
|
function startStateFlow(ctx: ExtensionContext): Promise<StateFlowTelegramControlResult> {
|
|
864
834
|
if (shuttingDown) return Promise.resolve({ ok: false, message: "State Flow is shutting down" });
|
|
865
|
-
if (startActivation?.operation) return startActivation.operation;
|
|
835
|
+
if (startActivation.current?.operation) return startActivation.current.operation;
|
|
866
836
|
telegramStartPending = false;
|
|
867
|
-
if (activeContext && !branchRestoration && isActive()) {
|
|
837
|
+
if (activeContext && !branchRestoration.current && isActive()) {
|
|
868
838
|
return Promise.resolve({ ok: true, message: "State Flow is already active", signal: sharedInspectionLifetime.signal });
|
|
869
839
|
}
|
|
870
|
-
const pending
|
|
871
|
-
startActivation
|
|
872
|
-
const operation = runStart(ctx, pending.controller);
|
|
873
|
-
pending.operation = operation;
|
|
874
|
-
const finished = () => { if (startActivation === pending) startActivation = undefined; };
|
|
875
|
-
void operation.then(finished, finished);
|
|
876
|
-
return operation;
|
|
840
|
+
const pending = startActivation.claim({ controller: new AbortController() });
|
|
841
|
+
return startActivation.track(pending, runStart(ctx, pending.controller));
|
|
877
842
|
}
|
|
878
843
|
|
|
879
844
|
/** Await restoration-owned attachment and exact-source fork recovery before current-head activation. */
|
|
880
845
|
async function runStart(ctx: ExtensionContext, pending: AbortController): Promise<StateFlowTelegramControlResult> {
|
|
881
|
-
const
|
|
882
|
-
const cwd = ctx.cwd;
|
|
883
|
-
const file = ctx.sessionManager.getSessionFile();
|
|
884
|
-
const timestamp = ctx.sessionManager.getHeader()?.timestamp;
|
|
846
|
+
const sameSession = sessionIdentity(ctx);
|
|
885
847
|
const signal = ctx.signal ? AbortSignal.any([pending.signal, ctx.signal]) : pending.signal;
|
|
886
|
-
const isOwner = () => startActivation?.controller === pending && !signal.aborted && !shuttingDown
|
|
887
|
-
&& ctx.cwd === cwd && ctx.sessionManager.getSessionId() === owner && ctx.sessionManager.getSessionFile() === file
|
|
888
|
-
&& ctx.sessionManager.getHeader()?.timestamp === timestamp;
|
|
848
|
+
const isOwner = () => startActivation.current?.controller === pending && !signal.aborted && !shuttingDown && sameSession();
|
|
889
849
|
const superseded = (): StateFlowTelegramControlResult => {
|
|
890
850
|
pending.abort();
|
|
891
851
|
return { ok: false, message: "State Flow activation was superseded", signal: pending.signal };
|
|
892
852
|
};
|
|
893
853
|
try {
|
|
894
|
-
if (
|
|
895
|
-
else if (
|
|
854
|
+
if (deferredBranch?.reason === "fork") await waitForRecovery(restoreActiveBranch(ctx, "fork", false, pending), signal);
|
|
855
|
+
else if (!activeContext) await waitForRecovery(restoreActiveBranch(ctx, undefined, false, pending), signal);
|
|
856
|
+
else if (branchRestoration.current?.operation) await waitForRecovery(branchRestoration.current.operation, signal);
|
|
896
857
|
if (!isOwner()) return superseded();
|
|
897
858
|
if (isActive()) return { ok: true, message: "State Flow is already active", signal: sharedInspectionLifetime.signal };
|
|
898
859
|
if (forkInitialization) {
|
|
899
860
|
// Withdraw this join on cancellation without revoking the independently owned mode persistence.
|
|
900
|
-
const stopping = inactivePersistence?.operation;
|
|
861
|
+
const stopping = inactivePersistence.current?.operation;
|
|
901
862
|
if (stopping) await waitForRecovery(stopping, signal);
|
|
902
863
|
if (!isOwner()) return superseded();
|
|
903
864
|
await waitForRecovery(restoreActiveBranch(ctx, "fork", false, pending), signal);
|
|
@@ -916,48 +877,59 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
916
877
|
async function activateCurrentState(ctx: ExtensionContext, pending: AbortController): Promise<StateFlowTelegramControlResult> {
|
|
917
878
|
let selected = runtime;
|
|
918
879
|
const owner = ctx.sessionManager.getSessionId();
|
|
919
|
-
const
|
|
920
|
-
const file = ctx.sessionManager.getSessionFile();
|
|
921
|
-
const timestamp = ctx.sessionManager.getHeader()?.timestamp;
|
|
880
|
+
const sameSession = sessionIdentity(ctx);
|
|
922
881
|
const initiallyActive = isActive();
|
|
923
882
|
let accepted = false;
|
|
924
883
|
let receipt = AbortSignal.any([pending.signal, sharedInspectionLifetime.signal]);
|
|
925
|
-
const
|
|
926
|
-
|
|
927
|
-
&&
|
|
884
|
+
const signal = ctx.signal ? AbortSignal.any([pending.signal, ctx.signal]) : pending.signal;
|
|
885
|
+
const isCurrent = () => startActivation.current?.controller === pending && !signal.aborted && runtime === selected && !shuttingDown
|
|
886
|
+
&& sameSession() && isActive() === (accepted || initiallyActive);
|
|
928
887
|
const superseded = (): StateFlowTelegramControlResult => {
|
|
929
888
|
pending.abort();
|
|
930
889
|
return { ok: false, message: "State Flow activation was superseded", signal: pending.signal };
|
|
931
890
|
};
|
|
932
891
|
try {
|
|
933
892
|
const activation = createRuntime(ctx);
|
|
934
|
-
const
|
|
893
|
+
const deferred = deferredBranch;
|
|
894
|
+
if (deferred) {
|
|
895
|
+
const discovery = discoverSnapshotData(ctx.sessionManager.getBranch());
|
|
896
|
+
const retained = selectRetainedCheckpoint(discovery.candidates, config.inactiveMode);
|
|
897
|
+
branchStartsWithoutRuntime = retained.kind === "pre-runtime" || (discovery.candidates.length === 0 && discovery.errors.length === 0);
|
|
898
|
+
}
|
|
899
|
+
// Off activation owns one cancellable current-head publication, not an independent read-only restore.
|
|
935
900
|
const result = await activation.withStartTransaction((current, publish) => {
|
|
936
901
|
signal.throwIfAborted();
|
|
937
902
|
if (!isCurrent()) throw new Error("State Flow activation selection changed while awaiting publication");
|
|
938
903
|
if (!current && !branchStartsWithoutRuntime) throw new Error(snapshot.meta.validation?.error ?? "Current State Flow session memory is unavailable");
|
|
939
|
-
const
|
|
940
|
-
|
|
904
|
+
const boundary = deferred && retainsPhysicalSessionProjection(deferred.reason)
|
|
905
|
+
? findPassiveStopBoundary(ctx.sessionManager.getBranch(), owner, PASSIVE_STOP_ENTRY_TYPE) : undefined;
|
|
906
|
+
const bootstrap = hasPriorConversation(ctx.sessionManager.getBranch()) || passiveContinuation !== undefined || boundary !== undefined;
|
|
941
907
|
const activated = current ? resumeEpisode(current, bootstrap) : startEpisode(bootstrap);
|
|
942
908
|
const recoveredCurrent = selectedHistoryExpired;
|
|
943
|
-
|
|
909
|
+
publish(activated);
|
|
944
910
|
accepted = true;
|
|
945
|
-
|
|
911
|
+
inactivePersistence.cancel();
|
|
946
912
|
runtime = selected = activation;
|
|
947
913
|
snapshot = activated;
|
|
948
914
|
activeContext = ctx;
|
|
949
915
|
selectedHistoryExpired = false;
|
|
950
916
|
modePersistenceError = undefined;
|
|
951
917
|
installScopeStates();
|
|
918
|
+
const continuation = passiveContinuation ?? bootstrapContinuation ?? (deferred ? selectedContinuation(ctx, deferred.reason) : undefined);
|
|
919
|
+
deferredBranch = undefined;
|
|
952
920
|
clearRunTransient();
|
|
953
921
|
contextProjection.reset();
|
|
954
922
|
passiveContinuation = undefined;
|
|
955
923
|
bootstrapContinuation = snapshot.meta.bootstrap ? continuation : undefined;
|
|
956
924
|
deferInferencePreparation();
|
|
957
925
|
receipt = AbortSignal.any([pending.signal, sharedInspectionLifetime.signal]);
|
|
958
|
-
|
|
926
|
+
recordPublication();
|
|
959
927
|
syncStateFlowTools();
|
|
960
928
|
updateUi(ctx);
|
|
929
|
+
if (forkInitialization) {
|
|
930
|
+
forkInitialization = false;
|
|
931
|
+
pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, { reset: true, owner });
|
|
932
|
+
}
|
|
961
933
|
appendCheckpoint();
|
|
962
934
|
ctx.ui.notify(
|
|
963
935
|
recoveredCurrent
|
|
@@ -1014,21 +986,22 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1014
986
|
}
|
|
1015
987
|
|
|
1016
988
|
function deactivateStateFlow(ctx: ExtensionContext, mode: InactiveMode): Promise<StateFlowTelegramControlResult> {
|
|
1017
|
-
cancelStartActivation();
|
|
1018
989
|
if (shuttingDown) return Promise.resolve({ ok: false, message: "State Flow is shutting down" });
|
|
990
|
+
if (mode === "off") return selectOff(ctx);
|
|
991
|
+
startActivation.cancel();
|
|
1019
992
|
telegramStartPending = false;
|
|
1020
|
-
const persisting = inactivePersistence;
|
|
993
|
+
const persisting = inactivePersistence.current;
|
|
1021
994
|
if (persisting?.operation && !persisting.published) {
|
|
1022
995
|
// Pending inactive choices coalesce: acceptance persists whichever inactive mode is current then.
|
|
1023
996
|
if (snapshot.config.mode !== mode) applyInactiveMode(ctx, mode);
|
|
1024
997
|
return persisting.operation;
|
|
1025
998
|
}
|
|
1026
999
|
if (!activeContext) void restoreActiveBranch(ctx, undefined, false);
|
|
1027
|
-
const restoring = branchRestoration;
|
|
1000
|
+
const restoring = branchRestoration.current;
|
|
1028
1001
|
// Read-only recovery also retains the latest policy; its native write fence is updated below.
|
|
1029
1002
|
if (restoring) restoring.requestedMode = mode;
|
|
1030
1003
|
if (restoring?.awaitingAcceptance) {
|
|
1031
|
-
//
|
|
1004
|
+
// Passive keeps independently owned restoration; Off withdraws it through selectOff.
|
|
1032
1005
|
applyInactiveMode(ctx, mode);
|
|
1033
1006
|
return Promise.resolve({ ok: true, message: `State Flow ${mode}; memory restoration continues` });
|
|
1034
1007
|
}
|
|
@@ -1037,23 +1010,98 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1037
1010
|
const recordedMode = retained.kind === "pre-runtime" ? retained.mode : retained.kind === "boundary" ? retained.checkpoint.mode : undefined;
|
|
1038
1011
|
if (modePersistenceError || recordedMode === mode) return Promise.resolve({ ok: true, message: `State Flow is already ${mode}` });
|
|
1039
1012
|
}
|
|
1040
|
-
|
|
1041
|
-
const pending
|
|
1042
|
-
inactivePersistence
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
const finished = () => { if (inactivePersistence === pending) inactivePersistence = undefined; };
|
|
1046
|
-
void operation.then(finished, finished);
|
|
1047
|
-
return operation;
|
|
1013
|
+
inactivePersistence.cancel();
|
|
1014
|
+
const pending = inactivePersistence.claim({ controller: new AbortController() });
|
|
1015
|
+
return inactivePersistence.track(pending, deferredBranch && mode === "passive" && !branchStartsWithoutRuntime
|
|
1016
|
+
? acquirePassiveMode(ctx, pending, deferredBranch.reason)
|
|
1017
|
+
: persistInactiveMode(ctx, pending, mode));
|
|
1048
1018
|
}
|
|
1049
1019
|
|
|
1050
|
-
|
|
1020
|
+
/** Off owns cancellation and native policy only; no canonical transaction or semantic handoff. */
|
|
1021
|
+
function selectOff(ctx: ExtensionContext): Promise<StateFlowTelegramControlResult> {
|
|
1022
|
+
const address = sessionAddress(ctx);
|
|
1023
|
+
const owner = ctx.sessionManager.getSessionId();
|
|
1024
|
+
const branch = ctx.sessionManager.getBranch();
|
|
1025
|
+
const selected = runtime;
|
|
1026
|
+
const sameOwner = selected?.sessionKey === address.key && selected.cwd === ctx.cwd;
|
|
1027
|
+
const forkRecorded = hasPendingFork(branch, owner, PASSIVE_STOP_ENTRY_TYPE);
|
|
1028
|
+
const pendingFork = forkRecorded || (sameOwner && forkInitialization) || (deferredBranch?.owner === address.key && deferredBranch.cwd === ctx.cwd && deferredBranch.reason === "fork");
|
|
1029
|
+
const policy = findBranchPolicy(branch, owner, PASSIVE_STOP_ENTRY_TYPE, config.inactiveMode);
|
|
1030
|
+
const sameDeferred = deferredBranch?.owner === address.key && deferredBranch.cwd === ctx.cwd;
|
|
1031
|
+
const current = sameOwner || sameDeferred ? snapshot : emptySnapshot("off");
|
|
1032
|
+
if (!sameOwner && !sameDeferred) modePersistenceError = policy?.persistenceError;
|
|
1033
|
+
const alreadyOff = current.config.mode === "off" && policy?.mode === "off";
|
|
1034
|
+
const incoming = sameOwner || sameDeferred ? current.meta.bootstrap ? bootstrapContinuation : passiveContinuation : undefined;
|
|
1035
|
+
const unfinished = current.meta.specification !== undefined || (inferencePreparation.current?.prompt !== undefined && !inferencePreparation.current.accepted)
|
|
1036
|
+
|| (isActive() && hasUncheckpointedConversation(branch));
|
|
1037
|
+
const at = incoming?.startedAt ?? Date.now();
|
|
1038
|
+
const from = incoming ? incoming.activeRunStartedAt : (sameOwner || sameDeferred) && (!ctx.isIdle() || unfinished) ? runAnchorTimestamp : undefined;
|
|
1039
|
+
const preserveContext = modePersistenceError !== undefined || (incoming ? incoming.preserveContext : current.meta.bootstrap === true || (unfinished && from === undefined));
|
|
1040
|
+
const discovery = discoverSnapshotData(branch);
|
|
1041
|
+
const preRuntime = (sameOwner && branchStartsWithoutRuntime) || (discovery.candidates.length === 0 && discovery.errors.length === 0);
|
|
1042
|
+
let checkpoint: RetainedPiCheckpoint | undefined;
|
|
1043
|
+
try {
|
|
1044
|
+
checkpoint = !modePersistenceError && current.meta.validation?.attempt !== 0
|
|
1045
|
+
? preRuntime ? preRuntimeCheckpoint("off") : sameOwner && selected?.view ? selected.retainedCheckpoint(deactivateEpisode(current, "off")) : undefined
|
|
1046
|
+
: undefined;
|
|
1047
|
+
} catch {
|
|
1048
|
+
// An unusable cached bookmark cannot prevent native Off policy or authorize replacement memory.
|
|
1049
|
+
}
|
|
1050
|
+
startActivation.cancel();
|
|
1051
|
+
branchRestoration.cancel();
|
|
1052
|
+
inactivePersistence.cancel();
|
|
1053
|
+
backup.cancel();
|
|
1054
|
+
memoryToolLifetime.renew();
|
|
1055
|
+
clearRunTransient();
|
|
1056
|
+
contextProjection.reset();
|
|
1057
|
+
telegramStartPending = false;
|
|
1058
|
+
bootstrapContinuation = undefined;
|
|
1059
|
+
passiveContinuation = undefined;
|
|
1060
|
+
acquisition.reset();
|
|
1061
|
+
runtime = undefined;
|
|
1062
|
+
snapshot = emptySnapshot("off");
|
|
1063
|
+
selectedHistoryExpired = false;
|
|
1064
|
+
branchStartsWithoutRuntime = preRuntime;
|
|
1065
|
+
forkInitialization = pendingFork;
|
|
1066
|
+
deferredBranch = { owner: address.key, cwd: ctx.cwd, reason: pendingFork ? "fork" : undefined };
|
|
1067
|
+
activeContext = ctx;
|
|
1068
|
+
installScopeStates();
|
|
1069
|
+
syncStateFlowTools();
|
|
1070
|
+
updateUi(ctx);
|
|
1071
|
+
try {
|
|
1072
|
+
if (!alreadyOff || (pendingFork && !forkRecorded)) {
|
|
1073
|
+
if (!checkpoint || !preRuntime || pendingFork || preserveContext || from !== undefined || modePersistenceError) pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, {
|
|
1074
|
+
owner, at, mode: "off", memoryDeferred: true,
|
|
1075
|
+
...(from === undefined ? {} : { from }),
|
|
1076
|
+
...(preserveContext ? { preserveContext: true } : {}),
|
|
1077
|
+
...(pendingFork ? { forkPending: true } : {}),
|
|
1078
|
+
...(modePersistenceError === undefined ? {} : { persistenceError: modePersistenceError }),
|
|
1079
|
+
});
|
|
1080
|
+
if (checkpoint) pi.appendEntry(SNAPSHOT_ENTRY_TYPE, checkpoint);
|
|
1081
|
+
}
|
|
1082
|
+
return Promise.resolve({ ok: true, message: alreadyOff ? "State Flow is already off" : "State Flow off" });
|
|
1083
|
+
} catch (error) {
|
|
1084
|
+
const message = conciseDiagnostic(`State Flow off; native mode recording failed: ${diagnosticText(error)}`);
|
|
1085
|
+
notifyProblem(ctx, message, "error");
|
|
1086
|
+
return Promise.resolve({ ok: false, message });
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
async function acquirePassiveMode(ctx: ExtensionContext, pending: InactivePersistence, reason: unknown): Promise<StateFlowTelegramControlResult> {
|
|
1091
|
+
await restoreActiveBranch(ctx, reason, true, pending.controller, "passive");
|
|
1092
|
+
if (!inactivePersistence.owns(pending) || pending.controller.signal.aborted || shuttingDown) {
|
|
1093
|
+
return { ok: false, message: "State Flow mode change was superseded" };
|
|
1094
|
+
}
|
|
1095
|
+
return persistInactiveMode(ctx, pending, "passive");
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
async function persistInactiveMode(ctx: ExtensionContext, pending: InactivePersistence, mode: InactiveMode): Promise<StateFlowTelegramControlResult> {
|
|
1051
1099
|
let selected = runtime;
|
|
1052
1100
|
const owner = ctx.sessionManager.getSessionId();
|
|
1053
1101
|
const current = snapshot;
|
|
1054
1102
|
const wasActive = current.config.mode === "active";
|
|
1055
1103
|
let unfinished = current.meta.specification !== undefined
|
|
1056
|
-
|| (inferencePreparation?.prompt !== undefined && !inferencePreparation.accepted);
|
|
1104
|
+
|| (inferencePreparation.current?.prompt !== undefined && !inferencePreparation.current.accepted);
|
|
1057
1105
|
applyInactiveMode(ctx, mode);
|
|
1058
1106
|
const stoppedAt = Date.now();
|
|
1059
1107
|
if (modePersistenceError) {
|
|
@@ -1069,7 +1117,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1069
1117
|
const incomingBoundary = current.meta.bootstrap ? bootstrapContinuation : undefined;
|
|
1070
1118
|
const anchor = runAnchorTimestamp;
|
|
1071
1119
|
const idle = ctx.isIdle();
|
|
1072
|
-
const isCurrent = () => inactivePersistence
|
|
1120
|
+
const isCurrent = () => inactivePersistence.owns(pending) && runtime === selected
|
|
1073
1121
|
&& !shuttingDown && ctx.sessionManager.getSessionId() === owner && !isActive();
|
|
1074
1122
|
const superseded = (): StateFlowTelegramControlResult => ({ ok: false, message: "State Flow mode change was superseded" });
|
|
1075
1123
|
const freezeHandoff = (): PassiveContinuation | undefined => {
|
|
@@ -1082,7 +1130,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1082
1130
|
modePersistenceError !== undefined || (incomingBoundary ? incomingBoundary.preserveContext : current.meta.bootstrap === true || (unfinished && anchor === undefined)),
|
|
1083
1131
|
)
|
|
1084
1132
|
: undefined;
|
|
1085
|
-
//
|
|
1133
|
+
// Repeated Passive choices keep the already selected continuation boundary.
|
|
1086
1134
|
passiveContinuation = handoff ?? (!wasActive ? passiveContinuation : undefined);
|
|
1087
1135
|
return handoff;
|
|
1088
1136
|
};
|
|
@@ -1110,8 +1158,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1110
1158
|
// Projection changes before any wait; capture the old run's boundary before later native input can replace it.
|
|
1111
1159
|
freezeHandoff();
|
|
1112
1160
|
bootstrapContinuation = undefined;
|
|
1113
|
-
|
|
1114
|
-
artifactReads.setCandidates([]);
|
|
1161
|
+
acquisition.clearInvalidations();
|
|
1115
1162
|
assertPublicationAvailable();
|
|
1116
1163
|
const signal = ctx.signal ? AbortSignal.any([pending.controller.signal, ctx.signal]) : pending.controller.signal;
|
|
1117
1164
|
if (branchStartsWithoutRuntime || !selected?.view) {
|
|
@@ -1131,11 +1178,11 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1131
1178
|
if (!isCurrent()) throw new Error("State Flow mode selection changed while awaiting publication");
|
|
1132
1179
|
assertPublicationAvailable();
|
|
1133
1180
|
const next = structuredClone(snapshot);
|
|
1134
|
-
|
|
1181
|
+
publish(next);
|
|
1135
1182
|
accepted = true;
|
|
1136
1183
|
snapshot = next;
|
|
1137
1184
|
installScopeStates();
|
|
1138
|
-
|
|
1185
|
+
recordPublication();
|
|
1139
1186
|
return complete();
|
|
1140
1187
|
}, signal);
|
|
1141
1188
|
return isCurrent() ? result : superseded();
|
|
@@ -1149,8 +1196,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1149
1196
|
}
|
|
1150
1197
|
modePersistenceError = cause.trim() ? cause : "Canonical State Flow persistence failed";
|
|
1151
1198
|
bootstrapContinuation = undefined;
|
|
1152
|
-
|
|
1153
|
-
artifactReads.setCandidates([]);
|
|
1199
|
+
acquisition.clearInvalidations();
|
|
1154
1200
|
return complete();
|
|
1155
1201
|
}
|
|
1156
1202
|
}
|
|
@@ -1184,7 +1230,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1184
1230
|
},
|
|
1185
1231
|
cancelStart: () => {
|
|
1186
1232
|
telegramStartPending = false;
|
|
1187
|
-
|
|
1233
|
+
startActivation.cancel();
|
|
1188
1234
|
},
|
|
1189
1235
|
},
|
|
1190
1236
|
});
|
|
@@ -1201,7 +1247,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1201
1247
|
contextProjection.reset();
|
|
1202
1248
|
skillReads.clear();
|
|
1203
1249
|
artifactReads.clear();
|
|
1204
|
-
|
|
1250
|
+
responseReconciliation.cancel();
|
|
1205
1251
|
completedRunAccepted = false;
|
|
1206
1252
|
// This native hook has no operation signal. Capture only; the first active context owns acceptance.
|
|
1207
1253
|
deferInferencePreparation(event.prompt);
|
|
@@ -1221,12 +1267,12 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1221
1267
|
if (runtime?.view) refreshArtifactHints();
|
|
1222
1268
|
if (!isActive() && !passiveContinuation && !passiveMemoryAvailable()) return;
|
|
1223
1269
|
// Idle inspection must not freeze a pre-acceptance snapshot for the live inference.
|
|
1224
|
-
const projection = isActive() && inferencePreparation && !inferencePreparation.accepted
|
|
1270
|
+
const projection = isActive() && inferencePreparation.current && !inferencePreparation.current.accepted
|
|
1225
1271
|
? new ContextProjection() : contextProjection;
|
|
1226
1272
|
const effective = effectiveState;
|
|
1227
|
-
const invalidations =
|
|
1228
|
-
const phase = isActive() ?
|
|
1229
|
-
const view = contextView(effective,
|
|
1273
|
+
const invalidations = acquisition.invalidations.map(({ path, scope, reason }) => ({ path, ...(scope === undefined ? {} : { scope }), reason }));
|
|
1274
|
+
const phase = isActive() ? rehydrationPhase : undefined;
|
|
1275
|
+
const view = contextView(effective, acquisition.hints, invalidations, phase);
|
|
1230
1276
|
if (passiveContinuation) {
|
|
1231
1277
|
const retained = passiveContinuationMessages(messages, passiveContinuation);
|
|
1232
1278
|
if (!runtime?.view) return { messages: retained };
|
|
@@ -1246,20 +1292,20 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1246
1292
|
}
|
|
1247
1293
|
|
|
1248
1294
|
function prepareContext(messages: AgentMessage[], ctx: ExtensionContext): ReturnType<typeof projectContext> | Promise<ReturnType<typeof projectContext>> {
|
|
1249
|
-
const pending = inferencePreparation;
|
|
1295
|
+
const pending = inferencePreparation.current;
|
|
1250
1296
|
const selected = runtime;
|
|
1251
1297
|
const operationSignal = ctx.signal;
|
|
1252
1298
|
// Idle projections remain observational; only a live native operation may await preparation.
|
|
1253
1299
|
if (!isActive() || !pending || !operationSignal || shuttingDown) return projectContext(messages);
|
|
1254
1300
|
pending.operation ??= prepareInference(pending, selected, ctx, operationSignal).finally(() => {
|
|
1255
1301
|
// A late withdrawal must not clear newer work; accepted lifecycle is never replayed after an error.
|
|
1256
|
-
if (!pending.accepted && inferencePreparation
|
|
1302
|
+
if (!pending.accepted && inferencePreparation.owns(pending)) pending.operation = undefined;
|
|
1257
1303
|
});
|
|
1258
1304
|
return pending.operation.then(() => {
|
|
1259
1305
|
if (shuttingDown || operationSignal.aborted || runtime !== selected || ctx.signal !== operationSignal) return;
|
|
1260
|
-
if (isActive() && inferencePreparation
|
|
1306
|
+
if (isActive() && !inferencePreparation.owns(pending)) {
|
|
1261
1307
|
// Start may require same-run maintenance. A new captured user prompt instead owns a different context request.
|
|
1262
|
-
if (inferencePreparation?.prompt === undefined) return prepareContext(messages, ctx);
|
|
1308
|
+
if (inferencePreparation.current?.prompt === undefined) return prepareContext(messages, ctx);
|
|
1263
1309
|
return;
|
|
1264
1310
|
}
|
|
1265
1311
|
if (isActive() && !pending.accepted) return;
|
|
@@ -1319,32 +1365,32 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1319
1365
|
if (event.message.role === "user" && runAnchorTimestamp === undefined) runAnchorTimestamp = event.message.timestamp;
|
|
1320
1366
|
if (shuttingDown || !isActive()) return;
|
|
1321
1367
|
if (event.message.role !== "assistant") return;
|
|
1322
|
-
|
|
1368
|
+
responseReconciliation.cancel();
|
|
1323
1369
|
const message = event.message as unknown as { role: "assistant"; stopReason?: string; content?: unknown };
|
|
1324
1370
|
if (message.stopReason === "aborted" || assistantToolCallCount(message.content) > 0 || message.stopReason === "toolUse") return;
|
|
1325
1371
|
if (message.stopReason === "length" || message.stopReason === "error") {
|
|
1326
1372
|
recordDiagnostic(`Assistant response ended with ${message.stopReason}`, "finalization", ctx, { content: message.content });
|
|
1327
1373
|
return;
|
|
1328
1374
|
}
|
|
1329
|
-
responseReconciliation
|
|
1375
|
+
responseReconciliation.claim({ controller: new AbortController() });
|
|
1330
1376
|
});
|
|
1331
1377
|
|
|
1332
1378
|
pi.on("turn_end", async (event, ctx) => {
|
|
1333
1379
|
if (shuttingDown) return;
|
|
1334
|
-
const pending = responseReconciliation;
|
|
1380
|
+
const pending = responseReconciliation.current;
|
|
1335
1381
|
if (!isActive() || !pending) {
|
|
1336
1382
|
updateUi(ctx);
|
|
1337
1383
|
return;
|
|
1338
1384
|
}
|
|
1339
1385
|
const selected = runtime;
|
|
1340
|
-
const signal = ctx.signal ? AbortSignal.any([pending.signal, ctx.signal]) : pending.signal;
|
|
1386
|
+
const signal = ctx.signal ? AbortSignal.any([pending.controller.signal, ctx.signal]) : pending.controller.signal;
|
|
1341
1387
|
let responseCommitted = false;
|
|
1342
1388
|
try {
|
|
1343
1389
|
const response = finalizedAssistantResponse(event.message);
|
|
1344
1390
|
if (!selected?.view) throw new Error("Temporal State Flow runtime is unavailable; reload before publishing");
|
|
1345
1391
|
await selected.withPatchTransaction((transaction) => {
|
|
1346
1392
|
signal.throwIfAborted();
|
|
1347
|
-
if (runtime !== selected || responseReconciliation
|
|
1393
|
+
if (runtime !== selected || !responseReconciliation.owns(pending) || !isActive()) {
|
|
1348
1394
|
throw new Error("State Flow response selection changed while awaiting publication");
|
|
1349
1395
|
}
|
|
1350
1396
|
assertPublicationAvailable();
|
|
@@ -1360,64 +1406,56 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1360
1406
|
installScopeStates();
|
|
1361
1407
|
responseCommitted = true;
|
|
1362
1408
|
contextProjection.reset();
|
|
1363
|
-
if (publication?.changed) recordPublication(
|
|
1409
|
+
if (publication?.changed) recordPublication();
|
|
1364
1410
|
appendCheckpoint();
|
|
1365
1411
|
clearAcceptedAcquisitions();
|
|
1366
1412
|
bootstrapContinuation = undefined;
|
|
1367
1413
|
rehydrationPhase = "step";
|
|
1368
1414
|
completedRunAccepted = !wasBootstrap;
|
|
1369
|
-
|
|
1415
|
+
backup.markTurnAccepted();
|
|
1370
1416
|
updateUi(ctx);
|
|
1371
1417
|
}, signal);
|
|
1372
1418
|
} catch (error) {
|
|
1373
1419
|
// Superseded completion owns neither the new lifecycle nor its diagnostics/UI.
|
|
1374
|
-
if (signal.aborted || runtime !== selected || responseReconciliation
|
|
1420
|
+
if (signal.aborted || runtime !== selected || !responseReconciliation.owns(pending)) return;
|
|
1375
1421
|
const cause = diagnosticText(error);
|
|
1376
1422
|
recordDiagnostic(cause, "finalization", ctx);
|
|
1377
1423
|
notifyProblem(ctx, responseCommitted
|
|
1378
1424
|
? `State Flow response saved; lifecycle update failed: ${cause}`
|
|
1379
1425
|
: `State Flow response reconciliation failed: ${cause}`, "error");
|
|
1380
1426
|
} finally {
|
|
1381
|
-
|
|
1427
|
+
responseReconciliation.release(pending);
|
|
1382
1428
|
}
|
|
1383
1429
|
});
|
|
1384
1430
|
|
|
1385
1431
|
pi.on("session_before_compact", (event) => {
|
|
1386
|
-
|
|
1387
|
-
if (compactionStopped && event.reason === "manual" && event.customInstructions === compactionMarker) return { cancel: true };
|
|
1388
|
-
const result = stateFlowCompactionResult(compactionPlan, compactionMarker, event);
|
|
1432
|
+
const result = compactionRequests.resolve(event, !shuttingDown && isActive());
|
|
1389
1433
|
if (result === undefined || "cancel" in result) return result;
|
|
1390
1434
|
return { compaction: result };
|
|
1391
1435
|
});
|
|
1392
1436
|
|
|
1393
1437
|
pi.on("agent_before_settle", async (_event, ctx) => {
|
|
1394
|
-
if (shuttingDown || !
|
|
1395
|
-
turnAcceptedForBackup = false;
|
|
1396
|
-
if (!backupPending) return;
|
|
1397
|
-
backupPending = false;
|
|
1438
|
+
if (shuttingDown || !memoryToolsAvailable() || !backup.takeSettledTurn()) return;
|
|
1398
1439
|
const operationSignal = ctx.signal;
|
|
1399
|
-
const
|
|
1440
|
+
const lifetime = backup.lifetime;
|
|
1441
|
+
const signal = operationSignal ? AbortSignal.any([lifetime, operationSignal]) : lifetime;
|
|
1400
1442
|
const selected = runtime;
|
|
1401
1443
|
try {
|
|
1402
1444
|
if (existsSync(join(repositoryRoot, ".git"))) {
|
|
1403
1445
|
const pushSessionId = sessionAddress(ctx).id;
|
|
1404
1446
|
const pushCwd = ctx.cwd;
|
|
1405
1447
|
// Pi 0.87 settles after its agent signal ends; waiting there would strand native Abort.
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
try { await operation; }
|
|
1409
|
-
finally { backupOperations.delete(operation); }
|
|
1410
|
-
if (signal.aborted) return;
|
|
1448
|
+
await backup.track(backupCurrentStateFlowFiles(repositoryRoot, signal, operationSignal !== undefined));
|
|
1449
|
+
if (signal.aborted || snapshot.config.mode === "off") return;
|
|
1411
1450
|
startStateFlowBackupPush(repositoryRoot, (error) => {
|
|
1412
|
-
if (shuttingDown) return;
|
|
1451
|
+
if (shuttingDown || lifetime.aborted || snapshot.config.mode === "off") return;
|
|
1413
1452
|
const message = diagnosticText(error);
|
|
1414
1453
|
const recorded = diagnosticWriter.recordBackupPushFailure(pushSessionId, pushCwd, message);
|
|
1415
|
-
if (
|
|
1416
|
-
pushFailureNotified = true;
|
|
1454
|
+
if (!backup.claimPushFailureNotice()) return;
|
|
1417
1455
|
notifyActiveContext(recorded
|
|
1418
1456
|
? `State Flow Git backup push failed; state is saved locally. Details: ${stateFlowLogPath(agentDir)}. A later accepted turn retries.`
|
|
1419
1457
|
: `State Flow Git backup push failed; local diagnostics unavailable: ${message}`);
|
|
1420
|
-
}, () =>
|
|
1458
|
+
}, () => backup.resetPushFailureNotice(), lifetime);
|
|
1421
1459
|
}
|
|
1422
1460
|
} catch (error) {
|
|
1423
1461
|
if (signal.aborted || runtime !== selected) return;
|
|
@@ -1435,20 +1473,22 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1435
1473
|
await startStateFlow(ctx);
|
|
1436
1474
|
return;
|
|
1437
1475
|
}
|
|
1438
|
-
if (!completedRunAccepted ||
|
|
1439
|
-
||
|
|
1476
|
+
if (!completedRunAccepted || compactionRequests.stopped || !isActive() || snapshot.meta.bootstrap
|
|
1477
|
+
|| compactionRequests.inFlight || !ctx.isIdle() || ctx.hasPendingMessages() || !runtime?.view
|
|
1440
1478
|
|| !shouldRequestStateFlowCompaction(ctx.getContextUsage())) return;
|
|
1441
1479
|
completedRunAccepted = false;
|
|
1442
1480
|
const entries = ctx.sessionManager.buildContextEntries();
|
|
1443
1481
|
if (!hasCompactionSizedTranscript(entries)) return;
|
|
1444
1482
|
const plan = planStateFlowCompaction(entries, runtime.causalBasis(), snapshot.meta.step, runAnchorTimestamp);
|
|
1445
1483
|
if (!plan) return;
|
|
1446
|
-
|
|
1447
|
-
compactionInFlight = true;
|
|
1484
|
+
const marker = compactionRequests.begin(plan);
|
|
1448
1485
|
// Pi dispatches deferred companion prompts after all settled handlers return.
|
|
1449
1486
|
await new Promise<void>((resolve) => {
|
|
1450
|
-
const finished = () => {
|
|
1451
|
-
|
|
1487
|
+
const finished = () => {
|
|
1488
|
+
compactionRequests.finish(marker);
|
|
1489
|
+
resolve();
|
|
1490
|
+
};
|
|
1491
|
+
ctx.compact({ customInstructions: marker, onComplete: finished, onError: finished });
|
|
1452
1492
|
});
|
|
1453
1493
|
});
|
|
1454
1494
|
|
|
@@ -1466,20 +1506,21 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1466
1506
|
});
|
|
1467
1507
|
pi.on("session_shutdown", async (_event, _ctx) => {
|
|
1468
1508
|
shuttingDown = true;
|
|
1509
|
+
memoryToolLifetime.end();
|
|
1469
1510
|
contextProjection.reset();
|
|
1470
|
-
const restoring =
|
|
1471
|
-
const starting =
|
|
1472
|
-
const stopping =
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1511
|
+
const restoring = branchRestoration.cancel();
|
|
1512
|
+
const starting = startActivation.cancel();
|
|
1513
|
+
const stopping = inactivePersistence.cancel();
|
|
1514
|
+
backup.abort();
|
|
1515
|
+
inferencePreparation.cancel();
|
|
1516
|
+
responseReconciliation.cancel();
|
|
1476
1517
|
cancelSharedInspections();
|
|
1477
1518
|
telegramStartPending = false;
|
|
1478
|
-
|
|
1519
|
+
compactionRequests.stop();
|
|
1479
1520
|
completedRunAccepted = false;
|
|
1480
1521
|
activeContext = undefined;
|
|
1481
1522
|
telegram.dispose();
|
|
1482
|
-
await Promise.allSettled([...
|
|
1523
|
+
await Promise.allSettled([...backup.operations, ...branchRestoration.inflight, ...[restoring, stopping, starting].filter((operation) => operation !== undefined)]);
|
|
1483
1524
|
await awaitInFlightBackupPushes(repositoryRoot);
|
|
1484
1525
|
});
|
|
1485
1526
|
}
|