@osolmaz/pi-workflows 0.12.1 → 0.13.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/README.md +52 -28
- package/dist/builtins/autodoc.workflow.d.ts +4 -4
- package/dist/builtins/autoimplement.workflow.d.ts +369 -73
- package/dist/builtins/autoplan.workflow.d.ts +126 -13
- package/dist/builtins/autoplan.workflow.js +200 -29
- package/dist/builtins/autoplan.workflow.js.map +1 -1
- package/dist/builtins/catalog.js +6 -4
- package/dist/builtins/catalog.js.map +1 -1
- package/dist/builtins/index.d.ts +2 -1
- package/dist/builtins/index.js +1 -0
- package/dist/builtins/index.js.map +1 -1
- package/dist/builtins/monitor.workflow.d.ts +56 -21
- package/dist/builtins/monitor.workflow.js +497 -216
- package/dist/builtins/monitor.workflow.js.map +1 -1
- package/dist/builtins/plain-summary.workflow.d.ts +32 -0
- package/dist/builtins/plain-summary.workflow.js +134 -0
- package/dist/builtins/plain-summary.workflow.js.map +1 -0
- package/dist/builtins/plan-change.workflow.d.ts +170 -22
- package/dist/builtins/sanity-check.workflow.d.ts +45 -3
- package/dist/builtins/sanity-check.workflow.js +45 -7
- package/dist/builtins/sanity-check.workflow.js.map +1 -1
- package/dist/controllers/effects.d.ts +3 -2
- package/dist/controllers/effects.js +8 -1
- package/dist/controllers/effects.js.map +1 -1
- package/dist/controllers/index.d.ts +1 -1
- package/dist/controllers/index.js +1 -1
- package/dist/controllers/index.js.map +1 -1
- package/dist/controllers/manager.d.ts +1 -0
- package/dist/controllers/manager.js +41 -17
- package/dist/controllers/manager.js.map +1 -1
- package/dist/controllers/sqlite.d.ts +107 -85
- package/dist/controllers/sqlite.js +1611 -1486
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/controllers/store.d.ts +9 -8
- package/dist/controllers/store.js +1 -31
- package/dist/controllers/store.js.map +1 -1
- package/dist/controllers/types.d.ts +3 -0
- package/dist/controllers/workflow-engine-scheduler.d.ts +1 -1
- package/dist/controllers/workflow-engine-scheduler.js +3 -5
- package/dist/controllers/workflow-engine-scheduler.js.map +1 -1
- package/dist/controllers/workflows.d.ts +2 -2
- package/dist/controllers/workflows.js +11 -10
- package/dist/controllers/workflows.js.map +1 -1
- package/dist/extension/controller-host.js +2 -2
- package/dist/extension/controller-host.js.map +1 -1
- package/dist/extension/decision-channels.d.ts +2 -0
- package/dist/extension/decision-channels.js +311 -118
- package/dist/extension/decision-channels.js.map +1 -1
- package/dist/extension/executor.d.ts +9 -2
- package/dist/extension/executor.js +89 -2
- package/dist/extension/executor.js.map +1 -1
- package/dist/extension/herdr-viewer.d.ts +0 -1
- package/dist/extension/herdr-viewer.js +0 -4
- package/dist/extension/herdr-viewer.js.map +1 -1
- package/dist/extension/index.js +156 -103
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/recorder.d.ts +3 -10
- package/dist/extension/recorder.js +13 -22
- package/dist/extension/recorder.js.map +1 -1
- package/dist/extension/step-message.d.ts +7 -2
- package/dist/extension/step-message.js +94 -2
- package/dist/extension/step-message.js.map +1 -1
- package/dist/extension/widget.js +6 -0
- package/dist/extension/widget.js.map +1 -1
- package/dist/host/rpc-executor.d.ts +3 -0
- package/dist/host/rpc-executor.js +2 -0
- package/dist/host/rpc-executor.js.map +1 -1
- package/dist/host/runner.d.ts +3 -6
- package/dist/host/runner.js +83 -32
- package/dist/host/runner.js.map +1 -1
- package/dist/render/graph-render.js +14 -8
- package/dist/render/graph-render.js.map +1 -1
- package/dist/state/database.d.ts +35 -0
- package/dist/state/database.js +287 -0
- package/dist/state/database.js.map +1 -0
- package/dist/state/index.d.ts +4 -0
- package/dist/state/index.js +5 -0
- package/dist/state/index.js.map +1 -0
- package/dist/state/json.d.ts +6 -0
- package/dist/state/json.js +38 -0
- package/dist/state/json.js.map +1 -0
- package/dist/state/mutation.d.ts +57 -0
- package/dist/state/mutation.js +255 -0
- package/dist/state/mutation.js.map +1 -0
- package/dist/state/schema.d.ts +6 -0
- package/dist/state/schema.js +473 -0
- package/dist/state/schema.js.map +1 -0
- package/dist/viewer/cli.d.ts +2 -2
- package/dist/viewer/cli.js +142 -122
- package/dist/viewer/cli.js.map +1 -1
- package/dist/viewer/render.d.ts +4 -4
- package/dist/viewer/render.js +2 -17
- package/dist/viewer/render.js.map +1 -1
- package/dist/viewer/session-reducer.d.ts +1 -1
- package/dist/viewer/session-reducer.js +1 -1
- package/dist/viewer/tui.d.ts +3 -3
- package/dist/viewer/tui.js +15 -13
- package/dist/viewer/tui.js.map +1 -1
- package/dist/viewer/watch.d.ts +2 -5
- package/dist/viewer/watch.js +13 -13
- package/dist/viewer/watch.js.map +1 -1
- package/dist/workflows/composition.js +12 -2
- package/dist/workflows/composition.js.map +1 -1
- package/dist/workflows/definition.d.ts +6 -2
- package/dist/workflows/definition.js +17 -0
- package/dist/workflows/definition.js.map +1 -1
- package/dist/workflows/engine.d.ts +6 -6
- package/dist/workflows/engine.js +183 -103
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/errors.d.ts +3 -3
- package/dist/workflows/errors.js +3 -3
- package/dist/workflows/human-decision.d.ts +29 -4
- package/dist/workflows/human-decision.js +547 -283
- package/dist/workflows/human-decision.js.map +1 -1
- package/dist/workflows/index.d.ts +4 -5
- package/dist/workflows/index.js +3 -4
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/schema.js +17 -1
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/store.d.ts +83 -103
- package/dist/workflows/store.js +902 -951
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/types.d.ts +55 -81
- package/docs/CONTROLLERS.md +10 -8
- package/docs/DEFERRED_TURNS.md +2 -2
- package/docs/DESIGN_PHILOSOPHY.md +4 -2
- package/docs/HUMAN_DECISIONS.md +13 -14
- package/docs/MONITOR.md +179 -231
- package/docs/SQLITE_STATE.md +219 -0
- package/docs/WORKFLOW_COMPOSITION.md +19 -19
- package/docs/WORKFLOW_STEP_MESSAGES.md +20 -18
- package/docs/WORKFLOW_UPDATES.md +10 -9
- package/docs/development.md +9 -9
- package/docs/live-replay-protocol.md +28 -37
- package/docs/plans/2026-08-16-workflow-updates-plan.md +1 -1
- package/docs/plans/2026-08-19-human-decision-gates-plan.md +1 -1
- package/docs/plans/2026-08-21-autoimplement-timeout-fallback-plan.md +1 -1
- package/docs/plans/2026-08-21-plan-change-approval-policy-plan.md +1 -1
- package/docs/plans/2026-08-21-sanity-check-plan.md +30 -16
- package/docs/plans/2026-08-22-goal-finishing-monitor-plan.md +200 -0
- package/docs/plans/2026-08-23-assistant-agent-completion-plan.md +701 -0
- package/docs/plans/2026-08-23-sqlite-state-plan.md +970 -0
- package/docs/plans/replayable-run-bundles-implementation-plan.md +2 -2
- package/docs/plans/session-event-replay-implementation-plan.md +1 -1
- package/docs/plans/tui-viewer-implementation-plan.md +1 -1
- package/docs/session-event-journal.md +63 -434
- package/docs/tui-viewer.md +13 -15
- package/docs/workflows.md +104 -50
- package/examples/workflows/plain-summary.workflow.ts +1 -0
- package/herdr-plugin.toml +1 -1
- package/package.json +1 -1
- package/plugins/herdr/viewer.mjs +1 -11
- package/skills/autoplan/SKILL.md +5 -4
- package/skills/monitor/SKILL.md +91 -163
- package/skills/pi-workflows/SKILL.md +7 -7
- package/skills/sanity-check/SKILL.md +1 -1
- package/src/builtins/autoplan.workflow.ts +290 -40
- package/src/builtins/catalog.ts +6 -4
- package/src/builtins/index.ts +7 -1
- package/src/builtins/monitor.workflow.ts +663 -249
- package/src/builtins/plain-summary.workflow.ts +185 -0
- package/src/builtins/sanity-check.workflow.ts +62 -7
- package/src/controllers/effects.ts +7 -0
- package/src/controllers/index.ts +0 -6
- package/src/controllers/manager.ts +72 -32
- package/src/controllers/sqlite.ts +2420 -1948
- package/src/controllers/store.ts +17 -38
- package/src/controllers/types.ts +3 -0
- package/src/controllers/workflow-engine-scheduler.ts +4 -8
- package/src/controllers/workflows.ts +45 -21
- package/src/extension/controller-host.ts +1 -2
- package/src/extension/decision-channels.ts +427 -144
- package/src/extension/executor.ts +105 -2
- package/src/extension/herdr-viewer.ts +0 -5
- package/src/extension/index.ts +180 -118
- package/src/extension/recorder.ts +13 -22
- package/src/extension/step-message.ts +106 -4
- package/src/extension/widget.ts +8 -0
- package/src/host/rpc-executor.ts +4 -0
- package/src/host/runner.ts +93 -41
- package/src/render/graph-render.ts +11 -4
- package/src/state/database.ts +375 -0
- package/src/state/index.ts +29 -0
- package/src/state/json.ts +42 -0
- package/src/state/mutation.ts +435 -0
- package/src/state/schema.ts +475 -0
- package/src/viewer/cli.ts +151 -144
- package/src/viewer/render.ts +12 -24
- package/src/viewer/session-reducer.ts +1 -1
- package/src/viewer/tui.ts +18 -17
- package/src/viewer/watch.ts +13 -17
- package/src/workflows/composition.ts +12 -3
- package/src/workflows/definition.ts +36 -3
- package/src/workflows/engine.ts +212 -116
- package/src/workflows/errors.ts +3 -3
- package/src/workflows/human-decision.ts +795 -340
- package/src/workflows/index.ts +15 -19
- package/src/workflows/schema.ts +19 -1
- package/src/workflows/store.ts +1295 -1163
- package/src/workflows/types.ts +61 -81
- package/dist/workflows/artifacts.d.ts +0 -40
- package/dist/workflows/artifacts.js +0 -155
- package/dist/workflows/artifacts.js.map +0 -1
- package/dist/workflows/migrate-sources.d.ts +0 -42
- package/dist/workflows/migrate-sources.js +0 -133
- package/dist/workflows/migrate-sources.js.map +0 -1
- package/docs/run-bundles.md +0 -481
- package/src/workflows/artifacts.ts +0 -188
- package/src/workflows/migrate-sources.ts +0 -178
package/src/extension/index.ts
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import path from "node:path";
|
|
3
2
|
import { isDeepStrictEqual } from "node:util";
|
|
4
3
|
import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
5
4
|
import { builtinWorkflowCatalog } from "../builtins/catalog.js";
|
|
6
|
-
import {
|
|
7
|
-
projectControllerStorePath,
|
|
8
|
-
SqliteControllerStore,
|
|
9
|
-
type WorkflowRunQueueRecord,
|
|
10
|
-
} from "../controllers/index.js";
|
|
5
|
+
import { SqliteControllerStore, type WorkflowRunQueueRecord } from "../controllers/index.js";
|
|
11
6
|
import type {
|
|
12
7
|
WorkflowTurnIntentCause,
|
|
13
8
|
WorkflowTurnIntentResolution,
|
|
@@ -30,13 +25,11 @@ import {
|
|
|
30
25
|
validateHumanDecisionResponse,
|
|
31
26
|
} from "../workflows/human-decision.js";
|
|
32
27
|
import { discoverWorkflows, resolveWorkflowRef } from "../workflows/loader.js";
|
|
33
|
-
import { migrateLegacyWorkflowSources } from "../workflows/migrate-sources.js";
|
|
34
28
|
import { appendProgressHistory, progressRecordsFromTrace } from "../workflows/progress.js";
|
|
35
29
|
import {
|
|
36
30
|
createRunId,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
readRunBundle,
|
|
31
|
+
listWorkflowRuns,
|
|
32
|
+
readWorkflowRun,
|
|
40
33
|
WorkflowRunStore,
|
|
41
34
|
createDefinitionSnapshot,
|
|
42
35
|
} from "../workflows/store.js";
|
|
@@ -96,6 +89,7 @@ import {
|
|
|
96
89
|
} from "./herdr-viewer.js";
|
|
97
90
|
import { SessionRecorder } from "./recorder.js";
|
|
98
91
|
import {
|
|
92
|
+
recoverAssistantStep,
|
|
99
93
|
registerWorkflowAgentStepMessageRenderer,
|
|
100
94
|
WORKFLOW_AGENT_STEP_MESSAGE_SCHEMA,
|
|
101
95
|
WORKFLOW_AGENT_STEP_MESSAGE_TYPE,
|
|
@@ -279,7 +273,7 @@ function safeLaunchError(error: unknown): { code: string; message: string } {
|
|
|
279
273
|
.replace(/(token|api[_-]?key|secret|password)(\s*[:=]\s*)\S+/giu, "$1$2[redacted]")
|
|
280
274
|
.replaceAll("\n", " ")
|
|
281
275
|
.trim();
|
|
282
|
-
const code = /not found|cannot find|unknown workflow/iu.test(raw)
|
|
276
|
+
const code = /not found|cannot find|unknown workflow|no such file or directory/iu.test(raw)
|
|
283
277
|
? "workflow_not_found"
|
|
284
278
|
: /source changed|source mismatch/iu.test(raw)
|
|
285
279
|
? "source_changed"
|
|
@@ -436,11 +430,29 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
436
430
|
...[...telegramDecisionChannels.values()].map(async (channel) => channel.settle(decision)),
|
|
437
431
|
]);
|
|
438
432
|
};
|
|
439
|
-
const migrationBlockedRuns = new Set<string>();
|
|
440
433
|
const ensureRunQueueStore = (cwd: string): SqliteControllerStore => {
|
|
441
|
-
runQueueStore ??= new SqliteControllerStore(
|
|
434
|
+
runQueueStore ??= new SqliteControllerStore(undefined, { projectPath: cwd });
|
|
442
435
|
return runQueueStore;
|
|
443
436
|
};
|
|
437
|
+
const workflowStore = (
|
|
438
|
+
cwd: string,
|
|
439
|
+
options: Omit<ConstructorParameters<typeof WorkflowRunStore>[1], "state"> = {},
|
|
440
|
+
): WorkflowRunStore => {
|
|
441
|
+
const queue = ensureRunQueueStore(cwd);
|
|
442
|
+
return new WorkflowRunStore(queue.filePath, { ...options, state: queue.state });
|
|
443
|
+
};
|
|
444
|
+
const humanDecisionStore = (
|
|
445
|
+
cwd: string,
|
|
446
|
+
authorityProvider?: NonNullable<
|
|
447
|
+
ConstructorParameters<typeof HumanDecisionStore>[1]
|
|
448
|
+
>["authorityProvider"],
|
|
449
|
+
): HumanDecisionStore => {
|
|
450
|
+
const queue = ensureRunQueueStore(cwd);
|
|
451
|
+
return new HumanDecisionStore(queue.filePath, {
|
|
452
|
+
state: queue.state,
|
|
453
|
+
...(authorityProvider === undefined ? {} : { authorityProvider }),
|
|
454
|
+
});
|
|
455
|
+
};
|
|
444
456
|
|
|
445
457
|
// Session-addressed delivery: each session polls only its durable outbox.
|
|
446
458
|
// Run events remain an audit feed and never enter a conversation.
|
|
@@ -838,7 +850,6 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
838
850
|
workflowViewTarget = {
|
|
839
851
|
runId: state.runId,
|
|
840
852
|
workflowName: state.workflowName,
|
|
841
|
-
runDir: path.resolve(new WorkflowRunStore().runDirFor(state.runId)),
|
|
842
853
|
};
|
|
843
854
|
renderWidget(ctx);
|
|
844
855
|
};
|
|
@@ -1048,12 +1059,16 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1048
1059
|
}
|
|
1049
1060
|
try {
|
|
1050
1061
|
if (outcome === "park") {
|
|
1051
|
-
runQueueStore.parkWorkflowRun({ runId: run.runId, claimToken: run.claimToken })
|
|
1052
|
-
|
|
1053
|
-
|
|
1062
|
+
if (runQueueStore.parkWorkflowRun({ runId: run.runId, claimToken: run.claimToken })) {
|
|
1063
|
+
runQueueStore.settleRunEffect(run.runId, "run.park_queue");
|
|
1064
|
+
}
|
|
1065
|
+
} else if (
|
|
1066
|
+
runQueueStore.completeWorkflowRun({ runId: run.runId, claimToken: run.claimToken })
|
|
1067
|
+
) {
|
|
1068
|
+
runQueueStore.settleRunEffect(run.runId, "run.settle_queue");
|
|
1054
1069
|
}
|
|
1055
1070
|
} catch {
|
|
1056
|
-
// Queue bookkeeping is best-effort next to the durable
|
|
1071
|
+
// Queue bookkeeping is best-effort next to the durable run fact.
|
|
1057
1072
|
} finally {
|
|
1058
1073
|
run.claimToken = undefined;
|
|
1059
1074
|
}
|
|
@@ -1068,7 +1083,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1068
1083
|
activeRun = null;
|
|
1069
1084
|
}
|
|
1070
1085
|
if (result.state.status === "running") {
|
|
1071
|
-
// The run was parked for another runner; its
|
|
1086
|
+
// The run was parked for another runner; its SQLite state stays resumable
|
|
1072
1087
|
// and its queue row becomes claimable. The recorder drains first: its
|
|
1073
1088
|
// finalization writes share the fenced store, so the claim must stay
|
|
1074
1089
|
// valid until capture is complete. Nothing else to present.
|
|
@@ -1095,7 +1110,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1095
1110
|
);
|
|
1096
1111
|
return;
|
|
1097
1112
|
}
|
|
1098
|
-
releaseClaim(run, "done");
|
|
1113
|
+
releaseClaim(run, result.state.status === "waiting" ? "park" : "done");
|
|
1099
1114
|
recordRunEvent({
|
|
1100
1115
|
runId: run.runId,
|
|
1101
1116
|
workflowRef: run.workflowName,
|
|
@@ -1223,7 +1238,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1223
1238
|
// refused continuation (edited source, missing parent) must not consume
|
|
1224
1239
|
// the parent's one-continuation slot.
|
|
1225
1240
|
if (options.parentRunId !== undefined) {
|
|
1226
|
-
const parent =
|
|
1241
|
+
const parent = readWorkflowRun(options.parentRunId);
|
|
1227
1242
|
if (parent === null || parent.state.status !== "waiting") {
|
|
1228
1243
|
throw new Error(`Workflow run ${options.parentRunId} is not waiting at a checkpoint`);
|
|
1229
1244
|
}
|
|
@@ -1258,6 +1273,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1258
1273
|
: workflowSource.path,
|
|
1259
1274
|
workflowSource: launchSourceIdentity(workflow, workflowSource),
|
|
1260
1275
|
definitionDigest: definitionDigest(snapshot),
|
|
1276
|
+
definitionSnapshot: snapshot,
|
|
1261
1277
|
input,
|
|
1262
1278
|
launchOptions: preparedLaunchOptions(options),
|
|
1263
1279
|
runnerId,
|
|
@@ -1339,16 +1355,20 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1339
1355
|
},
|
|
1340
1356
|
conversation: {
|
|
1341
1357
|
beginAttempt: (contract) => run.recorder?.beginAttempt(contract),
|
|
1358
|
+
recoverAssistant: (contract) =>
|
|
1359
|
+
recoverAssistantStep(ctx.sessionManager.getBranch(), contract),
|
|
1342
1360
|
mark: () => run.recorder?.mark() ?? 0,
|
|
1343
1361
|
rangeSince: (mark) => run.recorder?.rangeSince(mark),
|
|
1344
1362
|
},
|
|
1345
1363
|
});
|
|
1346
|
-
// The
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1364
|
+
// The engine and session recorder share one transactional SQLite store.
|
|
1365
|
+
const store = workflowStore(
|
|
1366
|
+
ctx.cwd,
|
|
1367
|
+
queueStore === null || claimToken === undefined
|
|
1368
|
+
? {}
|
|
1369
|
+
: {
|
|
1370
|
+
authorityProvider: () => queueStore.workflowRunAuthority(runId, claimToken),
|
|
1371
|
+
},
|
|
1352
1372
|
);
|
|
1353
1373
|
const engine = new WorkflowEngine({
|
|
1354
1374
|
executor,
|
|
@@ -1373,8 +1393,8 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1373
1393
|
},
|
|
1374
1394
|
// Awaited by the engine after run_started is persisted, so the session
|
|
1375
1395
|
// binding and its trace event always precede node and terminal events.
|
|
1376
|
-
onRunStarted: async (
|
|
1377
|
-
const recorder = new SessionRecorder(store,
|
|
1396
|
+
onRunStarted: async (runId) => {
|
|
1397
|
+
const recorder = new SessionRecorder(store, runId);
|
|
1378
1398
|
try {
|
|
1379
1399
|
await recorder.bind(ctx);
|
|
1380
1400
|
run.recorder = recorder;
|
|
@@ -1474,23 +1494,22 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1474
1494
|
// once the cause is fixed, and say so in the feed.
|
|
1475
1495
|
let resumeFailed = run.resume === true && !isClaimLostError(error);
|
|
1476
1496
|
let terminalStatus: string | undefined;
|
|
1477
|
-
// Re-parking only makes sense while the
|
|
1478
|
-
// A terminal or waiting
|
|
1479
|
-
//
|
|
1480
|
-
// the bundle's real state, not a bogus failure.
|
|
1497
|
+
// Re-parking only makes sense while the run is still resumable.
|
|
1498
|
+
// A terminal or waiting run closes the queue row instead, and the feed
|
|
1499
|
+
// reports the authoritative run state.
|
|
1481
1500
|
if (resumeFailed) {
|
|
1482
1501
|
try {
|
|
1483
|
-
const bundle =
|
|
1502
|
+
const bundle = readWorkflowRun(runId);
|
|
1484
1503
|
if (bundle === null || bundle.state.status !== "running") {
|
|
1485
1504
|
resumeFailed = false;
|
|
1486
1505
|
terminalStatus = bundle?.state.status;
|
|
1487
1506
|
}
|
|
1488
1507
|
} catch {
|
|
1489
|
-
// Unreadable
|
|
1508
|
+
// Unreadable run state closes the row too.
|
|
1490
1509
|
resumeFailed = false;
|
|
1491
1510
|
}
|
|
1492
1511
|
}
|
|
1493
|
-
// A continuation that failed before its
|
|
1512
|
+
// A continuation that failed before its run row exists frees the
|
|
1494
1513
|
// parent's continuation slot instead of consuming it forever.
|
|
1495
1514
|
let continuationSlotFreed = false;
|
|
1496
1515
|
if (
|
|
@@ -1499,7 +1518,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1499
1518
|
!isClaimLostError(error)
|
|
1500
1519
|
) {
|
|
1501
1520
|
try {
|
|
1502
|
-
const bundle =
|
|
1521
|
+
const bundle = readWorkflowRun(runId);
|
|
1503
1522
|
if (bundle === null) {
|
|
1504
1523
|
continuationSlotFreed =
|
|
1505
1524
|
runQueueStore?.deleteWorkflowRun({ runId, claimToken: run.claimToken }) === true;
|
|
@@ -1577,7 +1596,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1577
1596
|
runnerId,
|
|
1578
1597
|
claimToken,
|
|
1579
1598
|
leaseMs: RUN_CLAIM_LEASE_MS,
|
|
1580
|
-
excludeRunIds: [
|
|
1599
|
+
excludeRunIds: [],
|
|
1581
1600
|
sessionId: ctx.sessionManager.getSessionId(),
|
|
1582
1601
|
});
|
|
1583
1602
|
if (claimed === undefined) {
|
|
@@ -1585,7 +1604,11 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1585
1604
|
}
|
|
1586
1605
|
let started: string | undefined;
|
|
1587
1606
|
try {
|
|
1588
|
-
const bundle =
|
|
1607
|
+
const bundle = readWorkflowRun(claimed.runId);
|
|
1608
|
+
if (bundle?.state.status === "waiting") {
|
|
1609
|
+
queueStore.parkWorkflowRun({ runId: claimed.runId, claimToken });
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1589
1612
|
const sourceRef =
|
|
1590
1613
|
bundle?.state.workflowSource === undefined
|
|
1591
1614
|
? claimed.workflowSourceRef
|
|
@@ -1595,7 +1618,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1595
1618
|
const launchOptions = parsePreparedLaunchOptions(claimed.launchOptions);
|
|
1596
1619
|
started = await startRun(ctx, sourceRef, claimed.input, {
|
|
1597
1620
|
...launchOptions,
|
|
1598
|
-
resume:
|
|
1621
|
+
resume: claimed.initialized,
|
|
1599
1622
|
runId: claimed.runId,
|
|
1600
1623
|
claimToken,
|
|
1601
1624
|
});
|
|
@@ -1623,16 +1646,13 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1623
1646
|
? { state: "running", runId: activeRun.runId }
|
|
1624
1647
|
: workflowSchedulerResult(activeRun.lastState);
|
|
1625
1648
|
}
|
|
1626
|
-
const store =
|
|
1627
|
-
const bundle =
|
|
1649
|
+
const store = workflowStore(controllerContext?.cwd ?? process.cwd());
|
|
1650
|
+
const bundle = store.readRun(request.runId);
|
|
1628
1651
|
if (bundle !== null) {
|
|
1629
1652
|
const recovered =
|
|
1630
1653
|
bundle.state.status === "running" ? await store.markRunInterrupted(request.runId) : bundle;
|
|
1631
1654
|
if (recovered !== null) {
|
|
1632
|
-
const lastTraceEvent =
|
|
1633
|
-
recovered.runDir,
|
|
1634
|
-
recovered.manifest.paths.trace,
|
|
1635
|
-
);
|
|
1655
|
+
const lastTraceEvent = recovered.traceEvents?.at(-1) ?? null;
|
|
1636
1656
|
return workflowSchedulerResult(recovered.state, lastTraceEvent?.type === "run_interrupted");
|
|
1637
1657
|
}
|
|
1638
1658
|
return { state: "pending" };
|
|
@@ -1640,7 +1660,6 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1640
1660
|
if (controllerContext === null) {
|
|
1641
1661
|
return { state: "pending" };
|
|
1642
1662
|
}
|
|
1643
|
-
await store.quarantineIncompleteRun(request.runId);
|
|
1644
1663
|
const runId = await startRun(controllerContext, request.workflow, request.input, {
|
|
1645
1664
|
runId: request.runId,
|
|
1646
1665
|
childKey,
|
|
@@ -1746,11 +1765,17 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1746
1765
|
notify(ctx, result.message, result.level);
|
|
1747
1766
|
};
|
|
1748
1767
|
|
|
1749
|
-
const cancelHumanDecision = async (
|
|
1750
|
-
|
|
1768
|
+
const cancelHumanDecision = async (
|
|
1769
|
+
ctx: ExtensionContext,
|
|
1770
|
+
request: HumanDecisionRequest,
|
|
1771
|
+
): Promise<void> => {
|
|
1772
|
+
const store = humanDecisionStore(ctx.cwd);
|
|
1751
1773
|
await store.cancel(request, "cancelled");
|
|
1752
1774
|
const cancellation = await store.readCancellation(request.decisionId);
|
|
1753
|
-
if (cancellation !== null)
|
|
1775
|
+
if (cancellation !== null) {
|
|
1776
|
+
await settleHumanDecisionChannels(cancellation);
|
|
1777
|
+
store.markEffectApplied(request.decisionId, "decision.settle_presentations");
|
|
1778
|
+
}
|
|
1754
1779
|
lastWaitingRunId = null;
|
|
1755
1780
|
};
|
|
1756
1781
|
|
|
@@ -1767,7 +1792,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1767
1792
|
const continued = new Set(
|
|
1768
1793
|
rows.map((row) => row.parentRunId).filter((parent): parent is string => parent !== null),
|
|
1769
1794
|
);
|
|
1770
|
-
const bundles =
|
|
1795
|
+
const bundles = listWorkflowRuns();
|
|
1771
1796
|
for (const bundle of bundles) {
|
|
1772
1797
|
if (
|
|
1773
1798
|
bundle.state.status !== "waiting" ||
|
|
@@ -1833,7 +1858,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1833
1858
|
const { state } = widgetSource;
|
|
1834
1859
|
const request = humanDecisionRequest(state.finalOutput);
|
|
1835
1860
|
if (state.status === "waiting" && request !== null) {
|
|
1836
|
-
await cancelHumanDecision(request);
|
|
1861
|
+
await cancelHumanDecision(ctx, request);
|
|
1837
1862
|
clearWidgetTimer();
|
|
1838
1863
|
clearWidget(ctx);
|
|
1839
1864
|
return {
|
|
@@ -1854,7 +1879,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1854
1879
|
}
|
|
1855
1880
|
const recovered = await findOwnedWaitingHumanDecision(ctx);
|
|
1856
1881
|
if (recovered !== null) {
|
|
1857
|
-
await cancelHumanDecision(recovered.request);
|
|
1882
|
+
await cancelHumanDecision(ctx, recovered.request);
|
|
1858
1883
|
return {
|
|
1859
1884
|
message: `Cancelled the pending human decision for workflow ${recovered.state.workflowName}.`,
|
|
1860
1885
|
details: {
|
|
@@ -1959,9 +1984,15 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1959
1984
|
runId?: string,
|
|
1960
1985
|
): Promise<WorkflowControlResult> => {
|
|
1961
1986
|
if (runId !== undefined) {
|
|
1962
|
-
const
|
|
1987
|
+
const launch = ensureRunQueueStore(ctx.cwd).getWorkflowRun(runId);
|
|
1988
|
+
if (
|
|
1989
|
+
launch !== undefined &&
|
|
1990
|
+
["queued", "starting", "failed", "cancelled"].includes(launch.status)
|
|
1991
|
+
) {
|
|
1992
|
+
return workflowLaunchStatus(launch);
|
|
1993
|
+
}
|
|
1994
|
+
const bundle = readWorkflowRun(runId);
|
|
1963
1995
|
if (bundle === null) {
|
|
1964
|
-
const launch = ensureRunQueueStore(ctx.cwd).getWorkflowRun(runId);
|
|
1965
1996
|
if (launch === undefined) throw new Error(`Workflow run not found: ${runId}`);
|
|
1966
1997
|
return workflowLaunchStatus(launch);
|
|
1967
1998
|
}
|
|
@@ -2008,7 +2039,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2008
2039
|
const continued = new Set(
|
|
2009
2040
|
rows.map((row) => row.parentRunId).filter((parent): parent is string => parent !== null),
|
|
2010
2041
|
);
|
|
2011
|
-
const bundles =
|
|
2042
|
+
const bundles = listWorkflowRuns();
|
|
2012
2043
|
parentRunId =
|
|
2013
2044
|
bundles.find(
|
|
2014
2045
|
(bundle) =>
|
|
@@ -2029,7 +2060,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2029
2060
|
) {
|
|
2030
2061
|
throw new Error(`Workflow run ${parentRunId} belongs to another Pi session.`);
|
|
2031
2062
|
}
|
|
2032
|
-
const parent =
|
|
2063
|
+
const parent = readWorkflowRun(parentRunId);
|
|
2033
2064
|
if (
|
|
2034
2065
|
parent === null ||
|
|
2035
2066
|
parent.state.status !== "waiting" ||
|
|
@@ -2056,7 +2087,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2056
2087
|
verified?: HumanDecisionChannelAnswer,
|
|
2057
2088
|
): Promise<WorkflowControlResult> => {
|
|
2058
2089
|
const waiting = await resolveWaitingWorkflow(ctx, requestedRunId, verified !== undefined);
|
|
2059
|
-
const parent =
|
|
2090
|
+
const parent = readWorkflowRun(waiting.parentRunId);
|
|
2060
2091
|
if (parent === null) throw new Error(`Workflow run ${waiting.parentRunId} is unreadable.`);
|
|
2061
2092
|
const request = humanDecisionRequest(parent.state.finalOutput);
|
|
2062
2093
|
let accepted: AcceptedHumanDecision | undefined;
|
|
@@ -2082,7 +2113,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2082
2113
|
},
|
|
2083
2114
|
idempotencyKey: verified?.idempotencyKey ?? createHumanDecisionAttemptId(),
|
|
2084
2115
|
};
|
|
2085
|
-
const store =
|
|
2116
|
+
const store = humanDecisionStore(ctx.cwd);
|
|
2086
2117
|
const acceptance = await store.accept(request, submission);
|
|
2087
2118
|
if (acceptance.status === "conflict" || acceptance.decision.provenance !== "human") {
|
|
2088
2119
|
throw new Error("That human decision was already answered differently.");
|
|
@@ -2110,22 +2141,9 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2110
2141
|
};
|
|
2111
2142
|
}
|
|
2112
2143
|
}
|
|
2113
|
-
const decisionStore =
|
|
2114
|
-
if (request !== null && continuationRunId !== undefined && accepted !== undefined) {
|
|
2115
|
-
await decisionStore.recordContinuation(request.decisionId, {
|
|
2116
|
-
schema: "pi-workflows.human-decision-continuation.v1",
|
|
2117
|
-
decisionId: request.decisionId,
|
|
2118
|
-
requestDigest: request.requestDigest,
|
|
2119
|
-
provenance: accepted.provenance,
|
|
2120
|
-
parentRunId: waiting.parentRunId,
|
|
2121
|
-
runId: continuationRunId,
|
|
2122
|
-
createdAt: accepted.acceptedAt,
|
|
2123
|
-
});
|
|
2124
|
-
}
|
|
2144
|
+
const decisionStore = humanDecisionStore(ctx.cwd);
|
|
2125
2145
|
if (continuationRunId !== undefined) {
|
|
2126
|
-
const existingContinuation =
|
|
2127
|
-
new WorkflowRunStore().runDirFor(continuationRunId),
|
|
2128
|
-
);
|
|
2146
|
+
const existingContinuation = readWorkflowRun(continuationRunId);
|
|
2129
2147
|
if (existingContinuation !== null) {
|
|
2130
2148
|
if (accepted !== undefined) await settleHumanDecisionChannels(accepted);
|
|
2131
2149
|
lastWaitingRunId = null;
|
|
@@ -2149,7 +2167,18 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2149
2167
|
throw new Error("Could not start the checkpoint continuation.");
|
|
2150
2168
|
}
|
|
2151
2169
|
if (request !== null && accepted !== undefined) {
|
|
2170
|
+
await decisionStore.recordContinuation(request.decisionId, {
|
|
2171
|
+
schema: "pi-workflows.human-decision-continuation.v1",
|
|
2172
|
+
decisionId: request.decisionId,
|
|
2173
|
+
requestDigest: request.requestDigest,
|
|
2174
|
+
provenance: accepted.provenance,
|
|
2175
|
+
parentRunId: waiting.parentRunId,
|
|
2176
|
+
runId: continued,
|
|
2177
|
+
createdAt: accepted.acceptedAt,
|
|
2178
|
+
});
|
|
2179
|
+
decisionStore.markEffectApplied(request.decisionId, "decision.continue");
|
|
2152
2180
|
await settleHumanDecisionChannels(accepted);
|
|
2181
|
+
decisionStore.markEffectApplied(request.decisionId, "decision.settle_presentations");
|
|
2153
2182
|
}
|
|
2154
2183
|
lastWaitingRunId = null;
|
|
2155
2184
|
return {
|
|
@@ -2170,7 +2199,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2170
2199
|
const channel = new PiDecisionChannel({
|
|
2171
2200
|
actorId: ctx.sessionManager.getSessionId(),
|
|
2172
2201
|
ui: ctx.ui,
|
|
2173
|
-
store:
|
|
2202
|
+
store: humanDecisionStore(ctx.cwd),
|
|
2174
2203
|
onAnswer: async (answer) => {
|
|
2175
2204
|
const result = await answerWorkflowControl(ctx, answer.response, request.runId, answer);
|
|
2176
2205
|
notify(ctx, result.message, result.level);
|
|
@@ -2207,7 +2236,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2207
2236
|
config: loaded.channels,
|
|
2208
2237
|
credentials: loaded.credentials,
|
|
2209
2238
|
configDir: loaded.configDir,
|
|
2210
|
-
store:
|
|
2239
|
+
store: humanDecisionStore(ctx.cwd),
|
|
2211
2240
|
onAnswer: async (answer) => {
|
|
2212
2241
|
await answerWorkflowControl(ctx, answer.response, answer.request.runId, answer);
|
|
2213
2242
|
},
|
|
@@ -2221,13 +2250,13 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2221
2250
|
ctx: ExtensionContext,
|
|
2222
2251
|
deliverPending = true,
|
|
2223
2252
|
): Promise<void> => {
|
|
2224
|
-
const runStore =
|
|
2225
|
-
const store =
|
|
2253
|
+
const runStore = workflowStore(ctx.cwd);
|
|
2254
|
+
const store = humanDecisionStore(ctx.cwd);
|
|
2226
2255
|
const requests = (await store.listRequests()).sort((left, right) =>
|
|
2227
2256
|
left.createdAt.localeCompare(right.createdAt),
|
|
2228
2257
|
);
|
|
2229
2258
|
for (const request of requests) {
|
|
2230
|
-
const parent =
|
|
2259
|
+
const parent = runStore.readRun(request.runId);
|
|
2231
2260
|
if (parent === null || parent.state.status !== "waiting") continue;
|
|
2232
2261
|
const currentRequest = humanDecisionRequest(parent.state.finalOutput);
|
|
2233
2262
|
if (
|
|
@@ -2242,6 +2271,19 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2242
2271
|
queueRecord !== undefined &&
|
|
2243
2272
|
(queueRecord.originSessionId === null ||
|
|
2244
2273
|
queueRecord.originSessionId === ctx.sessionManager.getSessionId());
|
|
2274
|
+
if (!ownedBySession) continue;
|
|
2275
|
+
const recoveryToken = randomUUID();
|
|
2276
|
+
const recoveryQueue = ensureRunQueueStore(ctx.cwd);
|
|
2277
|
+
const claimed = recoveryQueue.claimWorkflowRun({
|
|
2278
|
+
runId: request.runId,
|
|
2279
|
+
runnerId: ctx.sessionManager.getSessionId(),
|
|
2280
|
+
claimToken: recoveryToken,
|
|
2281
|
+
leaseMs: RUN_CLAIM_LEASE_MS,
|
|
2282
|
+
});
|
|
2283
|
+
if (claimed === undefined) continue;
|
|
2284
|
+
const ownerStore = humanDecisionStore(ctx.cwd, () =>
|
|
2285
|
+
recoveryQueue.workflowRunAuthority(request.runId, recoveryToken),
|
|
2286
|
+
);
|
|
2245
2287
|
let resolved = await store.readResolved(request.decisionId);
|
|
2246
2288
|
if (resolved === null) {
|
|
2247
2289
|
let cancellation = await store.readCancellation(request.decisionId);
|
|
@@ -2249,11 +2291,11 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2249
2291
|
request.expiresAt !== undefined && Date.parse(request.expiresAt) <= Date.now();
|
|
2250
2292
|
if (cancellation === null && expired) {
|
|
2251
2293
|
if (request.defaultResponse === undefined) {
|
|
2252
|
-
await
|
|
2253
|
-
cancellation = await
|
|
2294
|
+
await ownerStore.cancel(request, "expired");
|
|
2295
|
+
cancellation = await ownerStore.readCancellation(request.decisionId);
|
|
2254
2296
|
} else {
|
|
2255
2297
|
try {
|
|
2256
|
-
resolved = (await
|
|
2298
|
+
resolved = (await ownerStore.resolveTimeout(request)).decision;
|
|
2257
2299
|
} catch {
|
|
2258
2300
|
cancellation = await store.readCancellation(request.decisionId);
|
|
2259
2301
|
resolved = await store.readResolved(request.decisionId);
|
|
@@ -2261,11 +2303,19 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2261
2303
|
}
|
|
2262
2304
|
}
|
|
2263
2305
|
if (cancellation !== null) {
|
|
2306
|
+
recoveryQueue.completeWorkflowRun({ runId: request.runId, claimToken: recoveryToken });
|
|
2307
|
+
ownerStore.markEffectApplied(request.decisionId, "decision.cancel_parent");
|
|
2264
2308
|
await settleHumanDecisionChannels(cancellation);
|
|
2309
|
+
ownerStore.markEffectApplied(request.decisionId, "decision.settle_presentations");
|
|
2310
|
+
ownerStore.close();
|
|
2265
2311
|
continue;
|
|
2266
2312
|
}
|
|
2267
2313
|
if (resolved === null) {
|
|
2268
|
-
if (!deliverPending
|
|
2314
|
+
if (!deliverPending) {
|
|
2315
|
+
recoveryQueue.parkWorkflowRun({ runId: request.runId, claimToken: recoveryToken });
|
|
2316
|
+
ownerStore.close();
|
|
2317
|
+
continue;
|
|
2318
|
+
}
|
|
2269
2319
|
const channels = audienceChannels(decisionChannelConfig, request.audience);
|
|
2270
2320
|
for (const channelId of channels) {
|
|
2271
2321
|
const channel = telegramDecisionChannels.get(channelId);
|
|
@@ -2275,13 +2325,18 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2275
2325
|
lastWaitingRunId = request.runId;
|
|
2276
2326
|
queueMicrotask(() => void promptHumanDecision(ctx, request));
|
|
2277
2327
|
}
|
|
2328
|
+
recoveryQueue.parkWorkflowRun({ runId: request.runId, claimToken: recoveryToken });
|
|
2329
|
+
ownerStore.close();
|
|
2278
2330
|
continue;
|
|
2279
2331
|
}
|
|
2280
2332
|
}
|
|
2281
2333
|
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2334
|
+
const currentParent = runStore.readRun(request.runId);
|
|
2335
|
+
if (currentParent === null || currentParent.state.status !== "waiting") {
|
|
2336
|
+
recoveryQueue.parkWorkflowRun({ runId: request.runId, claimToken: recoveryToken });
|
|
2337
|
+
ownerStore.close();
|
|
2338
|
+
continue;
|
|
2339
|
+
}
|
|
2285
2340
|
const runId = `continuation-${request.decisionId.slice("decision-".length)}`;
|
|
2286
2341
|
const continuation = (await store.readContinuation(request.decisionId)) ?? {
|
|
2287
2342
|
schema: "pi-workflows.human-decision-continuation.v1" as const,
|
|
@@ -2292,10 +2347,16 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2292
2347
|
runId,
|
|
2293
2348
|
createdAt: resolved.acceptedAt,
|
|
2294
2349
|
};
|
|
2295
|
-
|
|
2296
|
-
|
|
2350
|
+
recoveryQueue.parkWorkflowRun({
|
|
2351
|
+
runId: request.runId,
|
|
2352
|
+
claimToken: recoveryToken,
|
|
2353
|
+
});
|
|
2354
|
+
const existing = runStore.readRun(continuation.runId);
|
|
2297
2355
|
if (existing === null && activeRun === null) {
|
|
2298
|
-
if (currentParent.state.workflowSource === undefined)
|
|
2356
|
+
if (currentParent.state.workflowSource === undefined) {
|
|
2357
|
+
ownerStore.close();
|
|
2358
|
+
continue;
|
|
2359
|
+
}
|
|
2299
2360
|
const workflowRef =
|
|
2300
2361
|
currentParent.state.workflowSource.kind === "builtin"
|
|
2301
2362
|
? `builtin:${currentParent.state.workflowSource.id}`
|
|
@@ -2307,7 +2368,13 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2307
2368
|
quiet: true,
|
|
2308
2369
|
});
|
|
2309
2370
|
}
|
|
2371
|
+
if (runStore.readRun(continuation.runId) !== null) {
|
|
2372
|
+
await ownerStore.recordContinuation(request.decisionId, continuation);
|
|
2373
|
+
ownerStore.markEffectApplied(request.decisionId, "decision.continue");
|
|
2374
|
+
}
|
|
2310
2375
|
await settleHumanDecisionChannels(resolved);
|
|
2376
|
+
ownerStore.markEffectApplied(request.decisionId, "decision.settle_presentations");
|
|
2377
|
+
ownerStore.close();
|
|
2311
2378
|
if (activeRun !== null) break;
|
|
2312
2379
|
}
|
|
2313
2380
|
};
|
|
@@ -2368,7 +2435,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2368
2435
|
}
|
|
2369
2436
|
const queue = ensureRunQueueStore(ctx.cwd);
|
|
2370
2437
|
const existing = queue.findSessionReservation(ctx.sessionManager.getSessionId());
|
|
2371
|
-
if (existing !== undefined) {
|
|
2438
|
+
if (existing !== undefined && existing.runId !== options.parentRunId) {
|
|
2372
2439
|
throw new Error(
|
|
2373
2440
|
`Workflow ${existing.workflowName} is already ${existing.status} (run ${existing.runId}).`,
|
|
2374
2441
|
);
|
|
@@ -2380,7 +2447,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2380
2447
|
const workflow = resolved.definition;
|
|
2381
2448
|
const workflowSource = resolved.source;
|
|
2382
2449
|
if (options.parentRunId !== undefined) {
|
|
2383
|
-
const parent =
|
|
2450
|
+
const parent = readWorkflowRun(options.parentRunId);
|
|
2384
2451
|
if (parent === null || parent.state.status !== "waiting") {
|
|
2385
2452
|
throw new Error(`Workflow run ${options.parentRunId} is not waiting at a checkpoint`);
|
|
2386
2453
|
}
|
|
@@ -2405,6 +2472,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2405
2472
|
workflowSource.kind === "builtin" ? `builtin:${workflowSource.id}` : workflowSource.path,
|
|
2406
2473
|
workflowSource: launchSourceIdentity(workflow, workflowSource),
|
|
2407
2474
|
definitionDigest: definitionDigest(snapshot),
|
|
2475
|
+
definitionSnapshot: snapshot,
|
|
2408
2476
|
input,
|
|
2409
2477
|
launchOptions: preparedLaunchOptions(options),
|
|
2410
2478
|
runnerId,
|
|
@@ -2613,7 +2681,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2613
2681
|
const message = errorMessage(error);
|
|
2614
2682
|
notify(
|
|
2615
2683
|
ctx,
|
|
2616
|
-
/
|
|
2684
|
+
/continuations\.parent_run_id/.test(message)
|
|
2617
2685
|
? "That checkpoint was already answered; see its continuation run."
|
|
2618
2686
|
: `Could not continue workflow: ${message}`,
|
|
2619
2687
|
"error",
|
|
@@ -2768,9 +2836,10 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2768
2836
|
label: "Workflow",
|
|
2769
2837
|
description: [
|
|
2770
2838
|
"List, start, inspect, pause, resume, cancel, answer, update, or complete pi-workflows runs.",
|
|
2771
|
-
"When the user asks to monitor, watch, poll, or check something repeatedly, start the built-in monitor workflow with input keys task,
|
|
2839
|
+
"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.",
|
|
2840
|
+
"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.",
|
|
2772
2841
|
"Use update or submit only when a workflow step contract asks for it, and pass the exact step and attempt ids.",
|
|
2773
|
-
"Do not start repeated work without the user's request
|
|
2842
|
+
"Do not start repeated work without the user's request.",
|
|
2774
2843
|
].join(" "),
|
|
2775
2844
|
parameters: WorkflowToolParameters,
|
|
2776
2845
|
async execute(toolCallId, rawParams, _signal, _onUpdate, ctx) {
|
|
@@ -2797,7 +2866,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2797
2866
|
break;
|
|
2798
2867
|
case "answer": {
|
|
2799
2868
|
const waiting = await resolveWaitingWorkflow(ctx, params.runId);
|
|
2800
|
-
const parent =
|
|
2869
|
+
const parent = readWorkflowRun(waiting.parentRunId);
|
|
2801
2870
|
if (parent !== null && humanDecisionRequest(parent.state.finalOutput) !== null) {
|
|
2802
2871
|
throw new Error(
|
|
2803
2872
|
"This checkpoint requires a verified human answer from Pi UI or a configured decision channel.",
|
|
@@ -2835,7 +2904,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2835
2904
|
}
|
|
2836
2905
|
throw new Error("No workflow step is waiting for output.");
|
|
2837
2906
|
}
|
|
2838
|
-
// Flush the conversation into
|
|
2907
|
+
// Flush the conversation into SQLite before accepting, so the
|
|
2839
2908
|
// attempt range includes the assistant message carrying this call.
|
|
2840
2909
|
await activeRun.recorder?.record(ctx).catch(() => undefined);
|
|
2841
2910
|
await activeRun.recorder?.synchronize(ctx).catch(() => undefined);
|
|
@@ -2882,24 +2951,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2882
2951
|
sessionClosed = false;
|
|
2883
2952
|
controllerContext = ctx;
|
|
2884
2953
|
if (herdrEnabled) void refreshHerdrCapability(ctx);
|
|
2885
|
-
|
|
2886
|
-
const queue = ensureRunQueueStore(ctx.cwd);
|
|
2887
|
-
const migration = await migrateLegacyWorkflowSources({
|
|
2888
|
-
catalog: builtinWorkflowCatalog,
|
|
2889
|
-
queue,
|
|
2890
|
-
});
|
|
2891
|
-
migrationBlockedRuns.clear();
|
|
2892
|
-
for (const blocked of migration.blocked) migrationBlockedRuns.add(blocked.runId);
|
|
2893
|
-
if (migration.blocked.length > 0) {
|
|
2894
|
-
notify(
|
|
2895
|
-
ctx,
|
|
2896
|
-
`Could not migrate ${migration.blocked.length} legacy workflow source(s).`,
|
|
2897
|
-
"warning",
|
|
2898
|
-
);
|
|
2899
|
-
}
|
|
2900
|
-
} catch (error) {
|
|
2901
|
-
notify(ctx, `Could not migrate legacy workflow sources: ${errorMessage(error)}`, "warning");
|
|
2902
|
-
}
|
|
2954
|
+
ensureRunQueueStore(ctx.cwd);
|
|
2903
2955
|
try {
|
|
2904
2956
|
await reloadDecisionChannels(ctx);
|
|
2905
2957
|
} catch {
|
|
@@ -2991,6 +3043,9 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2991
3043
|
|
|
2992
3044
|
pi.on("turn_end", async (event, ctx) => {
|
|
2993
3045
|
await activeRun?.recorder?.handleTurnEnd(event, ctx).catch(() => undefined);
|
|
3046
|
+
// turn_end observes the message after message_end replacement handlers,
|
|
3047
|
+
// so this is the exact assistant message that remains visible.
|
|
3048
|
+
activeRun?.executor.handleMessageEnd(event.message);
|
|
2994
3049
|
});
|
|
2995
3050
|
|
|
2996
3051
|
pi.on("message_start", async (event, ctx) => {
|
|
@@ -3045,11 +3100,9 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
3045
3100
|
return;
|
|
3046
3101
|
}
|
|
3047
3102
|
}
|
|
3048
|
-
const flushedNatural = turnCoordinator.flushNatural(
|
|
3049
|
-
ctx.isIdle() && !sessionClosed && !runHeld(),
|
|
3050
|
-
);
|
|
3051
3103
|
const run = activeRun;
|
|
3052
3104
|
if (!run) {
|
|
3105
|
+
turnCoordinator.flushNatural(ctx.isIdle() && !sessionClosed && !runHeld());
|
|
3053
3106
|
presentationPending = null;
|
|
3054
3107
|
runSyncPass(ctx);
|
|
3055
3108
|
return;
|
|
@@ -3057,7 +3110,16 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
3057
3110
|
await run.recorder?.synchronize(ctx).catch(() => undefined);
|
|
3058
3111
|
run.recorder?.settleAttempt();
|
|
3059
3112
|
run.executor.setStreaming(false);
|
|
3060
|
-
if (
|
|
3113
|
+
if (run.executor.pendingCompletion === "assistant") {
|
|
3114
|
+
// The visible response is the step output. Resolve it before any
|
|
3115
|
+
// unrelated deferred turn can start and replace the captured message.
|
|
3116
|
+
run.executor.handleAgentSettled();
|
|
3117
|
+
} else {
|
|
3118
|
+
const flushedNatural = turnCoordinator.flushNatural(
|
|
3119
|
+
ctx.isIdle() && !sessionClosed && !runHeld(),
|
|
3120
|
+
);
|
|
3121
|
+
if (flushedNatural === 0) run.executor.handleAgentSettled();
|
|
3122
|
+
}
|
|
3061
3123
|
runSyncPass(ctx);
|
|
3062
3124
|
});
|
|
3063
3125
|
|