@llblab/pi-kit 0.6.0 → 0.7.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 +4 -0
- package/README.md +4 -4
- package/node_modules/@llblab/pi-actors/AGENTS.md +1 -1
- package/node_modules/@llblab/pi-actors/CHANGELOG.md +6 -0
- package/node_modules/@llblab/pi-actors/README.md +1 -1
- package/node_modules/@llblab/pi-actors/dist/lib/async-runs.d.ts +3 -0
- package/node_modules/@llblab/pi-actors/dist/lib/async-runs.js +14 -1
- package/node_modules/@llblab/pi-actors/dist/lib/command-templates.js +45 -3
- package/node_modules/@llblab/pi-actors/dist/lib/extension-runtime.js +1 -1
- package/node_modules/@llblab/pi-actors/dist/lib/observability.d.ts +16 -3
- package/node_modules/@llblab/pi-actors/dist/lib/observability.js +92 -7
- package/node_modules/@llblab/pi-actors/dist/lib/pi.d.ts +0 -1
- package/node_modules/@llblab/pi-actors/dist/lib/pi.js +15 -24
- package/node_modules/@llblab/pi-actors/dist/lib/run-delivery-lineage.d.ts +17 -0
- package/node_modules/@llblab/pi-actors/dist/lib/run-delivery-lineage.js +44 -0
- package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.d.ts +4 -0
- package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.js +102 -4
- package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.js +58 -39
- package/node_modules/@llblab/pi-actors/dist/lib/runtime.js +14 -6
- package/node_modules/@llblab/pi-actors/dist/skills/actors/SKILL.md +1 -1
- package/node_modules/@llblab/pi-actors/docs/async-runs.md +1 -1
- package/node_modules/@llblab/pi-actors/docs/coordinator-delivery.md +18 -23
- package/node_modules/@llblab/pi-actors/lib/async-runs.ts +18 -1
- package/node_modules/@llblab/pi-actors/lib/command-templates.ts +41 -3
- package/node_modules/@llblab/pi-actors/lib/extension-runtime.ts +1 -1
- package/node_modules/@llblab/pi-actors/lib/observability.ts +119 -5
- package/node_modules/@llblab/pi-actors/lib/pi.ts +15 -28
- package/node_modules/@llblab/pi-actors/lib/run-delivery-lineage.ts +68 -0
- package/node_modules/@llblab/pi-actors/lib/run-delivery.ts +120 -4
- package/node_modules/@llblab/pi-actors/lib/run-ui-runtime.ts +69 -44
- package/node_modules/@llblab/pi-actors/lib/runtime.ts +17 -6
- package/node_modules/@llblab/pi-actors/package.json +1 -1
- package/node_modules/@llblab/pi-actors/skills/actors/SKILL.md +1 -1
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +33 -12
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +125 -2
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +87 -45
- package/node_modules/@llblab/pi-state-flow/README.md +203 -107
- package/node_modules/@llblab/pi-state-flow/docs/README.md +4 -0
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +188 -0
- package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +42 -0
- package/node_modules/@llblab/pi-state-flow/index.ts +164 -2
- package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +138 -0
- package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +273 -0
- package/node_modules/@llblab/pi-state-flow/lib/config.ts +48 -0
- package/node_modules/@llblab/pi-state-flow/lib/context.ts +18 -5
- package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +268 -0
- package/node_modules/@llblab/pi-state-flow/lib/discovery.ts +117 -0
- package/node_modules/@llblab/pi-state-flow/lib/durable.ts +562 -0
- package/node_modules/@llblab/pi-state-flow/lib/episode.ts +24 -12
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +606 -70
- package/node_modules/@llblab/pi-state-flow/lib/git.ts +666 -0
- package/node_modules/@llblab/pi-state-flow/lib/history.ts +95 -0
- package/node_modules/@llblab/pi-state-flow/lib/json.ts +24 -0
- package/node_modules/@llblab/pi-state-flow/lib/maintenance.ts +141 -0
- package/node_modules/@llblab/pi-state-flow/lib/memory.ts +52 -0
- package/node_modules/@llblab/pi-state-flow/lib/migration.ts +88 -0
- package/node_modules/@llblab/pi-state-flow/lib/publication.ts +296 -0
- package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +23 -7
- package/node_modules/@llblab/pi-state-flow/lib/rehydration.ts +79 -0
- package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +264 -0
- package/node_modules/@llblab/pi-state-flow/lib/session.ts +6 -0
- package/node_modules/@llblab/pi-state-flow/lib/skills.ts +99 -7
- package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +305 -48
- package/node_modules/@llblab/pi-state-flow/lib/state.ts +74 -7
- package/node_modules/@llblab/pi-state-flow/lib/status.ts +125 -6
- package/node_modules/@llblab/pi-state-flow/lib/storage.ts +196 -0
- package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +233 -0
- package/node_modules/@llblab/pi-state-flow/lib/terminal.ts +70 -24
- package/node_modules/@llblab/pi-state-flow/lib/transition.ts +254 -29
- package/node_modules/@llblab/pi-state-flow/package.json +8 -2
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +128 -0
- package/node_modules/@llblab/pi-telegram/AGENTS.md +13 -8
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +20 -3
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +13 -0
- package/node_modules/@llblab/pi-telegram/README.md +12 -8
- package/node_modules/@llblab/pi-telegram/docs/README.md +1 -0
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +220 -18
- package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +70 -19
- package/node_modules/@llblab/pi-telegram/docs/outbound.md +6 -6
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +12 -5
- package/node_modules/@llblab/pi-telegram/docs/ui-style.md +3 -1
- package/node_modules/@llblab/pi-telegram/index.ts +4 -1418
- package/node_modules/@llblab/pi-telegram/lib/agent-messages.ts +6 -3
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +28 -1
- package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +600 -135
- package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +962 -55
- package/node_modules/@llblab/pi-telegram/lib/bus.ts +350 -26
- package/node_modules/@llblab/pi-telegram/lib/channel-posts.ts +544 -0
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +234 -11
- package/node_modules/@llblab/pi-telegram/lib/config.ts +177 -24
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +1792 -0
- package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +20 -2
- package/node_modules/@llblab/pi-telegram/lib/journal.ts +2184 -126
- package/node_modules/@llblab/pi-telegram/lib/locks.ts +38 -1
- package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +152 -13
- package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +51 -10
- package/node_modules/@llblab/pi-telegram/lib/paths.ts +29 -0
- package/node_modules/@llblab/pi-telegram/lib/polling.ts +85 -17
- package/node_modules/@llblab/pi-telegram/lib/prompts.ts +5 -2
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +52 -18
- package/node_modules/@llblab/pi-telegram/lib/replies.ts +2 -2
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +305 -112
- package/node_modules/@llblab/pi-telegram/lib/status.ts +10 -0
- package/node_modules/@llblab/pi-telegram/lib/sync.ts +308 -39
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +279 -4
- package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +664 -0
- package/node_modules/@llblab/pi-telegram/lib/thread-display.ts +226 -0
- package/node_modules/@llblab/pi-telegram/lib/thread-naming.ts +118 -0
- package/node_modules/@llblab/pi-telegram/lib/threads.ts +1686 -129
- package/node_modules/@llblab/pi-telegram/lib/updates.ts +1319 -97
- package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +1643 -0
- package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +968 -0
- package/node_modules/@llblab/pi-telegram/lib/workspace-slots.ts +84 -0
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/screenshot.png +0 -0
- package/node_modules/@llblab/pi-telegram/scripts/measure-bus.mjs +83 -0
- package/node_modules/@llblab/pi-telegram/scripts/measure-workspace.mjs +101 -0
- package/node_modules/@llblab/{skills → pi-telegram/skills}/show-me/SKILL.md +28 -6
- package/node_modules/@llblab/pi-telegram/skills/show-me/references/telegram-surfaces.md +43 -0
- package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/delivery-and-threads.md +1 -1
- package/node_modules/@llblab/skills/package.json +2 -3
- package/package.json +6 -5
|
@@ -1,60 +1,438 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
1
2
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
3
|
+
import { StringEnum, Type } from "@earendil-works/pi-ai";
|
|
2
4
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
5
|
import { assistantToolCallCount, finalizedAssistantResponse, parseTerminalPatch, stateFlowProtocol, stripStateComments } from "./terminal.ts";
|
|
4
6
|
import { currentRunTrajectory, runtimeContextMessage, VALIDATION_MESSAGE_TYPE, withoutPrivateValidation } from "./context.ts";
|
|
7
|
+
import { ArtifactReadTracker } from "./acquisition.ts";
|
|
8
|
+
import { loadStateFlowConfig } from "./config.ts";
|
|
9
|
+
import { resolve } from "node:path";
|
|
5
10
|
import { SkillReadTracker } from "./skills.ts";
|
|
6
|
-
import { migrationFailure, type Snapshot } from "./snapshot.ts";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
11
|
+
import { emptySnapshot, migrationFailure, persistableSnapshot, type Snapshot } from "./snapshot.ts";
|
|
12
|
+
import { inspectSnapshotRevision, TemporalRuntime, type RuntimePublication } from "./runtime.ts";
|
|
13
|
+
import { emptyState, overlayStates, type MaterializedState, type ScopePatch, type ScopedStates, type StateScope } from "./state.ts";
|
|
14
|
+
import { commitScopedTransition, stageScopedPatch, stageScopedTransition, type StagedScopedTransition } from "./transition.ts";
|
|
9
15
|
import { MAX_VALIDATION_RETRIES, nextValidation } from "./validation.ts";
|
|
10
|
-
import { discoverSnapshotData, hasPriorConversation, SNAPSHOT_ENTRY_TYPE } from "./session.ts";
|
|
11
|
-
import { compactStatus, detailedStatus, STATUS_KEY } from "./status.ts";
|
|
12
|
-
import { abandonValidation, prepareRun, startEpisode, stopEpisode } from "./episode.ts";
|
|
16
|
+
import { discoverSnapshotData, hasPriorConversation, isNewSession, SNAPSHOT_ENTRY_TYPE } from "./session.ts";
|
|
17
|
+
import { compactStatus, detailedStatus, STATUS_KEY, type PendingPublicationDiagnostic, type StatusDiagnostics } from "./status.ts";
|
|
18
|
+
import { abandonValidation, prepareRun, resumeEpisode, startEpisode, stopEpisode } from "./episode.ts";
|
|
13
19
|
import { recoverSnapshot } from "./recovery.ts";
|
|
20
|
+
import type { RehydrationPhase } from "./rehydration.ts";
|
|
21
|
+
import { resolveRemotePublicationPolicy, serializeRemotePublicationPolicyDocument } from "./publication.ts";
|
|
22
|
+
import { coalescePublicationTarget, createPublicationQueue, type PublicationQueueState } from "./publication.ts";
|
|
23
|
+
import { acquirePublicationWorkerLease, loadPublicationQueue, publicationQueuePath, removePublicationQueue, savePublicationQueue } from "./publication.ts";
|
|
24
|
+
import { runPublicationWorker } from "./publication.ts";
|
|
25
|
+
import { getKnowledgeRoot, GlobalMarkdownDiscovery } from "./discovery.ts";
|
|
26
|
+
import {
|
|
27
|
+
cwdScopeKey,
|
|
28
|
+
resolveSessionAddress,
|
|
29
|
+
sessionScopeKey,
|
|
30
|
+
type SessionAddress,
|
|
31
|
+
} from "./durable.ts";
|
|
32
|
+
import { projectRecentTransitionsWithLimit } from "./history.ts";
|
|
33
|
+
import { isGitCommitAncestor, pushGitCommit, resolveGitPushDestination } from "./git.ts";
|
|
34
|
+
import {
|
|
35
|
+
ORDINARY_ARTIFACT_COMPILER,
|
|
36
|
+
planArtifactInvalidation,
|
|
37
|
+
type ArtifactInvalidationRequest,
|
|
38
|
+
} from "./artifact.ts";
|
|
14
39
|
|
|
15
|
-
export
|
|
16
|
-
|
|
17
|
-
|
|
40
|
+
export interface StateFlowExtensionOptions {
|
|
41
|
+
agentDir?: string;
|
|
42
|
+
repositoryRoot?: string;
|
|
43
|
+
knowledgeRoot?: string;
|
|
44
|
+
onRuntime?: (accessor: { read(offset?: number, scope?: StateScope): MaterializedState }) => void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const PATCH_STATE_TOOL_NAME = "patch_state";
|
|
48
|
+
export const READ_STATE_TOOL_NAME = "read_state";
|
|
49
|
+
|
|
50
|
+
export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowExtensionOptions = {}): void {
|
|
51
|
+
const config = loadStateFlowConfig(options.agentDir);
|
|
52
|
+
let snapshot: Snapshot = emptySnapshot();
|
|
53
|
+
let scopeStates: ScopedStates = { global: emptyState(), cwd: emptyState(), session: emptyState() };
|
|
54
|
+
let branchHasSnapshot = false;
|
|
55
|
+
let branchStartsWithoutRuntime = false;
|
|
56
|
+
let stagedFinal: StagedScopedTransition | undefined;
|
|
18
57
|
let retryQueued = false;
|
|
19
58
|
let runAnchorTimestamp: number | undefined;
|
|
59
|
+
let runtime: TemporalRuntime | undefined;
|
|
60
|
+
let activeContext: ExtensionContext | undefined;
|
|
61
|
+
let pendingPublication: PendingPublicationDiagnostic | undefined;
|
|
62
|
+
let rehydrationPhase: RehydrationPhase | undefined;
|
|
63
|
+
let turnPublicationTarget: string | undefined;
|
|
64
|
+
const activePublicationWorkers = new Set<string>();
|
|
65
|
+
const repositoryRoot = resolve(options.repositoryRoot ?? config.directory);
|
|
20
66
|
const skillReads = new SkillReadTracker();
|
|
67
|
+
const artifactReads = new ArtifactReadTracker();
|
|
68
|
+
const globalMarkdown = new GlobalMarkdownDiscovery(options.knowledgeRoot ?? getKnowledgeRoot(options.agentDir));
|
|
69
|
+
let artifactInvalidations: ArtifactInvalidationRequest[] = [];
|
|
70
|
+
|
|
71
|
+
function sessionAddress(ctx: ExtensionContext): SessionAddress {
|
|
72
|
+
return resolveSessionAddress(ctx.sessionManager.getSessionFile(), ctx.sessionManager.getSessionId(), ctx.sessionManager.getHeader()?.timestamp);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function createRuntime(ctx: ExtensionContext): TemporalRuntime {
|
|
76
|
+
return new TemporalRuntime(ctx.cwd, sessionAddress(ctx), repositoryRoot);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
options.onRuntime?.({ read: (offset, scope) => {
|
|
80
|
+
if (!runtime) throw new Error("State Flow temporal runtime is unavailable");
|
|
81
|
+
return runtime.read(offset, scope);
|
|
82
|
+
} });
|
|
21
83
|
|
|
22
84
|
function persist(): void {
|
|
23
|
-
|
|
85
|
+
const mode = snapshot.meta.remotePublication?.mode ?? "transition";
|
|
86
|
+
const publication = runtime?.view ? runtime.publish(snapshot, false, undefined, { pushRemote: mode === "transition" }) : undefined;
|
|
87
|
+
if (publication?.commit && mode === "turn-end") turnPublicationTarget = publication.commit;
|
|
88
|
+
if (publication && activeContext) recordPublication(publication, activeContext);
|
|
89
|
+
const checkpoint = persistableSnapshot(snapshot);
|
|
90
|
+
if ("disabled" in checkpoint && !branchStartsWithoutRuntime) {
|
|
91
|
+
throw new Error("State Flow cannot checkpoint an unproven branch as ordinary disabled; restore a valid checkpoint first");
|
|
92
|
+
}
|
|
93
|
+
pi.appendEntry(SNAPSHOT_ENTRY_TYPE, checkpoint);
|
|
94
|
+
if ("revision" in checkpoint) branchStartsWithoutRuntime = false;
|
|
24
95
|
}
|
|
25
96
|
|
|
26
97
|
function updateUi(ctx: ExtensionContext): void {
|
|
27
98
|
ctx.ui.setStatus(STATUS_KEY, compactStatus(snapshot, (color, text) => ctx.ui.theme.fg(color, text)));
|
|
28
99
|
}
|
|
29
100
|
|
|
101
|
+
function setPendingPublication(value: PendingPublicationDiagnostic | undefined): void {
|
|
102
|
+
pendingPublication = value === undefined ? undefined : structuredClone(value);
|
|
103
|
+
if (value === undefined) delete snapshot.meta.pendingPublication;
|
|
104
|
+
else snapshot.meta.pendingPublication = structuredClone(value);
|
|
105
|
+
}
|
|
106
|
+
|
|
30
107
|
function clearRunTransient(): void {
|
|
31
108
|
stagedFinal = undefined;
|
|
32
109
|
retryQueued = false;
|
|
33
110
|
runAnchorTimestamp = undefined;
|
|
34
111
|
skillReads.clear();
|
|
112
|
+
artifactReads.clear();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function refreshArtifactInvalidations(ctx: ExtensionContext): void {
|
|
116
|
+
if (!snapshot.config.enabled) {
|
|
117
|
+
artifactInvalidations = [];
|
|
118
|
+
artifactReads.setCandidates([]);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
const discovery = globalMarkdown.refresh();
|
|
123
|
+
const plan = planArtifactInvalidation(
|
|
124
|
+
discovery.sources,
|
|
125
|
+
scopeStates.global.artifacts,
|
|
126
|
+
ORDINARY_ARTIFACT_COMPILER,
|
|
127
|
+
);
|
|
128
|
+
artifactInvalidations = structuredClone(plan.requiresCompilation);
|
|
129
|
+
if (plan.removed.length > 0) {
|
|
130
|
+
const removals = Object.fromEntries(plan.removed.map((path) => [path, null]));
|
|
131
|
+
const stage = stageScopedPatch(
|
|
132
|
+
scopeStates,
|
|
133
|
+
{ scope: "global", patch: { artifacts: removals } },
|
|
134
|
+
[],
|
|
135
|
+
runtime!.causalBasis(),
|
|
136
|
+
);
|
|
137
|
+
commitStage(stage, ctx, false);
|
|
138
|
+
}
|
|
139
|
+
artifactReads.setCandidates(artifactInvalidations);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
artifactInvalidations = [];
|
|
142
|
+
artifactReads.setCandidates([]);
|
|
143
|
+
ctx.ui.notify(
|
|
144
|
+
`State Flow could not discover global Markdown sources: ${error instanceof Error ? error.message : String(error)}`,
|
|
145
|
+
"warning",
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function installScopeStates(): void {
|
|
151
|
+
scopeStates = runtime?.view ? runtime.states() : { global: emptyState(), cwd: emptyState(), session: emptyState() };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function syncStateFlowTools(): void {
|
|
155
|
+
const active = pi.getActiveTools();
|
|
156
|
+
const owned = [PATCH_STATE_TOOL_NAME, READ_STATE_TOOL_NAME];
|
|
157
|
+
if (owned.every((name) => active.includes(name) === snapshot.config.enabled)) return;
|
|
158
|
+
pi.setActiveTools(snapshot.config.enabled
|
|
159
|
+
? [...new Set([...active, ...owned])]
|
|
160
|
+
: active.filter((name) => !owned.includes(name)));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function assistantToolBatch(ctx: ExtensionContext, toolCallId: string): string[] | undefined {
|
|
164
|
+
const branch = ctx.sessionManager.getBranch();
|
|
165
|
+
for (let index = branch.length - 1; index >= 0; index--) {
|
|
166
|
+
const entry = branch[index] as { type?: unknown; message?: { role?: unknown; content?: unknown } };
|
|
167
|
+
if (entry.type !== "message" || entry.message?.role !== "assistant" || !Array.isArray(entry.message.content)) continue;
|
|
168
|
+
const calls = entry.message.content.filter((block): block is { type: "toolCall"; id: string; name: string } => {
|
|
169
|
+
return typeof block === "object" && block !== null
|
|
170
|
+
&& (block as { type?: unknown }).type === "toolCall"
|
|
171
|
+
&& typeof (block as { id?: unknown }).id === "string"
|
|
172
|
+
&& typeof (block as { name?: unknown }).name === "string";
|
|
173
|
+
});
|
|
174
|
+
if (calls.some(({ id }) => id === toolCallId)) return calls.map(({ name }) => name);
|
|
175
|
+
}
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function recordPublication(publication: RuntimePublication, ctx: ExtensionContext): void {
|
|
180
|
+
const revision = publication.revision ?? publication.commit;
|
|
181
|
+
if (revision !== undefined) snapshot.meta.durableBase = revision;
|
|
182
|
+
if (publication.push?.status === "pending") {
|
|
183
|
+
setPendingPublication({
|
|
184
|
+
commit: publication.push.commit,
|
|
185
|
+
error: publication.push.error ?? "unknown push failure",
|
|
186
|
+
});
|
|
187
|
+
ctx.ui.notify(
|
|
188
|
+
`State Flow accepted durable commit ${publication.push.commit.slice(0, 12)}, but push is pending: ${publication.push.error}`,
|
|
189
|
+
"warning",
|
|
190
|
+
);
|
|
191
|
+
} else if (revision !== undefined) {
|
|
192
|
+
setPendingPublication(undefined);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function commitStage(stage: StagedScopedTransition, ctx: ExtensionContext, finalizeRun: boolean): boolean {
|
|
197
|
+
const acquiredArtifactPaths = new Set(artifactReads.successful.keys());
|
|
198
|
+
const committed = commitScopedTransition(snapshot, scopeStates, stage, (accepted, nextSnapshot) => {
|
|
199
|
+
if (!runtime?.view) throw new Error("Temporal State Flow runtime is unavailable; reload before publishing");
|
|
200
|
+
const mode = nextSnapshot.meta.remotePublication?.mode ?? "transition";
|
|
201
|
+
const publication = runtime.publish(nextSnapshot, accepted !== undefined, accepted, { pushRemote: mode === "transition" });
|
|
202
|
+
if (publication?.commit && mode === "turn-end") turnPublicationTarget = publication.commit;
|
|
203
|
+
if (publication) recordPublication(publication, ctx);
|
|
204
|
+
}, runtime!.causalBasis(), { finalizeRun });
|
|
205
|
+
if (!committed) return false;
|
|
206
|
+
installScopeStates();
|
|
207
|
+
artifactInvalidations = artifactInvalidations.filter(({ path }) => !acquiredArtifactPaths.has(path));
|
|
208
|
+
artifactReads.setCandidates(artifactInvalidations);
|
|
209
|
+
skillReads.clear();
|
|
210
|
+
artifactReads.clear();
|
|
211
|
+
persist();
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function enqueueTurnPublication(): void {
|
|
216
|
+
const target = turnPublicationTarget;
|
|
217
|
+
turnPublicationTarget = undefined;
|
|
218
|
+
if (!target) return;
|
|
219
|
+
const destination = resolveGitPushDestination(repositoryRoot);
|
|
220
|
+
if (!destination) return;
|
|
221
|
+
const path = publicationQueuePath(destination);
|
|
222
|
+
const previous = loadPublicationQueue(path);
|
|
223
|
+
const next = previous
|
|
224
|
+
? coalescePublicationTarget(previous, destination, target, (ancestor, descendant) => isGitCommitAncestor(repositoryRoot, ancestor, descendant))
|
|
225
|
+
: createPublicationQueue(destination, target);
|
|
226
|
+
savePublicationQueue(path, next, previous);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function launchPublicationWorker(): void {
|
|
230
|
+
let destination: ReturnType<typeof resolveGitPushDestination>;
|
|
231
|
+
try {
|
|
232
|
+
destination = resolveGitPushDestination(repositoryRoot);
|
|
233
|
+
} catch (error) {
|
|
234
|
+
if (error instanceof Error && /ENOENT/.test(error.message)) return;
|
|
235
|
+
throw error;
|
|
236
|
+
}
|
|
237
|
+
if (!destination) return;
|
|
238
|
+
const path = publicationQueuePath(destination);
|
|
239
|
+
if (activePublicationWorkers.has(path)) return;
|
|
240
|
+
let queued: PublicationQueueState | undefined;
|
|
241
|
+
let lease: ReturnType<typeof acquirePublicationWorkerLease>;
|
|
242
|
+
try {
|
|
243
|
+
queued = loadPublicationQueue(path);
|
|
244
|
+
if (!queued) return;
|
|
245
|
+
lease = acquirePublicationWorkerLease(path);
|
|
246
|
+
} catch {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (!lease) return;
|
|
250
|
+
activePublicationWorkers.add(path);
|
|
251
|
+
void runPublicationWorker(
|
|
252
|
+
queued,
|
|
253
|
+
({ target }) => new Promise<void>((resolve, reject) => {
|
|
254
|
+
execFile("git", ["-C", repositoryRoot, "push", destination.remote, `${target}:${destination.ref}`], {
|
|
255
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GCM_INTERACTIVE: "never" },
|
|
256
|
+
}, (error) => error ? reject(error) : resolve());
|
|
257
|
+
}),
|
|
258
|
+
() => loadPublicationQueue(path) ?? queued,
|
|
259
|
+
(ancestor, descendant) => isGitCommitAncestor(repositoryRoot, ancestor, descendant),
|
|
260
|
+
).then((result) => {
|
|
261
|
+
const current = loadPublicationQueue(path);
|
|
262
|
+
if (!current) return;
|
|
263
|
+
if (result.next === undefined) {
|
|
264
|
+
if (current.target === result.attempted.target) removePublicationQueue(path, current);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (current.target === result.attempted.target || result.next.target === current.target) {
|
|
268
|
+
savePublicationQueue(path, result.next, current);
|
|
269
|
+
}
|
|
270
|
+
}).catch(() => {
|
|
271
|
+
// Queue/CAS truth remains durable; status and a later activation expose retry.
|
|
272
|
+
}).finally(() => {
|
|
273
|
+
activePublicationWorkers.delete(path);
|
|
274
|
+
lease.release();
|
|
275
|
+
if (loadPublicationQueue(path)?.status === "pending") launchPublicationWorker();
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function retryPendingPush(ctx: ExtensionContext): void {
|
|
280
|
+
if (pendingPublication === undefined || !runtime?.view) return;
|
|
281
|
+
const result = pushGitCommit(repositoryRoot, pendingPublication.commit);
|
|
282
|
+
if (result.status !== "pending") {
|
|
283
|
+
setPendingPublication(undefined);
|
|
284
|
+
persist();
|
|
285
|
+
ctx.ui.notify(result.status === "local"
|
|
286
|
+
? `State Flow retained local-only durable commit ${result.commit.slice(0, 12)}; no remote configured.`
|
|
287
|
+
: `State Flow pushed pending durable commit ${result.commit.slice(0, 12)}.`, "info");
|
|
288
|
+
} else {
|
|
289
|
+
setPendingPublication({ commit: result.commit, error: result.error ?? "unknown push failure" });
|
|
290
|
+
persist();
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function currentRehydrationPhase(): RehydrationPhase | undefined {
|
|
295
|
+
return rehydrationPhase;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function statusDiagnostics(ctx: ExtensionContext): StatusDiagnostics {
|
|
299
|
+
const cwd = ctx.cwd;
|
|
300
|
+
const diagnosticStates = scopeStates;
|
|
301
|
+
const view = runtime?.view;
|
|
302
|
+
const diagnosticRecent = runtime?.recent() ?? [];
|
|
303
|
+
const durableStateError = view ? undefined
|
|
304
|
+
: snapshot.meta.validation?.attempt === 0 ? snapshot.meta.validation.error : "no temporal runtime is selected on this branch";
|
|
305
|
+
|
|
306
|
+
let staleArtifacts: StatusDiagnostics["staleArtifacts"] = [];
|
|
307
|
+
let artifactFreshnessError: string | undefined;
|
|
308
|
+
if (durableStateError !== undefined) {
|
|
309
|
+
artifactFreshnessError = "durable global artifact registry is unavailable";
|
|
310
|
+
} else {
|
|
311
|
+
try {
|
|
312
|
+
const discovery = globalMarkdown.refresh();
|
|
313
|
+
const plan = planArtifactInvalidation(
|
|
314
|
+
discovery.sources,
|
|
315
|
+
diagnosticStates.global.artifacts,
|
|
316
|
+
ORDINARY_ARTIFACT_COMPILER,
|
|
317
|
+
);
|
|
318
|
+
staleArtifacts = [
|
|
319
|
+
...plan.requiresCompilation.map(({ path, reason }) => ({ scope: "global" as const, path, reason })),
|
|
320
|
+
...plan.removed.map((path) => ({ scope: "global" as const, path, reason: "source-removed" as const })),
|
|
321
|
+
];
|
|
322
|
+
} catch (error) {
|
|
323
|
+
artifactFreshnessError = error instanceof Error ? error.message : String(error);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const session = sessionAddress(ctx);
|
|
328
|
+
let publicationQueue: PublicationQueueState | undefined;
|
|
329
|
+
let publicationQueueError: string | undefined;
|
|
330
|
+
try {
|
|
331
|
+
const destination = resolveGitPushDestination(repositoryRoot);
|
|
332
|
+
publicationQueue = destination ? loadPublicationQueue(publicationQueuePath(destination)) : undefined;
|
|
333
|
+
} catch (error) {
|
|
334
|
+
publicationQueue = undefined;
|
|
335
|
+
publicationQueueError = error instanceof Error ? error.message : String(error);
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
repositoryRoot,
|
|
339
|
+
cwdScopeKey: cwdScopeKey(cwd),
|
|
340
|
+
sessionScopeKey: sessionScopeKey(session.key),
|
|
341
|
+
scopeStates: diagnosticStates,
|
|
342
|
+
recent: diagnosticRecent,
|
|
343
|
+
...(view === undefined ? {} : { temporal: {
|
|
344
|
+
head: structuredClone(view.lineage.at(-1)!),
|
|
345
|
+
historyDepth: view.lineage.length - 1,
|
|
346
|
+
tailCounts: { global: view.scopes.global.patches.length, cwd: view.scopes.cwd.patches.length, session: view.scopes.session.patches.length },
|
|
347
|
+
} }),
|
|
348
|
+
staleArtifacts,
|
|
349
|
+
...(artifactFreshnessError === undefined ? {} : { artifactFreshnessError }),
|
|
350
|
+
...(durableStateError === undefined ? {} : { durableStateError }),
|
|
351
|
+
...(pendingPublication === undefined ? {} : { pendingPublication }),
|
|
352
|
+
retryQueued,
|
|
353
|
+
...(publicationQueue === undefined ? {} : { publicationQueue }),
|
|
354
|
+
...(publicationQueueError === undefined ? {} : { publicationQueueError }),
|
|
355
|
+
};
|
|
35
356
|
}
|
|
36
357
|
|
|
37
|
-
function restoreActiveBranch(ctx: ExtensionContext): void {
|
|
358
|
+
function restoreActiveBranch(ctx: ExtensionContext, sessionStartReason?: unknown): void {
|
|
38
359
|
clearRunTransient();
|
|
360
|
+
activeContext = ctx;
|
|
361
|
+
const session = sessionAddress(ctx);
|
|
362
|
+
runtime = new TemporalRuntime(ctx.cwd, session, repositoryRoot);
|
|
363
|
+
installScopeStates();
|
|
364
|
+
pendingPublication = undefined;
|
|
365
|
+
branchHasSnapshot = false;
|
|
366
|
+
branchStartsWithoutRuntime = false;
|
|
39
367
|
try {
|
|
40
|
-
const
|
|
41
|
-
const
|
|
368
|
+
const branch = ctx.sessionManager.getBranch();
|
|
369
|
+
const discovery = discoverSnapshotData(branch);
|
|
370
|
+
const recovery = recoverSnapshot(discovery.candidates, (revision, legacy) =>
|
|
371
|
+
inspectSnapshotRevision(ctx.cwd, session.id, repositoryRoot, revision, legacy, session.key).snapshot);
|
|
372
|
+
branchStartsWithoutRuntime = recovery.disabledMarker === true
|
|
373
|
+
|| (discovery.candidates.length === 0 && discovery.errors.length === 0);
|
|
42
374
|
const skipped = discovery.errors.length + recovery.skipped.length;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
:
|
|
46
|
-
if (
|
|
375
|
+
branchHasSnapshot = recovery.skipped.length < discovery.candidates.length;
|
|
376
|
+
if (discovery.candidates.length === 0 && discovery.errors.length > 0) {
|
|
377
|
+
snapshot = migrationFailure({}, `Snapshot restoration failed: ${discovery.errors[0]}`);
|
|
378
|
+
} else if (discovery.candidates.length > 0) {
|
|
379
|
+
snapshot = recovery.snapshot;
|
|
380
|
+
if (branchHasSnapshot && snapshot.meta.durableBase) {
|
|
381
|
+
const selectedRevision = snapshot.meta.durableBase;
|
|
382
|
+
snapshot = runtime.restore(selectedRevision, snapshot);
|
|
383
|
+
if (snapshot.meta.durableBase !== selectedRevision) persist();
|
|
384
|
+
} else if (branchHasSnapshot && snapshot.config.enabled) {
|
|
385
|
+
const publication = runtime.initialize(snapshot, true);
|
|
386
|
+
if (publication) recordPublication(publication, ctx);
|
|
387
|
+
delete snapshot.legacySession;
|
|
388
|
+
}
|
|
389
|
+
installScopeStates();
|
|
390
|
+
} else if (config.autoStart && isNewSession(sessionStartReason, branch)) {
|
|
391
|
+
snapshot = startEpisode(hasPriorConversation(branch));
|
|
392
|
+
snapshot.meta.remotePublication = serializeRemotePublicationPolicyDocument(
|
|
393
|
+
resolveRemotePublicationPolicy(config.remotePublication, { legacyRuntime: false }),
|
|
394
|
+
);
|
|
395
|
+
runtime.prepare();
|
|
396
|
+
const initialization = runtime.initialize(snapshot, true);
|
|
397
|
+
if (initialization) recordPublication(initialization, ctx);
|
|
398
|
+
if (!runtime.view) snapshot = emptySnapshot();
|
|
399
|
+
installScopeStates();
|
|
400
|
+
if (snapshot.config.enabled) {
|
|
401
|
+
branchHasSnapshot = true;
|
|
402
|
+
persist();
|
|
403
|
+
}
|
|
404
|
+
} else {
|
|
405
|
+
snapshot = emptySnapshot();
|
|
406
|
+
installScopeStates();
|
|
407
|
+
}
|
|
408
|
+
if (snapshot.config.enabled && skipped > 0) {
|
|
47
409
|
ctx.ui.notify(`State Flow recovered the previous valid snapshot after skipping ${skipped} malformed newer snapshot(s).`, "warning");
|
|
48
410
|
}
|
|
49
411
|
} catch (error) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
412
|
+
const failure = migrationFailure({}, `Snapshot restoration failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
413
|
+
snapshot = {
|
|
414
|
+
...snapshot,
|
|
415
|
+
config: { ...snapshot.config, enabled: false },
|
|
416
|
+
meta: { ...snapshot.meta, validation: failure.meta.validation },
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
if (branchHasSnapshot && snapshot.meta.remotePublication === undefined) {
|
|
420
|
+
snapshot.meta.remotePublication = serializeRemotePublicationPolicyDocument(
|
|
421
|
+
resolveRemotePublicationPolicy(undefined, { legacyRuntime: true }),
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
if (snapshot.config.enabled && snapshot.meta.remotePublication === undefined) {
|
|
425
|
+
snapshot.meta.remotePublication = serializeRemotePublicationPolicyDocument(
|
|
426
|
+
resolveRemotePublicationPolicy(undefined, { legacyRuntime: true }),
|
|
53
427
|
);
|
|
54
428
|
}
|
|
55
|
-
|
|
56
|
-
|
|
429
|
+
pendingPublication = snapshot.meta.pendingPublication === undefined
|
|
430
|
+
? undefined
|
|
431
|
+
: structuredClone(snapshot.meta.pendingPublication);
|
|
432
|
+
if (!snapshot.config.enabled && snapshot.meta.validation?.attempt === 0) {
|
|
433
|
+
ctx.ui.notify(`State Flow restored disabled: ${snapshot.meta.validation.error}`, "error");
|
|
57
434
|
}
|
|
435
|
+
syncStateFlowTools();
|
|
58
436
|
updateUi(ctx);
|
|
59
437
|
}
|
|
60
438
|
|
|
@@ -66,9 +444,9 @@ export default function stateFlowExtension(pi: ExtensionAPI): void {
|
|
|
66
444
|
}
|
|
67
445
|
|
|
68
446
|
function queueTerminalRegeneration(error: string, ctx: ExtensionContext): void {
|
|
69
|
-
const decision = nextValidation(snapshot.validation, error);
|
|
447
|
+
const decision = nextValidation(snapshot.meta.validation, error);
|
|
70
448
|
if (decision.kind === "retry") {
|
|
71
|
-
snapshot.validation = decision.feedback;
|
|
449
|
+
snapshot.meta.validation = decision.feedback;
|
|
72
450
|
persist();
|
|
73
451
|
retryQueued = true;
|
|
74
452
|
pi.sendMessage({
|
|
@@ -79,8 +457,9 @@ export default function stateFlowExtension(pi: ExtensionAPI): void {
|
|
|
79
457
|
return;
|
|
80
458
|
}
|
|
81
459
|
retryQueued = false;
|
|
82
|
-
snapshot.validation = undefined;
|
|
460
|
+
snapshot.meta.validation = undefined;
|
|
83
461
|
skillReads.clear();
|
|
462
|
+
artifactReads.clear();
|
|
84
463
|
persist();
|
|
85
464
|
ctx.ui.notify(`State Flow remains enabled after ${MAX_VALIDATION_RETRIES} automatic regeneration attempts; the last committed state was preserved: ${decision.error}`, "error");
|
|
86
465
|
}
|
|
@@ -96,95 +475,236 @@ export default function stateFlowExtension(pi: ExtensionAPI): void {
|
|
|
96
475
|
};
|
|
97
476
|
}
|
|
98
477
|
|
|
478
|
+
pi.registerTool({
|
|
479
|
+
name: READ_STATE_TOOL_NAME,
|
|
480
|
+
label: "Read State",
|
|
481
|
+
description: "Read one effective or scoped State Flow materialization at offset 0–7 in the active causal lineage. Read-only and lazy; unavailable pre-origin history is an error. Use only for a concrete historical or scope-specific gap, not routine rereading of current context.",
|
|
482
|
+
promptSnippet: "Read one cached effective/global/CWD/session state at temporal offset 0–7",
|
|
483
|
+
parameters: Type.Object({
|
|
484
|
+
offset: Type.Optional(Type.Integer({ minimum: 0, maximum: 7, description: "Accepted transitions before current state; defaults to 0" })),
|
|
485
|
+
scope: Type.Optional(StringEnum(["effective", "global", "cwd", "session"] as const, { description: "Projection at that same boundary; defaults to effective" })),
|
|
486
|
+
}, { additionalProperties: false }),
|
|
487
|
+
async execute(_toolCallId, params, signal) {
|
|
488
|
+
if (!snapshot.config.enabled) throw new Error("State Flow is disabled on this session branch");
|
|
489
|
+
if (signal?.aborted) throw new Error("State Flow read was aborted");
|
|
490
|
+
if (!runtime?.view) throw new Error("State Flow temporal runtime is unavailable");
|
|
491
|
+
const { offset = 0, scope = "effective" } = params;
|
|
492
|
+
const state = runtime.read(offset, scope === "effective" ? undefined : scope);
|
|
493
|
+
const boundary = runtime.view.lineage.at(-1 - offset)!;
|
|
494
|
+
return {
|
|
495
|
+
content: [{ type: "text", text: JSON.stringify({ offset, scope, boundary, state }) }],
|
|
496
|
+
details: { offset, scope, transitionId: boundary.id },
|
|
497
|
+
};
|
|
498
|
+
},
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
pi.registerTool({
|
|
502
|
+
name: PATCH_STATE_TOOL_NAME,
|
|
503
|
+
label: "Patch State",
|
|
504
|
+
description: "Materialize established future-relevant semantic state at a session, CWD, or global barrier, including a necessary write-and-verify step during explicitly requested curation. Do not use for scratchpad, narration, routine progress, or speculative churn. This call must be the only State Flow barrier in its assistant response; sibling tool calls are blocked and reconsidered after rematerialization.",
|
|
505
|
+
promptSnippet: "Materialize established future-relevant state as an immediate inference barrier",
|
|
506
|
+
promptGuidelines: [
|
|
507
|
+
"Use patch_state when established future-relevant information would face meaningful loss or recovery risk if delayed until terminal reconciliation, or for a necessary write-and-verify step in explicitly requested curation.",
|
|
508
|
+
"Call patch_state alone in an assistant response; choose subsequent actions only after its compact acknowledgement and rematerialized State Flow context.",
|
|
509
|
+
],
|
|
510
|
+
executionMode: "sequential",
|
|
511
|
+
parameters: Type.Object({
|
|
512
|
+
scope: StringEnum(["session", "cwd", "global"] as const),
|
|
513
|
+
patch: Type.Record(Type.String(), Type.Unknown()),
|
|
514
|
+
}, { additionalProperties: false }),
|
|
515
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
516
|
+
if (!snapshot.config.enabled) throw new Error("State Flow is disabled on this session branch");
|
|
517
|
+
if (signal?.aborted) throw new Error("State Flow patch was aborted before materialization");
|
|
518
|
+
const transition = { scope: params.scope as StateScope, patch: params.patch as ScopePatch };
|
|
519
|
+
const stage = stageScopedPatch(
|
|
520
|
+
scopeStates,
|
|
521
|
+
transition,
|
|
522
|
+
skillReads.successful.values(),
|
|
523
|
+
runtime!.causalBasis(),
|
|
524
|
+
artifactReads.successful.values(),
|
|
525
|
+
);
|
|
526
|
+
commitStage(stage, ctx, false);
|
|
527
|
+
updateUi(ctx);
|
|
528
|
+
const publication = pendingPublication === undefined ? "" : "; durable publication pending";
|
|
529
|
+
return {
|
|
530
|
+
content: [{ type: "text", text: `\nState materialized at ${params.scope} scope${publication}.` }],
|
|
531
|
+
details: { scope: params.scope, step: snapshot.meta.step },
|
|
532
|
+
};
|
|
533
|
+
},
|
|
534
|
+
});
|
|
535
|
+
|
|
99
536
|
pi.registerCommand("state-flow-start", {
|
|
100
537
|
description: "Start State Flow mode",
|
|
101
538
|
handler: async (_args, ctx) => {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
539
|
+
if (!activeContext) restoreActiveBranch(ctx);
|
|
540
|
+
const previousSnapshot = structuredClone(snapshot);
|
|
541
|
+
try {
|
|
542
|
+
if (!runtime?.view && !snapshot.meta.durableBase && !branchStartsWithoutRuntime) {
|
|
543
|
+
throw new Error("Selected branch revision is unavailable; restore its original Git history before starting State Flow");
|
|
544
|
+
}
|
|
545
|
+
const branch = ctx.sessionManager.getBranch();
|
|
546
|
+
activeContext = ctx;
|
|
547
|
+
runtime ??= createRuntime(ctx);
|
|
548
|
+
if (branchStartsWithoutRuntime) runtime.prepare();
|
|
549
|
+
const bootstrap = (!branchHasSnapshot || !snapshot.config.enabled)
|
|
550
|
+
&& hasPriorConversation(branch);
|
|
551
|
+
if (!runtime.view && snapshot.meta.durableBase) {
|
|
552
|
+
snapshot = runtime.restore(snapshot.meta.durableBase, snapshot);
|
|
553
|
+
setPendingPublication(snapshot.meta.pendingPublication);
|
|
554
|
+
branchHasSnapshot = true;
|
|
555
|
+
}
|
|
556
|
+
const existingBranch = branchHasSnapshot;
|
|
557
|
+
snapshot = existingBranch
|
|
558
|
+
? resumeEpisode(snapshot, bootstrap)
|
|
559
|
+
: startEpisode(bootstrap);
|
|
560
|
+
if (snapshot.meta.remotePublication === undefined) {
|
|
561
|
+
snapshot.meta.remotePublication = serializeRemotePublicationPolicyDocument(
|
|
562
|
+
resolveRemotePublicationPolicy(existingBranch ? undefined : config.remotePublication, { legacyRuntime: existingBranch }),
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
branchHasSnapshot = true;
|
|
566
|
+
const publication = runtime.view
|
|
567
|
+
? runtime.promote(snapshot) ?? runtime.publish(snapshot)
|
|
568
|
+
: runtime.initialize(snapshot, true, undefined, branchStartsWithoutRuntime);
|
|
569
|
+
if (publication) recordPublication(publication, ctx);
|
|
570
|
+
installScopeStates();
|
|
571
|
+
delete snapshot.legacySession;
|
|
572
|
+
clearRunTransient();
|
|
573
|
+
refreshArtifactInvalidations(ctx);
|
|
574
|
+
syncStateFlowTools();
|
|
575
|
+
persist();
|
|
576
|
+
updateUi(ctx);
|
|
577
|
+
ctx.ui.notify(
|
|
578
|
+
snapshot.meta.bootstrap
|
|
579
|
+
? "State Flow enabled. The next complete agent run will migrate active context into state."
|
|
580
|
+
: "State Flow enabled. The next prompt starts a stateful agent run.",
|
|
581
|
+
"info",
|
|
582
|
+
);
|
|
583
|
+
} catch (error) {
|
|
584
|
+
snapshot = previousSnapshot;
|
|
585
|
+
syncStateFlowTools();
|
|
586
|
+
ctx.ui.notify(
|
|
587
|
+
`State Flow could not initialize CWD state: ${error instanceof Error ? error.message : String(error)}`,
|
|
588
|
+
"error",
|
|
589
|
+
);
|
|
590
|
+
}
|
|
115
591
|
},
|
|
116
592
|
});
|
|
117
593
|
|
|
118
594
|
pi.registerCommand("state-flow-status", {
|
|
119
595
|
description: "Show State Flow runtime status",
|
|
120
596
|
handler: async (_args, ctx) => {
|
|
121
|
-
ctx.ui.notify(detailedStatus(snapshot), "info");
|
|
597
|
+
ctx.ui.notify(detailedStatus(snapshot, statusDiagnostics(ctx)), "info");
|
|
122
598
|
},
|
|
123
599
|
});
|
|
124
600
|
|
|
125
601
|
pi.registerCommand("state-flow-stop", {
|
|
126
|
-
description: "Stop State Flow
|
|
602
|
+
description: "Stop State Flow on the current session branch",
|
|
127
603
|
handler: async (_args, ctx) => {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
604
|
+
if (!activeContext) restoreActiveBranch(ctx);
|
|
605
|
+
let selected = runtime;
|
|
606
|
+
let current = snapshot;
|
|
607
|
+
if (!selected?.view && snapshot.meta.durableBase) {
|
|
608
|
+
selected = createRuntime(ctx);
|
|
609
|
+
current = selected.restore(snapshot.meta.durableBase, snapshot);
|
|
610
|
+
}
|
|
611
|
+
const stopped = stopEpisode(current);
|
|
612
|
+
const publication = selected?.view ? selected.publish(stopped) : undefined;
|
|
613
|
+
if (selected !== runtime) {
|
|
614
|
+
runtime = selected;
|
|
615
|
+
installScopeStates();
|
|
616
|
+
}
|
|
617
|
+
snapshot = stopped;
|
|
618
|
+
if (publication) recordPublication(publication, ctx);
|
|
619
|
+
branchHasSnapshot = true;
|
|
620
|
+
clearRunTransient();
|
|
621
|
+
syncStateFlowTools();
|
|
133
622
|
persist();
|
|
134
623
|
updateUi(ctx);
|
|
135
624
|
},
|
|
136
625
|
});
|
|
137
626
|
|
|
138
627
|
pi.on("before_agent_start", (event) => {
|
|
139
|
-
if (!snapshot.enabled) return;
|
|
628
|
+
if (!snapshot.config.enabled) return;
|
|
140
629
|
if (!retryQueued) {
|
|
141
630
|
skillReads.clear();
|
|
631
|
+
artifactReads.clear();
|
|
142
632
|
}
|
|
143
|
-
const rotatesRun = snapshot.specification !== undefined && !retryQueued;
|
|
633
|
+
const rotatesRun = snapshot.meta.specification !== undefined && !retryQueued;
|
|
634
|
+
if (rotatesRun) rehydrationPhase = "step";
|
|
144
635
|
if (prepareRun(snapshot, event.prompt, retryQueued)) {
|
|
145
636
|
if (rotatesRun) runAnchorTimestamp = undefined;
|
|
146
637
|
persist();
|
|
147
638
|
}
|
|
148
639
|
return {
|
|
149
|
-
systemPrompt: `${event.systemPrompt}\n\n${stateFlowProtocol(snapshot.bootstrap === true)}`,
|
|
640
|
+
systemPrompt: `${event.systemPrompt}\n\n${stateFlowProtocol(snapshot.meta.bootstrap === true)}`,
|
|
150
641
|
};
|
|
151
642
|
});
|
|
152
643
|
|
|
153
644
|
pi.on("context", (event) => {
|
|
154
|
-
if (!snapshot.enabled || snapshot.specification === undefined) return;
|
|
155
|
-
|
|
156
|
-
|
|
645
|
+
if (!snapshot.config.enabled || snapshot.meta.specification === undefined) return;
|
|
646
|
+
const effectiveState = overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session);
|
|
647
|
+
const recentTransitions = projectRecentTransitionsWithLimit(
|
|
648
|
+
snapshot.config.transitionWindow,
|
|
649
|
+
runtime?.recent() ?? [],
|
|
650
|
+
);
|
|
651
|
+
const activeRehydrationPhase = currentRehydrationPhase();
|
|
652
|
+
if (snapshot.meta.bootstrap) {
|
|
157
653
|
const messages = withoutPrivateValidation(event.messages as AgentMessage[]);
|
|
158
|
-
return messages
|
|
654
|
+
return { messages: [runtimeContextMessage(snapshot, effectiveState, recentTransitions, artifactInvalidations, activeRehydrationPhase), ...messages] };
|
|
159
655
|
}
|
|
160
656
|
const trajectory = currentRunTrajectory(
|
|
161
657
|
event.messages as AgentMessage[],
|
|
162
|
-
snapshot.specification,
|
|
658
|
+
snapshot.meta.specification,
|
|
163
659
|
runAnchorTimestamp,
|
|
164
660
|
);
|
|
165
661
|
runAnchorTimestamp = trajectory.anchorTimestamp;
|
|
166
|
-
return {
|
|
662
|
+
return {
|
|
663
|
+
messages: [
|
|
664
|
+
runtimeContextMessage(snapshot, effectiveState, recentTransitions, artifactInvalidations, activeRehydrationPhase),
|
|
665
|
+
...trajectory.messages,
|
|
666
|
+
],
|
|
667
|
+
};
|
|
167
668
|
});
|
|
168
669
|
|
|
169
670
|
pi.on("tool_execution_start", (event) => {
|
|
170
|
-
if (!snapshot.enabled) return;
|
|
671
|
+
if (!snapshot.config.enabled) return;
|
|
171
672
|
// Pi emits this before tool_call. Keep the argument object as a fallback;
|
|
172
673
|
// tool_call replaces it with the mutable, post-preflight input reference.
|
|
173
674
|
skillReads.recordStart(event.toolCallId, event.toolName, event.args);
|
|
675
|
+
artifactReads.recordStart(event.toolCallId, event.toolName, event.args);
|
|
174
676
|
});
|
|
175
677
|
|
|
176
|
-
pi.on("tool_call", (event) => {
|
|
177
|
-
if (!snapshot.enabled) return;
|
|
678
|
+
pi.on("tool_call", (event, ctx) => {
|
|
679
|
+
if (!snapshot.config.enabled) return;
|
|
680
|
+
const batch = assistantToolBatch(ctx, event.toolCallId);
|
|
681
|
+
const patchCalls = batch?.filter((name) => name === PATCH_STATE_TOOL_NAME).length ?? 0;
|
|
682
|
+
if (patchCalls > 0) {
|
|
683
|
+
if (patchCalls !== 1) {
|
|
684
|
+
return {
|
|
685
|
+
block: true,
|
|
686
|
+
reason: "A State Flow barrier response must contain exactly one patch_state call",
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
if (event.toolName !== PATCH_STATE_TOOL_NAME) {
|
|
690
|
+
return {
|
|
691
|
+
block: true,
|
|
692
|
+
reason: "Blocked by the patch_state barrier; reconsider this action after State Flow rematerializes context",
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
}
|
|
178
696
|
skillReads.recordCall(event.toolCallId, event.toolName, event.input);
|
|
697
|
+
artifactReads.recordCall(event.toolCallId, event.toolName, event.input);
|
|
179
698
|
});
|
|
180
699
|
|
|
181
700
|
pi.on("tool_execution_end", (event) => {
|
|
182
|
-
if (!snapshot.enabled) return;
|
|
701
|
+
if (!snapshot.config.enabled) return;
|
|
183
702
|
skillReads.recordEnd(event.toolCallId, event.toolName, event.isError);
|
|
703
|
+
artifactReads.recordEnd(event.toolCallId, event.toolName, event.isError);
|
|
184
704
|
});
|
|
185
705
|
|
|
186
706
|
pi.on("message_end", (event, ctx): any => {
|
|
187
|
-
if (!snapshot.enabled || event.message.role !== "assistant") return;
|
|
707
|
+
if (!snapshot.config.enabled || event.message.role !== "assistant") return;
|
|
188
708
|
stagedFinal = undefined;
|
|
189
709
|
const message = event.message as unknown as { role: "assistant"; stopReason?: string; content?: unknown };
|
|
190
710
|
if (message.stopReason === "aborted") {
|
|
@@ -195,7 +715,7 @@ export default function stateFlowExtension(pi: ExtensionAPI): void {
|
|
|
195
715
|
return rejectTerminal(message, `Assistant response ended with ${message.stopReason}`, ctx);
|
|
196
716
|
}
|
|
197
717
|
if (assistantToolCallCount(message.content) > 0 || message.stopReason === "toolUse") {
|
|
198
|
-
retryQueued = snapshot.validation !== undefined;
|
|
718
|
+
retryQueued = snapshot.meta.validation !== undefined;
|
|
199
719
|
const cleaned = stripStateComments(message.content);
|
|
200
720
|
return cleaned.changed
|
|
201
721
|
? { message: { ...message, role: "assistant" as const, content: cleaned.content } }
|
|
@@ -203,7 +723,13 @@ export default function stateFlowExtension(pi: ExtensionAPI): void {
|
|
|
203
723
|
}
|
|
204
724
|
try {
|
|
205
725
|
const parsed = parseTerminalPatch(message.content);
|
|
206
|
-
stagedFinal =
|
|
726
|
+
stagedFinal = stageScopedTransition(
|
|
727
|
+
scopeStates,
|
|
728
|
+
parsed.transition,
|
|
729
|
+
skillReads.successful.values(),
|
|
730
|
+
runtime!.causalBasis(),
|
|
731
|
+
artifactReads.successful.values(),
|
|
732
|
+
);
|
|
207
733
|
retryQueued = false;
|
|
208
734
|
return { message: { ...message, role: "assistant" as const, content: parsed.responseContent } };
|
|
209
735
|
} catch (error) {
|
|
@@ -212,16 +738,15 @@ export default function stateFlowExtension(pi: ExtensionAPI): void {
|
|
|
212
738
|
});
|
|
213
739
|
|
|
214
740
|
pi.on("turn_end", (event, ctx) => {
|
|
215
|
-
if (!snapshot.enabled || !stagedFinal) {
|
|
741
|
+
if (!snapshot.config.enabled || !stagedFinal) {
|
|
216
742
|
updateUi(ctx);
|
|
217
743
|
return;
|
|
218
744
|
}
|
|
219
745
|
try {
|
|
220
|
-
stagedFinal.
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
746
|
+
stagedFinal.nextStates.session.response = finalizedAssistantResponse(event.message);
|
|
747
|
+
commitStage(stagedFinal, ctx, true);
|
|
748
|
+
enqueueTurnPublication();
|
|
749
|
+
if (snapshot.meta.remotePublication?.mode === "turn-end") launchPublicationWorker();
|
|
225
750
|
} catch (error) {
|
|
226
751
|
queueTerminalRegeneration(error instanceof Error ? error.message : String(error), ctx);
|
|
227
752
|
}
|
|
@@ -230,9 +755,20 @@ export default function stateFlowExtension(pi: ExtensionAPI): void {
|
|
|
230
755
|
});
|
|
231
756
|
|
|
232
757
|
pi.on("agent_settled", (_event, ctx) => {
|
|
233
|
-
if (snapshot.enabled && retryQueued) abandonRun(ctx);
|
|
758
|
+
if (snapshot.config.enabled && retryQueued) abandonRun(ctx);
|
|
759
|
+
if (snapshot.meta.remotePublication?.mode === "turn-end") launchPublicationWorker();
|
|
234
760
|
});
|
|
235
761
|
|
|
236
|
-
pi.on("session_start", (
|
|
237
|
-
|
|
762
|
+
pi.on("session_start", (event, ctx) => {
|
|
763
|
+
rehydrationPhase = event.reason === "resume" ? "resume-bootstrap" : "new-bootstrap";
|
|
764
|
+
restoreActiveBranch(ctx, event.reason);
|
|
765
|
+
refreshArtifactInvalidations(ctx);
|
|
766
|
+
retryPendingPush(ctx);
|
|
767
|
+
if (snapshot.meta.remotePublication?.mode === "turn-end") launchPublicationWorker();
|
|
768
|
+
updateUi(ctx);
|
|
769
|
+
});
|
|
770
|
+
pi.on("session_tree", (_event, ctx) => {
|
|
771
|
+
restoreActiveBranch(ctx);
|
|
772
|
+
refreshArtifactInvalidations(ctx);
|
|
773
|
+
});
|
|
238
774
|
}
|