@llblab/pi-kit 0.18.1 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +4 -2
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +35 -37
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +5 -3
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +19 -1
- package/node_modules/@llblab/pi-state-flow/README.md +102 -48
- package/node_modules/@llblab/pi-state-flow/dist/index.d.ts +6 -9
- package/node_modules/@llblab/pi-state-flow/dist/index.js +6 -9
- package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.d.ts +2 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.js +21 -8
- package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.d.ts +40 -24
- package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.js +93 -63
- package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.d.ts +4 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.js +9 -9
- package/node_modules/@llblab/pi-state-flow/dist/lib/config.d.ts +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/config.js +6 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +8 -7
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +56 -58
- package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +12 -24
- package/node_modules/@llblab/pi-state-flow/dist/lib/durable.d.ts +7 -18
- package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +47 -78
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.d.ts +4 -6
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +241 -437
- package/node_modules/@llblab/pi-state-flow/dist/lib/git.d.ts +2 -72
- package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +120 -499
- package/node_modules/@llblab/pi-state-flow/dist/lib/history.d.ts +2 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/history.js +4 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/json.d.ts +3 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/json.js +43 -22
- package/node_modules/@llblab/pi-state-flow/dist/lib/logging.d.ts +1 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/logging.js +0 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/memory.d.ts +1 -14
- package/node_modules/@llblab/pi-state-flow/dist/lib/memory.js +5 -37
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.d.ts +1 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +13 -32
- package/node_modules/@llblab/pi-state-flow/dist/lib/query.d.ts +6 -6
- package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +25 -21
- package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.d.ts +3 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.js +15 -12
- package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.d.ts +2 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.js +5 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +26 -88
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +159 -255
- package/node_modules/@llblab/pi-state-flow/dist/lib/skills.d.ts +0 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/skills.js +1 -47
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +16 -33
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +48 -137
- package/node_modules/@llblab/pi-state-flow/dist/lib/state.d.ts +16 -11
- package/node_modules/@llblab/pi-state-flow/dist/lib/state.js +13 -14
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +1 -9
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +15 -43
- package/node_modules/@llblab/pi-state-flow/dist/lib/storage.d.ts +5 -9
- package/node_modules/@llblab/pi-state-flow/dist/lib/storage.js +13 -45
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.d.ts +15 -7
- package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.js +104 -24
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.d.ts +0 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +11 -14
- package/node_modules/@llblab/pi-state-flow/dist/package.json +9 -6
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +11 -17
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +8 -8
- package/node_modules/@llblab/pi-state-flow/docs/README.md +2 -2
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +64 -67
- package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +24 -6
- package/node_modules/@llblab/pi-state-flow/docs/filesystem-recovery.md +12 -15
- package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +22 -27
- package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +25 -41
- package/node_modules/@llblab/pi-state-flow/docs/performance.md +38 -421
- package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +33 -46
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +37 -61
- package/node_modules/@llblab/pi-state-flow/index.ts +7 -71
- package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +26 -11
- package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +116 -88
- package/node_modules/@llblab/pi-state-flow/lib/compaction.ts +12 -10
- package/node_modules/@llblab/pi-state-flow/lib/config.ts +6 -6
- package/node_modules/@llblab/pi-state-flow/lib/context.ts +52 -59
- package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +11 -20
- package/node_modules/@llblab/pi-state-flow/lib/durable.ts +44 -86
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +237 -460
- package/node_modules/@llblab/pi-state-flow/lib/git.ts +110 -552
- package/node_modules/@llblab/pi-state-flow/lib/history.ts +4 -3
- package/node_modules/@llblab/pi-state-flow/lib/json.ts +39 -23
- package/node_modules/@llblab/pi-state-flow/lib/logging.ts +1 -7
- package/node_modules/@llblab/pi-state-flow/lib/memory.ts +5 -44
- package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +14 -25
- package/node_modules/@llblab/pi-state-flow/lib/query.ts +26 -22
- package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +17 -11
- package/node_modules/@llblab/pi-state-flow/lib/rehydration.ts +7 -5
- package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +154 -254
- package/node_modules/@llblab/pi-state-flow/lib/skills.ts +1 -49
- package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +58 -142
- package/node_modules/@llblab/pi-state-flow/lib/state.ts +27 -19
- package/node_modules/@llblab/pi-state-flow/lib/status.ts +16 -54
- package/node_modules/@llblab/pi-state-flow/lib/storage.ts +12 -40
- package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +1 -1
- package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +104 -22
- package/node_modules/@llblab/pi-state-flow/lib/transition.ts +16 -26
- package/node_modules/@llblab/pi-state-flow/package.json +9 -6
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +11 -17
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +8 -8
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +7 -0
- package/node_modules/@llblab/pi-telegram/README.md +1 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +1 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +1 -6
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +1 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +13 -33
- package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +1 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-queue.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-queue.js +72 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +7 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +27 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/replies.d.ts +3 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/replies.js +17 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/sync.js +8 -1
- package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +2 -0
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +1 -1
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +0 -10
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +18 -41
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +1 -8
- package/node_modules/@llblab/pi-telegram/lib/menu-queue.ts +126 -1
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +46 -0
- package/node_modules/@llblab/pi-telegram/lib/replies.ts +19 -0
- package/node_modules/@llblab/pi-telegram/lib/sync.ts +8 -1
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/package.json +3 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/discovery.d.ts +0 -21
- package/node_modules/@llblab/pi-state-flow/dist/lib/discovery.js +0 -125
- package/node_modules/@llblab/pi-state-flow/dist/lib/maintenance.d.ts +0 -36
- package/node_modules/@llblab/pi-state-flow/dist/lib/maintenance.js +0 -98
- package/node_modules/@llblab/pi-state-flow/dist/lib/migration.d.ts +0 -13
- package/node_modules/@llblab/pi-state-flow/dist/lib/migration.js +0 -167
- package/node_modules/@llblab/pi-state-flow/dist/lib/publication.d.ts +0 -86
- package/node_modules/@llblab/pi-state-flow/dist/lib/publication.js +0 -437
- package/node_modules/@llblab/pi-state-flow/lib/discovery.ts +0 -133
- package/node_modules/@llblab/pi-state-flow/lib/maintenance.ts +0 -147
- package/node_modules/@llblab/pi-state-flow/lib/migration.ts +0 -171
- package/node_modules/@llblab/pi-state-flow/lib/publication.ts +0 -458
|
@@ -1,63 +1,61 @@
|
|
|
1
1
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
3
2
|
import { StringEnum, Type } from "@earendil-works/pi-ai";
|
|
4
3
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
4
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
6
5
|
import { Text } from "@earendil-works/pi-tui";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { existsSync } from "node:fs";
|
|
8
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
9
9
|
import { ArtifactReadTracker } from "./acquisition.ts";
|
|
10
|
+
import {
|
|
11
|
+
classifyArtifactCompilationNeed,
|
|
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";
|
|
10
18
|
import { loadStateFlowConfig } from "./config.ts";
|
|
11
|
-
import {
|
|
12
|
-
import { isAbsolute, resolve } from "node:path";
|
|
13
|
-
import { SkillReadTracker } from "./skills.ts";
|
|
14
|
-
import { emptySnapshot, migrationFailure, persistableSnapshot, RevisionUnavailableError, type Snapshot } from "./snapshot.ts";
|
|
19
|
+
import { createPassiveContinuation, currentRunTrajectory, lazyNavigationHint, passiveContinuationMessages, projectSystemProtocol, runtimeContextMessage, syntheticUser, type PassiveContinuation } from "./context.ts";
|
|
15
20
|
import { readNativeSessionHeader } from "./continuation.ts";
|
|
16
|
-
import { MissingSessionRuntimeError, SharedScopeRemovalConflictError, TemporalRuntime, type RuntimePublication } from "./runtime.ts";
|
|
17
|
-
import { emptyState, overlayStates, projectStateForModel, type AtomicScopePatches, type MaterializedState, type ScopedStates, type StateScope } from "./state.ts";
|
|
18
|
-
import { commitScopedTransition, stageAtomicScopePatches, stageScopedTransition, validateFinalEligibility, type StagedScopedTransition } from "./transition.ts";
|
|
19
|
-
import { discoverSnapshotData, findAssistantToolBatch, findPassiveStopBoundary, hasPriorConversation, isNewSession, retainsPhysicalSessionProjection, SNAPSHOT_ENTRY_TYPE } from "./session.ts";
|
|
20
|
-
import { compactStatus, detailedStatus, STATUS_KEY, type PendingPublicationDiagnostic, type StatusDiagnostics } from "./status.ts";
|
|
21
|
-
import { completeRun, prepareRun, resumeEpisode, startEpisode, stopEpisode } from "./episode.ts";
|
|
22
|
-
import { recoverSnapshot } from "./recovery.ts";
|
|
23
|
-
import type { RehydrationPhase } from "./rehydration.ts";
|
|
24
|
-
import { loadPublicationQueue, publicationQueuePath, PublicationWorkerController, resolveRemotePublicationPolicy, serializeRemotePublicationPolicyDocument, type PublicationQueueState } from "./publication.ts";
|
|
25
|
-
import { getKnowledgeRoot, GlobalMarkdownDiscovery } from "./discovery.ts";
|
|
26
|
-
import { canonicalJson, isObject, sameJson } from "./json.ts";
|
|
27
|
-
import { readProjectedState, readStatePath } from "./query.ts";
|
|
28
21
|
import {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
22
|
+
cwdScopeKey,
|
|
23
|
+
resolveSessionAddress,
|
|
24
|
+
sessionScopeKey,
|
|
25
|
+
type SessionAddress,
|
|
33
26
|
} from "./durable.ts";
|
|
34
|
-
import {
|
|
27
|
+
import { completeRun, prepareRun, resumeEpisode, startEpisode, stopEpisode } from "./episode.ts";
|
|
28
|
+
import { backupCurrentStateFlowFiles } from "./git.ts";
|
|
29
|
+
import { projectRecentTransitionsWithLimit } from "./history.ts";
|
|
30
|
+
import { isObject, presentationJson, sameJson, type JsonObject } from "./json.ts";
|
|
35
31
|
import { StateFlowDiagnosticWriter, stateFlowLogPath, type DiagnosticExtras, type StateFlowDiagnosticCategory } from "./logging.ts";
|
|
36
|
-
import {
|
|
37
|
-
import {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
} from "./
|
|
42
|
-
import {
|
|
32
|
+
import { assistantToolCallCount, finalizedAssistantResponse, formatPatchStateArguments, PASSIVE_MEMORY_PROTOCOL, separatedFailure, separatedOutput, stateFlowProtocol } from "./protocol.ts";
|
|
33
|
+
import { readProjectedState, readStatePath } from "./query.ts";
|
|
34
|
+
import { recoverSnapshot } from "./recovery.ts";
|
|
35
|
+
import type { RehydrationPhase } from "./rehydration.ts";
|
|
36
|
+
import { SharedScopeRemovalConflictError, TemporalRuntime, type RuntimePublication } from "./runtime.ts";
|
|
37
|
+
import { discoverSnapshotData, findAssistantToolBatch, findPassiveStopBoundary, hasPriorConversation, isNewSession, retainsPhysicalSessionProjection, SNAPSHOT_ENTRY_TYPE } from "./session.ts";
|
|
38
|
+
import { SkillReadTracker } from "./skills.ts";
|
|
39
|
+
import { emptySnapshot, migrationFailure, type Snapshot } from "./snapshot.ts";
|
|
40
|
+
import { emptyState, overlayStates, projectStateForModel, type AtomicScopePatches, type MaterializedState, type ModelState, type ScopedStates, type StateScope } from "./state.ts";
|
|
41
|
+
import { compactStatus, detailedStatus, STATUS_KEY, type StatusDiagnostics } from "./status.ts";
|
|
42
|
+
import { createStateFlowTelegramAdapter, type StateFlowTelegramControlResult, type StateFlowTelegramLoader } from "./telegram.ts";
|
|
43
|
+
import { commitScopedTransition, stageAtomicScopePatches, stageScopedTransition, type StagedScopedTransition } from "./transition.ts";
|
|
43
44
|
|
|
44
45
|
export interface StateFlowExtensionOptions {
|
|
45
46
|
agentDir?: string;
|
|
46
47
|
repositoryRoot?: string;
|
|
47
|
-
|
|
48
|
-
onRuntime?: (accessor: { read(offset?: number, scope?: StateScope): MaterializedState }) => void;
|
|
48
|
+
onRuntime?: (accessor: { read(offset?: number, scope?: StateScope): ModelState }) => void;
|
|
49
49
|
telegram?: { load?: StateFlowTelegramLoader };
|
|
50
50
|
/** Test/SDK capability override; repository config remains the Pi default. */
|
|
51
51
|
passive?: { bootstrap?: boolean; tools?: boolean };
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
export { formatPatchStateArguments
|
|
54
|
+
export { formatPatchStateArguments };
|
|
55
55
|
|
|
56
56
|
export const PATCH_STATE_TOOL_NAME = "patch_state";
|
|
57
57
|
export const READ_STATE_TOOL_NAME = "read_state";
|
|
58
|
-
export const MAX_FALLBACK_ATTEMPTS: number = 2;
|
|
59
58
|
const PASSIVE_STOP_ENTRY_TYPE = "state-flow-passive-stop";
|
|
60
|
-
const PUBLICATION_SHUTDOWN_WAIT_MS = 2_000;
|
|
61
59
|
|
|
62
60
|
export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowExtensionOptions = {}): void {
|
|
63
61
|
const agentDir = options.agentDir ?? getAgentDir();
|
|
@@ -72,12 +70,9 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
72
70
|
let branchHasSnapshot = false;
|
|
73
71
|
let branchStartsWithoutRuntime = false;
|
|
74
72
|
let forkInitialization = false;
|
|
75
|
-
let terminalEligible = false;
|
|
76
|
-
let resolutionPending = false;
|
|
77
|
-
let fallbackAttempts = 0;
|
|
78
|
-
let fallbackFailureReported = false;
|
|
79
73
|
let responseAwaitingReconciliation = false;
|
|
80
74
|
let completedRunAccepted = false;
|
|
75
|
+
let turnAcceptedForBackup = false;
|
|
81
76
|
let compactionPlan: StateFlowCompactionPlan | undefined;
|
|
82
77
|
let compactionInFlight = false;
|
|
83
78
|
let compactionStopped = false;
|
|
@@ -88,25 +83,15 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
88
83
|
let runAnchorTimestamp: number | undefined;
|
|
89
84
|
let runtime: TemporalRuntime | undefined;
|
|
90
85
|
let activeContext: ExtensionContext | undefined;
|
|
91
|
-
let pendingPublication: PendingPublicationDiagnostic | undefined;
|
|
92
86
|
let rehydrationPhase: RehydrationPhase | undefined;
|
|
93
|
-
let turnPublicationTarget: string | undefined;
|
|
94
|
-
let publicationShutdown: Promise<void> | undefined;
|
|
95
87
|
const repositoryRoot = resolve(options.repositoryRoot ?? config.directory);
|
|
96
88
|
const diagnosticWriter = new StateFlowDiagnosticWriter(config.logging, stateFlowLogPath(agentDir), repositoryRoot, (message) => activeContext?.ui.notify(message, "warning"));
|
|
97
|
-
|
|
98
|
-
resolveDestination: () => resolveGitPushDestination(repositoryRoot),
|
|
99
|
-
isAncestor: (ancestor, descendant) => isGitCommitAncestor(repositoryRoot, ancestor, descendant),
|
|
100
|
-
push: (destination, target, signal) => pushGitTarget(repositoryRoot, destination, target, signal),
|
|
101
|
-
onDiverged: (dropped, target) => activeContext && recordDiagnostic(
|
|
102
|
-
`Retired publication queue target ${dropped.target} after a journal lineage rewrite; retargeting to ${target}`,
|
|
103
|
-
"publication-conflict", activeContext,
|
|
104
|
-
),
|
|
105
|
-
});
|
|
89
|
+
let backupPending = false;
|
|
106
90
|
const skillReads = new SkillReadTracker();
|
|
107
91
|
const artifactReads = new ArtifactReadTracker();
|
|
108
|
-
const globalMarkdown = new GlobalMarkdownDiscovery(options.knowledgeRoot ?? getKnowledgeRoot(agentDir));
|
|
109
92
|
let artifactInvalidations: ArtifactInvalidationRequest[] = [];
|
|
93
|
+
let artifactHints: Record<string, string> = {};
|
|
94
|
+
const SOURCE_CHANGED_HINT = "Source changed since this artifact was compiled. Read and recompile it before relying on it.";
|
|
110
95
|
let telegramStartPending = false;
|
|
111
96
|
|
|
112
97
|
function sessionAddress(ctx: ExtensionContext): SessionAddress {
|
|
@@ -114,47 +99,49 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
114
99
|
}
|
|
115
100
|
|
|
116
101
|
function createRuntime(ctx: ExtensionContext): TemporalRuntime {
|
|
117
|
-
return new TemporalRuntime(ctx.cwd, sessionAddress(ctx), repositoryRoot);
|
|
102
|
+
return new TemporalRuntime(ctx.cwd, sessionAddress(ctx), repositoryRoot, undefined, config.historyLimit);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function projectModelState(state: MaterializedState): ModelState {
|
|
106
|
+
return projectStateForModel(state, artifactHints);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function assertSelectedBranchAvailable(): void {
|
|
110
|
+
if (snapshot.meta.validation?.attempt === 0) {
|
|
111
|
+
throw new Error(`State Flow selected branch is unavailable: ${snapshot.meta.validation.error}. Restore its canonical files and retry /state-flow-start, or select a retained boundary.`);
|
|
112
|
+
}
|
|
118
113
|
}
|
|
119
114
|
|
|
120
115
|
options.onRuntime?.({ read: (offset, scope) => {
|
|
116
|
+
if (scope === "session") assertSelectedBranchAvailable();
|
|
121
117
|
if (!runtime) throw new Error("State Flow temporal runtime is unavailable");
|
|
122
|
-
return
|
|
118
|
+
return projectModelState(runtime.read(offset, scope));
|
|
123
119
|
} });
|
|
124
120
|
|
|
125
121
|
function persist(): void {
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
|
|
122
|
+
assertSelectedBranchAvailable();
|
|
123
|
+
const previousView = runtime?.view;
|
|
124
|
+
const publication = !branchStartsWithoutRuntime && runtime?.view ? runtime.publish(snapshot, false, undefined) : undefined;
|
|
125
|
+
if (runtime?.view !== previousView) installScopeStates();
|
|
129
126
|
if (publication && activeContext) recordPublication(publication, activeContext);
|
|
130
|
-
const checkpoint =
|
|
127
|
+
const checkpoint = branchStartsWithoutRuntime ? { disabled: true } : runtime?.retainedCheckpoint(snapshot) ?? { disabled: true };
|
|
131
128
|
if ("disabled" in checkpoint && !branchStartsWithoutRuntime) {
|
|
132
129
|
throw new Error("State Flow cannot checkpoint an unproven branch as ordinary disabled; restore a valid checkpoint first");
|
|
133
130
|
}
|
|
134
131
|
pi.appendEntry(SNAPSHOT_ENTRY_TYPE, checkpoint);
|
|
135
|
-
if ("
|
|
132
|
+
if ("boundary" in checkpoint) branchStartsWithoutRuntime = false;
|
|
136
133
|
}
|
|
137
134
|
|
|
138
135
|
function updateUi(ctx: ExtensionContext): void {
|
|
139
136
|
ctx.ui.setStatus(STATUS_KEY, compactStatus(snapshot, (color, text) => ctx.ui.theme.fg(color, text)));
|
|
140
137
|
}
|
|
141
138
|
|
|
142
|
-
function setPendingPublication(value: PendingPublicationDiagnostic | undefined): void {
|
|
143
|
-
pendingPublication = value === undefined ? undefined : structuredClone(value);
|
|
144
|
-
if (value === undefined) delete snapshot.meta.pendingPublication;
|
|
145
|
-
else snapshot.meta.pendingPublication = structuredClone(value);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
139
|
function clearRunTransient(): void {
|
|
149
|
-
terminalEligible = false;
|
|
150
|
-
resolutionPending = false;
|
|
151
|
-
fallbackAttempts = 0;
|
|
152
|
-
fallbackFailureReported = false;
|
|
153
140
|
responseAwaitingReconciliation = false;
|
|
154
141
|
completedRunAccepted = false;
|
|
142
|
+
turnAcceptedForBackup = false;
|
|
155
143
|
compactionPlan = undefined;
|
|
156
144
|
compactionInFlight = false;
|
|
157
|
-
runAnchorTimestamp = undefined;
|
|
158
145
|
skillReads.clear();
|
|
159
146
|
artifactReads.clear();
|
|
160
147
|
}
|
|
@@ -165,44 +152,59 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
165
152
|
artifactRefreshPending = true;
|
|
166
153
|
}
|
|
167
154
|
|
|
168
|
-
function
|
|
169
|
-
|
|
170
|
-
|
|
155
|
+
function refreshArtifactHints(): void {
|
|
156
|
+
if (!runtime?.view) {
|
|
157
|
+
artifactHints = {};
|
|
171
158
|
artifactInvalidations = [];
|
|
172
159
|
artifactReads.setCandidates([]);
|
|
173
160
|
return;
|
|
174
161
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
);
|
|
193
|
-
commitStage(stage, ctx, false);
|
|
162
|
+
const paths = new Set<string>();
|
|
163
|
+
for (const scope of ["global", "cwd", "session"] as const) for (const path of Object.keys(scopeStates[scope].artifacts)) paths.add(path);
|
|
164
|
+
const observations = new Map(inspectRegisteredArtifactPaths(paths).map((observation) => [observation.path, observation]));
|
|
165
|
+
const nextHints: Record<string, string> = {};
|
|
166
|
+
const nextInvalidations = new Map<string, ArtifactInvalidationRequest>();
|
|
167
|
+
for (const scope of ["global", "cwd", "session"] as const) {
|
|
168
|
+
const provenance = runtime.artifactProvenance(scope);
|
|
169
|
+
for (const [path, metadata] of Object.entries(scopeStates[scope].artifacts)) {
|
|
170
|
+
delete nextHints[path];
|
|
171
|
+
nextInvalidations.delete(path);
|
|
172
|
+
if (metadata.kind === "skill") continue;
|
|
173
|
+
const observed = observations.get(path);
|
|
174
|
+
if (observed?.kind !== "present") continue;
|
|
175
|
+
const need = classifyArtifactCompilationNeed({ path, scope, sourceFingerprint: observed.fingerprint }, metadata, ORDINARY_ARTIFACT_COMPILER, false, provenance[path]);
|
|
176
|
+
if (need.kind !== "requires-compilation") continue;
|
|
177
|
+
if (need.reason === "source-changed") nextHints[path] = SOURCE_CHANGED_HINT;
|
|
178
|
+
nextInvalidations.set(path, { path, scope, reason: need.reason });
|
|
194
179
|
}
|
|
195
|
-
artifactReads.setCandidates(artifactInvalidations);
|
|
196
|
-
} catch (error) {
|
|
197
|
-
artifactInvalidations = [];
|
|
198
|
-
artifactReads.setCandidates([]);
|
|
199
|
-
ctx.ui.notify(
|
|
200
|
-
`State Flow could not discover global Markdown sources: ${error instanceof Error ? error.message : String(error)}`,
|
|
201
|
-
"warning",
|
|
202
|
-
);
|
|
203
180
|
}
|
|
181
|
+
artifactHints = nextHints;
|
|
182
|
+
artifactInvalidations = [...nextInvalidations.values()].sort((left, right) => left.path.localeCompare(right.path));
|
|
183
|
+
artifactReads.setCandidates(artifactInvalidations);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function removeMissingRegisteredArtifacts(ctx: ExtensionContext): void {
|
|
187
|
+
if (!snapshot.config.enabled || !runtime?.view) return;
|
|
188
|
+
const owners = new Map<string, StateScope[]>();
|
|
189
|
+
for (const scope of ["global", "cwd", "session"] as const) for (const path of Object.keys(scopeStates[scope].artifacts)) {
|
|
190
|
+
owners.set(path, [...owners.get(path) ?? [], scope]);
|
|
191
|
+
}
|
|
192
|
+
const removals: AtomicScopePatches = {};
|
|
193
|
+
for (const observation of inspectRegisteredArtifactPaths(owners.keys())) {
|
|
194
|
+
if (observation.kind !== "missing") continue;
|
|
195
|
+
for (const scope of owners.get(observation.path) ?? []) {
|
|
196
|
+
const artifacts = (removals[scope]?.artifacts ?? {}) as JsonObject;
|
|
197
|
+
removals[scope] = { ...(removals[scope] ?? {}), artifacts: { ...artifacts, [observation.path]: null } };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (Object.keys(removals).length > 0) {
|
|
201
|
+
const stage = stageAtomicScopePatches(scopeStates, removals, [], runtime.causalBasis());
|
|
202
|
+
commitStage(stage, ctx, false);
|
|
203
|
+
}
|
|
204
|
+
refreshArtifactHints();
|
|
204
205
|
}
|
|
205
206
|
|
|
207
|
+
|
|
206
208
|
function installScopeStates(): void {
|
|
207
209
|
scopeStates = runtime?.view ? runtime.states() : { global: emptyState(), cwd: emptyState(), session: emptyState() };
|
|
208
210
|
}
|
|
@@ -221,40 +223,15 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
221
223
|
: active.filter((name) => !owned.includes(name)));
|
|
222
224
|
}
|
|
223
225
|
|
|
224
|
-
function recordPublication(
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
setPendingPublication({
|
|
229
|
-
commit: publication.push.commit,
|
|
230
|
-
error: publication.push.error ?? "unknown push failure",
|
|
231
|
-
});
|
|
232
|
-
ctx.ui.notify(
|
|
233
|
-
`State Flow accepted durable commit ${publication.push.commit.slice(0, 12)}, but push is pending: ${publication.push.error}`,
|
|
234
|
-
"warning",
|
|
235
|
-
);
|
|
236
|
-
} else if (revision !== undefined) {
|
|
237
|
-
setPendingPublication(undefined);
|
|
238
|
-
}
|
|
226
|
+
function recordPublication(_publication: RuntimePublication, _ctx: ExtensionContext): void {
|
|
227
|
+
// A passive view is not runtime authority; only accepted canonical publication establishes it.
|
|
228
|
+
branchStartsWithoutRuntime = false;
|
|
229
|
+
backupPending = true;
|
|
239
230
|
}
|
|
240
231
|
|
|
241
|
-
/**
|
|
232
|
+
/** Record accepted lifecycle persistence; Git backup is scheduled only after an accepted turn settles. */
|
|
242
233
|
function recordPolicyPublication(publication: RuntimePublication | undefined, ctx: ExtensionContext): void {
|
|
243
|
-
if (
|
|
244
|
-
recordPublication(publication, ctx);
|
|
245
|
-
const mode = snapshot.meta.remotePublication?.mode ?? "transition";
|
|
246
|
-
const target = publication.commit ?? publication.revision;
|
|
247
|
-
if (mode !== "turn-end" || target === undefined || !/^[0-9a-f]{40,64}$/.test(target)) return;
|
|
248
|
-
turnPublicationTarget = target;
|
|
249
|
-
try {
|
|
250
|
-
enqueueTurnPublication();
|
|
251
|
-
publicationWorker.launch();
|
|
252
|
-
} catch (error) {
|
|
253
|
-
ctx.ui.notify(
|
|
254
|
-
`State Flow accepted the local commit; remote publication is deferred: ${error instanceof Error ? error.message : String(error)}`,
|
|
255
|
-
"warning",
|
|
256
|
-
);
|
|
257
|
-
}
|
|
234
|
+
if (publication) recordPublication(publication, ctx);
|
|
258
235
|
}
|
|
259
236
|
|
|
260
237
|
function commitStage(stage: StagedScopedTransition, ctx: ExtensionContext, finalizeRun: boolean): boolean {
|
|
@@ -263,12 +240,9 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
263
240
|
try {
|
|
264
241
|
committed = commitScopedTransition(snapshot, scopeStates, stage, (accepted, nextSnapshot) => {
|
|
265
242
|
if (!runtime?.view) throw new Error("Temporal State Flow runtime is unavailable; reload before publishing");
|
|
266
|
-
const mode = nextSnapshot.meta.remotePublication?.mode ?? "transition";
|
|
267
243
|
const publication = runtime.publish(nextSnapshot, accepted !== undefined, accepted, {
|
|
268
|
-
pushRemote: mode === "transition",
|
|
269
244
|
provenance: stage.provenanceUpdates,
|
|
270
245
|
});
|
|
271
|
-
if (publication?.commit && mode === "turn-end") turnPublicationTarget = publication.commit;
|
|
272
246
|
if (publication) recordPublication(publication, ctx);
|
|
273
247
|
}, runtime!.causalBasis(), { finalizeRun });
|
|
274
248
|
} catch (error) {
|
|
@@ -277,59 +251,14 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
277
251
|
}
|
|
278
252
|
if (!committed) return false;
|
|
279
253
|
installScopeStates();
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
artifactReads.setCandidates(artifactInvalidations);
|
|
285
|
-
skillReads.clear();
|
|
286
|
-
artifactReads.clear();
|
|
287
|
-
}
|
|
254
|
+
artifactInvalidations = artifactInvalidations.filter(({ path }) => !acquiredArtifactPaths.has(path));
|
|
255
|
+
artifactReads.setCandidates(artifactInvalidations);
|
|
256
|
+
skillReads.clear();
|
|
257
|
+
artifactReads.clear();
|
|
288
258
|
persist();
|
|
289
259
|
return true;
|
|
290
260
|
}
|
|
291
261
|
|
|
292
|
-
function enqueueTurnPublication(): void {
|
|
293
|
-
const target = turnPublicationTarget;
|
|
294
|
-
turnPublicationTarget = undefined;
|
|
295
|
-
if (target) publicationWorker.enqueue(target);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function shutdownPublicationWorkers(ctx: ExtensionContext): Promise<void> {
|
|
299
|
-
return publicationShutdown ??= publicationWorker.shutdown(PUBLICATION_SHUTDOWN_WAIT_MS).then((completed) => {
|
|
300
|
-
if (!completed) ctx.ui.notify(`State Flow push cleanup is unconfirmed after ${PUBLICATION_SHUTDOWN_WAIT_MS}ms; worker leases remain held until child exit.`, "warning");
|
|
301
|
-
});
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
function retryPendingPush(ctx: ExtensionContext): void {
|
|
305
|
-
if (pendingPublication === undefined || !runtime?.view) return;
|
|
306
|
-
const mode = snapshot.meta.remotePublication?.mode ?? "transition";
|
|
307
|
-
if (mode !== "transition") {
|
|
308
|
-
const target = pendingPublication.commit;
|
|
309
|
-
setPendingPublication(undefined);
|
|
310
|
-
if (mode === "turn-end") {
|
|
311
|
-
turnPublicationTarget = target;
|
|
312
|
-
try {
|
|
313
|
-
enqueueTurnPublication();
|
|
314
|
-
publicationWorker.launch();
|
|
315
|
-
} catch (error) {
|
|
316
|
-
ctx.ui.notify(`State Flow retained local state; asynchronous publication recovery is deferred: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
const result = pushGitCommit(repositoryRoot, pendingPublication.commit);
|
|
322
|
-
if (result.status !== "pending") {
|
|
323
|
-
setPendingPublication(undefined);
|
|
324
|
-
persist();
|
|
325
|
-
ctx.ui.notify(result.status === "local"
|
|
326
|
-
? `State Flow retained local-only durable commit ${result.commit.slice(0, 12)}; no remote configured.`
|
|
327
|
-
: `State Flow pushed pending durable commit ${result.commit.slice(0, 12)}.`, "info");
|
|
328
|
-
} else {
|
|
329
|
-
setPendingPublication({ commit: result.commit, error: result.error ?? "unknown push failure" });
|
|
330
|
-
persist();
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
262
|
|
|
334
263
|
function currentRehydrationPhase(): RehydrationPhase | undefined {
|
|
335
264
|
return rehydrationPhase;
|
|
@@ -340,89 +269,36 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
340
269
|
const diagnosticStates = scopeStates;
|
|
341
270
|
const view = runtime?.view;
|
|
342
271
|
const diagnosticRecent = runtime?.recent() ?? [];
|
|
343
|
-
const durableStateError =
|
|
344
|
-
:
|
|
345
|
-
|
|
346
|
-
let staleArtifacts: StatusDiagnostics["staleArtifacts"] = [];
|
|
347
|
-
let artifactFreshnessError: string | undefined;
|
|
348
|
-
if (durableStateError !== undefined) {
|
|
349
|
-
artifactFreshnessError = "durable global artifact registry is unavailable";
|
|
350
|
-
} else {
|
|
351
|
-
try {
|
|
352
|
-
const discovery = globalMarkdown.refresh(Object.keys(diagnosticStates.global.artifacts));
|
|
353
|
-
artifactFreshnessError = discovery.unavailable;
|
|
354
|
-
const plan = planArtifactInvalidation(
|
|
355
|
-
discovery.sources,
|
|
356
|
-
diagnosticStates.global.artifacts,
|
|
357
|
-
ORDINARY_ARTIFACT_COMPILER,
|
|
358
|
-
{ removed: discovery.removed },
|
|
359
|
-
runtime?.artifactProvenance("global") ?? {},
|
|
360
|
-
);
|
|
361
|
-
staleArtifacts = [
|
|
362
|
-
...plan.requiresCompilation.map(({ path, reason }) => ({ scope: "global" as const, path, reason })),
|
|
363
|
-
...plan.removed.map((path) => ({ scope: "global" as const, path, reason: "source-removed" as const })),
|
|
364
|
-
];
|
|
365
|
-
} catch (error) {
|
|
366
|
-
artifactFreshnessError = error instanceof Error ? error.message : String(error);
|
|
367
|
-
}
|
|
368
|
-
}
|
|
272
|
+
const durableStateError = snapshot.meta.validation?.attempt === 0 ? snapshot.meta.validation.error
|
|
273
|
+
: view ? undefined : "no temporal runtime is selected on this branch";
|
|
369
274
|
|
|
275
|
+
const staleArtifacts: StatusDiagnostics["staleArtifacts"] = durableStateError === undefined
|
|
276
|
+
? artifactInvalidations.map(({ path, scope, reason }) => ({ scope: scope ?? "global", path, reason }))
|
|
277
|
+
: [];
|
|
370
278
|
const session = sessionAddress(ctx);
|
|
371
|
-
let publicationQueue: PublicationQueueState | undefined;
|
|
372
|
-
let publicationQueueError: string | undefined;
|
|
373
|
-
try {
|
|
374
|
-
const destination = resolveGitPushDestination(repositoryRoot);
|
|
375
|
-
publicationQueue = destination ? loadPublicationQueue(publicationQueuePath(destination)) : undefined;
|
|
376
|
-
} catch (error) {
|
|
377
|
-
publicationQueue = undefined;
|
|
378
|
-
publicationQueueError = error instanceof Error ? error.message : String(error);
|
|
379
|
-
}
|
|
380
279
|
return {
|
|
381
280
|
repositoryRoot,
|
|
382
281
|
cwdScopeKey: cwdScopeKey(cwd),
|
|
383
282
|
sessionScopeKey: sessionScopeKey(session.key),
|
|
384
283
|
scopeStates: diagnosticStates,
|
|
385
284
|
recent: diagnosticRecent,
|
|
285
|
+
historyLimit: config.historyLimit,
|
|
386
286
|
...(view === undefined ? {} : { temporal: {
|
|
387
287
|
head: structuredClone(view.lineage.at(-1)!),
|
|
388
288
|
historyDepth: view.lineage.length - 1,
|
|
389
289
|
tailCounts: { global: view.scopes.global.patches.length, cwd: view.scopes.cwd.patches.length, session: view.scopes.session.patches.length },
|
|
390
290
|
} }),
|
|
391
291
|
staleArtifacts,
|
|
392
|
-
...(artifactFreshnessError === undefined ? {} : { artifactFreshnessError }),
|
|
393
292
|
...(durableStateError === undefined ? {} : { durableStateError }),
|
|
394
|
-
...(pendingPublication === undefined ? {} : { pendingPublication }),
|
|
395
|
-
...(publicationQueue === undefined ? {} : { publicationQueue }),
|
|
396
|
-
...(publicationQueueError === undefined ? {} : { publicationQueueError }),
|
|
397
293
|
};
|
|
398
294
|
}
|
|
399
295
|
|
|
400
|
-
function
|
|
401
|
-
if (!forkInitialization) {
|
|
402
|
-
try {
|
|
403
|
-
return runtime!.prepareRestore(revision, legacy);
|
|
404
|
-
} catch (error) {
|
|
405
|
-
if (error instanceof MissingSessionRuntimeError && typeof ctx.sessionManager.getHeader()?.parentSession === "string") {
|
|
406
|
-
throw new RevisionUnavailableError("State Flow checkpoint has no child-owned runtime; select a child checkpoint or resume the parent");
|
|
407
|
-
}
|
|
408
|
-
throw error;
|
|
409
|
-
}
|
|
410
|
-
}
|
|
296
|
+
function forkSource(ctx: ExtensionContext): SessionAddress {
|
|
411
297
|
const file = ctx.sessionManager.getHeader()?.parentSession;
|
|
412
298
|
if (typeof file !== "string" || !isAbsolute(file)) throw new Error("State Flow fork requires a persisted native parent session");
|
|
413
299
|
const parent = readNativeSessionHeader(file);
|
|
414
300
|
if (parent.cwd !== resolve(ctx.cwd)) throw new Error("State Flow fork parent CWD identity mismatch");
|
|
415
|
-
|
|
416
|
-
const prepared = runtime!.prepareFork(source, revision);
|
|
417
|
-
return { snapshot: prepared.snapshot, restore: () => {
|
|
418
|
-
const accepted = prepared.fork();
|
|
419
|
-
snapshot = accepted.snapshot;
|
|
420
|
-
recordPolicyPublication(accepted.publication, ctx);
|
|
421
|
-
// Copied Stop markers belong to the parent, including after a child reload/resume.
|
|
422
|
-
pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, { reset: true, owner: ctx.sessionManager.getSessionId() });
|
|
423
|
-
forkInitialization = false;
|
|
424
|
-
return snapshot;
|
|
425
|
-
} };
|
|
301
|
+
return resolveSessionAddress(parent.file, parent.id, parent.timestamp);
|
|
426
302
|
}
|
|
427
303
|
|
|
428
304
|
function restoreActiveBranch(ctx: ExtensionContext, sessionStartReason?: unknown): void {
|
|
@@ -435,9 +311,8 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
435
311
|
telegramStartPending = false;
|
|
436
312
|
activeContext = ctx;
|
|
437
313
|
const session = sessionAddress(ctx);
|
|
438
|
-
runtime = new TemporalRuntime(ctx.cwd, session, repositoryRoot);
|
|
314
|
+
runtime = new TemporalRuntime(ctx.cwd, session, repositoryRoot, undefined, config.historyLimit);
|
|
439
315
|
installScopeStates();
|
|
440
|
-
pendingPublication = undefined;
|
|
441
316
|
branchHasSnapshot = false;
|
|
442
317
|
branchStartsWithoutRuntime = false;
|
|
443
318
|
forkInitialization = sessionStartReason === "fork";
|
|
@@ -445,16 +320,27 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
445
320
|
const branch = ctx.sessionManager.getBranch();
|
|
446
321
|
const discovery = discoverSnapshotData(branch);
|
|
447
322
|
let restoreSelected: (() => Snapshot) | undefined;
|
|
448
|
-
const recovery = recoverSnapshot(discovery.candidates, (
|
|
449
|
-
|
|
450
|
-
const prepared =
|
|
451
|
-
restoreSelected =
|
|
323
|
+
const recovery = recoverSnapshot(discovery.candidates, (checkpoint) => {
|
|
324
|
+
if (forkInitialization) {
|
|
325
|
+
const prepared = runtime!.prepareBoundaryFork(forkSource(ctx), checkpoint);
|
|
326
|
+
restoreSelected = () => {
|
|
327
|
+
const accepted = prepared.fork();
|
|
328
|
+
snapshot = accepted.snapshot;
|
|
329
|
+
recordPolicyPublication(accepted.publication, ctx);
|
|
330
|
+
pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, { reset: true, owner: ctx.sessionManager.getSessionId() });
|
|
331
|
+
forkInitialization = false;
|
|
332
|
+
return snapshot;
|
|
333
|
+
};
|
|
452
334
|
return prepared.snapshot;
|
|
453
|
-
} catch (error) {
|
|
454
|
-
// A failed source proof never licenses an older/empty private copy.
|
|
455
|
-
if (forkInitialization) throw new RevisionUnavailableError(`Cannot copy State Flow fork source: ${error instanceof Error ? error.message : String(error)}`);
|
|
456
|
-
throw error;
|
|
457
335
|
}
|
|
336
|
+
const prepared = runtime!.prepareBoundaryRestore(checkpoint);
|
|
337
|
+
restoreSelected = () => {
|
|
338
|
+
const restored = prepared.restore();
|
|
339
|
+
const publication = runtime!.acceptRestoredOrigin(restored);
|
|
340
|
+
recordPolicyPublication(publication, ctx);
|
|
341
|
+
return restored;
|
|
342
|
+
};
|
|
343
|
+
return prepared.snapshot;
|
|
458
344
|
});
|
|
459
345
|
branchStartsWithoutRuntime = recovery.disabledMarker === true
|
|
460
346
|
|| (discovery.candidates.length === 0 && discovery.errors.length === 0);
|
|
@@ -464,10 +350,9 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
464
350
|
snapshot = migrationFailure({}, `Snapshot restoration failed: ${discovery.errors[0]}`);
|
|
465
351
|
} else if (discovery.candidates.length > 0) {
|
|
466
352
|
snapshot = recovery.snapshot;
|
|
467
|
-
if (branchHasSnapshot &&
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
if (snapshot.meta.durableBase !== selectedRevision) persist();
|
|
353
|
+
if (branchHasSnapshot && restoreSelected) {
|
|
354
|
+
snapshot = restoreSelected();
|
|
355
|
+
persist();
|
|
471
356
|
} else if (branchHasSnapshot && snapshot.config.enabled) {
|
|
472
357
|
const publication = runtime.initialize(snapshot, true);
|
|
473
358
|
recordPolicyPublication(publication, ctx);
|
|
@@ -475,9 +360,6 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
475
360
|
installScopeStates();
|
|
476
361
|
} else if (config.autoStart && isNewSession(sessionStartReason, branch)) {
|
|
477
362
|
snapshot = startEpisode(hasPriorConversation(branch));
|
|
478
|
-
snapshot.meta.remotePublication = serializeRemotePublicationPolicyDocument(
|
|
479
|
-
resolveRemotePublicationPolicy(config.remotePublication, { legacyRuntime: false }),
|
|
480
|
-
);
|
|
481
363
|
runtime.prepare();
|
|
482
364
|
const initialization = runtime.initialize(snapshot, true);
|
|
483
365
|
recordPolicyPublication(initialization, ctx);
|
|
@@ -502,27 +384,17 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
502
384
|
meta: { ...snapshot.meta, validation: failure.meta.validation },
|
|
503
385
|
};
|
|
504
386
|
}
|
|
505
|
-
if (branchHasSnapshot && snapshot.meta.remotePublication === undefined) {
|
|
506
|
-
snapshot.meta.remotePublication = serializeRemotePublicationPolicyDocument(
|
|
507
|
-
resolveRemotePublicationPolicy(undefined, { legacyRuntime: true }),
|
|
508
|
-
);
|
|
509
|
-
}
|
|
510
|
-
if (snapshot.config.enabled && snapshot.meta.remotePublication === undefined) {
|
|
511
|
-
snapshot.meta.remotePublication = serializeRemotePublicationPolicyDocument(
|
|
512
|
-
resolveRemotePublicationPolicy(undefined, { legacyRuntime: true }),
|
|
513
|
-
);
|
|
514
|
-
}
|
|
515
|
-
pendingPublication = snapshot.meta.pendingPublication === undefined
|
|
516
|
-
? undefined
|
|
517
|
-
: structuredClone(snapshot.meta.pendingPublication);
|
|
518
387
|
if (!snapshot.config.enabled && snapshot.meta.validation?.attempt === 0) {
|
|
388
|
+
// A failed acceptance may have installed a prepared view; only shared passive reads may survive.
|
|
389
|
+
runtime = createRuntime(ctx);
|
|
390
|
+
installScopeStates();
|
|
519
391
|
ctx.ui.notify(`State Flow restored disabled: ${snapshot.meta.validation.error}`, "error");
|
|
520
392
|
}
|
|
521
393
|
if (retainsPhysicalSessionProjection(sessionStartReason) && runtime?.view) {
|
|
522
394
|
const boundary = findPassiveStopBoundary(ctx.sessionManager.getBranch(), ctx.sessionManager.getSessionId(), PASSIVE_STOP_ENTRY_TYPE);
|
|
523
395
|
if (boundary !== undefined) {
|
|
524
396
|
const continuation = createPassiveContinuation(
|
|
525
|
-
|
|
397
|
+
projectModelState(overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session)),
|
|
526
398
|
boundary.at,
|
|
527
399
|
boundary.from,
|
|
528
400
|
);
|
|
@@ -547,77 +419,6 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
547
419
|
diagnosticWriter.record(sessionAddress(ctx).id, ctx.cwd, error, category, extras);
|
|
548
420
|
}
|
|
549
421
|
|
|
550
|
-
function continueFallbackResolution(lastWarning: boolean): void {
|
|
551
|
-
resolutionPending = true;
|
|
552
|
-
pi.sendMessage({
|
|
553
|
-
customType: VALIDATION_MESSAGE_TYPE,
|
|
554
|
-
content: lastWarning
|
|
555
|
-
? "This is the last State Flow fallback turn. The iteration's answer is preserved and will not change. Apply the final:true patch now: call patch_state with any remaining durable scope changes and final:true, or with {\"final\":true} when nothing remains. Do not restate the answer."
|
|
556
|
-
: "The iteration's answer is preserved as the final response; later turns cannot replace it. Apply the final:true patch: call patch_state with any durable scope changes from this iteration (including required artifact or Skill compilation) and final:true, or with {\"final\":true} when nothing remains to persist. Do not restate the answer.",
|
|
557
|
-
display: false,
|
|
558
|
-
}, { deliverAs: "steer", triggerTurn: true });
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
/** Preserve the primary answer as this iteration's response and steer bounded fallback turns whose only purpose is the final:true patch. */
|
|
562
|
-
function beginFallbackResolution(ctx: ExtensionContext, message: { content?: unknown }, reason: string): void {
|
|
563
|
-
responseAwaitingReconciliation = true;
|
|
564
|
-
fallbackAttempts = 0;
|
|
565
|
-
fallbackFailureReported = false;
|
|
566
|
-
recordDiagnostic(`Preserved the terminal draft as the iteration response; fallback resolution started: ${reason}`, "terminal-pending", ctx, {
|
|
567
|
-
content: message.content,
|
|
568
|
-
resolutionAttempt: 0,
|
|
569
|
-
terminalEligible,
|
|
570
|
-
});
|
|
571
|
-
continueFallbackResolution(MAX_FALLBACK_ATTEMPTS === 1);
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
/** Fallback turns never become the response; they exist only to supply the final:true patch. */
|
|
575
|
-
function resolveFallbackTurn(ctx: ExtensionContext, message: { content?: unknown }): any {
|
|
576
|
-
let resolved = terminalEligible;
|
|
577
|
-
if (resolved) {
|
|
578
|
-
try {
|
|
579
|
-
validateFinalEligibility(scopeStates, skillReads.successful.values(), runtime!.causalBasis(), artifactReads.successful.values());
|
|
580
|
-
} catch (error) {
|
|
581
|
-
resolved = false;
|
|
582
|
-
recordDiagnostic(error instanceof Error ? error.message : String(error), "terminal-pending", ctx, {
|
|
583
|
-
content: message.content,
|
|
584
|
-
resolutionAttempt: fallbackAttempts,
|
|
585
|
-
terminalEligible,
|
|
586
|
-
});
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
if (resolved) {
|
|
590
|
-
resolutionPending = false;
|
|
591
|
-
recordDiagnostic(`Fallback resolution obtained final:true after ${fallbackAttempts} fallback turn${fallbackAttempts === 1 ? "" : "s"}; the preserved answer stands`, "finalization", ctx, {
|
|
592
|
-
content: message.content,
|
|
593
|
-
resolutionAttempt: fallbackAttempts,
|
|
594
|
-
terminalEligible,
|
|
595
|
-
});
|
|
596
|
-
return { message: { ...message, role: "assistant" as const, content: [] } };
|
|
597
|
-
}
|
|
598
|
-
fallbackAttempts = Math.min(MAX_FALLBACK_ATTEMPTS, fallbackAttempts + 1);
|
|
599
|
-
if (fallbackAttempts < MAX_FALLBACK_ATTEMPTS) {
|
|
600
|
-
recordDiagnostic(`Fallback turn ended without final:true (${fallbackAttempts}/${MAX_FALLBACK_ATTEMPTS})`, "terminal-pending", ctx, {
|
|
601
|
-
content: message.content,
|
|
602
|
-
resolutionAttempt: fallbackAttempts,
|
|
603
|
-
terminalEligible,
|
|
604
|
-
});
|
|
605
|
-
continueFallbackResolution(fallbackAttempts + 1 >= MAX_FALLBACK_ATTEMPTS);
|
|
606
|
-
return { message: { ...message, role: "assistant" as const, content: [] } };
|
|
607
|
-
}
|
|
608
|
-
resolutionPending = false;
|
|
609
|
-
recordDiagnostic(`Fallback resolution exhausted without final:true (${fallbackAttempts}/${MAX_FALLBACK_ATTEMPTS}); the preserved response and current state remain`, "finalization", ctx, {
|
|
610
|
-
content: message.content,
|
|
611
|
-
resolutionAttempt: fallbackAttempts,
|
|
612
|
-
terminalEligible,
|
|
613
|
-
});
|
|
614
|
-
if (!fallbackFailureReported) {
|
|
615
|
-
fallbackFailureReported = true;
|
|
616
|
-
ctx.ui.notify(`State Flow kept the preserved answer; no final:true patch arrived after ${MAX_FALLBACK_ATTEMPTS} fallback turns, so the iteration closed with its current state.`, "warning");
|
|
617
|
-
}
|
|
618
|
-
return { message: { ...message, role: "assistant" as const, content: [] } };
|
|
619
|
-
}
|
|
620
|
-
|
|
621
422
|
pi.registerTool({
|
|
622
423
|
name: READ_STATE_TOOL_NAME,
|
|
623
424
|
label: "Read State",
|
|
@@ -638,16 +439,17 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
638
439
|
if (params.path !== undefined && params.paths !== undefined) throw new Error("read_state accepts path or paths, not both");
|
|
639
440
|
{
|
|
640
441
|
const paths = params.paths ?? [params.path!];
|
|
442
|
+
if (paths.some((path) => /^session(?:\.|\[|$)/.test(path))) assertSelectedBranchAvailable();
|
|
641
443
|
if (paths.length === 1 && /^(?:global|cwd|session)\.patches(?:\[\d+\])?$/.test(paths[0]!)) {
|
|
642
444
|
if (params.projection !== undefined && params.projection !== "value") throw new Error("Scope patch paths support only the value projection");
|
|
643
|
-
const result = readStatePath(runtime.view, paths[0]
|
|
445
|
+
const result = readStatePath(runtime.view, paths[0]!, config.historyLimit);
|
|
644
446
|
if (!("patch" in result)) throw new Error("Expected a scope patch path");
|
|
645
447
|
return {
|
|
646
448
|
content: [{ type: "text", text: `\n${JSON.stringify({ patch: result.patch })}` }],
|
|
647
449
|
details: { path: paths[0], transitionId: result.boundary.id } as ReadDetails,
|
|
648
450
|
};
|
|
649
451
|
}
|
|
650
|
-
const result = readProjectedState(runtime.view, paths, params.projection);
|
|
452
|
+
const result = readProjectedState(runtime.view, paths, params.projection, config.historyLimit);
|
|
651
453
|
return {
|
|
652
454
|
content: [{ type: "text", text: `\n${JSON.stringify(result)}` }],
|
|
653
455
|
details: { ...(params.path === undefined ? { paths } : { path: params.path }), projection: params.projection ?? "value" } as ReadDetails,
|
|
@@ -662,10 +464,10 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
662
464
|
pi.registerTool({
|
|
663
465
|
name: PATCH_STATE_TOOL_NAME,
|
|
664
466
|
label: "Patch State",
|
|
665
|
-
description: "The sole State Flow semantic mutation protocol. Supply
|
|
666
|
-
promptSnippet: "Atomically patch global/cwd/session
|
|
467
|
+
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.",
|
|
468
|
+
promptSnippet: "Atomically patch one or more global/cwd/session scopes",
|
|
667
469
|
promptGuidelines: [
|
|
668
|
-
"Use patch_state for durable semantic changes
|
|
470
|
+
"Use patch_state only for material durable semantic changes; ordinary answers need no finalization call.",
|
|
669
471
|
"Use patch_state as reconciliation, not append-only notes: place new knowledge at the narrowest valid scope and remove superseded or completed state from touched branches.",
|
|
670
472
|
"Call patch_state alone in an assistant response; after its acknowledgement, further reasoning, tools, and later patch_state calls remain allowed.",
|
|
671
473
|
],
|
|
@@ -674,9 +476,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
674
476
|
global: Type.Optional(Type.Record(Type.String(), Type.Unknown(), { description: "Optional global semantic patch" })),
|
|
675
477
|
cwd: Type.Optional(Type.Record(Type.String(), Type.Unknown(), { description: "Optional project semantic patch" })),
|
|
676
478
|
session: Type.Optional(Type.Record(Type.String(), Type.Unknown(), { description: "Optional session semantic patch" })),
|
|
677
|
-
final: Type.Optional(Type.Boolean({ description: "Set exactly true to permit this iteration to finish at a later turn_end" })),
|
|
678
479
|
}, { additionalProperties: false }),
|
|
679
|
-
prepareArguments: normalizePatchStateArguments,
|
|
680
480
|
renderResult(result, { isPartial }, theme, context) {
|
|
681
481
|
const text = result.content.find((block) => block.type === "text")?.text ?? "";
|
|
682
482
|
if (context.isError) return new Text(separatedOutput(text), 0, 0);
|
|
@@ -686,13 +486,13 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
686
486
|
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
687
487
|
try {
|
|
688
488
|
if (!passiveToolsAvailable()) throw new Error("State Flow tools are disabled by configuration");
|
|
489
|
+
assertSelectedBranchAvailable();
|
|
689
490
|
if (signal?.aborted) throw new Error("State Flow patch was aborted before materialization");
|
|
690
491
|
if (!isObject(params)) throw new Error("patch_state requires an object");
|
|
691
|
-
const allowed = new Set(["global", "cwd", "session"
|
|
492
|
+
const allowed = new Set(["global", "cwd", "session"]);
|
|
692
493
|
for (const key of Object.keys(params)) {
|
|
693
494
|
if (!allowed.has(key)) throw new Error(`patch_state does not accept field ${key}`);
|
|
694
495
|
}
|
|
695
|
-
if (Object.hasOwn(params, "final") && typeof params.final !== "boolean") throw new Error("patch_state final must be a Boolean when supplied");
|
|
696
496
|
const patches: AtomicScopePatches = {};
|
|
697
497
|
for (const scope of ["global", "cwd", "session"] as const) {
|
|
698
498
|
if (!Object.hasOwn(params, scope)) continue;
|
|
@@ -702,16 +502,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
702
502
|
patches[scope] = patch;
|
|
703
503
|
}
|
|
704
504
|
const scopes = Object.keys(patches) as StateScope[];
|
|
705
|
-
if (scopes.length === 0)
|
|
706
|
-
if (params.final === false) {
|
|
707
|
-
return { content: [{ type: "text", text: "\nState unchanged; iteration remains non-terminal." }], details: { final: false } };
|
|
708
|
-
}
|
|
709
|
-
if (params.final !== true) throw new Error('patch_state requires at least one scope patch or {"final":true}');
|
|
710
|
-
if (!snapshot.config.enabled) return { content: [{ type: "text", text: "\nState unchanged; passive turns have no terminal barrier." }], details: { final: false } };
|
|
711
|
-
validateFinalEligibility(scopeStates, skillReads.successful.values(), runtime!.causalBasis(), artifactReads.successful.values());
|
|
712
|
-
terminalEligible = true;
|
|
713
|
-
return { content: [{ type: "text", text: "\nState iteration is terminal-eligible." }], details: { final: true } };
|
|
714
|
-
}
|
|
505
|
+
if (scopes.length === 0) throw new Error("patch_state requires at least one materially changed scope patch");
|
|
715
506
|
if (!runtime?.view) {
|
|
716
507
|
runtime ??= createRuntime(ctx);
|
|
717
508
|
runtime.prepare();
|
|
@@ -720,19 +511,21 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
720
511
|
installScopeStates();
|
|
721
512
|
branchHasSnapshot = true;
|
|
722
513
|
}
|
|
723
|
-
runtime.migrateLegacyStorage();
|
|
724
514
|
installScopeStates();
|
|
515
|
+
for (const acquired of artifactReads.successful.values()) {
|
|
516
|
+
const observation = inspectRegisteredArtifactPaths([acquired.path])[0];
|
|
517
|
+
if (acquired.sourceFingerprint === undefined || observation?.kind !== "present"
|
|
518
|
+
|| !sameArtifactSourceFingerprint(acquired.sourceFingerprint, observation.fingerprint)) {
|
|
519
|
+
throw new Error(`Artifact source changed after acquisition: ${acquired.path}`);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
725
522
|
const stage = stageAtomicScopePatches(scopeStates, patches, skillReads.successful.values(), runtime.causalBasis(), artifactReads.successful.values());
|
|
726
523
|
const semanticChange = (["global", "cwd", "session"] as const).some((scope) => !sameJson(scopeStates[scope], stage.nextStates[scope]));
|
|
727
524
|
const provenanceChange = Object.values(stage.provenanceUpdates).some((updates) => Object.keys(updates).length > 0);
|
|
728
|
-
if (!semanticChange && !provenanceChange) throw new Error(
|
|
525
|
+
if (!semanticChange && !provenanceChange) throw new Error("patch_state scope patches must materially update state or required provenance");
|
|
729
526
|
commitStage(stage, ctx, false);
|
|
730
|
-
if (params.final === true && snapshot.config.enabled) {
|
|
731
|
-
terminalEligible = true;
|
|
732
|
-
}
|
|
733
527
|
updateUi(ctx);
|
|
734
|
-
|
|
735
|
-
return { content: [{ type: "text", text: `\nState materialized atomically at ${scopes.join("+")} scope${scopes.length === 1 ? "" : "s"}${publication}.` }], details: { scopes, final: params.final === true, step: snapshot.meta.step } };
|
|
528
|
+
return { content: [{ type: "text", text: `\nState materialized atomically at ${scopes.join("+")} scope${scopes.length === 1 ? "" : "s"}.` }], details: { scopes, step: snapshot.meta.step } };
|
|
736
529
|
} catch (error) {
|
|
737
530
|
let attempted: unknown;
|
|
738
531
|
try { attempted = structuredClone(params); } catch { attempted = undefined; }
|
|
@@ -740,8 +533,6 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
740
533
|
input: attempted,
|
|
741
534
|
tool: PATCH_STATE_TOOL_NAME,
|
|
742
535
|
toolCallId,
|
|
743
|
-
resolutionAttempt: fallbackAttempts,
|
|
744
|
-
terminalEligible,
|
|
745
536
|
});
|
|
746
537
|
throw separatedFailure(error);
|
|
747
538
|
}
|
|
@@ -757,36 +548,28 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
757
548
|
const previousArtifactRefreshPending = artifactRefreshPending;
|
|
758
549
|
const previousArtifactInvalidations = structuredClone(artifactInvalidations);
|
|
759
550
|
try {
|
|
760
|
-
if (!runtime?.view
|
|
761
|
-
|
|
551
|
+
if (!branchStartsWithoutRuntime && (!runtime?.view || snapshot.meta.validation?.attempt === 0)) {
|
|
552
|
+
const retryFork = forkInitialization;
|
|
553
|
+
restoreActiveBranch(ctx, retryFork ? "fork" : undefined);
|
|
762
554
|
}
|
|
555
|
+
assertSelectedBranchAvailable();
|
|
556
|
+
if (!runtime?.view && !branchStartsWithoutRuntime) throw new Error("Selected State Flow boundary is unavailable; restore its canonical files before starting State Flow");
|
|
763
557
|
const branch = ctx.sessionManager.getBranch();
|
|
764
558
|
activeContext = ctx;
|
|
765
559
|
runtime ??= createRuntime(ctx);
|
|
766
560
|
if (branchStartsWithoutRuntime) runtime.prepare();
|
|
767
|
-
runtime.migrateLegacyStorage();
|
|
768
561
|
const bootstrap = (!branchHasSnapshot || !snapshot.config.enabled)
|
|
769
562
|
&& (hasPriorConversation(branch) || previousPassiveContinuation !== undefined);
|
|
770
|
-
if (!runtime.view && snapshot.meta.durableBase) {
|
|
771
|
-
snapshot = prepareBranchRestore(ctx, snapshot.meta.durableBase, snapshot).restore();
|
|
772
|
-
setPendingPublication(snapshot.meta.pendingPublication);
|
|
773
|
-
branchHasSnapshot = true;
|
|
774
|
-
}
|
|
775
563
|
const existingBranch = branchHasSnapshot;
|
|
776
564
|
snapshot = existingBranch
|
|
777
565
|
? resumeEpisode(snapshot, bootstrap)
|
|
778
566
|
: startEpisode(bootstrap);
|
|
779
|
-
if (snapshot.meta.remotePublication === undefined) {
|
|
780
|
-
snapshot.meta.remotePublication = serializeRemotePublicationPolicyDocument(
|
|
781
|
-
resolveRemotePublicationPolicy(existingBranch ? undefined : config.remotePublication, { legacyRuntime: existingBranch }),
|
|
782
|
-
);
|
|
783
|
-
}
|
|
784
567
|
branchHasSnapshot = true;
|
|
785
568
|
// Activation reasserts the selected session stream as a complete cohort. A resumed
|
|
786
569
|
// branch may have no reusable temporal revision after an extension upgrade; a
|
|
787
570
|
// runtime-only write would then reject its selected session as an omitted change.
|
|
788
571
|
const publication = runtime.view
|
|
789
|
-
? runtime.
|
|
572
|
+
? runtime.publish(snapshot, true)
|
|
790
573
|
: runtime.initialize(snapshot, true, undefined, branchStartsWithoutRuntime);
|
|
791
574
|
recordPolicyPublication(publication, ctx);
|
|
792
575
|
installScopeStates();
|
|
@@ -836,29 +619,22 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
836
619
|
|
|
837
620
|
function stopStateFlow(ctx: ExtensionContext): StateFlowTelegramControlResult {
|
|
838
621
|
if (!activeContext) restoreActiveBranch(ctx);
|
|
622
|
+
assertSelectedBranchAvailable();
|
|
839
623
|
telegramStartPending = false;
|
|
840
|
-
|
|
841
|
-
let current = snapshot;
|
|
842
|
-
if (!selected?.view && snapshot.meta.durableBase) {
|
|
843
|
-
selected = createRuntime(ctx);
|
|
844
|
-
current = selected.restore(snapshot.meta.durableBase, snapshot);
|
|
845
|
-
}
|
|
624
|
+
const current = snapshot;
|
|
846
625
|
const stoppedAt = Date.now();
|
|
847
|
-
const
|
|
848
|
-
const
|
|
626
|
+
const stopped = stopEpisode(current);
|
|
627
|
+
const publication = !branchStartsWithoutRuntime && runtime?.view ? runtime.publish(stopped) : undefined;
|
|
628
|
+
installScopeStates();
|
|
629
|
+
// Freeze the accepted shared adoption, not the pre-publication cache.
|
|
630
|
+
const exitHandoff = current.config.enabled && runtime?.view
|
|
849
631
|
? createPassiveContinuation(
|
|
850
|
-
|
|
632
|
+
projectModelState(overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session)),
|
|
851
633
|
stoppedAt,
|
|
852
634
|
ctx.isIdle() ? undefined : runAnchorTimestamp,
|
|
853
635
|
)
|
|
854
636
|
: undefined;
|
|
855
637
|
const retainedHandoff = exitHandoff ?? (!current.config.enabled ? passiveContinuation : undefined);
|
|
856
|
-
const stopped = stopEpisode(current);
|
|
857
|
-
const publication = selected?.view ? selected.publish(stopped) : undefined;
|
|
858
|
-
if (selected !== runtime) {
|
|
859
|
-
runtime = selected;
|
|
860
|
-
installScopeStates();
|
|
861
|
-
}
|
|
862
638
|
snapshot = stopped;
|
|
863
639
|
recordPolicyPublication(publication, ctx);
|
|
864
640
|
branchHasSnapshot = true;
|
|
@@ -895,13 +671,11 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
895
671
|
startPending: telegramStartPending,
|
|
896
672
|
}),
|
|
897
673
|
state: (scope) => {
|
|
674
|
+
if (scope === "session") assertSelectedBranchAvailable();
|
|
898
675
|
const selected = scope === "effective"
|
|
899
676
|
? overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session)
|
|
900
677
|
: scopeStates[scope];
|
|
901
|
-
return {
|
|
902
|
-
...projectStateForModel(selected),
|
|
903
|
-
...(Object.hasOwn(selected, "lazy") ? { lazy: structuredClone(selected.lazy) } : {}),
|
|
904
|
-
};
|
|
678
|
+
return { ...projectModelState(selected), lazy: structuredClone(selected.lazy) };
|
|
905
679
|
},
|
|
906
680
|
canStartNow: () => activeContext === undefined || activeContext.isIdle(),
|
|
907
681
|
start: () => {
|
|
@@ -927,46 +701,50 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
927
701
|
void telegram.ensure();
|
|
928
702
|
|
|
929
703
|
pi.on("before_agent_start", (event, ctx) => {
|
|
704
|
+
// Native run identity is independent of semantic enablement and survives mode toggles.
|
|
705
|
+
runAnchorTimestamp = undefined;
|
|
930
706
|
if (!snapshot.config.enabled) {
|
|
931
707
|
if (!config.passiveBootstrap || !runtime?.view) return;
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
};
|
|
708
|
+
(event.systemPromptOptions.sections ??= {}).state_flow = PASSIVE_MEMORY_PROTOCOL;
|
|
709
|
+
return;
|
|
935
710
|
}
|
|
936
711
|
skillReads.clear();
|
|
937
712
|
artifactReads.clear();
|
|
938
|
-
if (artifactRefreshPending) refreshArtifactInvalidations(ctx);
|
|
939
|
-
terminalEligible = false;
|
|
940
|
-
resolutionPending = false;
|
|
941
|
-
fallbackAttempts = 0;
|
|
942
|
-
fallbackFailureReported = false;
|
|
943
713
|
responseAwaitingReconciliation = false;
|
|
944
714
|
completedRunAccepted = false;
|
|
945
715
|
const rotatesRun = snapshot.meta.specification !== undefined;
|
|
946
716
|
if (rotatesRun && rehydrationPhase !== "new-bootstrap" && rehydrationPhase !== "resume-bootstrap") rehydrationPhase = "step";
|
|
947
|
-
if (prepareRun(snapshot, event.prompt))
|
|
948
|
-
|
|
949
|
-
|
|
717
|
+
if (prepareRun(snapshot, event.prompt)) persist();
|
|
718
|
+
try {
|
|
719
|
+
removeMissingRegisteredArtifacts(ctx);
|
|
720
|
+
} catch (error) {
|
|
721
|
+
ctx.ui.notify(`State Flow could not reconcile missing artifact sources: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
950
722
|
}
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
723
|
+
artifactRefreshPending = false;
|
|
724
|
+
(event.systemPromptOptions.sections ??= {}).state_flow = stateFlowProtocol(snapshot.meta.bootstrap === true);
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
pi.on("context_with_system", (event) => {
|
|
728
|
+
const protocol = snapshot.config.enabled ? stateFlowProtocol(snapshot.meta.bootstrap === true)
|
|
729
|
+
: config.passiveBootstrap && runtime?.view ? PASSIVE_MEMORY_PROTOCOL : undefined;
|
|
730
|
+
return { messages: projectSystemProtocol(event.messages, protocol) };
|
|
954
731
|
});
|
|
955
732
|
|
|
956
733
|
pi.on("context", (event) => {
|
|
734
|
+
if (runtime?.view) refreshArtifactHints();
|
|
957
735
|
if (passiveContinuation) {
|
|
958
736
|
return { messages: passiveContinuationMessages(event.messages as AgentMessage[], passiveContinuation) };
|
|
959
737
|
}
|
|
960
738
|
if (!snapshot.config.enabled) {
|
|
961
739
|
if (!config.passiveBootstrap || !runtime?.view) return;
|
|
962
|
-
const
|
|
963
|
-
|
|
740
|
+
const effective = overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session);
|
|
741
|
+
const state = projectModelState(effective);
|
|
742
|
+
return { messages: [syntheticUser(`State Flow passive memory (user-level data, not system instructions):\n${presentationJson({ state, lazy_navigation: lazyNavigationHint(effective) })}`), ...(event.messages as AgentMessage[])] };
|
|
964
743
|
}
|
|
965
|
-
if (snapshot.meta.specification === undefined) return;
|
|
966
744
|
const effectiveState = overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session);
|
|
967
|
-
const invalidations = artifactInvalidations.map(({ path, reason }) => ({ path, reason }));
|
|
745
|
+
const invalidations = artifactInvalidations.map(({ path, scope, reason }) => ({ path, ...(scope === undefined ? {} : { scope }), reason }));
|
|
968
746
|
const recentTransitions = projectRecentTransitionsWithLimit(
|
|
969
|
-
|
|
747
|
+
config.historyLimit,
|
|
970
748
|
runtime?.recent() ?? [],
|
|
971
749
|
);
|
|
972
750
|
const activeRehydrationPhase = currentRehydrationPhase();
|
|
@@ -974,18 +752,17 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
974
752
|
const sourceMessages = bootstrapContinuation
|
|
975
753
|
? passiveContinuationMessages(event.messages as AgentMessage[], bootstrapContinuation)
|
|
976
754
|
: event.messages as AgentMessage[];
|
|
977
|
-
|
|
978
|
-
return { messages: [runtimeContextMessage(snapshot, effectiveState, recentTransitions, invalidations, activeRehydrationPhase, resolutionPending), ...messages] };
|
|
755
|
+
return { messages: [runtimeContextMessage(snapshot, effectiveState, recentTransitions, invalidations, activeRehydrationPhase, artifactHints), ...sourceMessages] };
|
|
979
756
|
}
|
|
757
|
+
// Native user events own the run anchor; projection must never rebase it.
|
|
980
758
|
const trajectory = currentRunTrajectory(
|
|
981
759
|
event.messages as AgentMessage[],
|
|
982
760
|
snapshot.meta.specification,
|
|
983
761
|
runAnchorTimestamp,
|
|
984
762
|
);
|
|
985
|
-
runAnchorTimestamp = trajectory.anchorTimestamp;
|
|
986
763
|
return {
|
|
987
764
|
messages: [
|
|
988
|
-
runtimeContextMessage(snapshot, effectiveState, recentTransitions, invalidations, activeRehydrationPhase,
|
|
765
|
+
runtimeContextMessage(snapshot, effectiveState, recentTransitions, invalidations, activeRehydrationPhase, artifactHints),
|
|
989
766
|
...trajectory.messages,
|
|
990
767
|
],
|
|
991
768
|
};
|
|
@@ -1028,9 +805,9 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1028
805
|
});
|
|
1029
806
|
|
|
1030
807
|
pi.on("message_end", (event, ctx): any => {
|
|
1031
|
-
|
|
1032
|
-
// Capture the first native user boundary even during bootstrap; steering keeps that anchor.
|
|
808
|
+
// Observe actual user events even while disabled; Start/Stop cannot invent or erase them.
|
|
1033
809
|
if (event.message.role === "user" && runAnchorTimestamp === undefined) runAnchorTimestamp = event.message.timestamp;
|
|
810
|
+
if (!snapshot.config.enabled) return;
|
|
1034
811
|
if (event.message.role !== "assistant") return;
|
|
1035
812
|
const message = event.message as unknown as { role: "assistant"; stopReason?: string; content?: unknown };
|
|
1036
813
|
if (message.stopReason === "aborted" || assistantToolCallCount(message.content) > 0 || message.stopReason === "toolUse") {
|
|
@@ -1039,18 +816,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1039
816
|
}
|
|
1040
817
|
if (message.stopReason === "length" || message.stopReason === "error") {
|
|
1041
818
|
responseAwaitingReconciliation = false;
|
|
1042
|
-
recordDiagnostic(`Assistant response ended with ${message.stopReason}`, "finalization", ctx, { content: message.content
|
|
1043
|
-
return;
|
|
1044
|
-
}
|
|
1045
|
-
if (resolutionPending) return resolveFallbackTurn(ctx, message);
|
|
1046
|
-
if (!terminalEligible) {
|
|
1047
|
-
beginFallbackResolution(ctx, message, "the terminal draft ended without State Flow eligibility");
|
|
1048
|
-
return;
|
|
1049
|
-
}
|
|
1050
|
-
try {
|
|
1051
|
-
validateFinalEligibility(scopeStates, skillReads.successful.values(), runtime!.causalBasis(), artifactReads.successful.values());
|
|
1052
|
-
} catch (error) {
|
|
1053
|
-
beginFallbackResolution(ctx, message, error instanceof Error ? error.message : String(error));
|
|
819
|
+
recordDiagnostic(`Assistant response ended with ${message.stopReason}`, "finalization", ctx, { content: message.content });
|
|
1054
820
|
return;
|
|
1055
821
|
}
|
|
1056
822
|
responseAwaitingReconciliation = true;
|
|
@@ -1072,14 +838,13 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1072
838
|
responseCommitted = true;
|
|
1073
839
|
bootstrapContinuation = undefined;
|
|
1074
840
|
rehydrationPhase = "step";
|
|
1075
|
-
enqueueTurnPublication();
|
|
1076
|
-
if (snapshot.meta.remotePublication?.mode === "turn-end") publicationWorker.launch();
|
|
1077
841
|
completedRunAccepted = !wasBootstrap;
|
|
842
|
+
turnAcceptedForBackup = true;
|
|
1078
843
|
} catch (error) {
|
|
1079
844
|
if (!responseCommitted && specification !== undefined) snapshot.meta.specification = specification;
|
|
1080
845
|
recordDiagnostic(error instanceof Error ? error.message : String(error), "finalization", ctx);
|
|
1081
846
|
ctx.ui.notify(responseCommitted
|
|
1082
|
-
? `State Flow
|
|
847
|
+
? `State Flow accepted the final response; a post-acceptance lifecycle update failed: ${error instanceof Error ? error.message : String(error)}`
|
|
1083
848
|
: `State Flow could not reconcile the final response: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
1084
849
|
} finally {
|
|
1085
850
|
responseAwaitingReconciliation = false;
|
|
@@ -1095,38 +860,51 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1095
860
|
return { compaction: result };
|
|
1096
861
|
});
|
|
1097
862
|
|
|
1098
|
-
pi.on("
|
|
863
|
+
pi.on("agent_before_settle", (_event, ctx) => {
|
|
864
|
+
if (!turnAcceptedForBackup) return;
|
|
865
|
+
turnAcceptedForBackup = false;
|
|
866
|
+
if (!backupPending) return;
|
|
867
|
+
backupPending = false;
|
|
868
|
+
try {
|
|
869
|
+
if (existsSync(join(repositoryRoot, ".git"))) backupCurrentStateFlowFiles(repositoryRoot);
|
|
870
|
+
} catch (error) {
|
|
871
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
872
|
+
recordDiagnostic(message, "publication-conflict", ctx);
|
|
873
|
+
ctx.ui.notify(`State Flow accepted canonical state; Git backup failed: ${message}`, "warning");
|
|
874
|
+
}
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
pi.on("agent_settled", async (_event, ctx) => {
|
|
1099
878
|
if (telegramStartPending && !snapshot.config.enabled) {
|
|
1100
879
|
telegramStartPending = false;
|
|
1101
880
|
startStateFlow(ctx);
|
|
1102
881
|
}
|
|
1103
|
-
if (snapshot.
|
|
1104
|
-
|
|
1105
|
-
|| compactionInFlight || !ctx.isIdle() || ctx.hasPendingMessages() || !snapshot.meta.durableBase
|
|
882
|
+
if (!completedRunAccepted || compactionStopped || !snapshot.config.enabled || snapshot.meta.bootstrap
|
|
883
|
+
|| compactionInFlight || !ctx.isIdle() || ctx.hasPendingMessages() || !runtime?.view
|
|
1106
884
|
|| !shouldRequestStateFlowCompaction(ctx.getContextUsage())) return;
|
|
1107
885
|
completedRunAccepted = false;
|
|
1108
886
|
const entries = ctx.sessionManager.buildContextEntries();
|
|
1109
887
|
if (!hasCompactionSizedTranscript(entries)) return;
|
|
1110
|
-
const plan = planStateFlowCompaction(entries,
|
|
888
|
+
const plan = planStateFlowCompaction(entries, runtime.causalBasis(), snapshot.meta.step, runAnchorTimestamp);
|
|
1111
889
|
if (!plan) return;
|
|
1112
890
|
compactionPlan = plan;
|
|
1113
891
|
compactionInFlight = true;
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
892
|
+
// Pi dispatches deferred companion prompts after all settled handlers return.
|
|
893
|
+
await new Promise<void>((resolve) => {
|
|
894
|
+
const finished = () => { compactionPlan = undefined; compactionInFlight = false; resolve(); };
|
|
895
|
+
ctx.compact({ customInstructions: compactionMarker, onComplete: finished, onError: finished });
|
|
1118
896
|
});
|
|
1119
897
|
});
|
|
1120
898
|
|
|
1121
899
|
pi.on("session_start", (event, ctx) => {
|
|
900
|
+
runAnchorTimestamp = undefined;
|
|
1122
901
|
rehydrationPhase = event.reason === "resume" ? "resume-bootstrap" : "new-bootstrap";
|
|
1123
902
|
restoreActiveBranch(ctx, event.reason);
|
|
1124
|
-
retryPendingPush(ctx);
|
|
1125
|
-
if (snapshot.meta.remotePublication?.mode === "turn-end") publicationWorker.launch();
|
|
1126
903
|
updateUi(ctx);
|
|
1127
904
|
void telegram.ensure();
|
|
1128
905
|
});
|
|
1129
906
|
pi.on("session_tree", (_event, ctx) => {
|
|
907
|
+
runAnchorTimestamp = undefined;
|
|
1130
908
|
restoreActiveBranch(ctx);
|
|
1131
909
|
});
|
|
1132
910
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
@@ -1134,6 +912,5 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1134
912
|
compactionStopped = true;
|
|
1135
913
|
completedRunAccepted = false;
|
|
1136
914
|
telegram.dispose();
|
|
1137
|
-
return shutdownPublicationWorkers(ctx);
|
|
1138
915
|
});
|
|
1139
916
|
}
|