@llblab/pi-kit 0.7.0 → 0.8.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 +8 -0
- package/README.md +2 -2
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +21 -21
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +3 -122
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +11 -0
- package/node_modules/@llblab/pi-state-flow/README.md +41 -35
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +36 -16
- package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +4 -4
- package/node_modules/@llblab/pi-state-flow/index.ts +23 -1
- package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +3 -0
- package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +191 -29
- package/node_modules/@llblab/pi-state-flow/lib/config.ts +6 -1
- package/node_modules/@llblab/pi-state-flow/lib/context.ts +42 -7
- package/node_modules/@llblab/pi-state-flow/lib/durable.ts +38 -8
- package/node_modules/@llblab/pi-state-flow/lib/episode.ts +1 -13
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +290 -134
- package/node_modules/@llblab/pi-state-flow/lib/git.ts +32 -16
- package/node_modules/@llblab/pi-state-flow/lib/logging.ts +41 -0
- package/node_modules/@llblab/pi-state-flow/lib/maintenance.ts +12 -6
- package/node_modules/@llblab/pi-state-flow/lib/migration.ts +16 -0
- package/node_modules/@llblab/pi-state-flow/lib/rehydration.ts +3 -0
- package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +167 -31
- package/node_modules/@llblab/pi-state-flow/lib/skills.ts +15 -6
- package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +40 -34
- package/node_modules/@llblab/pi-state-flow/lib/state.ts +6 -0
- package/node_modules/@llblab/pi-state-flow/lib/status.ts +4 -9
- package/node_modules/@llblab/pi-state-flow/lib/storage.ts +25 -5
- package/node_modules/@llblab/pi-state-flow/lib/terminal.ts +17 -147
- package/node_modules/@llblab/pi-state-flow/lib/transition.ts +49 -27
- package/node_modules/@llblab/pi-state-flow/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/AGENTS.md +2 -2
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +3 -2
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +6 -1
- package/node_modules/@llblab/pi-telegram/README.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +8 -6
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +4 -4
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +18 -0
- package/node_modules/@llblab/pi-telegram/lib/bus-api.ts +32 -19
- package/node_modules/@llblab/pi-telegram/lib/bus.ts +5 -0
- package/node_modules/@llblab/pi-telegram/lib/channel-posts.ts +189 -15
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +3 -0
- package/node_modules/@llblab/pi-telegram/lib/config.ts +67 -4
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +65 -6
- package/node_modules/@llblab/pi-telegram/lib/locks.ts +6 -1
- package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +49 -3
- package/node_modules/@llblab/pi-telegram/lib/preview.ts +17 -0
- package/node_modules/@llblab/pi-telegram/lib/prompts.ts +1 -0
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +66 -8
- package/node_modules/@llblab/pi-telegram/lib/rendering.ts +4 -1
- package/node_modules/@llblab/pi-telegram/lib/replies.ts +19 -0
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +39 -0
- package/node_modules/@llblab/pi-telegram/lib/setup.ts +44 -4
- package/node_modules/@llblab/pi-telegram/lib/status.ts +41 -4
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +74 -18
- package/node_modules/@llblab/pi-telegram/lib/turns.ts +7 -0
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/package.json +3 -3
- package/node_modules/@llblab/pi-state-flow/lib/validation.ts +0 -27
- /package/node_modules/@llblab/pi-telegram/lib/{logs.ts → logging.ts} +0 -0
|
@@ -2,20 +2,20 @@ import { execFile } from "node:child_process";
|
|
|
2
2
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
3
3
|
import { StringEnum, Type } from "@earendil-works/pi-ai";
|
|
4
4
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { assistantToolCallCount, finalizedAssistantResponse, stateFlowProtocol } from "./terminal.ts";
|
|
7
|
+
import { createPassiveContinuation, currentRunTrajectory, passiveContinuationMessages, runtimeContextMessage, VALIDATION_MESSAGE_TYPE, withoutPrivateValidation, type PassiveContinuation } from "./context.ts";
|
|
7
8
|
import { ArtifactReadTracker } from "./acquisition.ts";
|
|
8
9
|
import { loadStateFlowConfig } from "./config.ts";
|
|
9
|
-
import { resolve } from "node:path";
|
|
10
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
10
11
|
import { SkillReadTracker } from "./skills.ts";
|
|
11
12
|
import { emptySnapshot, migrationFailure, persistableSnapshot, type Snapshot } from "./snapshot.ts";
|
|
12
13
|
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";
|
|
15
|
-
import { MAX_VALIDATION_RETRIES, nextValidation } from "./validation.ts";
|
|
14
|
+
import { emptyState, overlayStates, projectStateForModel, type MaterializedState, type ScopePatch, type ScopedStates, type StateScope } from "./state.ts";
|
|
15
|
+
import { commitScopedTransition, stageScopedPatch, stageScopedTransition, validateUnchangedResolution, type StagedScopedTransition } from "./transition.ts";
|
|
16
16
|
import { discoverSnapshotData, hasPriorConversation, isNewSession, SNAPSHOT_ENTRY_TYPE } from "./session.ts";
|
|
17
17
|
import { compactStatus, detailedStatus, STATUS_KEY, type PendingPublicationDiagnostic, type StatusDiagnostics } from "./status.ts";
|
|
18
|
-
import {
|
|
18
|
+
import { prepareRun, resumeEpisode, startEpisode, stopEpisode } from "./episode.ts";
|
|
19
19
|
import { recoverSnapshot } from "./recovery.ts";
|
|
20
20
|
import type { RehydrationPhase } from "./rehydration.ts";
|
|
21
21
|
import { resolveRemotePublicationPolicy, serializeRemotePublicationPolicyDocument } from "./publication.ts";
|
|
@@ -23,13 +23,15 @@ import { coalescePublicationTarget, createPublicationQueue, type PublicationQueu
|
|
|
23
23
|
import { acquirePublicationWorkerLease, loadPublicationQueue, publicationQueuePath, removePublicationQueue, savePublicationQueue } from "./publication.ts";
|
|
24
24
|
import { runPublicationWorker } from "./publication.ts";
|
|
25
25
|
import { getKnowledgeRoot, GlobalMarkdownDiscovery } from "./discovery.ts";
|
|
26
|
+
import { isObject, sameJson } from "./json.ts";
|
|
26
27
|
import {
|
|
27
28
|
cwdScopeKey,
|
|
28
29
|
resolveSessionAddress,
|
|
29
30
|
sessionScopeKey,
|
|
30
31
|
type SessionAddress,
|
|
31
32
|
} from "./durable.ts";
|
|
32
|
-
import { projectRecentTransitionsWithLimit } from "./history.ts";
|
|
33
|
+
import { projectRecentTransitionsWithLimit, RECENT_TRANSITION_LIMIT } from "./history.ts";
|
|
34
|
+
import { appendStateFlowDiagnostic, projectDiagnosticContent, stateFlowLogPath, type StateFlowDiagnosticCategory } from "./logging.ts";
|
|
33
35
|
import { isGitCommitAncestor, pushGitCommit, resolveGitPushDestination } from "./git.ts";
|
|
34
36
|
import {
|
|
35
37
|
ORDINARY_ARTIFACT_COMPILER,
|
|
@@ -46,15 +48,27 @@ export interface StateFlowExtensionOptions {
|
|
|
46
48
|
|
|
47
49
|
export const PATCH_STATE_TOOL_NAME = "patch_state";
|
|
48
50
|
export const READ_STATE_TOOL_NAME = "read_state";
|
|
51
|
+
const PASSIVE_STOP_ENTRY_TYPE = "state-flow-passive-stop";
|
|
52
|
+
|
|
53
|
+
/** Keep a failed tool invocation visually separated from its rendered error without changing error semantics. */
|
|
54
|
+
function separatedFailure(error: unknown): Error {
|
|
55
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
56
|
+
return new Error(`\n${message}`, error instanceof Error ? { cause: error } : undefined);
|
|
57
|
+
}
|
|
49
58
|
|
|
50
59
|
export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowExtensionOptions = {}): void {
|
|
51
|
-
const
|
|
60
|
+
const agentDir = options.agentDir ?? getAgentDir();
|
|
61
|
+
const config = loadStateFlowConfig(agentDir);
|
|
52
62
|
let snapshot: Snapshot = emptySnapshot();
|
|
53
63
|
let scopeStates: ScopedStates = { global: emptyState(), cwd: emptyState(), session: emptyState() };
|
|
54
64
|
let branchHasSnapshot = false;
|
|
55
65
|
let branchStartsWithoutRuntime = false;
|
|
56
|
-
let
|
|
57
|
-
let
|
|
66
|
+
let stateResolutionSatisfied = false;
|
|
67
|
+
let terminalDraftIntercepted = false;
|
|
68
|
+
let responseAwaitingReconciliation = false;
|
|
69
|
+
let passiveContinuation: PassiveContinuation | undefined;
|
|
70
|
+
let bootstrapContinuation: PassiveContinuation | undefined;
|
|
71
|
+
let artifactRefreshPending = false;
|
|
58
72
|
let runAnchorTimestamp: number | undefined;
|
|
59
73
|
let runtime: TemporalRuntime | undefined;
|
|
60
74
|
let activeContext: ExtensionContext | undefined;
|
|
@@ -65,8 +79,9 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
65
79
|
const repositoryRoot = resolve(options.repositoryRoot ?? config.directory);
|
|
66
80
|
const skillReads = new SkillReadTracker();
|
|
67
81
|
const artifactReads = new ArtifactReadTracker();
|
|
68
|
-
const globalMarkdown = new GlobalMarkdownDiscovery(options.knowledgeRoot ?? getKnowledgeRoot(
|
|
82
|
+
const globalMarkdown = new GlobalMarkdownDiscovery(options.knowledgeRoot ?? getKnowledgeRoot(agentDir));
|
|
69
83
|
let artifactInvalidations: ArtifactInvalidationRequest[] = [];
|
|
84
|
+
let loggingWarningReported = false;
|
|
70
85
|
|
|
71
86
|
function sessionAddress(ctx: ExtensionContext): SessionAddress {
|
|
72
87
|
return resolveSessionAddress(ctx.sessionManager.getSessionFile(), ctx.sessionManager.getSessionId(), ctx.sessionManager.getHeader()?.timestamp);
|
|
@@ -78,7 +93,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
78
93
|
|
|
79
94
|
options.onRuntime?.({ read: (offset, scope) => {
|
|
80
95
|
if (!runtime) throw new Error("State Flow temporal runtime is unavailable");
|
|
81
|
-
return runtime.read(offset, scope);
|
|
96
|
+
return projectStateForModel(runtime.read(offset, scope));
|
|
82
97
|
} });
|
|
83
98
|
|
|
84
99
|
function persist(): void {
|
|
@@ -105,14 +120,39 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
105
120
|
}
|
|
106
121
|
|
|
107
122
|
function clearRunTransient(): void {
|
|
108
|
-
|
|
109
|
-
|
|
123
|
+
stateResolutionSatisfied = false;
|
|
124
|
+
terminalDraftIntercepted = false;
|
|
125
|
+
responseAwaitingReconciliation = false;
|
|
110
126
|
runAnchorTimestamp = undefined;
|
|
111
127
|
skillReads.clear();
|
|
112
128
|
artifactReads.clear();
|
|
113
129
|
}
|
|
114
130
|
|
|
131
|
+
function passiveStopTimestamp(ctx: ExtensionContext): number | undefined {
|
|
132
|
+
for (const entry of [...ctx.sessionManager.getBranch()].reverse()) {
|
|
133
|
+
try {
|
|
134
|
+
if (entry?.type !== "custom" || entry.customType !== PASSIVE_STOP_ENTRY_TYPE) continue;
|
|
135
|
+
const at = (entry.data as { at?: unknown } | undefined)?.at;
|
|
136
|
+
if (typeof at === "number" && Number.isSafeInteger(at) && at >= 0) return at;
|
|
137
|
+
} catch {
|
|
138
|
+
// A hostile unrelated branch entry cannot manufacture or suppress a valid marker.
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function retainsPhysicalSessionProjection(reason: unknown): boolean {
|
|
145
|
+
return reason === undefined || reason === "startup" || reason === "reload" || reason === "resume";
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function deferArtifactRefresh(): void {
|
|
149
|
+
artifactInvalidations = [];
|
|
150
|
+
artifactReads.setCandidates([]);
|
|
151
|
+
artifactRefreshPending = true;
|
|
152
|
+
}
|
|
153
|
+
|
|
115
154
|
function refreshArtifactInvalidations(ctx: ExtensionContext): void {
|
|
155
|
+
artifactRefreshPending = false;
|
|
116
156
|
if (!snapshot.config.enabled) {
|
|
117
157
|
artifactInvalidations = [];
|
|
118
158
|
artifactReads.setCandidates([]);
|
|
@@ -124,6 +164,8 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
124
164
|
discovery.sources,
|
|
125
165
|
scopeStates.global.artifacts,
|
|
126
166
|
ORDINARY_ARTIFACT_COMPILER,
|
|
167
|
+
{},
|
|
168
|
+
runtime?.artifactProvenance("global") ?? {},
|
|
127
169
|
);
|
|
128
170
|
artifactInvalidations = structuredClone(plan.requiresCompilation);
|
|
129
171
|
if (plan.removed.length > 0) {
|
|
@@ -193,12 +235,34 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
193
235
|
}
|
|
194
236
|
}
|
|
195
237
|
|
|
238
|
+
/** Accept an activation or lifecycle commit locally; turn-end policy queues it for the asynchronous worker. */
|
|
239
|
+
function recordPolicyPublication(publication: RuntimePublication | undefined, ctx: ExtensionContext): void {
|
|
240
|
+
if (!publication) return;
|
|
241
|
+
recordPublication(publication, ctx);
|
|
242
|
+
const mode = snapshot.meta.remotePublication?.mode ?? "transition";
|
|
243
|
+
const target = publication.commit ?? publication.revision;
|
|
244
|
+
if (mode !== "turn-end" || target === undefined || !/^[0-9a-f]{40,64}$/.test(target)) return;
|
|
245
|
+
turnPublicationTarget = target;
|
|
246
|
+
try {
|
|
247
|
+
enqueueTurnPublication();
|
|
248
|
+
launchPublicationWorker();
|
|
249
|
+
} catch (error) {
|
|
250
|
+
ctx.ui.notify(
|
|
251
|
+
`State Flow accepted the local commit; remote publication is deferred: ${error instanceof Error ? error.message : String(error)}`,
|
|
252
|
+
"warning",
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
196
257
|
function commitStage(stage: StagedScopedTransition, ctx: ExtensionContext, finalizeRun: boolean): boolean {
|
|
197
258
|
const acquiredArtifactPaths = new Set(artifactReads.successful.keys());
|
|
198
259
|
const committed = commitScopedTransition(snapshot, scopeStates, stage, (accepted, nextSnapshot) => {
|
|
199
260
|
if (!runtime?.view) throw new Error("Temporal State Flow runtime is unavailable; reload before publishing");
|
|
200
261
|
const mode = nextSnapshot.meta.remotePublication?.mode ?? "transition";
|
|
201
|
-
const publication = runtime.publish(nextSnapshot, accepted !== undefined, accepted, {
|
|
262
|
+
const publication = runtime.publish(nextSnapshot, accepted !== undefined, accepted, {
|
|
263
|
+
pushRemote: mode === "transition",
|
|
264
|
+
provenance: stage.provenanceUpdates,
|
|
265
|
+
});
|
|
202
266
|
if (publication?.commit && mode === "turn-end") turnPublicationTarget = publication.commit;
|
|
203
267
|
if (publication) recordPublication(publication, ctx);
|
|
204
268
|
}, runtime!.causalBasis(), { finalizeRun });
|
|
@@ -272,12 +336,31 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
272
336
|
}).finally(() => {
|
|
273
337
|
activePublicationWorkers.delete(path);
|
|
274
338
|
lease.release();
|
|
275
|
-
|
|
339
|
+
try {
|
|
340
|
+
if (loadPublicationQueue(path)?.status === "pending") launchPublicationWorker();
|
|
341
|
+
} catch {
|
|
342
|
+
// Malformed queue persistence stays inert until an explicit retry or repair.
|
|
343
|
+
}
|
|
276
344
|
});
|
|
277
345
|
}
|
|
278
346
|
|
|
279
347
|
function retryPendingPush(ctx: ExtensionContext): void {
|
|
280
348
|
if (pendingPublication === undefined || !runtime?.view) return;
|
|
349
|
+
const mode = snapshot.meta.remotePublication?.mode ?? "transition";
|
|
350
|
+
if (mode !== "transition") {
|
|
351
|
+
const target = pendingPublication.commit;
|
|
352
|
+
setPendingPublication(undefined);
|
|
353
|
+
if (mode === "turn-end") {
|
|
354
|
+
turnPublicationTarget = target;
|
|
355
|
+
try {
|
|
356
|
+
enqueueTurnPublication();
|
|
357
|
+
launchPublicationWorker();
|
|
358
|
+
} catch (error) {
|
|
359
|
+
ctx.ui.notify(`State Flow retained local state; asynchronous publication recovery is deferred: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
281
364
|
const result = pushGitCommit(repositoryRoot, pendingPublication.commit);
|
|
282
365
|
if (result.status !== "pending") {
|
|
283
366
|
setPendingPublication(undefined);
|
|
@@ -314,6 +397,8 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
314
397
|
discovery.sources,
|
|
315
398
|
diagnosticStates.global.artifacts,
|
|
316
399
|
ORDINARY_ARTIFACT_COMPILER,
|
|
400
|
+
{},
|
|
401
|
+
runtime?.artifactProvenance("global") ?? {},
|
|
317
402
|
);
|
|
318
403
|
staleArtifacts = [
|
|
319
404
|
...plan.requiresCompilation.map(({ path, reason }) => ({ scope: "global" as const, path, reason })),
|
|
@@ -349,7 +434,6 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
349
434
|
...(artifactFreshnessError === undefined ? {} : { artifactFreshnessError }),
|
|
350
435
|
...(durableStateError === undefined ? {} : { durableStateError }),
|
|
351
436
|
...(pendingPublication === undefined ? {} : { pendingPublication }),
|
|
352
|
-
retryQueued,
|
|
353
437
|
...(publicationQueue === undefined ? {} : { publicationQueue }),
|
|
354
438
|
...(publicationQueueError === undefined ? {} : { publicationQueueError }),
|
|
355
439
|
};
|
|
@@ -357,6 +441,11 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
357
441
|
|
|
358
442
|
function restoreActiveBranch(ctx: ExtensionContext, sessionStartReason?: unknown): void {
|
|
359
443
|
clearRunTransient();
|
|
444
|
+
passiveContinuation = undefined;
|
|
445
|
+
bootstrapContinuation = undefined;
|
|
446
|
+
artifactInvalidations = [];
|
|
447
|
+
artifactReads.setCandidates([]);
|
|
448
|
+
artifactRefreshPending = false;
|
|
360
449
|
activeContext = ctx;
|
|
361
450
|
const session = sessionAddress(ctx);
|
|
362
451
|
runtime = new TemporalRuntime(ctx.cwd, session, repositoryRoot);
|
|
@@ -383,7 +472,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
383
472
|
if (snapshot.meta.durableBase !== selectedRevision) persist();
|
|
384
473
|
} else if (branchHasSnapshot && snapshot.config.enabled) {
|
|
385
474
|
const publication = runtime.initialize(snapshot, true);
|
|
386
|
-
|
|
475
|
+
recordPolicyPublication(publication, ctx);
|
|
387
476
|
delete snapshot.legacySession;
|
|
388
477
|
}
|
|
389
478
|
installScopeStates();
|
|
@@ -394,7 +483,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
394
483
|
);
|
|
395
484
|
runtime.prepare();
|
|
396
485
|
const initialization = runtime.initialize(snapshot, true);
|
|
397
|
-
|
|
486
|
+
recordPolicyPublication(initialization, ctx);
|
|
398
487
|
if (!runtime.view) snapshot = emptySnapshot();
|
|
399
488
|
installScopeStates();
|
|
400
489
|
if (snapshot.config.enabled) {
|
|
@@ -432,47 +521,52 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
432
521
|
if (!snapshot.config.enabled && snapshot.meta.validation?.attempt === 0) {
|
|
433
522
|
ctx.ui.notify(`State Flow restored disabled: ${snapshot.meta.validation.error}`, "error");
|
|
434
523
|
}
|
|
524
|
+
if (retainsPhysicalSessionProjection(sessionStartReason) && runtime?.view) {
|
|
525
|
+
const stoppedAt = passiveStopTimestamp(ctx);
|
|
526
|
+
if (stoppedAt !== undefined) {
|
|
527
|
+
const continuation = createPassiveContinuation(
|
|
528
|
+
projectStateForModel(overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session)),
|
|
529
|
+
stoppedAt,
|
|
530
|
+
);
|
|
531
|
+
if (!snapshot.config.enabled) passiveContinuation = continuation;
|
|
532
|
+
else if (snapshot.meta.bootstrap) bootstrapContinuation = continuation;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
if (snapshot.config.enabled) deferArtifactRefresh();
|
|
435
536
|
syncStateFlowTools();
|
|
436
537
|
updateUi(ctx);
|
|
437
538
|
}
|
|
438
539
|
|
|
439
|
-
function
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
540
|
+
function recordDiagnostic(error: string, category: StateFlowDiagnosticCategory, ctx: ExtensionContext, content?: unknown): void {
|
|
541
|
+
if (!config.logging) return;
|
|
542
|
+
try {
|
|
543
|
+
const path = stateFlowLogPath(agentDir);
|
|
544
|
+
const fromRepository = relative(repositoryRoot, path);
|
|
545
|
+
if (fromRepository === "" || (!isAbsolute(fromRepository) && fromRepository !== ".." && !fromRepository.startsWith(`..${sep}`))) {
|
|
546
|
+
throw new Error("diagnostic path overlaps the State Flow repository");
|
|
547
|
+
}
|
|
548
|
+
appendStateFlowDiagnostic(path, {
|
|
549
|
+
at: new Date().toISOString(),
|
|
550
|
+
sessionId: sessionAddress(ctx).id,
|
|
551
|
+
cwd: resolve(ctx.cwd),
|
|
552
|
+
category,
|
|
553
|
+
error,
|
|
554
|
+
...(content === undefined ? {} : { content: projectDiagnosticContent(content) }),
|
|
555
|
+
});
|
|
556
|
+
} catch (failure) {
|
|
557
|
+
if (loggingWarningReported) return;
|
|
558
|
+
loggingWarningReported = true;
|
|
559
|
+
ctx.ui.notify(`State Flow could not write diagnostics: ${failure instanceof Error ? failure.message : String(failure)}`, "warning");
|
|
458
560
|
}
|
|
459
|
-
retryQueued = false;
|
|
460
|
-
snapshot.meta.validation = undefined;
|
|
461
|
-
skillReads.clear();
|
|
462
|
-
artifactReads.clear();
|
|
463
|
-
persist();
|
|
464
|
-
ctx.ui.notify(`State Flow remains enabled after ${MAX_VALIDATION_RETRIES} automatic regeneration attempts; the last committed state was preserved: ${decision.error}`, "error");
|
|
465
561
|
}
|
|
466
562
|
|
|
467
|
-
function
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
},
|
|
475
|
-
};
|
|
563
|
+
function continueForResolution(): void {
|
|
564
|
+
terminalDraftIntercepted = true;
|
|
565
|
+
pi.sendMessage({
|
|
566
|
+
customType: VALIDATION_MESSAGE_TYPE,
|
|
567
|
+
content: "Before completing this turn, resolve State Flow. Call patch_state with durable semantic changes, or call patch_state with {\"unchanged\":true} if no state update is required. Then provide the final answer normally.",
|
|
568
|
+
display: false,
|
|
569
|
+
}, { deliverAs: "steer", triggerTurn: true });
|
|
476
570
|
}
|
|
477
571
|
|
|
478
572
|
pi.registerTool({
|
|
@@ -485,51 +579,74 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
485
579
|
scope: Type.Optional(StringEnum(["effective", "global", "cwd", "session"] as const, { description: "Projection at that same boundary; defaults to effective" })),
|
|
486
580
|
}, { additionalProperties: false }),
|
|
487
581
|
async execute(_toolCallId, params, signal) {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
582
|
+
try {
|
|
583
|
+
if (!snapshot.config.enabled) throw new Error("State Flow is disabled on this session branch");
|
|
584
|
+
if (signal?.aborted) throw new Error("State Flow read was aborted");
|
|
585
|
+
if (!runtime?.view) throw new Error("State Flow temporal runtime is unavailable");
|
|
586
|
+
const { offset = 0, scope = "effective" } = params;
|
|
587
|
+
const state = runtime.read(offset, scope === "effective" ? undefined : scope);
|
|
588
|
+
const boundary = runtime.view.lineage.at(-1 - offset)!;
|
|
589
|
+
return {
|
|
590
|
+
content: [{ type: "text", text: `\n${JSON.stringify({ offset, scope, boundary, state: projectStateForModel(state) })}` }],
|
|
591
|
+
details: { offset, scope, transitionId: boundary.id },
|
|
592
|
+
};
|
|
593
|
+
} catch (error) {
|
|
594
|
+
throw separatedFailure(error);
|
|
595
|
+
}
|
|
498
596
|
},
|
|
499
597
|
});
|
|
500
598
|
|
|
501
599
|
pi.registerTool({
|
|
502
600
|
name: PATCH_STATE_TOOL_NAME,
|
|
503
601
|
label: "Patch State",
|
|
504
|
-
description: "
|
|
505
|
-
promptSnippet: "
|
|
602
|
+
description: "The sole State Flow semantic mutation protocol. Use exactly one form: PATCH {scope, patch} to materialize established future-relevant state, or UNCHANGED {unchanged:true} after explicitly deciding no durable update is needed. Never combine unchanged with scope or patch. This call must be the only State Flow barrier in its assistant response; sibling tool calls are reconsidered after rematerialization.",
|
|
603
|
+
promptSnippet: "PATCH {scope, patch} or UNCHANGED {unchanged:true}",
|
|
506
604
|
promptGuidelines: [
|
|
507
|
-
"Use patch_state
|
|
508
|
-
"Call patch_state alone in an assistant response; choose subsequent actions only after its
|
|
605
|
+
"Use patch_state for every durable semantic change. Before a final answer, resolve State Flow with PATCH {scope, patch} or UNCHANGED {unchanged:true}.",
|
|
606
|
+
"Call patch_state alone in an assistant response; choose subsequent actions only after its acknowledgement and rematerialized State Flow context.",
|
|
509
607
|
],
|
|
510
608
|
executionMode: "sequential",
|
|
609
|
+
// Type.Union/Type.Literal schemas are not portable across Pi's Google-compatible tool adapters.
|
|
610
|
+
// Field descriptions expose the discriminated forms while runtime validation preserves exclusivity.
|
|
511
611
|
parameters: Type.Object({
|
|
512
|
-
scope: StringEnum(["session", "cwd", "global"] as const),
|
|
513
|
-
patch: Type.Record(Type.String(), Type.Unknown()),
|
|
612
|
+
scope: Type.Optional(StringEnum(["session", "cwd", "global"] as const, { description: "PATCH form only: required with patch; forbidden with unchanged" })),
|
|
613
|
+
patch: Type.Optional(Type.Record(Type.String(), Type.Unknown(), { description: "PATCH form only: required with scope; forbidden with unchanged" })),
|
|
614
|
+
unchanged: Type.Optional(Type.Boolean({ description: "UNCHANGED form only: set exactly true and omit scope and patch" })),
|
|
514
615
|
}, { additionalProperties: false }),
|
|
515
616
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
617
|
+
try {
|
|
618
|
+
if (!snapshot.config.enabled) throw new Error("State Flow is disabled on this session branch");
|
|
619
|
+
if (signal?.aborted) throw new Error("State Flow patch was aborted before materialization");
|
|
620
|
+
if (!isObject(params)) throw new Error("patch_state requires an object in exactly one supported form");
|
|
621
|
+
const keys = Object.keys(params).sort();
|
|
622
|
+
if (params.unchanged === true) {
|
|
623
|
+
if (keys.length !== 1 || keys[0] !== "unchanged") throw new Error('patch_state {"unchanged":true} cannot include any other field');
|
|
624
|
+
validateUnchangedResolution(scopeStates, skillReads.successful.values(), runtime!.causalBasis(), artifactReads.successful.values());
|
|
625
|
+
stateResolutionSatisfied = true;
|
|
626
|
+
terminalDraftIntercepted = false;
|
|
627
|
+
return { content: [{ type: "text", text: "\nState resolution acknowledged unchanged." }], details: { unchanged: true } };
|
|
628
|
+
}
|
|
629
|
+
if (keys.length !== 2 || keys[0] !== "patch" || keys[1] !== "scope"
|
|
630
|
+
|| params.scope === undefined || !isObject(params.patch)) {
|
|
631
|
+
throw new Error("patch_state requires exactly scope and patch, or {\"unchanged\":true}");
|
|
632
|
+
}
|
|
633
|
+
if (Object.keys(params.patch).length === 0) throw new Error("An empty semantic patch is not an unchanged acknowledgement; use {\"unchanged\":true}");
|
|
634
|
+
const transition = { scope: params.scope as StateScope, patch: params.patch as ScopePatch };
|
|
635
|
+
const stage = stageScopedPatch(scopeStates, transition, skillReads.successful.values(), runtime!.causalBasis(), artifactReads.successful.values());
|
|
636
|
+
const semanticChange = (["global", "cwd", "session"] as const).some((scope) => !sameJson(scopeStates[scope], stage.nextStates[scope]));
|
|
637
|
+
const provenanceChange = Object.values(stage.provenanceUpdates).some((updates) => Object.keys(updates).length > 0);
|
|
638
|
+
if (!semanticChange && !provenanceChange) throw new Error('patch_state PATCH must materially update state or required provenance; use {"unchanged":true} instead');
|
|
639
|
+
commitStage(stage, ctx, false);
|
|
640
|
+
stateResolutionSatisfied = true;
|
|
641
|
+
terminalDraftIntercepted = false;
|
|
642
|
+
updateUi(ctx);
|
|
643
|
+
const publication = pendingPublication === undefined ? "" : "; durable publication pending";
|
|
644
|
+
return { content: [{ type: "text", text: `\nState materialized at ${params.scope} scope${publication}.` }], details: { scope: params.scope, step: snapshot.meta.step } };
|
|
645
|
+
} catch (error) {
|
|
646
|
+
stateResolutionSatisfied = false;
|
|
647
|
+
recordDiagnostic(error instanceof Error ? error.message : String(error), /concurrently|advanced/.test(String(error)) ? "publication-conflict" : "invalid-patch", ctx);
|
|
648
|
+
throw separatedFailure(error);
|
|
649
|
+
}
|
|
533
650
|
},
|
|
534
651
|
});
|
|
535
652
|
|
|
@@ -538,6 +655,10 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
538
655
|
handler: async (_args, ctx) => {
|
|
539
656
|
if (!activeContext) restoreActiveBranch(ctx);
|
|
540
657
|
const previousSnapshot = structuredClone(snapshot);
|
|
658
|
+
const previousPassiveContinuation = passiveContinuation;
|
|
659
|
+
const previousBootstrapContinuation = bootstrapContinuation;
|
|
660
|
+
const previousArtifactRefreshPending = artifactRefreshPending;
|
|
661
|
+
const previousArtifactInvalidations = structuredClone(artifactInvalidations);
|
|
541
662
|
try {
|
|
542
663
|
if (!runtime?.view && !snapshot.meta.durableBase && !branchStartsWithoutRuntime) {
|
|
543
664
|
throw new Error("Selected branch revision is unavailable; restore its original Git history before starting State Flow");
|
|
@@ -547,7 +668,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
547
668
|
runtime ??= createRuntime(ctx);
|
|
548
669
|
if (branchStartsWithoutRuntime) runtime.prepare();
|
|
549
670
|
const bootstrap = (!branchHasSnapshot || !snapshot.config.enabled)
|
|
550
|
-
&& hasPriorConversation(branch);
|
|
671
|
+
&& (hasPriorConversation(branch) || previousPassiveContinuation !== undefined);
|
|
551
672
|
if (!runtime.view && snapshot.meta.durableBase) {
|
|
552
673
|
snapshot = runtime.restore(snapshot.meta.durableBase, snapshot);
|
|
553
674
|
setPendingPublication(snapshot.meta.pendingPublication);
|
|
@@ -566,11 +687,15 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
566
687
|
const publication = runtime.view
|
|
567
688
|
? runtime.promote(snapshot) ?? runtime.publish(snapshot)
|
|
568
689
|
: runtime.initialize(snapshot, true, undefined, branchStartsWithoutRuntime);
|
|
569
|
-
|
|
690
|
+
recordPolicyPublication(publication, ctx);
|
|
570
691
|
installScopeStates();
|
|
571
692
|
delete snapshot.legacySession;
|
|
572
693
|
clearRunTransient();
|
|
573
|
-
|
|
694
|
+
passiveContinuation = undefined;
|
|
695
|
+
bootstrapContinuation = snapshot.meta.bootstrap
|
|
696
|
+
? previousPassiveContinuation ?? previousBootstrapContinuation
|
|
697
|
+
: undefined;
|
|
698
|
+
deferArtifactRefresh();
|
|
574
699
|
syncStateFlowTools();
|
|
575
700
|
persist();
|
|
576
701
|
updateUi(ctx);
|
|
@@ -582,6 +707,11 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
582
707
|
);
|
|
583
708
|
} catch (error) {
|
|
584
709
|
snapshot = previousSnapshot;
|
|
710
|
+
passiveContinuation = previousPassiveContinuation;
|
|
711
|
+
bootstrapContinuation = previousBootstrapContinuation;
|
|
712
|
+
artifactRefreshPending = previousArtifactRefreshPending;
|
|
713
|
+
artifactInvalidations = previousArtifactInvalidations;
|
|
714
|
+
artifactReads.setCandidates(artifactInvalidations);
|
|
585
715
|
syncStateFlowTools();
|
|
586
716
|
ctx.ui.notify(
|
|
587
717
|
`State Flow could not initialize CWD state: ${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -608,6 +738,15 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
608
738
|
selected = createRuntime(ctx);
|
|
609
739
|
current = selected.restore(snapshot.meta.durableBase, snapshot);
|
|
610
740
|
}
|
|
741
|
+
const stoppedAt = Date.now();
|
|
742
|
+
const exitStates = selected?.view ? selected.states() : undefined;
|
|
743
|
+
const exitHandoff = current.config.enabled && exitStates
|
|
744
|
+
? createPassiveContinuation(
|
|
745
|
+
projectStateForModel(overlayStates(exitStates.global, exitStates.cwd, exitStates.session)),
|
|
746
|
+
stoppedAt,
|
|
747
|
+
)
|
|
748
|
+
: undefined;
|
|
749
|
+
const retainedHandoff = exitHandoff ?? (!current.config.enabled ? passiveContinuation : undefined);
|
|
611
750
|
const stopped = stopEpisode(current);
|
|
612
751
|
const publication = selected?.view ? selected.publish(stopped) : undefined;
|
|
613
752
|
if (selected !== runtime) {
|
|
@@ -615,24 +754,32 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
615
754
|
installScopeStates();
|
|
616
755
|
}
|
|
617
756
|
snapshot = stopped;
|
|
618
|
-
|
|
757
|
+
recordPolicyPublication(publication, ctx);
|
|
619
758
|
branchHasSnapshot = true;
|
|
620
759
|
clearRunTransient();
|
|
760
|
+
passiveContinuation = retainedHandoff;
|
|
761
|
+
bootstrapContinuation = undefined;
|
|
762
|
+
artifactInvalidations = [];
|
|
763
|
+
artifactReads.setCandidates([]);
|
|
764
|
+
artifactRefreshPending = false;
|
|
765
|
+
if (exitHandoff) pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, { at: stoppedAt });
|
|
621
766
|
syncStateFlowTools();
|
|
622
767
|
persist();
|
|
623
768
|
updateUi(ctx);
|
|
624
769
|
},
|
|
625
770
|
});
|
|
626
771
|
|
|
627
|
-
pi.on("before_agent_start", (event) => {
|
|
772
|
+
pi.on("before_agent_start", (event, ctx) => {
|
|
628
773
|
if (!snapshot.config.enabled) return;
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
774
|
+
skillReads.clear();
|
|
775
|
+
artifactReads.clear();
|
|
776
|
+
if (artifactRefreshPending) refreshArtifactInvalidations(ctx);
|
|
777
|
+
stateResolutionSatisfied = false;
|
|
778
|
+
terminalDraftIntercepted = false;
|
|
779
|
+
responseAwaitingReconciliation = false;
|
|
780
|
+
const rotatesRun = snapshot.meta.specification !== undefined;
|
|
781
|
+
if (rotatesRun && rehydrationPhase !== "new-bootstrap" && rehydrationPhase !== "resume-bootstrap") rehydrationPhase = "step";
|
|
782
|
+
if (prepareRun(snapshot, event.prompt)) {
|
|
636
783
|
if (rotatesRun) runAnchorTimestamp = undefined;
|
|
637
784
|
persist();
|
|
638
785
|
}
|
|
@@ -642,16 +789,23 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
642
789
|
});
|
|
643
790
|
|
|
644
791
|
pi.on("context", (event) => {
|
|
792
|
+
if (passiveContinuation) {
|
|
793
|
+
return { messages: passiveContinuationMessages(event.messages as AgentMessage[], passiveContinuation) };
|
|
794
|
+
}
|
|
645
795
|
if (!snapshot.config.enabled || snapshot.meta.specification === undefined) return;
|
|
646
|
-
const effectiveState = overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session);
|
|
796
|
+
const effectiveState = projectStateForModel(overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session));
|
|
797
|
+
const invalidations = artifactInvalidations.map(({ path, reason }) => ({ path, reason }));
|
|
647
798
|
const recentTransitions = projectRecentTransitionsWithLimit(
|
|
648
|
-
|
|
799
|
+
RECENT_TRANSITION_LIMIT,
|
|
649
800
|
runtime?.recent() ?? [],
|
|
650
801
|
);
|
|
651
802
|
const activeRehydrationPhase = currentRehydrationPhase();
|
|
652
803
|
if (snapshot.meta.bootstrap) {
|
|
653
|
-
const
|
|
654
|
-
|
|
804
|
+
const sourceMessages = bootstrapContinuation
|
|
805
|
+
? passiveContinuationMessages(event.messages as AgentMessage[], bootstrapContinuation)
|
|
806
|
+
: event.messages as AgentMessage[];
|
|
807
|
+
const messages = withoutPrivateValidation(sourceMessages);
|
|
808
|
+
return { messages: [runtimeContextMessage(snapshot, effectiveState, recentTransitions, invalidations, activeRehydrationPhase, terminalDraftIntercepted), ...messages] };
|
|
655
809
|
}
|
|
656
810
|
const trajectory = currentRunTrajectory(
|
|
657
811
|
event.messages as AgentMessage[],
|
|
@@ -661,7 +815,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
661
815
|
runAnchorTimestamp = trajectory.anchorTimestamp;
|
|
662
816
|
return {
|
|
663
817
|
messages: [
|
|
664
|
-
runtimeContextMessage(snapshot, effectiveState, recentTransitions,
|
|
818
|
+
runtimeContextMessage(snapshot, effectiveState, recentTransitions, invalidations, activeRehydrationPhase, terminalDraftIntercepted),
|
|
665
819
|
...trajectory.messages,
|
|
666
820
|
],
|
|
667
821
|
};
|
|
@@ -699,76 +853,78 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
699
853
|
|
|
700
854
|
pi.on("tool_execution_end", (event) => {
|
|
701
855
|
if (!snapshot.config.enabled) return;
|
|
856
|
+
if (event.toolName === PATCH_STATE_TOOL_NAME && event.isError) stateResolutionSatisfied = false;
|
|
702
857
|
skillReads.recordEnd(event.toolCallId, event.toolName, event.isError);
|
|
703
858
|
artifactReads.recordEnd(event.toolCallId, event.toolName, event.isError);
|
|
704
859
|
});
|
|
705
860
|
|
|
706
861
|
pi.on("message_end", (event, ctx): any => {
|
|
707
862
|
if (!snapshot.config.enabled || event.message.role !== "assistant") return;
|
|
708
|
-
stagedFinal = undefined;
|
|
709
863
|
const message = event.message as unknown as { role: "assistant"; stopReason?: string; content?: unknown };
|
|
710
|
-
if (message.stopReason === "aborted") {
|
|
711
|
-
|
|
864
|
+
if (message.stopReason === "aborted" || assistantToolCallCount(message.content) > 0 || message.stopReason === "toolUse") {
|
|
865
|
+
responseAwaitingReconciliation = false;
|
|
712
866
|
return;
|
|
713
867
|
}
|
|
714
868
|
if (message.stopReason === "length" || message.stopReason === "error") {
|
|
715
|
-
|
|
869
|
+
responseAwaitingReconciliation = false;
|
|
870
|
+
recordDiagnostic(`Assistant response ended with ${message.stopReason}`, "finalization", ctx, message.content);
|
|
871
|
+
return;
|
|
716
872
|
}
|
|
717
|
-
if (
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
873
|
+
if (!stateResolutionSatisfied) {
|
|
874
|
+
responseAwaitingReconciliation = false;
|
|
875
|
+
terminalDraftIntercepted = true;
|
|
876
|
+
recordDiagnostic("Terminal draft intercepted before State Flow resolution", "terminal-pending", ctx, message.content);
|
|
877
|
+
continueForResolution();
|
|
878
|
+
return { message: { ...message, role: "assistant" as const, content: [] } };
|
|
723
879
|
}
|
|
724
880
|
try {
|
|
725
|
-
|
|
726
|
-
stagedFinal = stageScopedTransition(
|
|
727
|
-
scopeStates,
|
|
728
|
-
parsed.transition,
|
|
729
|
-
skillReads.successful.values(),
|
|
730
|
-
runtime!.causalBasis(),
|
|
731
|
-
artifactReads.successful.values(),
|
|
732
|
-
);
|
|
733
|
-
retryQueued = false;
|
|
734
|
-
return { message: { ...message, role: "assistant" as const, content: parsed.responseContent } };
|
|
881
|
+
validateUnchangedResolution(scopeStates, skillReads.successful.values(), runtime!.causalBasis(), artifactReads.successful.values());
|
|
735
882
|
} catch (error) {
|
|
736
|
-
|
|
883
|
+
stateResolutionSatisfied = false;
|
|
884
|
+
responseAwaitingReconciliation = false;
|
|
885
|
+
terminalDraftIntercepted = true;
|
|
886
|
+
recordDiagnostic(error instanceof Error ? error.message : String(error), "terminal-pending", ctx, message.content);
|
|
887
|
+
continueForResolution();
|
|
888
|
+
return { message: { ...message, role: "assistant" as const, content: [] } };
|
|
737
889
|
}
|
|
890
|
+
responseAwaitingReconciliation = true;
|
|
738
891
|
});
|
|
739
892
|
|
|
740
893
|
pi.on("turn_end", (event, ctx) => {
|
|
741
|
-
if (!snapshot.config.enabled || !
|
|
894
|
+
if (!snapshot.config.enabled || !responseAwaitingReconciliation) {
|
|
742
895
|
updateUi(ctx);
|
|
743
896
|
return;
|
|
744
897
|
}
|
|
745
898
|
try {
|
|
746
|
-
|
|
747
|
-
|
|
899
|
+
const response = finalizedAssistantResponse(event.message);
|
|
900
|
+
const stage = stageScopedTransition(scopeStates, { transitions: [], response }, [], runtime!.causalBasis());
|
|
901
|
+
commitStage(stage, ctx, true);
|
|
902
|
+
bootstrapContinuation = undefined;
|
|
903
|
+
rehydrationPhase = "step";
|
|
748
904
|
enqueueTurnPublication();
|
|
749
905
|
if (snapshot.meta.remotePublication?.mode === "turn-end") launchPublicationWorker();
|
|
750
906
|
} catch (error) {
|
|
751
|
-
|
|
907
|
+
recordDiagnostic(error instanceof Error ? error.message : String(error), "finalization", ctx);
|
|
908
|
+
ctx.ui.notify(`State Flow could not reconcile the final response: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
909
|
+
} finally {
|
|
910
|
+
responseAwaitingReconciliation = false;
|
|
911
|
+
terminalDraftIntercepted = false;
|
|
752
912
|
}
|
|
753
|
-
stagedFinal = undefined;
|
|
754
913
|
updateUi(ctx);
|
|
755
914
|
});
|
|
756
915
|
|
|
757
|
-
pi.on("agent_settled", (_event,
|
|
758
|
-
if (snapshot.config.enabled && retryQueued) abandonRun(ctx);
|
|
916
|
+
pi.on("agent_settled", (_event, _ctx) => {
|
|
759
917
|
if (snapshot.meta.remotePublication?.mode === "turn-end") launchPublicationWorker();
|
|
760
918
|
});
|
|
761
919
|
|
|
762
920
|
pi.on("session_start", (event, ctx) => {
|
|
763
921
|
rehydrationPhase = event.reason === "resume" ? "resume-bootstrap" : "new-bootstrap";
|
|
764
922
|
restoreActiveBranch(ctx, event.reason);
|
|
765
|
-
refreshArtifactInvalidations(ctx);
|
|
766
923
|
retryPendingPush(ctx);
|
|
767
924
|
if (snapshot.meta.remotePublication?.mode === "turn-end") launchPublicationWorker();
|
|
768
925
|
updateUi(ctx);
|
|
769
926
|
});
|
|
770
927
|
pi.on("session_tree", (_event, ctx) => {
|
|
771
928
|
restoreActiveBranch(ctx);
|
|
772
|
-
refreshArtifactInvalidations(ctx);
|
|
773
929
|
});
|
|
774
930
|
}
|