@llblab/pi-kit 0.10.8 → 0.11.1
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 +1 -1
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +14 -10
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +9 -2
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +19 -0
- package/node_modules/@llblab/pi-state-flow/README.md +52 -261
- package/node_modules/@llblab/pi-state-flow/docs/README.md +6 -1
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +47 -26
- package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +97 -0
- package/node_modules/@llblab/pi-state-flow/docs/filesystem-recovery.md +35 -0
- package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +47 -0
- package/node_modules/@llblab/pi-state-flow/docs/performance.md +459 -0
- package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +35 -3
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +142 -0
- package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +19 -3
- package/node_modules/@llblab/pi-state-flow/lib/compaction.ts +74 -0
- package/node_modules/@llblab/pi-state-flow/lib/context.ts +12 -12
- package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +4 -2
- package/node_modules/@llblab/pi-state-flow/lib/discovery.ts +21 -5
- package/node_modules/@llblab/pi-state-flow/lib/durable.ts +20 -5
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +146 -37
- package/node_modules/@llblab/pi-state-flow/lib/git.ts +142 -42
- package/node_modules/@llblab/pi-state-flow/lib/publication.ts +80 -27
- package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +157 -20
- package/node_modules/@llblab/pi-state-flow/lib/status.ts +7 -12
- package/node_modules/@llblab/pi-state-flow/lib/storage.ts +2 -1
- package/node_modules/@llblab/pi-state-flow/lib/transition.ts +2 -3
- package/node_modules/@llblab/pi-state-flow/package.json +5 -4
- package/package.json +2 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
1
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { StringEnum, Type } from "@earendil-works/pi-ai";
|
|
4
4
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
@@ -10,8 +10,9 @@ import { loadStateFlowConfig } from "./config.ts";
|
|
|
10
10
|
import { createStateFlowTelegramAdapter, type StateFlowTelegramControlResult, type StateFlowTelegramLoader } from "./telegram.ts";
|
|
11
11
|
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
12
12
|
import { SkillReadTracker } from "./skills.ts";
|
|
13
|
-
import { emptySnapshot, migrationFailure, persistableSnapshot, type Snapshot } from "./snapshot.ts";
|
|
14
|
-
import {
|
|
13
|
+
import { emptySnapshot, migrationFailure, persistableSnapshot, RevisionUnavailableError, type Snapshot } from "./snapshot.ts";
|
|
14
|
+
import { readNativeSessionHeader } from "./continuation.ts";
|
|
15
|
+
import { MissingSessionRuntimeError, TemporalRuntime, type RuntimePublication } from "./runtime.ts";
|
|
15
16
|
import { emptyState, overlayStates, projectStateForModel, type AtomicScopePatches, type MaterializedState, type ScopedStates, type StateScope } from "./state.ts";
|
|
16
17
|
import { commitScopedTransition, stageAtomicScopePatches, stageScopedTransition, validateFinalEligibility, type StagedScopedTransition } from "./transition.ts";
|
|
17
18
|
import { discoverSnapshotData, hasPriorConversation, isNewSession, SNAPSHOT_ENTRY_TYPE } from "./session.ts";
|
|
@@ -33,12 +34,13 @@ import {
|
|
|
33
34
|
} from "./durable.ts";
|
|
34
35
|
import { projectRecentTransitionsWithLimit, RECENT_TRANSITION_LIMIT } from "./history.ts";
|
|
35
36
|
import { appendStateFlowDiagnostic, projectDiagnosticContent, stateFlowLogPath, type StateFlowDiagnosticCategory } from "./logging.ts";
|
|
36
|
-
import { isGitCommitAncestor, pushGitCommit, resolveGitPushDestination } from "./git.ts";
|
|
37
|
+
import { isGitCommitAncestor, pushGitCommit, pushGitTarget, resolveGitPushDestination } from "./git.ts";
|
|
37
38
|
import {
|
|
38
39
|
ORDINARY_ARTIFACT_COMPILER,
|
|
39
40
|
planArtifactInvalidation,
|
|
40
41
|
type ArtifactInvalidationRequest,
|
|
41
42
|
} from "./artifact.ts";
|
|
43
|
+
import { planStateFlowCompaction, stateFlowCompactionResult, type StateFlowCompactionPlan } from "./compaction.ts";
|
|
42
44
|
|
|
43
45
|
export interface StateFlowExtensionOptions {
|
|
44
46
|
agentDir?: string;
|
|
@@ -52,6 +54,7 @@ export const PATCH_STATE_TOOL_NAME = "patch_state";
|
|
|
52
54
|
export const READ_STATE_TOOL_NAME = "read_state";
|
|
53
55
|
export const MAX_FALLBACK_ATTEMPTS: number = 2;
|
|
54
56
|
const PASSIVE_STOP_ENTRY_TYPE = "state-flow-passive-stop";
|
|
57
|
+
const PUBLICATION_SHUTDOWN_WAIT_MS = 2_000;
|
|
55
58
|
|
|
56
59
|
/** Keep a failed tool invocation visually separated from its rendered error without changing error semantics. */
|
|
57
60
|
function separatedFailure(error: unknown): Error {
|
|
@@ -66,11 +69,17 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
66
69
|
let scopeStates: ScopedStates = { global: emptyState(), cwd: emptyState(), session: emptyState() };
|
|
67
70
|
let branchHasSnapshot = false;
|
|
68
71
|
let branchStartsWithoutRuntime = false;
|
|
72
|
+
let forkInitialization = false;
|
|
69
73
|
let terminalEligible = false;
|
|
70
74
|
let resolutionPending = false;
|
|
71
75
|
let fallbackAttempts = 0;
|
|
72
76
|
let fallbackFailureReported = false;
|
|
73
77
|
let responseAwaitingReconciliation = false;
|
|
78
|
+
let completedRunAccepted = false;
|
|
79
|
+
let compactionPlan: StateFlowCompactionPlan | undefined;
|
|
80
|
+
let compactionInFlight = false;
|
|
81
|
+
let compactionStopped = false;
|
|
82
|
+
const compactionMarker = `state-flow-boundary:${randomUUID()}`;
|
|
74
83
|
let passiveContinuation: PassiveContinuation | undefined;
|
|
75
84
|
let bootstrapContinuation: PassiveContinuation | undefined;
|
|
76
85
|
let artifactRefreshPending = false;
|
|
@@ -80,7 +89,9 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
80
89
|
let pendingPublication: PendingPublicationDiagnostic | undefined;
|
|
81
90
|
let rehydrationPhase: RehydrationPhase | undefined;
|
|
82
91
|
let turnPublicationTarget: string | undefined;
|
|
83
|
-
const activePublicationWorkers = new
|
|
92
|
+
const activePublicationWorkers = new Map<string, { controller: AbortController; done: Promise<void> }>();
|
|
93
|
+
let publicationStopped = false;
|
|
94
|
+
let publicationShutdown: Promise<void> | undefined;
|
|
84
95
|
const repositoryRoot = resolve(options.repositoryRoot ?? config.directory);
|
|
85
96
|
const skillReads = new SkillReadTracker();
|
|
86
97
|
const artifactReads = new ArtifactReadTracker();
|
|
@@ -131,17 +142,24 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
131
142
|
fallbackAttempts = 0;
|
|
132
143
|
fallbackFailureReported = false;
|
|
133
144
|
responseAwaitingReconciliation = false;
|
|
145
|
+
completedRunAccepted = false;
|
|
146
|
+
compactionPlan = undefined;
|
|
147
|
+
compactionInFlight = false;
|
|
134
148
|
runAnchorTimestamp = undefined;
|
|
135
149
|
skillReads.clear();
|
|
136
150
|
artifactReads.clear();
|
|
137
151
|
}
|
|
138
152
|
|
|
139
|
-
function
|
|
153
|
+
function passiveStopBoundary(ctx: ExtensionContext): { at: number; from?: number } | undefined {
|
|
140
154
|
for (const entry of [...ctx.sessionManager.getBranch()].reverse()) {
|
|
141
155
|
try {
|
|
142
156
|
if (entry?.type !== "custom" || entry.customType !== PASSIVE_STOP_ENTRY_TYPE) continue;
|
|
143
|
-
const at = (entry.data as { at?: unknown } | undefined)
|
|
144
|
-
if (
|
|
157
|
+
const { at, from, reset, owner } = (entry.data as { at?: unknown; from?: unknown; reset?: unknown; owner?: unknown } | undefined) ?? {};
|
|
158
|
+
if (reset === true && owner === ctx.sessionManager.getSessionId()) return undefined;
|
|
159
|
+
if (typeof at === "number" && Number.isSafeInteger(at) && at >= 0) return {
|
|
160
|
+
at,
|
|
161
|
+
...(typeof from === "number" && Number.isSafeInteger(from) && from >= 0 ? { from } : {}),
|
|
162
|
+
};
|
|
145
163
|
} catch {
|
|
146
164
|
// A hostile unrelated branch entry cannot manufacture or suppress a valid marker.
|
|
147
165
|
}
|
|
@@ -167,12 +185,12 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
167
185
|
return;
|
|
168
186
|
}
|
|
169
187
|
try {
|
|
170
|
-
const discovery = globalMarkdown.refresh();
|
|
188
|
+
const discovery = globalMarkdown.refresh(Object.keys(scopeStates.global.artifacts));
|
|
171
189
|
const plan = planArtifactInvalidation(
|
|
172
190
|
discovery.sources,
|
|
173
191
|
scopeStates.global.artifacts,
|
|
174
192
|
ORDINARY_ARTIFACT_COMPILER,
|
|
175
|
-
{},
|
|
193
|
+
{ removed: discovery.removed },
|
|
176
194
|
runtime?.artifactProvenance("global") ?? {},
|
|
177
195
|
);
|
|
178
196
|
artifactInvalidations = structuredClone(plan.requiresCompilation);
|
|
@@ -211,9 +229,8 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
211
229
|
}
|
|
212
230
|
|
|
213
231
|
function assistantToolBatch(ctx: ExtensionContext, toolCallId: string): string[] | undefined {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const entry = branch[index] as { type?: unknown; message?: { role?: unknown; content?: unknown } };
|
|
232
|
+
for (let cursor = ctx.sessionManager.getLeafEntry(); cursor; cursor = cursor.parentId ? ctx.sessionManager.getEntry(cursor.parentId) : undefined) {
|
|
233
|
+
const entry = cursor as { type?: unknown; message?: { role?: unknown; content?: unknown } };
|
|
217
234
|
if (entry.type !== "message" || entry.message?.role !== "assistant" || !Array.isArray(entry.message.content)) continue;
|
|
218
235
|
const calls = entry.message.content.filter((block): block is { type: "toolCall"; id: string; name: string } => {
|
|
219
236
|
return typeof block === "object" && block !== null
|
|
@@ -308,6 +325,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
308
325
|
}
|
|
309
326
|
|
|
310
327
|
function launchPublicationWorker(): void {
|
|
328
|
+
if (publicationStopped) return;
|
|
311
329
|
let destination: ReturnType<typeof resolveGitPushDestination>;
|
|
312
330
|
try {
|
|
313
331
|
destination = resolveGitPushDestination(repositoryRoot);
|
|
@@ -328,17 +346,14 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
328
346
|
return;
|
|
329
347
|
}
|
|
330
348
|
if (!lease) return;
|
|
331
|
-
|
|
332
|
-
|
|
349
|
+
const controller = new AbortController();
|
|
350
|
+
const done = runPublicationWorker(
|
|
333
351
|
queued,
|
|
334
|
-
({ target }) =>
|
|
335
|
-
execFile("git", ["-C", repositoryRoot, "push", destination.remote, `${target}:${destination.ref}`], {
|
|
336
|
-
env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GCM_INTERACTIVE: "never" },
|
|
337
|
-
}, (error) => error ? reject(error) : resolve());
|
|
338
|
-
}),
|
|
352
|
+
({ target }) => pushGitTarget(repositoryRoot, destination, target, controller.signal),
|
|
339
353
|
() => loadPublicationQueue(path) ?? queued,
|
|
340
354
|
(ancestor, descendant) => isGitCommitAncestor(repositoryRoot, ancestor, descendant),
|
|
341
355
|
).then((result) => {
|
|
356
|
+
if (publicationStopped) return; // Late outcomes belong to an unconfirmed queue, not the replacement generation.
|
|
342
357
|
const current = loadPublicationQueue(path);
|
|
343
358
|
if (!current) return;
|
|
344
359
|
if (result.next === undefined) {
|
|
@@ -352,13 +367,33 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
352
367
|
// Queue/CAS truth remains durable; status and a later activation expose retry.
|
|
353
368
|
}).finally(() => {
|
|
354
369
|
activePublicationWorkers.delete(path);
|
|
355
|
-
lease.release();
|
|
356
370
|
try {
|
|
357
|
-
|
|
371
|
+
lease.release();
|
|
372
|
+
if (!publicationStopped && loadPublicationQueue(path)?.status === "pending") launchPublicationWorker();
|
|
358
373
|
} catch {
|
|
359
|
-
//
|
|
374
|
+
// Failed lease cleanup or malformed persistence stays inert until retry or repair.
|
|
360
375
|
}
|
|
361
376
|
});
|
|
377
|
+
activePublicationWorkers.set(path, { controller, done });
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function shutdownPublicationWorkers(ctx: ExtensionContext): Promise<void> {
|
|
381
|
+
publicationStopped = true;
|
|
382
|
+
return publicationShutdown ??= (async () => {
|
|
383
|
+
const workers = [...activePublicationWorkers.values()];
|
|
384
|
+
for (const { controller } of workers) controller.abort();
|
|
385
|
+
if (workers.length === 0) return;
|
|
386
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
387
|
+
try {
|
|
388
|
+
const completed = await Promise.race([
|
|
389
|
+
Promise.all(workers.map(({ done }) => done)).then(() => true),
|
|
390
|
+
new Promise<false>((resolve) => { timeout = setTimeout(() => resolve(false), PUBLICATION_SHUTDOWN_WAIT_MS); }),
|
|
391
|
+
]);
|
|
392
|
+
if (!completed) ctx.ui.notify(`State Flow push cleanup is unconfirmed after ${PUBLICATION_SHUTDOWN_WAIT_MS}ms; worker leases remain held until child exit.`, "warning");
|
|
393
|
+
} finally {
|
|
394
|
+
clearTimeout(timeout);
|
|
395
|
+
}
|
|
396
|
+
})();
|
|
362
397
|
}
|
|
363
398
|
|
|
364
399
|
function retryPendingPush(ctx: ExtensionContext): void {
|
|
@@ -409,12 +444,13 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
409
444
|
artifactFreshnessError = "durable global artifact registry is unavailable";
|
|
410
445
|
} else {
|
|
411
446
|
try {
|
|
412
|
-
const discovery = globalMarkdown.refresh();
|
|
447
|
+
const discovery = globalMarkdown.refresh(Object.keys(diagnosticStates.global.artifacts));
|
|
448
|
+
artifactFreshnessError = discovery.unavailable;
|
|
413
449
|
const plan = planArtifactInvalidation(
|
|
414
450
|
discovery.sources,
|
|
415
451
|
diagnosticStates.global.artifacts,
|
|
416
452
|
ORDINARY_ARTIFACT_COMPILER,
|
|
417
|
-
{},
|
|
453
|
+
{ removed: discovery.removed },
|
|
418
454
|
runtime?.artifactProvenance("global") ?? {},
|
|
419
455
|
);
|
|
420
456
|
staleArtifacts = [
|
|
@@ -456,6 +492,34 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
456
492
|
};
|
|
457
493
|
}
|
|
458
494
|
|
|
495
|
+
function prepareBranchRestore(ctx: ExtensionContext, revision: string, legacy?: Snapshot): { snapshot: Snapshot; restore: () => Snapshot } {
|
|
496
|
+
if (!forkInitialization) {
|
|
497
|
+
try {
|
|
498
|
+
return runtime!.prepareRestore(revision, legacy);
|
|
499
|
+
} catch (error) {
|
|
500
|
+
if (error instanceof MissingSessionRuntimeError && typeof ctx.sessionManager.getHeader()?.parentSession === "string") {
|
|
501
|
+
throw new RevisionUnavailableError("State Flow checkpoint has no child-owned runtime; select a child checkpoint or resume the parent");
|
|
502
|
+
}
|
|
503
|
+
throw error;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
const file = ctx.sessionManager.getHeader()?.parentSession;
|
|
507
|
+
if (typeof file !== "string" || !isAbsolute(file)) throw new Error("State Flow fork requires a persisted native parent session");
|
|
508
|
+
const parent = readNativeSessionHeader(file);
|
|
509
|
+
if (parent.cwd !== resolve(ctx.cwd)) throw new Error("State Flow fork parent CWD identity mismatch");
|
|
510
|
+
const source = resolveSessionAddress(parent.file, parent.id, parent.timestamp);
|
|
511
|
+
const prepared = runtime!.prepareFork(source, revision);
|
|
512
|
+
return { snapshot: prepared.snapshot, restore: () => {
|
|
513
|
+
const accepted = prepared.fork();
|
|
514
|
+
snapshot = accepted.snapshot;
|
|
515
|
+
recordPolicyPublication(accepted.publication, ctx);
|
|
516
|
+
// Copied Stop markers belong to the parent, including after a child reload/resume.
|
|
517
|
+
pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, { reset: true, owner: ctx.sessionManager.getSessionId() });
|
|
518
|
+
forkInitialization = false;
|
|
519
|
+
return snapshot;
|
|
520
|
+
} };
|
|
521
|
+
}
|
|
522
|
+
|
|
459
523
|
function restoreActiveBranch(ctx: ExtensionContext, sessionStartReason?: unknown): void {
|
|
460
524
|
clearRunTransient();
|
|
461
525
|
passiveContinuation = undefined;
|
|
@@ -471,11 +535,22 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
471
535
|
pendingPublication = undefined;
|
|
472
536
|
branchHasSnapshot = false;
|
|
473
537
|
branchStartsWithoutRuntime = false;
|
|
538
|
+
forkInitialization = sessionStartReason === "fork";
|
|
474
539
|
try {
|
|
475
540
|
const branch = ctx.sessionManager.getBranch();
|
|
476
541
|
const discovery = discoverSnapshotData(branch);
|
|
477
|
-
|
|
478
|
-
|
|
542
|
+
let restoreSelected: (() => Snapshot) | undefined;
|
|
543
|
+
const recovery = recoverSnapshot(discovery.candidates, (revision, legacy) => {
|
|
544
|
+
try {
|
|
545
|
+
const prepared = prepareBranchRestore(ctx, revision, legacy);
|
|
546
|
+
restoreSelected = prepared.restore;
|
|
547
|
+
return prepared.snapshot;
|
|
548
|
+
} catch (error) {
|
|
549
|
+
// A failed source proof never licenses an older/empty private copy.
|
|
550
|
+
if (forkInitialization) throw new RevisionUnavailableError(`Cannot copy State Flow fork source: ${error instanceof Error ? error.message : String(error)}`);
|
|
551
|
+
throw error;
|
|
552
|
+
}
|
|
553
|
+
});
|
|
479
554
|
branchStartsWithoutRuntime = recovery.disabledMarker === true
|
|
480
555
|
|| (discovery.candidates.length === 0 && discovery.errors.length === 0);
|
|
481
556
|
const skipped = discovery.errors.length + recovery.skipped.length;
|
|
@@ -486,7 +561,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
486
561
|
snapshot = recovery.snapshot;
|
|
487
562
|
if (branchHasSnapshot && snapshot.meta.durableBase) {
|
|
488
563
|
const selectedRevision = snapshot.meta.durableBase;
|
|
489
|
-
snapshot =
|
|
564
|
+
snapshot = restoreSelected ? restoreSelected() : prepareBranchRestore(ctx, selectedRevision, snapshot).restore();
|
|
490
565
|
if (snapshot.meta.durableBase !== selectedRevision) persist();
|
|
491
566
|
} else if (branchHasSnapshot && snapshot.config.enabled) {
|
|
492
567
|
const publication = runtime.initialize(snapshot, true);
|
|
@@ -540,11 +615,12 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
540
615
|
ctx.ui.notify(`State Flow restored disabled: ${snapshot.meta.validation.error}`, "error");
|
|
541
616
|
}
|
|
542
617
|
if (retainsPhysicalSessionProjection(sessionStartReason) && runtime?.view) {
|
|
543
|
-
const
|
|
544
|
-
if (
|
|
618
|
+
const boundary = passiveStopBoundary(ctx);
|
|
619
|
+
if (boundary !== undefined) {
|
|
545
620
|
const continuation = createPassiveContinuation(
|
|
546
621
|
projectStateForModel(overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session)),
|
|
547
|
-
|
|
622
|
+
boundary.at,
|
|
623
|
+
boundary.from,
|
|
548
624
|
);
|
|
549
625
|
if (!snapshot.config.enabled) passiveContinuation = continuation;
|
|
550
626
|
else if (snapshot.meta.bootstrap) bootstrapContinuation = continuation;
|
|
@@ -776,7 +852,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
776
852
|
const bootstrap = (!branchHasSnapshot || !snapshot.config.enabled)
|
|
777
853
|
&& (hasPriorConversation(branch) || previousPassiveContinuation !== undefined);
|
|
778
854
|
if (!runtime.view && snapshot.meta.durableBase) {
|
|
779
|
-
snapshot =
|
|
855
|
+
snapshot = prepareBranchRestore(ctx, snapshot.meta.durableBase, snapshot).restore();
|
|
780
856
|
setPendingPublication(snapshot.meta.pendingPublication);
|
|
781
857
|
branchHasSnapshot = true;
|
|
782
858
|
}
|
|
@@ -855,6 +931,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
855
931
|
? createPassiveContinuation(
|
|
856
932
|
projectStateForModel(overlayStates(exitStates.global, exitStates.cwd, exitStates.session)),
|
|
857
933
|
stoppedAt,
|
|
934
|
+
ctx.isIdle() ? undefined : runAnchorTimestamp,
|
|
858
935
|
)
|
|
859
936
|
: undefined;
|
|
860
937
|
const retainedHandoff = exitHandoff ?? (!current.config.enabled ? passiveContinuation : undefined);
|
|
@@ -873,7 +950,10 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
873
950
|
artifactInvalidations = [];
|
|
874
951
|
artifactReads.setCandidates([]);
|
|
875
952
|
artifactRefreshPending = false;
|
|
876
|
-
if (exitHandoff) pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, {
|
|
953
|
+
if (exitHandoff) pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, {
|
|
954
|
+
at: stoppedAt,
|
|
955
|
+
...(exitHandoff.activeRunStartedAt === undefined ? {} : { from: exitHandoff.activeRunStartedAt }),
|
|
956
|
+
});
|
|
877
957
|
syncStateFlowTools();
|
|
878
958
|
persist();
|
|
879
959
|
updateUi(ctx);
|
|
@@ -929,10 +1009,11 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
929
1009
|
fallbackAttempts = 0;
|
|
930
1010
|
fallbackFailureReported = false;
|
|
931
1011
|
responseAwaitingReconciliation = false;
|
|
1012
|
+
completedRunAccepted = false;
|
|
932
1013
|
const rotatesRun = snapshot.meta.specification !== undefined;
|
|
933
1014
|
if (rotatesRun && rehydrationPhase !== "new-bootstrap" && rehydrationPhase !== "resume-bootstrap") rehydrationPhase = "step";
|
|
934
1015
|
if (prepareRun(snapshot, event.prompt)) {
|
|
935
|
-
|
|
1016
|
+
runAnchorTimestamp = undefined;
|
|
936
1017
|
persist();
|
|
937
1018
|
}
|
|
938
1019
|
return {
|
|
@@ -945,7 +1026,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
945
1026
|
return { messages: passiveContinuationMessages(event.messages as AgentMessage[], passiveContinuation) };
|
|
946
1027
|
}
|
|
947
1028
|
if (!snapshot.config.enabled || snapshot.meta.specification === undefined) return;
|
|
948
|
-
const effectiveState =
|
|
1029
|
+
const effectiveState = overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session);
|
|
949
1030
|
const invalidations = artifactInvalidations.map(({ path, reason }) => ({ path, reason }));
|
|
950
1031
|
const recentTransitions = projectRecentTransitionsWithLimit(
|
|
951
1032
|
RECENT_TRANSITION_LIMIT,
|
|
@@ -1010,7 +1091,10 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1010
1091
|
});
|
|
1011
1092
|
|
|
1012
1093
|
pi.on("message_end", (event, ctx): any => {
|
|
1013
|
-
if (!snapshot.config.enabled
|
|
1094
|
+
if (!snapshot.config.enabled) return;
|
|
1095
|
+
// Capture the first native user boundary even during bootstrap; steering keeps that anchor.
|
|
1096
|
+
if (event.message.role === "user" && runAnchorTimestamp === undefined) runAnchorTimestamp = event.message.timestamp;
|
|
1097
|
+
if (event.message.role !== "assistant") return;
|
|
1014
1098
|
const message = event.message as unknown as { role: "assistant"; stopReason?: string; content?: unknown };
|
|
1015
1099
|
if (message.stopReason === "aborted" || assistantToolCallCount(message.content) > 0 || message.stopReason === "toolUse") {
|
|
1016
1100
|
responseAwaitingReconciliation = false;
|
|
@@ -1036,6 +1120,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1036
1120
|
});
|
|
1037
1121
|
|
|
1038
1122
|
pi.on("turn_end", (event, ctx) => {
|
|
1123
|
+
const wasBootstrap = snapshot.meta.bootstrap === true;
|
|
1039
1124
|
if (!snapshot.config.enabled || !responseAwaitingReconciliation) {
|
|
1040
1125
|
updateUi(ctx);
|
|
1041
1126
|
return;
|
|
@@ -1050,6 +1135,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1050
1135
|
rehydrationPhase = "step";
|
|
1051
1136
|
enqueueTurnPublication(ctx);
|
|
1052
1137
|
if (snapshot.meta.remotePublication?.mode === "turn-end") launchPublicationWorker();
|
|
1138
|
+
completedRunAccepted = !wasBootstrap;
|
|
1053
1139
|
} catch (error) {
|
|
1054
1140
|
recordDiagnostic(error instanceof Error ? error.message : String(error), "finalization", ctx);
|
|
1055
1141
|
ctx.ui.notify(responseCommitted
|
|
@@ -1061,12 +1147,32 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1061
1147
|
updateUi(ctx);
|
|
1062
1148
|
});
|
|
1063
1149
|
|
|
1150
|
+
pi.on("session_before_compact", (event) => {
|
|
1151
|
+
if (!compactionPlan) return;
|
|
1152
|
+
if (compactionStopped && event.reason === "manual" && event.customInstructions === compactionMarker) return { cancel: true };
|
|
1153
|
+
const result = stateFlowCompactionResult(compactionPlan, compactionMarker, event);
|
|
1154
|
+
if (result === undefined || "cancel" in result) return result;
|
|
1155
|
+
return { compaction: result };
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1064
1158
|
pi.on("agent_settled", (_event, ctx) => {
|
|
1065
1159
|
if (telegramStartPending && !snapshot.config.enabled) {
|
|
1066
1160
|
telegramStartPending = false;
|
|
1067
1161
|
startStateFlow(ctx);
|
|
1068
1162
|
}
|
|
1069
1163
|
if (snapshot.meta.remotePublication?.mode === "turn-end") launchPublicationWorker();
|
|
1164
|
+
if (!completedRunAccepted || compactionStopped || !snapshot.config.enabled || snapshot.meta.bootstrap || resolutionPending
|
|
1165
|
+
|| compactionInFlight || !ctx.isIdle() || ctx.hasPendingMessages() || !snapshot.meta.durableBase) return;
|
|
1166
|
+
completedRunAccepted = false;
|
|
1167
|
+
const plan = planStateFlowCompaction(ctx.sessionManager.buildContextEntries(), snapshot.meta.durableBase, snapshot.meta.step);
|
|
1168
|
+
if (!plan) return;
|
|
1169
|
+
compactionPlan = plan;
|
|
1170
|
+
compactionInFlight = true;
|
|
1171
|
+
ctx.compact({
|
|
1172
|
+
customInstructions: compactionMarker,
|
|
1173
|
+
onComplete: () => { compactionPlan = undefined; compactionInFlight = false; },
|
|
1174
|
+
onError: () => { compactionPlan = undefined; compactionInFlight = false; },
|
|
1175
|
+
});
|
|
1070
1176
|
});
|
|
1071
1177
|
|
|
1072
1178
|
pi.on("session_start", (event, ctx) => {
|
|
@@ -1080,8 +1186,11 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1080
1186
|
pi.on("session_tree", (_event, ctx) => {
|
|
1081
1187
|
restoreActiveBranch(ctx);
|
|
1082
1188
|
});
|
|
1083
|
-
pi.on("session_shutdown", () => {
|
|
1189
|
+
pi.on("session_shutdown", (_event, ctx) => {
|
|
1084
1190
|
telegramStartPending = false;
|
|
1191
|
+
compactionStopped = true;
|
|
1192
|
+
completedRunAccepted = false;
|
|
1085
1193
|
telegram.dispose();
|
|
1194
|
+
return shutdownPublicationWorkers(ctx);
|
|
1086
1195
|
});
|
|
1087
1196
|
}
|