@osolmaz/pi-workflows 0.13.2 → 0.13.3
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/README.md +5 -3
- package/dist/builtins/autodoc.workflow.d.ts +5 -5
- package/dist/builtins/autoimplement.workflow.d.ts +65 -61
- package/dist/builtins/autoimplement.workflow.js +47 -8
- package/dist/builtins/autoimplement.workflow.js.map +1 -1
- package/dist/builtins/autoplan.workflow.d.ts +5 -5
- package/dist/builtins/catalog.js +1 -1
- package/dist/builtins/change-verification.workflow.d.ts +1 -1
- package/dist/builtins/plain-summary.workflow.d.ts +1 -1
- package/dist/builtins/plan-approval.workflow.d.ts +1 -1
- package/dist/builtins/plan-change.workflow.d.ts +23 -23
- package/dist/builtins/sanity-check.workflow.d.ts +3 -3
- package/dist/builtins/workspace-preparation.workflow.d.ts +1 -1
- package/dist/controllers/index.d.ts +2 -2
- package/dist/controllers/index.js.map +1 -1
- package/dist/controllers/sqlite.js +85 -23
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/controllers/types.d.ts +34 -0
- package/dist/controllers/workflow-engine-scheduler.d.ts +5 -1
- package/dist/controllers/workflow-engine-scheduler.js +83 -1
- package/dist/controllers/workflow-engine-scheduler.js.map +1 -1
- package/dist/controllers/workflows.d.ts +8 -1
- package/dist/controllers/workflows.js +36 -0
- package/dist/controllers/workflows.js.map +1 -1
- package/dist/extension/controller-host.d.ts +2 -1
- package/dist/extension/controller-host.js +1 -1
- package/dist/extension/controller-host.js.map +1 -1
- package/dist/extension/follow-up-coordinator.d.ts +27 -0
- package/dist/extension/follow-up-coordinator.js +131 -0
- package/dist/extension/follow-up-coordinator.js.map +1 -0
- package/dist/extension/index.d.ts +9 -0
- package/dist/extension/index.js +460 -75
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/session-events.d.ts +2 -3
- package/dist/extension/session-events.js +11 -10
- package/dist/extension/session-events.js.map +1 -1
- package/dist/extension/widget.js +9 -0
- package/dist/extension/widget.js.map +1 -1
- package/dist/state/database.d.ts +2 -1
- package/dist/state/database.js +11 -13
- package/dist/state/database.js.map +1 -1
- package/dist/state/json.js +6 -1
- package/dist/state/json.js.map +1 -1
- package/dist/state/mutation.d.ts +1 -1
- package/dist/state/mutation.js.map +1 -1
- package/dist/state/prune.d.ts +18 -0
- package/dist/state/prune.js +373 -0
- package/dist/state/prune.js.map +1 -0
- package/dist/state/schema.d.ts +1 -1
- package/dist/state/schema.js +116 -18
- package/dist/state/schema.js.map +1 -1
- package/dist/viewer/cli.d.ts +3 -1
- package/dist/viewer/cli.js +55 -4
- package/dist/viewer/cli.js.map +1 -1
- package/dist/viewer/render.js +19 -1
- package/dist/viewer/render.js.map +1 -1
- package/dist/workflows/composition.d.ts +2 -0
- package/dist/workflows/composition.js +15 -0
- package/dist/workflows/composition.js.map +1 -1
- package/dist/workflows/definition.d.ts +6 -3
- package/dist/workflows/definition.js +3 -0
- package/dist/workflows/definition.js.map +1 -1
- package/dist/workflows/engine.d.ts +6 -1
- package/dist/workflows/engine.js +301 -35
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/human-decision.d.ts +1 -0
- package/dist/workflows/human-decision.js +25 -9
- package/dist/workflows/human-decision.js.map +1 -1
- package/dist/workflows/index.d.ts +3 -1
- package/dist/workflows/index.js +2 -0
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/json-patch.d.ts +47 -0
- package/dist/workflows/json-patch.js +298 -0
- package/dist/workflows/json-patch.js.map +1 -0
- package/dist/workflows/schema.js +29 -1
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/settings.d.ts +128 -0
- package/dist/workflows/settings.js +199 -0
- package/dist/workflows/settings.js.map +1 -0
- package/dist/workflows/store.d.ts +75 -2
- package/dist/workflows/store.js +1349 -89
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/tool-input.d.ts +22 -0
- package/dist/workflows/tool-input.js +43 -0
- package/dist/workflows/tool-input.js.map +1 -1
- package/dist/workflows/types.d.ts +40 -5
- package/docs/2026-08-25-workflow-follow-ups.md +132 -0
- package/docs/2026-08-25-workflow-settings.md +169 -0
- package/docs/DEFERRED_TURNS.md +6 -0
- package/docs/DESIGN_PHILOSOPHY.md +2 -0
- package/docs/SQLITE_STATE.md +31 -18
- package/docs/WORKFLOW_COMPOSITION.md +15 -0
- package/docs/WORKFLOW_STEP_MESSAGES.md +4 -2
- package/docs/plans/2026-08-25-live-workflow-settings-plan.md +532 -0
- package/docs/plans/2026-08-25-workflow-run-storage-plan.md +67 -0
- package/docs/tui-viewer.md +7 -6
- package/docs/workflows.md +22 -1
- package/examples/workflows/live-settings.workflow.ts +93 -0
- package/herdr-plugin.toml +1 -1
- package/package.json +1 -1
- package/src/builtins/autoimplement.workflow.ts +56 -8
- package/src/builtins/catalog.ts +1 -1
- package/src/controllers/index.ts +6 -0
- package/src/controllers/sqlite.ts +129 -35
- package/src/controllers/types.ts +40 -0
- package/src/controllers/workflow-engine-scheduler.ts +103 -1
- package/src/controllers/workflows.ts +68 -0
- package/src/extension/controller-host.ts +6 -1
- package/src/extension/follow-up-coordinator.ts +151 -0
- package/src/extension/index.ts +538 -88
- package/src/extension/session-events.ts +15 -13
- package/src/extension/widget.ts +8 -0
- package/src/state/database.ts +12 -12
- package/src/state/json.ts +6 -1
- package/src/state/mutation.ts +4 -1
- package/src/state/prune.ts +502 -0
- package/src/state/schema.ts +116 -18
- package/src/viewer/cli.ts +52 -5
- package/src/viewer/render.ts +43 -1
- package/src/workflows/composition.ts +19 -0
- package/src/workflows/definition.ts +19 -5
- package/src/workflows/engine.ts +363 -35
- package/src/workflows/human-decision.ts +41 -11
- package/src/workflows/index.ts +43 -0
- package/src/workflows/json-patch.ts +375 -0
- package/src/workflows/schema.ts +31 -1
- package/src/workflows/settings.ts +430 -0
- package/src/workflows/store.ts +1977 -184
- package/src/workflows/tool-input.ts +58 -0
- package/src/workflows/types.ts +43 -4
package/dist/extension/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { SqliteControllerStore } from "../controllers/index.js";
|
|
|
5
5
|
import { canonicalJson } from "../state/json.js";
|
|
6
6
|
import { compositionMetadata } from "../workflows/composition.js";
|
|
7
7
|
import { humanDecisionChannelRequest } from "../workflows/decision-presentation.js";
|
|
8
|
-
import { WorkflowEngine } from "../workflows/engine.js";
|
|
8
|
+
import { WorkflowEngine, workflowIdentityMismatch } from "../workflows/engine.js";
|
|
9
9
|
import { BuiltinWorkflowRevisionChangedError, ClaimLostError, errorMessage, isClaimLostError, TimeoutError, } from "../workflows/errors.js";
|
|
10
10
|
import { HumanDecisionStore, createHumanDecisionAttemptId, validateHumanDecisionResponse, } from "../workflows/human-decision.js";
|
|
11
11
|
import { discoverWorkflows, resolveWorkflowRef } from "../workflows/loader.js";
|
|
@@ -16,6 +16,7 @@ import { PiDecisionChannel, TelegramDecisionChannel, audienceChannels, createTel
|
|
|
16
16
|
import { DeferredTurnCoordinator, } from "./deferred-turn-coordinator.js";
|
|
17
17
|
import { buildDeferredTurnContent, createDeferredTurnDescriptor, DEFERRED_TURN_MESSAGE_TYPE, deferredTurnMessageDetails, deferredTurnMessageId, deferredTurnSourceEventId, } from "./deferred-turn.js";
|
|
18
18
|
import { ConversationStepExecutor } from "./executor.js";
|
|
19
|
+
import { FollowUpCoordinator, findSettledPresentationEntries } from "./follow-up-coordinator.js";
|
|
19
20
|
import { HerdrWorkflowViewer, parseViewerPlacement, PIW_SHORTCUT, PIW_SHORTCUT_HINT, VIEWER_PLACEMENTS, } from "./herdr-viewer.js";
|
|
20
21
|
import { SessionRecorder } from "./recorder.js";
|
|
21
22
|
import { recoverAssistantStep, registerWorkflowAgentStepMessageRenderer, WORKFLOW_AGENT_STEP_MESSAGE_SCHEMA, WORKFLOW_AGENT_STEP_MESSAGE_TYPE, } from "./step-message.js";
|
|
@@ -122,6 +123,28 @@ export function parseWorkflowArgs(args) {
|
|
|
122
123
|
if (trimmed === "status") {
|
|
123
124
|
return { kind: "status" };
|
|
124
125
|
}
|
|
126
|
+
if (trimmed.startsWith("change-settings ")) {
|
|
127
|
+
const text = trimmed.slice("change-settings".length).trim();
|
|
128
|
+
try {
|
|
129
|
+
return { kind: "change-settings", patch: JSON.parse(text) };
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
throw new Error(`change-settings requires a JSON Patch array: ${error instanceof Error ? error.message : String(error)}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (trimmed.startsWith("queue-follow-up ")) {
|
|
136
|
+
const prompt = trimmed.slice("queue-follow-up".length).trim();
|
|
137
|
+
if (prompt.length === 0)
|
|
138
|
+
throw new Error("queue-follow-up requires a prompt");
|
|
139
|
+
return { kind: "queue-follow-up", prompt };
|
|
140
|
+
}
|
|
141
|
+
if (trimmed.startsWith("remove-follow-up ")) {
|
|
142
|
+
const followUpId = trimmed.slice("remove-follow-up".length).trim();
|
|
143
|
+
if (!/^follow-up-[a-f0-9]{40}$/u.test(followUpId)) {
|
|
144
|
+
throw new Error("remove-follow-up requires one valid follow-up id");
|
|
145
|
+
}
|
|
146
|
+
return { kind: "remove-follow-up", followUpId };
|
|
147
|
+
}
|
|
125
148
|
if (trimmed.startsWith("status ")) {
|
|
126
149
|
const runId = trimmed.slice("status".length).trim();
|
|
127
150
|
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/.test(runId)) {
|
|
@@ -177,7 +200,7 @@ export function parseWorkflowArgs(args) {
|
|
|
177
200
|
}
|
|
178
201
|
return { kind: "run", ref, input: rest.length > 0 ? { task: rest } : {} };
|
|
179
202
|
}
|
|
180
|
-
function workflowStateSummary(state) {
|
|
203
|
+
function workflowStateSummary(state, controls) {
|
|
181
204
|
const error = state.error === undefined
|
|
182
205
|
? undefined
|
|
183
206
|
: state.error.length <= MAX_STATUS_ERROR_CHARS
|
|
@@ -197,6 +220,32 @@ function workflowStateSummary(state) {
|
|
|
197
220
|
...(state.waitingOn !== undefined ? { waitingOn: state.waitingOn } : {}),
|
|
198
221
|
...(state.startedAt !== undefined ? { startedAt: state.startedAt } : {}),
|
|
199
222
|
...(state.finishedAt !== undefined ? { finishedAt: state.finishedAt } : {}),
|
|
223
|
+
...(controls !== undefined
|
|
224
|
+
? {
|
|
225
|
+
settingsScopes: (controls.settingsScopes ?? []).map((scope) => ({
|
|
226
|
+
scopeId: scope.scopeId,
|
|
227
|
+
mountPath: scope.mountPath,
|
|
228
|
+
invocation: scope.invocation,
|
|
229
|
+
changeNumber: scope.changeNumber,
|
|
230
|
+
settingsHash: scope.settingsHash,
|
|
231
|
+
activeRunId: scope.activeRunId,
|
|
232
|
+
})),
|
|
233
|
+
followUps: controls.followUpQueue === null || controls.followUpQueue === undefined
|
|
234
|
+
? null
|
|
235
|
+
: {
|
|
236
|
+
presentationState: controls.followUpQueue.presentationState,
|
|
237
|
+
items: controls.followUpQueue.followUps.map((followUp) => ({
|
|
238
|
+
followUpId: followUp.followUpId,
|
|
239
|
+
order: followUp.order,
|
|
240
|
+
state: followUp.state,
|
|
241
|
+
source: followUp.source,
|
|
242
|
+
...(followUp.sessionEntryId !== undefined
|
|
243
|
+
? { sessionEntryId: followUp.sessionEntryId }
|
|
244
|
+
: {}),
|
|
245
|
+
})),
|
|
246
|
+
},
|
|
247
|
+
}
|
|
248
|
+
: {}),
|
|
200
249
|
...(error !== undefined ? { error } : {}),
|
|
201
250
|
};
|
|
202
251
|
}
|
|
@@ -314,6 +363,7 @@ export default function piWorkflows(pi) {
|
|
|
314
363
|
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
315
364
|
},
|
|
316
365
|
}, idle);
|
|
366
|
+
void followUpCoordinator?.synchronize(ctx, activeRun !== null).catch(() => undefined);
|
|
317
367
|
}
|
|
318
368
|
catch {
|
|
319
369
|
// Delivery retries on the next poll. It never affects workflow execution.
|
|
@@ -346,6 +396,8 @@ export default function piWorkflows(pi) {
|
|
|
346
396
|
let runGeneration = 0;
|
|
347
397
|
let presentationAbort = null;
|
|
348
398
|
let presentationPending = null;
|
|
399
|
+
let followUpStore = null;
|
|
400
|
+
let followUpCoordinator = null;
|
|
349
401
|
let controllerHost;
|
|
350
402
|
let controllerContext = null;
|
|
351
403
|
const branchIntentResolution = (intentId) => {
|
|
@@ -377,6 +429,38 @@ export default function piWorkflows(pi) {
|
|
|
377
429
|
branchResolution: branchIntentResolution,
|
|
378
430
|
leaseMs: TURN_INTENT_DELIVERY_LEASE_MS,
|
|
379
431
|
});
|
|
432
|
+
const ensureFollowUpDelivery = () => {
|
|
433
|
+
if (followUpStore !== null && followUpCoordinator !== null)
|
|
434
|
+
return;
|
|
435
|
+
followUpStore?.close();
|
|
436
|
+
followUpStore = new WorkflowRunStore();
|
|
437
|
+
followUpCoordinator = new FollowUpCoordinator(followUpStore, `follow-up-${runnerId}`, (text) => pi.sendUserMessage(text));
|
|
438
|
+
};
|
|
439
|
+
const settlePresentationFromBranch = (ctx, runId, unavailableReason) => {
|
|
440
|
+
if (followUpStore === null ||
|
|
441
|
+
followUpStore.readFollowUpQueue(runId)?.presentationState !== "pending") {
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
const settled = findSettledPresentationEntries(ctx.sessionManager.getBranch(), runId);
|
|
445
|
+
if (settled !== undefined) {
|
|
446
|
+
followUpStore.settleFollowUpPresentation({
|
|
447
|
+
runId,
|
|
448
|
+
state: "settled",
|
|
449
|
+
presentationEntryId: settled.presentationEntryId,
|
|
450
|
+
assistantEntryId: settled.assistantEntryId,
|
|
451
|
+
});
|
|
452
|
+
return true;
|
|
453
|
+
}
|
|
454
|
+
if (unavailableReason !== undefined) {
|
|
455
|
+
followUpStore.settleFollowUpPresentation({
|
|
456
|
+
runId,
|
|
457
|
+
state: "unavailable",
|
|
458
|
+
reason: unavailableReason,
|
|
459
|
+
});
|
|
460
|
+
return true;
|
|
461
|
+
}
|
|
462
|
+
return false;
|
|
463
|
+
};
|
|
380
464
|
// UI updates are best-effort: a captured ctx becomes stale after session
|
|
381
465
|
// replacement or shutdown, and pi throws on any access (even `ctx.hasUI`).
|
|
382
466
|
// A workflow finishing right as the session goes away must not crash pi.
|
|
@@ -689,6 +773,7 @@ export default function piWorkflows(pi) {
|
|
|
689
773
|
return;
|
|
690
774
|
}
|
|
691
775
|
if (instructions === undefined || instructions.trim().length === 0) {
|
|
776
|
+
settlePresentationFromBranch(ctx, run.runId, "No result presentation was available");
|
|
692
777
|
if (run.abortProvenance !== undefined) {
|
|
693
778
|
makeRunTurnIntentEligible(ctx, run, state.status, "No result presentation was available");
|
|
694
779
|
}
|
|
@@ -699,13 +784,14 @@ export default function piWorkflows(pi) {
|
|
|
699
784
|
targetSessionId: originSessionId(ctx, run.runId),
|
|
700
785
|
resolution: "presentation",
|
|
701
786
|
send: (turnIntentId) => {
|
|
702
|
-
presentationPending = run.generation;
|
|
787
|
+
presentationPending = { generation: run.generation, runId: run.runId };
|
|
703
788
|
pi.sendMessage({
|
|
704
789
|
customType: PRESENTATION_MESSAGE_TYPE,
|
|
705
790
|
content: buildPresentationMessage(instructions, state),
|
|
706
791
|
display: false,
|
|
707
792
|
details: {
|
|
708
793
|
schema: PRESENTATION_MESSAGE_SCHEMA,
|
|
794
|
+
runId: run.runId,
|
|
709
795
|
...(turnIntentId === undefined ? {} : { turnIntentId }),
|
|
710
796
|
},
|
|
711
797
|
}, {
|
|
@@ -716,7 +802,7 @@ export default function piWorkflows(pi) {
|
|
|
716
802
|
}, ctx.isIdle() && systemTurnAbort === null);
|
|
717
803
|
}
|
|
718
804
|
catch (error) {
|
|
719
|
-
if (presentationPending === run.generation) {
|
|
805
|
+
if (presentationPending?.generation === run.generation) {
|
|
720
806
|
presentationPending = null;
|
|
721
807
|
}
|
|
722
808
|
if (error instanceof PresentationSupersededError ||
|
|
@@ -728,6 +814,7 @@ export default function piWorkflows(pi) {
|
|
|
728
814
|
? `timed out after ${PRESENTATION_TIMEOUT_MS}ms`
|
|
729
815
|
: errorMessage(error);
|
|
730
816
|
notify(ctx, `Could not present workflow result: ${message}`, "warning");
|
|
817
|
+
settlePresentationFromBranch(ctx, run.runId, message);
|
|
731
818
|
if (run.abortProvenance !== undefined) {
|
|
732
819
|
makeRunTurnIntentEligible(ctx, run, state.status, message);
|
|
733
820
|
}
|
|
@@ -876,6 +963,7 @@ export default function piWorkflows(pi) {
|
|
|
876
963
|
if (options.signal?.aborted) {
|
|
877
964
|
throw options.signal.reason ?? new Error("Workflow startup aborted");
|
|
878
965
|
}
|
|
966
|
+
ensureFollowUpDelivery();
|
|
879
967
|
if (activeRun) {
|
|
880
968
|
if (!options.quiet) {
|
|
881
969
|
notify(ctx, `A workflow is already running: ${activeRun.workflowName}. Use /workflow cancel first.`, "error");
|
|
@@ -1310,7 +1398,65 @@ export default function piWorkflows(pi) {
|
|
|
1310
1398
|
if (controllerHost !== undefined) {
|
|
1311
1399
|
return controllerHost;
|
|
1312
1400
|
}
|
|
1313
|
-
controllerHost = await PiControllerHost.create({
|
|
1401
|
+
controllerHost = await PiControllerHost.create({
|
|
1402
|
+
cwd: ctx.cwd,
|
|
1403
|
+
startChild,
|
|
1404
|
+
workflowControls: {
|
|
1405
|
+
changeSettings: async (request) => {
|
|
1406
|
+
const result = await changeWorkflowSettings(ctx, {
|
|
1407
|
+
runId: request.runId,
|
|
1408
|
+
...(request.scopeId !== undefined ? { scopeId: request.scopeId } : {}),
|
|
1409
|
+
...(request.expectedChangeNumber !== undefined
|
|
1410
|
+
? { expectedChangeNumber: request.expectedChangeNumber }
|
|
1411
|
+
: {}),
|
|
1412
|
+
patch: request.patch,
|
|
1413
|
+
requestId: request.actorRequestKey,
|
|
1414
|
+
actor: "controller",
|
|
1415
|
+
actorId: request.controllerResourceUid,
|
|
1416
|
+
source: "controller-request",
|
|
1417
|
+
});
|
|
1418
|
+
return {
|
|
1419
|
+
runId: request.runId,
|
|
1420
|
+
scopeId: String(result.details.scopeId),
|
|
1421
|
+
changeNumber: Number(result.details.changeNumber),
|
|
1422
|
+
adopted: result.details.adopted === true,
|
|
1423
|
+
};
|
|
1424
|
+
},
|
|
1425
|
+
queueFollowUp: async (request) => {
|
|
1426
|
+
const result = await queueWorkflowFollowUp(ctx, {
|
|
1427
|
+
runId: request.runId,
|
|
1428
|
+
prompt: request.prompt,
|
|
1429
|
+
requestId: request.actorRequestKey,
|
|
1430
|
+
actor: "controller",
|
|
1431
|
+
actorId: request.controllerResourceUid,
|
|
1432
|
+
source: "controller-request",
|
|
1433
|
+
});
|
|
1434
|
+
return {
|
|
1435
|
+
runId: request.runId,
|
|
1436
|
+
followUpId: String(result.details.followUpId),
|
|
1437
|
+
order: Number(result.details.order),
|
|
1438
|
+
state: "queued",
|
|
1439
|
+
adopted: result.details.adopted === true,
|
|
1440
|
+
};
|
|
1441
|
+
},
|
|
1442
|
+
removeFollowUp: async (request) => {
|
|
1443
|
+
const result = await removeWorkflowFollowUp(ctx, {
|
|
1444
|
+
runId: request.runId,
|
|
1445
|
+
followUpId: request.followUpId,
|
|
1446
|
+
actor: "controller",
|
|
1447
|
+
actorId: request.controllerResourceUid,
|
|
1448
|
+
source: "controller-request",
|
|
1449
|
+
});
|
|
1450
|
+
return {
|
|
1451
|
+
runId: request.runId,
|
|
1452
|
+
followUpId: String(result.details.followUpId),
|
|
1453
|
+
order: Number(result.details.order),
|
|
1454
|
+
state: String(result.details.state),
|
|
1455
|
+
adopted: false,
|
|
1456
|
+
};
|
|
1457
|
+
},
|
|
1458
|
+
},
|
|
1459
|
+
});
|
|
1314
1460
|
controllerHost?.start();
|
|
1315
1461
|
updateControllerStatus(ctx);
|
|
1316
1462
|
return controllerHost;
|
|
@@ -1574,7 +1720,7 @@ export default function piWorkflows(pi) {
|
|
|
1574
1720
|
const { state } = bundle;
|
|
1575
1721
|
return {
|
|
1576
1722
|
message: `Workflow ${state.workflowName} is ${state.status} (run ${state.runId}).`,
|
|
1577
|
-
details: workflowStateSummary(state),
|
|
1723
|
+
details: workflowStateSummary(state, bundle),
|
|
1578
1724
|
};
|
|
1579
1725
|
}
|
|
1580
1726
|
const state = activeRun?.lastState ?? widgetSource?.state;
|
|
@@ -1592,9 +1738,136 @@ export default function piWorkflows(pi) {
|
|
|
1592
1738
|
}
|
|
1593
1739
|
return {
|
|
1594
1740
|
message: `Workflow ${state.workflowName} is ${state.status} (run ${state.runId}).`,
|
|
1595
|
-
details: workflowStateSummary(state),
|
|
1741
|
+
details: workflowStateSummary(state, readWorkflowRun(state.runId) ?? undefined),
|
|
1596
1742
|
};
|
|
1597
1743
|
};
|
|
1744
|
+
const resolveMutableWorkflow = async (ctx, requestedRunId) => {
|
|
1745
|
+
const runId = requestedRunId ?? activeRun?.runId ?? lastWaitingRunId ?? undefined;
|
|
1746
|
+
if (runId === undefined) {
|
|
1747
|
+
throw new Error("No running, parked, or waiting workflow is available for changes.");
|
|
1748
|
+
}
|
|
1749
|
+
const loaded = readWorkflowRun(runId);
|
|
1750
|
+
if (loaded === null)
|
|
1751
|
+
throw new Error(`Workflow run not found: ${runId}`);
|
|
1752
|
+
if (originSessionId(ctx, runId) !== ctx.sessionManager.getSessionId()) {
|
|
1753
|
+
throw new Error("Workflow changes must come from the Pi session that owns the run.");
|
|
1754
|
+
}
|
|
1755
|
+
const source = loaded.state.workflowSource;
|
|
1756
|
+
if (source === undefined)
|
|
1757
|
+
throw new Error(`Workflow run ${runId} has no saved source`);
|
|
1758
|
+
const ref = source.kind === "builtin" ? `builtin:${source.id}` : source.path;
|
|
1759
|
+
const resolved = await resolveWorkflowRef(ref, { cwd: ctx.cwd }, builtinWorkflowCatalog);
|
|
1760
|
+
if (workflowIdentityMismatch(loaded.state, resolved.definition, resolved.source)) {
|
|
1761
|
+
throw new Error(`Workflow source changed since run ${runId} started`);
|
|
1762
|
+
}
|
|
1763
|
+
return { runId, workflow: resolved.definition, store: new WorkflowRunStore() };
|
|
1764
|
+
};
|
|
1765
|
+
const changeWorkflowSettings = async (ctx, options) => {
|
|
1766
|
+
const target = await resolveMutableWorkflow(ctx, options.runId);
|
|
1767
|
+
try {
|
|
1768
|
+
const scopes = target.store.listSettingsScopes(target.runId);
|
|
1769
|
+
const scopeId = options.scopeId ??
|
|
1770
|
+
(activeRun?.runId === target.runId ? activeRun.engine.activeSettingsScopeId : undefined) ??
|
|
1771
|
+
(scopes.length === 1 ? scopes[0]?.scopeId : undefined);
|
|
1772
|
+
if (scopeId === undefined) {
|
|
1773
|
+
throw new Error("Specify scopeId because this workflow has more than one settings scope.");
|
|
1774
|
+
}
|
|
1775
|
+
const scope = target.store.getSettingsScope(scopeId);
|
|
1776
|
+
if (scope === undefined || scope.activeRunId !== target.runId) {
|
|
1777
|
+
throw new Error(`Workflow settings scope not found for run ${target.runId}: ${scopeId}`);
|
|
1778
|
+
}
|
|
1779
|
+
const definition = scope.mountPath === ""
|
|
1780
|
+
? target.workflow.settings
|
|
1781
|
+
: compositionMetadata(target.workflow)?.scopes[scope.mountPath]?.settings;
|
|
1782
|
+
if (definition === undefined) {
|
|
1783
|
+
throw new Error(`Workflow scope ${scopeId} does not declare editable settings.`);
|
|
1784
|
+
}
|
|
1785
|
+
const result = await target.store.changeSettings(definition, {
|
|
1786
|
+
runId: target.runId,
|
|
1787
|
+
scopeId,
|
|
1788
|
+
requestId: options.requestId,
|
|
1789
|
+
...(options.expectedChangeNumber !== undefined
|
|
1790
|
+
? { expectedChangeNumber: options.expectedChangeNumber }
|
|
1791
|
+
: {}),
|
|
1792
|
+
actor: {
|
|
1793
|
+
type: options.actor,
|
|
1794
|
+
id: options.actorId ?? ctx.sessionManager.getSessionId(),
|
|
1795
|
+
},
|
|
1796
|
+
source: options.source,
|
|
1797
|
+
patch: options.patch,
|
|
1798
|
+
});
|
|
1799
|
+
renderWidget(ctx);
|
|
1800
|
+
return {
|
|
1801
|
+
message: `Workflow settings changed to number ${result.scope.changeNumber}.`,
|
|
1802
|
+
details: {
|
|
1803
|
+
action: "change-settings",
|
|
1804
|
+
runId: target.runId,
|
|
1805
|
+
scopeId,
|
|
1806
|
+
changeNumber: result.scope.changeNumber,
|
|
1807
|
+
adopted: result.adopted,
|
|
1808
|
+
},
|
|
1809
|
+
};
|
|
1810
|
+
}
|
|
1811
|
+
finally {
|
|
1812
|
+
target.store.close();
|
|
1813
|
+
}
|
|
1814
|
+
};
|
|
1815
|
+
const queueWorkflowFollowUp = async (ctx, options) => {
|
|
1816
|
+
const target = await resolveMutableWorkflow(ctx, options.runId);
|
|
1817
|
+
try {
|
|
1818
|
+
const result = target.store.queueFollowUp({
|
|
1819
|
+
runId: target.runId,
|
|
1820
|
+
requestId: options.requestId,
|
|
1821
|
+
targetSessionId: ctx.sessionManager.getSessionId(),
|
|
1822
|
+
actor: {
|
|
1823
|
+
type: options.actor,
|
|
1824
|
+
id: options.actorId ?? ctx.sessionManager.getSessionId(),
|
|
1825
|
+
},
|
|
1826
|
+
source: options.source,
|
|
1827
|
+
prompt: options.prompt,
|
|
1828
|
+
});
|
|
1829
|
+
return {
|
|
1830
|
+
message: `Queued workflow follow-up ${result.followUp.order}.`,
|
|
1831
|
+
details: {
|
|
1832
|
+
action: "queue-follow-up",
|
|
1833
|
+
runId: target.runId,
|
|
1834
|
+
followUpId: result.followUp.followUpId,
|
|
1835
|
+
order: result.followUp.order,
|
|
1836
|
+
adopted: result.adopted,
|
|
1837
|
+
},
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
|
+
finally {
|
|
1841
|
+
target.store.close();
|
|
1842
|
+
}
|
|
1843
|
+
};
|
|
1844
|
+
const removeWorkflowFollowUp = async (ctx, options) => {
|
|
1845
|
+
const target = await resolveMutableWorkflow(ctx, options.runId);
|
|
1846
|
+
try {
|
|
1847
|
+
const followUp = target.store.removeFollowUp({
|
|
1848
|
+
runId: target.runId,
|
|
1849
|
+
followUpId: options.followUpId,
|
|
1850
|
+
actor: {
|
|
1851
|
+
type: options.actor,
|
|
1852
|
+
id: options.actorId ?? ctx.sessionManager.getSessionId(),
|
|
1853
|
+
},
|
|
1854
|
+
source: options.source,
|
|
1855
|
+
});
|
|
1856
|
+
return {
|
|
1857
|
+
message: `Removed workflow follow-up ${followUp.order}.`,
|
|
1858
|
+
details: {
|
|
1859
|
+
action: "remove-follow-up",
|
|
1860
|
+
runId: target.runId,
|
|
1861
|
+
followUpId: followUp.followUpId,
|
|
1862
|
+
order: followUp.order,
|
|
1863
|
+
state: followUp.state,
|
|
1864
|
+
},
|
|
1865
|
+
};
|
|
1866
|
+
}
|
|
1867
|
+
finally {
|
|
1868
|
+
target.store.close();
|
|
1869
|
+
}
|
|
1870
|
+
};
|
|
1598
1871
|
const resolveWaitingWorkflow = async (ctx, requestedRunId, allowOtherSession = false) => {
|
|
1599
1872
|
let parentRunId = requestedRunId ?? lastWaitingRunId;
|
|
1600
1873
|
if (parentRunId === null) {
|
|
@@ -2116,7 +2389,7 @@ export default function piWorkflows(pi) {
|
|
|
2116
2389
|
},
|
|
2117
2390
|
});
|
|
2118
2391
|
pi.registerCommand("workflow", {
|
|
2119
|
-
description: "Run or manage a workflow: /workflow <name-or-path> [task | --input-json {…}]; also: status, pause, resume, cancel, answer",
|
|
2392
|
+
description: "Run or manage a workflow: /workflow <name-or-path> [task | --input-json {…}]; also: status, pause, resume, cancel, answer, change-settings, queue-follow-up, remove-follow-up",
|
|
2120
2393
|
getArgumentCompletions: async (prefix) => {
|
|
2121
2394
|
const discovered = await discoverWorkflows({ cwd: process.cwd() }, builtinWorkflowCatalog);
|
|
2122
2395
|
const items = [
|
|
@@ -2126,6 +2399,9 @@ export default function piWorkflows(pi) {
|
|
|
2126
2399
|
{ value: "resume", label: "resume" },
|
|
2127
2400
|
{ value: "cancel", label: "cancel" },
|
|
2128
2401
|
{ value: "answer", label: "answer" },
|
|
2402
|
+
{ value: "change-settings", label: "change-settings" },
|
|
2403
|
+
{ value: "queue-follow-up", label: "queue-follow-up" },
|
|
2404
|
+
{ value: "remove-follow-up", label: "remove-follow-up" },
|
|
2129
2405
|
].filter((item) => item.value.startsWith(prefix));
|
|
2130
2406
|
return items.length > 0 ? items : null;
|
|
2131
2407
|
},
|
|
@@ -2167,6 +2443,50 @@ export default function piWorkflows(pi) {
|
|
|
2167
2443
|
}
|
|
2168
2444
|
return;
|
|
2169
2445
|
}
|
|
2446
|
+
if (parsed.kind === "change-settings") {
|
|
2447
|
+
try {
|
|
2448
|
+
const result = await changeWorkflowSettings(ctx, {
|
|
2449
|
+
patch: parsed.patch,
|
|
2450
|
+
requestId: `interactive-${randomUUID()}`,
|
|
2451
|
+
actor: "human",
|
|
2452
|
+
source: "interactive-command",
|
|
2453
|
+
});
|
|
2454
|
+
notify(ctx, result.message, result.level);
|
|
2455
|
+
}
|
|
2456
|
+
catch (error) {
|
|
2457
|
+
notify(ctx, errorMessage(error), "error");
|
|
2458
|
+
}
|
|
2459
|
+
return;
|
|
2460
|
+
}
|
|
2461
|
+
if (parsed.kind === "queue-follow-up") {
|
|
2462
|
+
try {
|
|
2463
|
+
const result = await queueWorkflowFollowUp(ctx, {
|
|
2464
|
+
prompt: parsed.prompt,
|
|
2465
|
+
requestId: `interactive-${randomUUID()}`,
|
|
2466
|
+
actor: "human",
|
|
2467
|
+
source: "interactive-command",
|
|
2468
|
+
});
|
|
2469
|
+
notify(ctx, result.message, result.level);
|
|
2470
|
+
}
|
|
2471
|
+
catch (error) {
|
|
2472
|
+
notify(ctx, errorMessage(error), "error");
|
|
2473
|
+
}
|
|
2474
|
+
return;
|
|
2475
|
+
}
|
|
2476
|
+
if (parsed.kind === "remove-follow-up") {
|
|
2477
|
+
try {
|
|
2478
|
+
const result = await removeWorkflowFollowUp(ctx, {
|
|
2479
|
+
followUpId: parsed.followUpId,
|
|
2480
|
+
actor: "human",
|
|
2481
|
+
source: "interactive-command",
|
|
2482
|
+
});
|
|
2483
|
+
notify(ctx, result.message, result.level);
|
|
2484
|
+
}
|
|
2485
|
+
catch (error) {
|
|
2486
|
+
notify(ctx, errorMessage(error), "error");
|
|
2487
|
+
}
|
|
2488
|
+
return;
|
|
2489
|
+
}
|
|
2170
2490
|
if (parsed.kind === "answer") {
|
|
2171
2491
|
try {
|
|
2172
2492
|
const result = await answerWorkflowControl(ctx, parsed.input, parsed.runId);
|
|
@@ -2316,11 +2636,26 @@ export default function piWorkflows(pi) {
|
|
|
2316
2636
|
}
|
|
2317
2637
|
},
|
|
2318
2638
|
});
|
|
2639
|
+
let workflowToolTail = Promise.resolve();
|
|
2640
|
+
const withWorkflowToolOrder = async (operation) => {
|
|
2641
|
+
const prior = workflowToolTail;
|
|
2642
|
+
let release;
|
|
2643
|
+
workflowToolTail = new Promise((resolve) => {
|
|
2644
|
+
release = resolve;
|
|
2645
|
+
});
|
|
2646
|
+
await prior;
|
|
2647
|
+
try {
|
|
2648
|
+
return await operation();
|
|
2649
|
+
}
|
|
2650
|
+
finally {
|
|
2651
|
+
release?.();
|
|
2652
|
+
}
|
|
2653
|
+
};
|
|
2319
2654
|
pi.registerTool({
|
|
2320
2655
|
name: "workflow",
|
|
2321
2656
|
label: "Workflow",
|
|
2322
2657
|
description: [
|
|
2323
|
-
"List, start, inspect, pause, resume, cancel, answer, update, or complete pi-workflows runs.",
|
|
2658
|
+
"List, start, inspect, change settings, queue or remove follow-ups, pause, resume, cancel, answer, update, or complete pi-workflows runs.",
|
|
2324
2659
|
"When the user asks to monitor, watch, poll, or check something repeatedly, start the built-in monitor workflow with input keys task, stopWhen, everyMinutes, and optional maxChecks.",
|
|
2325
2660
|
"Put the exact goal, authority, limits, and recovery rules in task; Monitor observes first, performs only safe authorized actions, verifies them immediately, and waits only while target work is moving or an external event is pending.",
|
|
2326
2661
|
"Use update or submit only when a workflow step contract asks for it, and pass the exact step and attempt ids.",
|
|
@@ -2328,75 +2663,107 @@ export default function piWorkflows(pi) {
|
|
|
2328
2663
|
].join(" "),
|
|
2329
2664
|
parameters: WorkflowToolParameters,
|
|
2330
2665
|
async execute(toolCallId, rawParams, _signal, _onUpdate, ctx) {
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
control = await queueToolLaunch(ctx, waiting.workflowRef, params.input, {
|
|
2359
|
-
parentRunId: waiting.parentRunId,
|
|
2360
|
-
});
|
|
2361
|
-
break;
|
|
2362
|
-
}
|
|
2363
|
-
case "update": {
|
|
2364
|
-
if (!activeRun)
|
|
2365
|
-
throw new Error("No workflow step is active.");
|
|
2366
|
-
const receipt = await activeRun.engine.publishUpdate(params.step, params.attempt, params.update, toolCallId);
|
|
2367
|
-
return {
|
|
2368
|
-
content: [
|
|
2369
|
-
{ type: "text", text: "Workflow update published; the step remains active." },
|
|
2370
|
-
],
|
|
2371
|
-
details: { action: "update", ...receipt },
|
|
2372
|
-
};
|
|
2373
|
-
}
|
|
2374
|
-
case "submit": {
|
|
2375
|
-
if (!activeRun) {
|
|
2376
|
-
if (lastExpiredAttempt?.contract.attemptId === params.attempt &&
|
|
2377
|
-
lastExpiredAttempt.contract.nodeId === params.step) {
|
|
2378
|
-
throw new Error(`Workflow step ${JSON.stringify(params.step)} attempt ${JSON.stringify(params.attempt)} ${lastExpiredAttempt.reason}; its output is no longer accepted.`);
|
|
2666
|
+
return await withWorkflowToolOrder(async () => {
|
|
2667
|
+
const params = parseWorkflowToolInput(rawParams);
|
|
2668
|
+
let control;
|
|
2669
|
+
switch (params.action) {
|
|
2670
|
+
case "list":
|
|
2671
|
+
control = await listWorkflowControl(ctx, params.offset);
|
|
2672
|
+
break;
|
|
2673
|
+
case "start":
|
|
2674
|
+
control = await queueToolLaunch(ctx, params.workflow, params.input ?? {});
|
|
2675
|
+
break;
|
|
2676
|
+
case "status":
|
|
2677
|
+
control = await statusWorkflowControl(ctx, params.runId);
|
|
2678
|
+
break;
|
|
2679
|
+
case "pause":
|
|
2680
|
+
control = pauseWorkflowControl(ctx);
|
|
2681
|
+
break;
|
|
2682
|
+
case "resume":
|
|
2683
|
+
control = resumeWorkflowControl(ctx);
|
|
2684
|
+
break;
|
|
2685
|
+
case "cancel":
|
|
2686
|
+
control = await cancelWorkflowControl(ctx, "agent", params.runId);
|
|
2687
|
+
break;
|
|
2688
|
+
case "answer": {
|
|
2689
|
+
const waiting = await resolveWaitingWorkflow(ctx, params.runId);
|
|
2690
|
+
const parent = readWorkflowRun(waiting.parentRunId);
|
|
2691
|
+
if (parent !== null && humanDecisionRequest(parent.state.finalOutput) !== null) {
|
|
2692
|
+
throw new Error("This checkpoint requires a verified human answer from Pi UI or a configured decision channel.");
|
|
2379
2693
|
}
|
|
2380
|
-
|
|
2694
|
+
control = await queueToolLaunch(ctx, waiting.workflowRef, params.input, {
|
|
2695
|
+
parentRunId: waiting.parentRunId,
|
|
2696
|
+
});
|
|
2697
|
+
break;
|
|
2698
|
+
}
|
|
2699
|
+
case "change-settings":
|
|
2700
|
+
control = await changeWorkflowSettings(ctx, {
|
|
2701
|
+
...(params.runId !== undefined ? { runId: params.runId } : {}),
|
|
2702
|
+
...(params.scopeId !== undefined ? { scopeId: params.scopeId } : {}),
|
|
2703
|
+
...(params.expectedChangeNumber !== undefined
|
|
2704
|
+
? { expectedChangeNumber: params.expectedChangeNumber }
|
|
2705
|
+
: {}),
|
|
2706
|
+
patch: params.patch,
|
|
2707
|
+
requestId: `tool-${toolCallId}`,
|
|
2708
|
+
actor: "session",
|
|
2709
|
+
source: "workflow-tool",
|
|
2710
|
+
});
|
|
2711
|
+
break;
|
|
2712
|
+
case "queue-follow-up":
|
|
2713
|
+
control = await queueWorkflowFollowUp(ctx, {
|
|
2714
|
+
...(params.runId !== undefined ? { runId: params.runId } : {}),
|
|
2715
|
+
prompt: params.prompt,
|
|
2716
|
+
requestId: `tool-${toolCallId}`,
|
|
2717
|
+
actor: "session",
|
|
2718
|
+
source: "workflow-tool",
|
|
2719
|
+
});
|
|
2720
|
+
break;
|
|
2721
|
+
case "remove-follow-up":
|
|
2722
|
+
control = await removeWorkflowFollowUp(ctx, {
|
|
2723
|
+
...(params.runId !== undefined ? { runId: params.runId } : {}),
|
|
2724
|
+
followUpId: params.followUpId,
|
|
2725
|
+
actor: "session",
|
|
2726
|
+
source: "workflow-tool",
|
|
2727
|
+
});
|
|
2728
|
+
break;
|
|
2729
|
+
case "update": {
|
|
2730
|
+
if (!activeRun)
|
|
2731
|
+
throw new Error("No workflow step is active.");
|
|
2732
|
+
const receipt = await activeRun.engine.publishUpdate(params.step, params.attempt, params.update, toolCallId);
|
|
2733
|
+
return {
|
|
2734
|
+
content: [
|
|
2735
|
+
{ type: "text", text: "Workflow update published; the step remains active." },
|
|
2736
|
+
],
|
|
2737
|
+
details: { action: "update", ...receipt },
|
|
2738
|
+
};
|
|
2381
2739
|
}
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2740
|
+
case "submit": {
|
|
2741
|
+
if (!activeRun) {
|
|
2742
|
+
if (lastExpiredAttempt?.contract.attemptId === params.attempt &&
|
|
2743
|
+
lastExpiredAttempt.contract.nodeId === params.step) {
|
|
2744
|
+
throw new Error(`Workflow step ${JSON.stringify(params.step)} attempt ${JSON.stringify(params.attempt)} ${lastExpiredAttempt.reason}; its output is no longer accepted.`);
|
|
2745
|
+
}
|
|
2746
|
+
throw new Error("No workflow step is waiting for output.");
|
|
2747
|
+
}
|
|
2748
|
+
// Flush the conversation into SQLite before accepting, so the
|
|
2749
|
+
// attempt range includes the assistant message carrying this call.
|
|
2750
|
+
await activeRun.recorder?.record(ctx).catch(() => undefined);
|
|
2751
|
+
await activeRun.recorder?.synchronize(ctx).catch(() => undefined);
|
|
2752
|
+
const result = await activeRun.executor.submit(params.step, params.attempt, params.output);
|
|
2753
|
+
if (!result.accepted) {
|
|
2754
|
+
throw new Error(result.message);
|
|
2755
|
+
}
|
|
2756
|
+
return {
|
|
2757
|
+
content: [{ type: "text", text: result.message }],
|
|
2758
|
+
details: { action: "submit", step: params.step, accepted: true },
|
|
2759
|
+
};
|
|
2389
2760
|
}
|
|
2390
|
-
return {
|
|
2391
|
-
content: [{ type: "text", text: result.message }],
|
|
2392
|
-
details: { action: "submit", step: params.step, accepted: true },
|
|
2393
|
-
};
|
|
2394
2761
|
}
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
};
|
|
2762
|
+
return {
|
|
2763
|
+
content: [{ type: "text", text: control.message }],
|
|
2764
|
+
details: control.details,
|
|
2765
|
+
};
|
|
2766
|
+
});
|
|
2400
2767
|
},
|
|
2401
2768
|
});
|
|
2402
2769
|
if (herdrEnabled) {
|
|
@@ -2419,6 +2786,16 @@ export default function piWorkflows(pi) {
|
|
|
2419
2786
|
if (herdrEnabled)
|
|
2420
2787
|
void refreshHerdrCapability(ctx);
|
|
2421
2788
|
ensureRunQueueStore(ctx.cwd);
|
|
2789
|
+
ensureFollowUpDelivery();
|
|
2790
|
+
const sessionFollowUpStore = followUpStore;
|
|
2791
|
+
const sessionFollowUpCoordinator = followUpCoordinator;
|
|
2792
|
+
if (sessionFollowUpStore === null || sessionFollowUpCoordinator === null) {
|
|
2793
|
+
throw new Error("Workflow follow-up delivery did not initialize");
|
|
2794
|
+
}
|
|
2795
|
+
for (const runId of sessionFollowUpStore.listPendingPresentations(ctx.sessionManager.getSessionId())) {
|
|
2796
|
+
settlePresentationFromBranch(ctx, runId, "The final workflow response did not settle before the extension restarted");
|
|
2797
|
+
}
|
|
2798
|
+
await sessionFollowUpCoordinator.synchronize(ctx, false).catch(() => undefined);
|
|
2422
2799
|
try {
|
|
2423
2800
|
await reloadDecisionChannels(ctx);
|
|
2424
2801
|
}
|
|
@@ -2522,6 +2899,10 @@ export default function piWorkflows(pi) {
|
|
|
2522
2899
|
});
|
|
2523
2900
|
pi.on("agent_settled", async (_event, ctx) => {
|
|
2524
2901
|
systemTurnAbort = null;
|
|
2902
|
+
if (presentationPending !== null) {
|
|
2903
|
+
settlePresentationFromBranch(ctx, presentationPending.runId, "The final workflow response settled without durable session evidence");
|
|
2904
|
+
presentationPending = null;
|
|
2905
|
+
}
|
|
2525
2906
|
if (activeRun === null) {
|
|
2526
2907
|
try {
|
|
2527
2908
|
const prepared = ensureRunQueueStore(ctx.cwd).findSessionReservation(ctx.sessionManager.getSessionId());
|
|
@@ -2538,7 +2919,7 @@ export default function piWorkflows(pi) {
|
|
|
2538
2919
|
const run = activeRun;
|
|
2539
2920
|
if (!run) {
|
|
2540
2921
|
turnCoordinator.flushNatural(ctx.isIdle() && !sessionClosed && !runHeld());
|
|
2541
|
-
|
|
2922
|
+
await followUpCoordinator?.synchronize(ctx, false).catch(() => undefined);
|
|
2542
2923
|
runSyncPass(ctx);
|
|
2543
2924
|
return;
|
|
2544
2925
|
}
|
|
@@ -2594,6 +2975,10 @@ export default function piWorkflows(pi) {
|
|
|
2594
2975
|
controllerContext = null;
|
|
2595
2976
|
runQueueStore?.close();
|
|
2596
2977
|
runQueueStore = null;
|
|
2978
|
+
followUpCoordinator?.clear();
|
|
2979
|
+
followUpCoordinator = null;
|
|
2980
|
+
followUpStore?.close();
|
|
2981
|
+
followUpStore = null;
|
|
2597
2982
|
presentationPending = null;
|
|
2598
2983
|
clearWidgetTimer();
|
|
2599
2984
|
stopWidgetTicker();
|