@osolmaz/pi-workflows 0.12.1 → 0.13.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/README.md +59 -33
- package/dist/builtins/autodoc.workflow.d.ts +194 -7
- package/dist/builtins/autodoc.workflow.js +156 -30
- package/dist/builtins/autodoc.workflow.js.map +1 -1
- package/dist/builtins/autoimplement-command-batches.d.ts +1 -0
- package/dist/builtins/autoimplement-command-batches.js +29 -31
- package/dist/builtins/autoimplement-command-batches.js.map +1 -1
- package/dist/builtins/autoimplement.workflow.d.ts +1619 -97
- package/dist/builtins/autoimplement.workflow.js +416 -153
- package/dist/builtins/autoimplement.workflow.js.map +1 -1
- 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 +7 -5
- package/dist/builtins/catalog.js.map +1 -1
- package/dist/builtins/change-verification.workflow.d.ts +110 -0
- package/dist/builtins/change-verification.workflow.js +860 -0
- package/dist/builtins/change-verification.workflow.js.map +1 -0
- 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 +499 -217
- 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 +527 -26
- package/dist/builtins/plan-change.workflow.js +26 -0
- package/dist/builtins/plan-change.workflow.js.map +1 -1
- 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/builtins/workspace-preparation.workflow.d.ts +75 -0
- package/dist/builtins/workspace-preparation.workflow.js +498 -0
- package/dist/builtins/workspace-preparation.workflow.js.map +1 -0
- 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 +90 -6
- 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 -117
- 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 +27 -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/2026-08-24-change-scoped-verification-plan.md +419 -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 +111 -62
- 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/autodoc/SKILL.md +7 -0
- package/skills/autoimplement/SKILL.md +4 -0
- 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/autodoc.workflow.ts +184 -33
- package/src/builtins/autoimplement-command-batches.ts +39 -33
- package/src/builtins/autoimplement.workflow.ts +483 -175
- package/src/builtins/autoplan.workflow.ts +290 -40
- package/src/builtins/catalog.ts +7 -5
- package/src/builtins/change-verification.workflow.ts +1143 -0
- package/src/builtins/index.ts +7 -1
- package/src/builtins/monitor.workflow.ts +667 -250
- package/src/builtins/plain-summary.workflow.ts +185 -0
- package/src/builtins/plan-change.workflow.ts +35 -0
- package/src/builtins/sanity-check.workflow.ts +62 -7
- package/src/builtins/workspace-preparation.workflow.ts +668 -0
- 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 +106 -6
- package/src/extension/herdr-viewer.ts +0 -5
- package/src/extension/index.ts +180 -132
- 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.
|
|
@@ -548,7 +560,6 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
548
560
|
};
|
|
549
561
|
let activeRun: ActiveRun | null = null;
|
|
550
562
|
let systemTurnAbort: { contract: AgentStepContract; intentId?: string } | null = null;
|
|
551
|
-
let suppressWorkflowAssistantTail = false;
|
|
552
563
|
let lastExpiredAttempt: { contract: AgentStepContract; reason: string } | null = null;
|
|
553
564
|
// The interactive run currently parked at a checkpoint, if any.
|
|
554
565
|
let lastWaitingRunId: string | null = null;
|
|
@@ -838,7 +849,6 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
838
849
|
workflowViewTarget = {
|
|
839
850
|
runId: state.runId,
|
|
840
851
|
workflowName: state.workflowName,
|
|
841
|
-
runDir: path.resolve(new WorkflowRunStore().runDirFor(state.runId)),
|
|
842
852
|
};
|
|
843
853
|
renderWidget(ctx);
|
|
844
854
|
};
|
|
@@ -988,7 +998,6 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
988
998
|
resolution: "presentation",
|
|
989
999
|
send: (turnIntentId) => {
|
|
990
1000
|
presentationPending = run.generation;
|
|
991
|
-
suppressWorkflowAssistantTail = false;
|
|
992
1001
|
pi.sendMessage(
|
|
993
1002
|
{
|
|
994
1003
|
customType: PRESENTATION_MESSAGE_TYPE,
|
|
@@ -1048,12 +1057,16 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1048
1057
|
}
|
|
1049
1058
|
try {
|
|
1050
1059
|
if (outcome === "park") {
|
|
1051
|
-
runQueueStore.parkWorkflowRun({ runId: run.runId, claimToken: run.claimToken })
|
|
1052
|
-
|
|
1053
|
-
|
|
1060
|
+
if (runQueueStore.parkWorkflowRun({ runId: run.runId, claimToken: run.claimToken })) {
|
|
1061
|
+
runQueueStore.settleRunEffect(run.runId, "run.park_queue");
|
|
1062
|
+
}
|
|
1063
|
+
} else if (
|
|
1064
|
+
runQueueStore.completeWorkflowRun({ runId: run.runId, claimToken: run.claimToken })
|
|
1065
|
+
) {
|
|
1066
|
+
runQueueStore.settleRunEffect(run.runId, "run.settle_queue");
|
|
1054
1067
|
}
|
|
1055
1068
|
} catch {
|
|
1056
|
-
// Queue bookkeeping is best-effort next to the durable
|
|
1069
|
+
// Queue bookkeeping is best-effort next to the durable run fact.
|
|
1057
1070
|
} finally {
|
|
1058
1071
|
run.claimToken = undefined;
|
|
1059
1072
|
}
|
|
@@ -1068,7 +1081,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1068
1081
|
activeRun = null;
|
|
1069
1082
|
}
|
|
1070
1083
|
if (result.state.status === "running") {
|
|
1071
|
-
// The run was parked for another runner; its
|
|
1084
|
+
// The run was parked for another runner; its SQLite state stays resumable
|
|
1072
1085
|
// and its queue row becomes claimable. The recorder drains first: its
|
|
1073
1086
|
// finalization writes share the fenced store, so the claim must stay
|
|
1074
1087
|
// valid until capture is complete. Nothing else to present.
|
|
@@ -1095,7 +1108,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1095
1108
|
);
|
|
1096
1109
|
return;
|
|
1097
1110
|
}
|
|
1098
|
-
releaseClaim(run, "done");
|
|
1111
|
+
releaseClaim(run, result.state.status === "waiting" ? "park" : "done");
|
|
1099
1112
|
recordRunEvent({
|
|
1100
1113
|
runId: run.runId,
|
|
1101
1114
|
workflowRef: run.workflowName,
|
|
@@ -1223,7 +1236,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1223
1236
|
// refused continuation (edited source, missing parent) must not consume
|
|
1224
1237
|
// the parent's one-continuation slot.
|
|
1225
1238
|
if (options.parentRunId !== undefined) {
|
|
1226
|
-
const parent =
|
|
1239
|
+
const parent = readWorkflowRun(options.parentRunId);
|
|
1227
1240
|
if (parent === null || parent.state.status !== "waiting") {
|
|
1228
1241
|
throw new Error(`Workflow run ${options.parentRunId} is not waiting at a checkpoint`);
|
|
1229
1242
|
}
|
|
@@ -1258,6 +1271,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1258
1271
|
: workflowSource.path,
|
|
1259
1272
|
workflowSource: launchSourceIdentity(workflow, workflowSource),
|
|
1260
1273
|
definitionDigest: definitionDigest(snapshot),
|
|
1274
|
+
definitionSnapshot: snapshot,
|
|
1261
1275
|
input,
|
|
1262
1276
|
launchOptions: preparedLaunchOptions(options),
|
|
1263
1277
|
runnerId,
|
|
@@ -1339,16 +1353,20 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1339
1353
|
},
|
|
1340
1354
|
conversation: {
|
|
1341
1355
|
beginAttempt: (contract) => run.recorder?.beginAttempt(contract),
|
|
1356
|
+
recoverAssistant: (contract) =>
|
|
1357
|
+
recoverAssistantStep(ctx.sessionManager.getBranch(), contract),
|
|
1342
1358
|
mark: () => run.recorder?.mark() ?? 0,
|
|
1343
1359
|
rangeSince: (mark) => run.recorder?.rangeSince(mark),
|
|
1344
1360
|
},
|
|
1345
1361
|
});
|
|
1346
|
-
// The
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1362
|
+
// The engine and session recorder share one transactional SQLite store.
|
|
1363
|
+
const store = workflowStore(
|
|
1364
|
+
ctx.cwd,
|
|
1365
|
+
queueStore === null || claimToken === undefined
|
|
1366
|
+
? {}
|
|
1367
|
+
: {
|
|
1368
|
+
authorityProvider: () => queueStore.workflowRunAuthority(runId, claimToken),
|
|
1369
|
+
},
|
|
1352
1370
|
);
|
|
1353
1371
|
const engine = new WorkflowEngine({
|
|
1354
1372
|
executor,
|
|
@@ -1373,8 +1391,8 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1373
1391
|
},
|
|
1374
1392
|
// Awaited by the engine after run_started is persisted, so the session
|
|
1375
1393
|
// binding and its trace event always precede node and terminal events.
|
|
1376
|
-
onRunStarted: async (
|
|
1377
|
-
const recorder = new SessionRecorder(store,
|
|
1394
|
+
onRunStarted: async (runId) => {
|
|
1395
|
+
const recorder = new SessionRecorder(store, runId);
|
|
1378
1396
|
try {
|
|
1379
1397
|
await recorder.bind(ctx);
|
|
1380
1398
|
run.recorder = recorder;
|
|
@@ -1474,23 +1492,22 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1474
1492
|
// once the cause is fixed, and say so in the feed.
|
|
1475
1493
|
let resumeFailed = run.resume === true && !isClaimLostError(error);
|
|
1476
1494
|
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.
|
|
1495
|
+
// Re-parking only makes sense while the run is still resumable.
|
|
1496
|
+
// A terminal or waiting run closes the queue row instead, and the feed
|
|
1497
|
+
// reports the authoritative run state.
|
|
1481
1498
|
if (resumeFailed) {
|
|
1482
1499
|
try {
|
|
1483
|
-
const bundle =
|
|
1500
|
+
const bundle = readWorkflowRun(runId);
|
|
1484
1501
|
if (bundle === null || bundle.state.status !== "running") {
|
|
1485
1502
|
resumeFailed = false;
|
|
1486
1503
|
terminalStatus = bundle?.state.status;
|
|
1487
1504
|
}
|
|
1488
1505
|
} catch {
|
|
1489
|
-
// Unreadable
|
|
1506
|
+
// Unreadable run state closes the row too.
|
|
1490
1507
|
resumeFailed = false;
|
|
1491
1508
|
}
|
|
1492
1509
|
}
|
|
1493
|
-
// A continuation that failed before its
|
|
1510
|
+
// A continuation that failed before its run row exists frees the
|
|
1494
1511
|
// parent's continuation slot instead of consuming it forever.
|
|
1495
1512
|
let continuationSlotFreed = false;
|
|
1496
1513
|
if (
|
|
@@ -1499,7 +1516,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1499
1516
|
!isClaimLostError(error)
|
|
1500
1517
|
) {
|
|
1501
1518
|
try {
|
|
1502
|
-
const bundle =
|
|
1519
|
+
const bundle = readWorkflowRun(runId);
|
|
1503
1520
|
if (bundle === null) {
|
|
1504
1521
|
continuationSlotFreed =
|
|
1505
1522
|
runQueueStore?.deleteWorkflowRun({ runId, claimToken: run.claimToken }) === true;
|
|
@@ -1577,7 +1594,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1577
1594
|
runnerId,
|
|
1578
1595
|
claimToken,
|
|
1579
1596
|
leaseMs: RUN_CLAIM_LEASE_MS,
|
|
1580
|
-
excludeRunIds: [
|
|
1597
|
+
excludeRunIds: [],
|
|
1581
1598
|
sessionId: ctx.sessionManager.getSessionId(),
|
|
1582
1599
|
});
|
|
1583
1600
|
if (claimed === undefined) {
|
|
@@ -1585,7 +1602,11 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1585
1602
|
}
|
|
1586
1603
|
let started: string | undefined;
|
|
1587
1604
|
try {
|
|
1588
|
-
const bundle =
|
|
1605
|
+
const bundle = readWorkflowRun(claimed.runId);
|
|
1606
|
+
if (bundle?.state.status === "waiting") {
|
|
1607
|
+
queueStore.parkWorkflowRun({ runId: claimed.runId, claimToken });
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1589
1610
|
const sourceRef =
|
|
1590
1611
|
bundle?.state.workflowSource === undefined
|
|
1591
1612
|
? claimed.workflowSourceRef
|
|
@@ -1595,7 +1616,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1595
1616
|
const launchOptions = parsePreparedLaunchOptions(claimed.launchOptions);
|
|
1596
1617
|
started = await startRun(ctx, sourceRef, claimed.input, {
|
|
1597
1618
|
...launchOptions,
|
|
1598
|
-
resume:
|
|
1619
|
+
resume: claimed.initialized,
|
|
1599
1620
|
runId: claimed.runId,
|
|
1600
1621
|
claimToken,
|
|
1601
1622
|
});
|
|
@@ -1623,16 +1644,13 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1623
1644
|
? { state: "running", runId: activeRun.runId }
|
|
1624
1645
|
: workflowSchedulerResult(activeRun.lastState);
|
|
1625
1646
|
}
|
|
1626
|
-
const store =
|
|
1627
|
-
const bundle =
|
|
1647
|
+
const store = workflowStore(controllerContext?.cwd ?? process.cwd());
|
|
1648
|
+
const bundle = store.readRun(request.runId);
|
|
1628
1649
|
if (bundle !== null) {
|
|
1629
1650
|
const recovered =
|
|
1630
1651
|
bundle.state.status === "running" ? await store.markRunInterrupted(request.runId) : bundle;
|
|
1631
1652
|
if (recovered !== null) {
|
|
1632
|
-
const lastTraceEvent =
|
|
1633
|
-
recovered.runDir,
|
|
1634
|
-
recovered.manifest.paths.trace,
|
|
1635
|
-
);
|
|
1653
|
+
const lastTraceEvent = recovered.traceEvents?.at(-1) ?? null;
|
|
1636
1654
|
return workflowSchedulerResult(recovered.state, lastTraceEvent?.type === "run_interrupted");
|
|
1637
1655
|
}
|
|
1638
1656
|
return { state: "pending" };
|
|
@@ -1640,7 +1658,6 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1640
1658
|
if (controllerContext === null) {
|
|
1641
1659
|
return { state: "pending" };
|
|
1642
1660
|
}
|
|
1643
|
-
await store.quarantineIncompleteRun(request.runId);
|
|
1644
1661
|
const runId = await startRun(controllerContext, request.workflow, request.input, {
|
|
1645
1662
|
runId: request.runId,
|
|
1646
1663
|
childKey,
|
|
@@ -1746,11 +1763,17 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1746
1763
|
notify(ctx, result.message, result.level);
|
|
1747
1764
|
};
|
|
1748
1765
|
|
|
1749
|
-
const cancelHumanDecision = async (
|
|
1750
|
-
|
|
1766
|
+
const cancelHumanDecision = async (
|
|
1767
|
+
ctx: ExtensionContext,
|
|
1768
|
+
request: HumanDecisionRequest,
|
|
1769
|
+
): Promise<void> => {
|
|
1770
|
+
const store = humanDecisionStore(ctx.cwd);
|
|
1751
1771
|
await store.cancel(request, "cancelled");
|
|
1752
1772
|
const cancellation = await store.readCancellation(request.decisionId);
|
|
1753
|
-
if (cancellation !== null)
|
|
1773
|
+
if (cancellation !== null) {
|
|
1774
|
+
await settleHumanDecisionChannels(cancellation);
|
|
1775
|
+
store.markEffectApplied(request.decisionId, "decision.settle_presentations");
|
|
1776
|
+
}
|
|
1754
1777
|
lastWaitingRunId = null;
|
|
1755
1778
|
};
|
|
1756
1779
|
|
|
@@ -1767,7 +1790,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1767
1790
|
const continued = new Set(
|
|
1768
1791
|
rows.map((row) => row.parentRunId).filter((parent): parent is string => parent !== null),
|
|
1769
1792
|
);
|
|
1770
|
-
const bundles =
|
|
1793
|
+
const bundles = listWorkflowRuns();
|
|
1771
1794
|
for (const bundle of bundles) {
|
|
1772
1795
|
if (
|
|
1773
1796
|
bundle.state.status !== "waiting" ||
|
|
@@ -1833,7 +1856,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1833
1856
|
const { state } = widgetSource;
|
|
1834
1857
|
const request = humanDecisionRequest(state.finalOutput);
|
|
1835
1858
|
if (state.status === "waiting" && request !== null) {
|
|
1836
|
-
await cancelHumanDecision(request);
|
|
1859
|
+
await cancelHumanDecision(ctx, request);
|
|
1837
1860
|
clearWidgetTimer();
|
|
1838
1861
|
clearWidget(ctx);
|
|
1839
1862
|
return {
|
|
@@ -1854,7 +1877,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1854
1877
|
}
|
|
1855
1878
|
const recovered = await findOwnedWaitingHumanDecision(ctx);
|
|
1856
1879
|
if (recovered !== null) {
|
|
1857
|
-
await cancelHumanDecision(recovered.request);
|
|
1880
|
+
await cancelHumanDecision(ctx, recovered.request);
|
|
1858
1881
|
return {
|
|
1859
1882
|
message: `Cancelled the pending human decision for workflow ${recovered.state.workflowName}.`,
|
|
1860
1883
|
details: {
|
|
@@ -1959,9 +1982,15 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1959
1982
|
runId?: string,
|
|
1960
1983
|
): Promise<WorkflowControlResult> => {
|
|
1961
1984
|
if (runId !== undefined) {
|
|
1962
|
-
const
|
|
1985
|
+
const launch = ensureRunQueueStore(ctx.cwd).getWorkflowRun(runId);
|
|
1986
|
+
if (
|
|
1987
|
+
launch !== undefined &&
|
|
1988
|
+
["queued", "starting", "failed", "cancelled"].includes(launch.status)
|
|
1989
|
+
) {
|
|
1990
|
+
return workflowLaunchStatus(launch);
|
|
1991
|
+
}
|
|
1992
|
+
const bundle = readWorkflowRun(runId);
|
|
1963
1993
|
if (bundle === null) {
|
|
1964
|
-
const launch = ensureRunQueueStore(ctx.cwd).getWorkflowRun(runId);
|
|
1965
1994
|
if (launch === undefined) throw new Error(`Workflow run not found: ${runId}`);
|
|
1966
1995
|
return workflowLaunchStatus(launch);
|
|
1967
1996
|
}
|
|
@@ -2008,7 +2037,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2008
2037
|
const continued = new Set(
|
|
2009
2038
|
rows.map((row) => row.parentRunId).filter((parent): parent is string => parent !== null),
|
|
2010
2039
|
);
|
|
2011
|
-
const bundles =
|
|
2040
|
+
const bundles = listWorkflowRuns();
|
|
2012
2041
|
parentRunId =
|
|
2013
2042
|
bundles.find(
|
|
2014
2043
|
(bundle) =>
|
|
@@ -2029,7 +2058,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2029
2058
|
) {
|
|
2030
2059
|
throw new Error(`Workflow run ${parentRunId} belongs to another Pi session.`);
|
|
2031
2060
|
}
|
|
2032
|
-
const parent =
|
|
2061
|
+
const parent = readWorkflowRun(parentRunId);
|
|
2033
2062
|
if (
|
|
2034
2063
|
parent === null ||
|
|
2035
2064
|
parent.state.status !== "waiting" ||
|
|
@@ -2056,7 +2085,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2056
2085
|
verified?: HumanDecisionChannelAnswer,
|
|
2057
2086
|
): Promise<WorkflowControlResult> => {
|
|
2058
2087
|
const waiting = await resolveWaitingWorkflow(ctx, requestedRunId, verified !== undefined);
|
|
2059
|
-
const parent =
|
|
2088
|
+
const parent = readWorkflowRun(waiting.parentRunId);
|
|
2060
2089
|
if (parent === null) throw new Error(`Workflow run ${waiting.parentRunId} is unreadable.`);
|
|
2061
2090
|
const request = humanDecisionRequest(parent.state.finalOutput);
|
|
2062
2091
|
let accepted: AcceptedHumanDecision | undefined;
|
|
@@ -2082,7 +2111,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2082
2111
|
},
|
|
2083
2112
|
idempotencyKey: verified?.idempotencyKey ?? createHumanDecisionAttemptId(),
|
|
2084
2113
|
};
|
|
2085
|
-
const store =
|
|
2114
|
+
const store = humanDecisionStore(ctx.cwd);
|
|
2086
2115
|
const acceptance = await store.accept(request, submission);
|
|
2087
2116
|
if (acceptance.status === "conflict" || acceptance.decision.provenance !== "human") {
|
|
2088
2117
|
throw new Error("That human decision was already answered differently.");
|
|
@@ -2110,22 +2139,9 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2110
2139
|
};
|
|
2111
2140
|
}
|
|
2112
2141
|
}
|
|
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
|
-
}
|
|
2142
|
+
const decisionStore = humanDecisionStore(ctx.cwd);
|
|
2125
2143
|
if (continuationRunId !== undefined) {
|
|
2126
|
-
const existingContinuation =
|
|
2127
|
-
new WorkflowRunStore().runDirFor(continuationRunId),
|
|
2128
|
-
);
|
|
2144
|
+
const existingContinuation = readWorkflowRun(continuationRunId);
|
|
2129
2145
|
if (existingContinuation !== null) {
|
|
2130
2146
|
if (accepted !== undefined) await settleHumanDecisionChannels(accepted);
|
|
2131
2147
|
lastWaitingRunId = null;
|
|
@@ -2149,7 +2165,18 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2149
2165
|
throw new Error("Could not start the checkpoint continuation.");
|
|
2150
2166
|
}
|
|
2151
2167
|
if (request !== null && accepted !== undefined) {
|
|
2168
|
+
await decisionStore.recordContinuation(request.decisionId, {
|
|
2169
|
+
schema: "pi-workflows.human-decision-continuation.v1",
|
|
2170
|
+
decisionId: request.decisionId,
|
|
2171
|
+
requestDigest: request.requestDigest,
|
|
2172
|
+
provenance: accepted.provenance,
|
|
2173
|
+
parentRunId: waiting.parentRunId,
|
|
2174
|
+
runId: continued,
|
|
2175
|
+
createdAt: accepted.acceptedAt,
|
|
2176
|
+
});
|
|
2177
|
+
decisionStore.markEffectApplied(request.decisionId, "decision.continue");
|
|
2152
2178
|
await settleHumanDecisionChannels(accepted);
|
|
2179
|
+
decisionStore.markEffectApplied(request.decisionId, "decision.settle_presentations");
|
|
2153
2180
|
}
|
|
2154
2181
|
lastWaitingRunId = null;
|
|
2155
2182
|
return {
|
|
@@ -2170,7 +2197,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2170
2197
|
const channel = new PiDecisionChannel({
|
|
2171
2198
|
actorId: ctx.sessionManager.getSessionId(),
|
|
2172
2199
|
ui: ctx.ui,
|
|
2173
|
-
store:
|
|
2200
|
+
store: humanDecisionStore(ctx.cwd),
|
|
2174
2201
|
onAnswer: async (answer) => {
|
|
2175
2202
|
const result = await answerWorkflowControl(ctx, answer.response, request.runId, answer);
|
|
2176
2203
|
notify(ctx, result.message, result.level);
|
|
@@ -2207,7 +2234,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2207
2234
|
config: loaded.channels,
|
|
2208
2235
|
credentials: loaded.credentials,
|
|
2209
2236
|
configDir: loaded.configDir,
|
|
2210
|
-
store:
|
|
2237
|
+
store: humanDecisionStore(ctx.cwd),
|
|
2211
2238
|
onAnswer: async (answer) => {
|
|
2212
2239
|
await answerWorkflowControl(ctx, answer.response, answer.request.runId, answer);
|
|
2213
2240
|
},
|
|
@@ -2221,13 +2248,13 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2221
2248
|
ctx: ExtensionContext,
|
|
2222
2249
|
deliverPending = true,
|
|
2223
2250
|
): Promise<void> => {
|
|
2224
|
-
const runStore =
|
|
2225
|
-
const store =
|
|
2251
|
+
const runStore = workflowStore(ctx.cwd);
|
|
2252
|
+
const store = humanDecisionStore(ctx.cwd);
|
|
2226
2253
|
const requests = (await store.listRequests()).sort((left, right) =>
|
|
2227
2254
|
left.createdAt.localeCompare(right.createdAt),
|
|
2228
2255
|
);
|
|
2229
2256
|
for (const request of requests) {
|
|
2230
|
-
const parent =
|
|
2257
|
+
const parent = runStore.readRun(request.runId);
|
|
2231
2258
|
if (parent === null || parent.state.status !== "waiting") continue;
|
|
2232
2259
|
const currentRequest = humanDecisionRequest(parent.state.finalOutput);
|
|
2233
2260
|
if (
|
|
@@ -2242,6 +2269,19 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2242
2269
|
queueRecord !== undefined &&
|
|
2243
2270
|
(queueRecord.originSessionId === null ||
|
|
2244
2271
|
queueRecord.originSessionId === ctx.sessionManager.getSessionId());
|
|
2272
|
+
if (!ownedBySession) continue;
|
|
2273
|
+
const recoveryToken = randomUUID();
|
|
2274
|
+
const recoveryQueue = ensureRunQueueStore(ctx.cwd);
|
|
2275
|
+
const claimed = recoveryQueue.claimWorkflowRun({
|
|
2276
|
+
runId: request.runId,
|
|
2277
|
+
runnerId: ctx.sessionManager.getSessionId(),
|
|
2278
|
+
claimToken: recoveryToken,
|
|
2279
|
+
leaseMs: RUN_CLAIM_LEASE_MS,
|
|
2280
|
+
});
|
|
2281
|
+
if (claimed === undefined) continue;
|
|
2282
|
+
const ownerStore = humanDecisionStore(ctx.cwd, () =>
|
|
2283
|
+
recoveryQueue.workflowRunAuthority(request.runId, recoveryToken),
|
|
2284
|
+
);
|
|
2245
2285
|
let resolved = await store.readResolved(request.decisionId);
|
|
2246
2286
|
if (resolved === null) {
|
|
2247
2287
|
let cancellation = await store.readCancellation(request.decisionId);
|
|
@@ -2249,11 +2289,11 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2249
2289
|
request.expiresAt !== undefined && Date.parse(request.expiresAt) <= Date.now();
|
|
2250
2290
|
if (cancellation === null && expired) {
|
|
2251
2291
|
if (request.defaultResponse === undefined) {
|
|
2252
|
-
await
|
|
2253
|
-
cancellation = await
|
|
2292
|
+
await ownerStore.cancel(request, "expired");
|
|
2293
|
+
cancellation = await ownerStore.readCancellation(request.decisionId);
|
|
2254
2294
|
} else {
|
|
2255
2295
|
try {
|
|
2256
|
-
resolved = (await
|
|
2296
|
+
resolved = (await ownerStore.resolveTimeout(request)).decision;
|
|
2257
2297
|
} catch {
|
|
2258
2298
|
cancellation = await store.readCancellation(request.decisionId);
|
|
2259
2299
|
resolved = await store.readResolved(request.decisionId);
|
|
@@ -2261,11 +2301,19 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2261
2301
|
}
|
|
2262
2302
|
}
|
|
2263
2303
|
if (cancellation !== null) {
|
|
2304
|
+
recoveryQueue.completeWorkflowRun({ runId: request.runId, claimToken: recoveryToken });
|
|
2305
|
+
ownerStore.markEffectApplied(request.decisionId, "decision.cancel_parent");
|
|
2264
2306
|
await settleHumanDecisionChannels(cancellation);
|
|
2307
|
+
ownerStore.markEffectApplied(request.decisionId, "decision.settle_presentations");
|
|
2308
|
+
ownerStore.close();
|
|
2265
2309
|
continue;
|
|
2266
2310
|
}
|
|
2267
2311
|
if (resolved === null) {
|
|
2268
|
-
if (!deliverPending
|
|
2312
|
+
if (!deliverPending) {
|
|
2313
|
+
recoveryQueue.parkWorkflowRun({ runId: request.runId, claimToken: recoveryToken });
|
|
2314
|
+
ownerStore.close();
|
|
2315
|
+
continue;
|
|
2316
|
+
}
|
|
2269
2317
|
const channels = audienceChannels(decisionChannelConfig, request.audience);
|
|
2270
2318
|
for (const channelId of channels) {
|
|
2271
2319
|
const channel = telegramDecisionChannels.get(channelId);
|
|
@@ -2275,13 +2323,18 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2275
2323
|
lastWaitingRunId = request.runId;
|
|
2276
2324
|
queueMicrotask(() => void promptHumanDecision(ctx, request));
|
|
2277
2325
|
}
|
|
2326
|
+
recoveryQueue.parkWorkflowRun({ runId: request.runId, claimToken: recoveryToken });
|
|
2327
|
+
ownerStore.close();
|
|
2278
2328
|
continue;
|
|
2279
2329
|
}
|
|
2280
2330
|
}
|
|
2281
2331
|
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2332
|
+
const currentParent = runStore.readRun(request.runId);
|
|
2333
|
+
if (currentParent === null || currentParent.state.status !== "waiting") {
|
|
2334
|
+
recoveryQueue.parkWorkflowRun({ runId: request.runId, claimToken: recoveryToken });
|
|
2335
|
+
ownerStore.close();
|
|
2336
|
+
continue;
|
|
2337
|
+
}
|
|
2285
2338
|
const runId = `continuation-${request.decisionId.slice("decision-".length)}`;
|
|
2286
2339
|
const continuation = (await store.readContinuation(request.decisionId)) ?? {
|
|
2287
2340
|
schema: "pi-workflows.human-decision-continuation.v1" as const,
|
|
@@ -2292,10 +2345,16 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2292
2345
|
runId,
|
|
2293
2346
|
createdAt: resolved.acceptedAt,
|
|
2294
2347
|
};
|
|
2295
|
-
|
|
2296
|
-
|
|
2348
|
+
recoveryQueue.parkWorkflowRun({
|
|
2349
|
+
runId: request.runId,
|
|
2350
|
+
claimToken: recoveryToken,
|
|
2351
|
+
});
|
|
2352
|
+
const existing = runStore.readRun(continuation.runId);
|
|
2297
2353
|
if (existing === null && activeRun === null) {
|
|
2298
|
-
if (currentParent.state.workflowSource === undefined)
|
|
2354
|
+
if (currentParent.state.workflowSource === undefined) {
|
|
2355
|
+
ownerStore.close();
|
|
2356
|
+
continue;
|
|
2357
|
+
}
|
|
2299
2358
|
const workflowRef =
|
|
2300
2359
|
currentParent.state.workflowSource.kind === "builtin"
|
|
2301
2360
|
? `builtin:${currentParent.state.workflowSource.id}`
|
|
@@ -2307,7 +2366,13 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2307
2366
|
quiet: true,
|
|
2308
2367
|
});
|
|
2309
2368
|
}
|
|
2369
|
+
if (runStore.readRun(continuation.runId) !== null) {
|
|
2370
|
+
await ownerStore.recordContinuation(request.decisionId, continuation);
|
|
2371
|
+
ownerStore.markEffectApplied(request.decisionId, "decision.continue");
|
|
2372
|
+
}
|
|
2310
2373
|
await settleHumanDecisionChannels(resolved);
|
|
2374
|
+
ownerStore.markEffectApplied(request.decisionId, "decision.settle_presentations");
|
|
2375
|
+
ownerStore.close();
|
|
2311
2376
|
if (activeRun !== null) break;
|
|
2312
2377
|
}
|
|
2313
2378
|
};
|
|
@@ -2368,7 +2433,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2368
2433
|
}
|
|
2369
2434
|
const queue = ensureRunQueueStore(ctx.cwd);
|
|
2370
2435
|
const existing = queue.findSessionReservation(ctx.sessionManager.getSessionId());
|
|
2371
|
-
if (existing !== undefined) {
|
|
2436
|
+
if (existing !== undefined && existing.runId !== options.parentRunId) {
|
|
2372
2437
|
throw new Error(
|
|
2373
2438
|
`Workflow ${existing.workflowName} is already ${existing.status} (run ${existing.runId}).`,
|
|
2374
2439
|
);
|
|
@@ -2380,7 +2445,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2380
2445
|
const workflow = resolved.definition;
|
|
2381
2446
|
const workflowSource = resolved.source;
|
|
2382
2447
|
if (options.parentRunId !== undefined) {
|
|
2383
|
-
const parent =
|
|
2448
|
+
const parent = readWorkflowRun(options.parentRunId);
|
|
2384
2449
|
if (parent === null || parent.state.status !== "waiting") {
|
|
2385
2450
|
throw new Error(`Workflow run ${options.parentRunId} is not waiting at a checkpoint`);
|
|
2386
2451
|
}
|
|
@@ -2405,6 +2470,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2405
2470
|
workflowSource.kind === "builtin" ? `builtin:${workflowSource.id}` : workflowSource.path,
|
|
2406
2471
|
workflowSource: launchSourceIdentity(workflow, workflowSource),
|
|
2407
2472
|
definitionDigest: definitionDigest(snapshot),
|
|
2473
|
+
definitionSnapshot: snapshot,
|
|
2408
2474
|
input,
|
|
2409
2475
|
launchOptions: preparedLaunchOptions(options),
|
|
2410
2476
|
runnerId,
|
|
@@ -2613,7 +2679,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2613
2679
|
const message = errorMessage(error);
|
|
2614
2680
|
notify(
|
|
2615
2681
|
ctx,
|
|
2616
|
-
/
|
|
2682
|
+
/continuations\.parent_run_id/.test(message)
|
|
2617
2683
|
? "That checkpoint was already answered; see its continuation run."
|
|
2618
2684
|
: `Could not continue workflow: ${message}`,
|
|
2619
2685
|
"error",
|
|
@@ -2768,9 +2834,10 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2768
2834
|
label: "Workflow",
|
|
2769
2835
|
description: [
|
|
2770
2836
|
"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,
|
|
2837
|
+
"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.",
|
|
2838
|
+
"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
2839
|
"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
|
|
2840
|
+
"Do not start repeated work without the user's request.",
|
|
2774
2841
|
].join(" "),
|
|
2775
2842
|
parameters: WorkflowToolParameters,
|
|
2776
2843
|
async execute(toolCallId, rawParams, _signal, _onUpdate, ctx) {
|
|
@@ -2797,7 +2864,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2797
2864
|
break;
|
|
2798
2865
|
case "answer": {
|
|
2799
2866
|
const waiting = await resolveWaitingWorkflow(ctx, params.runId);
|
|
2800
|
-
const parent =
|
|
2867
|
+
const parent = readWorkflowRun(waiting.parentRunId);
|
|
2801
2868
|
if (parent !== null && humanDecisionRequest(parent.state.finalOutput) !== null) {
|
|
2802
2869
|
throw new Error(
|
|
2803
2870
|
"This checkpoint requires a verified human answer from Pi UI or a configured decision channel.",
|
|
@@ -2835,7 +2902,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2835
2902
|
}
|
|
2836
2903
|
throw new Error("No workflow step is waiting for output.");
|
|
2837
2904
|
}
|
|
2838
|
-
// Flush the conversation into
|
|
2905
|
+
// Flush the conversation into SQLite before accepting, so the
|
|
2839
2906
|
// attempt range includes the assistant message carrying this call.
|
|
2840
2907
|
await activeRun.recorder?.record(ctx).catch(() => undefined);
|
|
2841
2908
|
await activeRun.recorder?.synchronize(ctx).catch(() => undefined);
|
|
@@ -2847,7 +2914,6 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2847
2914
|
if (!result.accepted) {
|
|
2848
2915
|
throw new Error(result.message);
|
|
2849
2916
|
}
|
|
2850
|
-
suppressWorkflowAssistantTail = true;
|
|
2851
2917
|
return {
|
|
2852
2918
|
content: [{ type: "text", text: result.message }],
|
|
2853
2919
|
details: { action: "submit", step: params.step, accepted: true },
|
|
@@ -2882,24 +2948,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2882
2948
|
sessionClosed = false;
|
|
2883
2949
|
controllerContext = ctx;
|
|
2884
2950
|
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
|
-
}
|
|
2951
|
+
ensureRunQueueStore(ctx.cwd);
|
|
2903
2952
|
try {
|
|
2904
2953
|
await reloadDecisionChannels(ctx);
|
|
2905
2954
|
} catch {
|
|
@@ -2942,7 +2991,6 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2942
2991
|
});
|
|
2943
2992
|
|
|
2944
2993
|
pi.on("agent_start", () => {
|
|
2945
|
-
suppressWorkflowAssistantTail = false;
|
|
2946
2994
|
if (!activeRun && presentationPending === null && presentationAbort) {
|
|
2947
2995
|
// A normal user turn started while an async presentation prompt was
|
|
2948
2996
|
// still resolving. The user's new request supersedes that old result.
|
|
@@ -2953,7 +3001,6 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2953
3001
|
});
|
|
2954
3002
|
|
|
2955
3003
|
pi.on("agent_end", (event, ctx) => {
|
|
2956
|
-
suppressWorkflowAssistantTail = false;
|
|
2957
3004
|
const aborted = event.messages.some(
|
|
2958
3005
|
(message) =>
|
|
2959
3006
|
typeof message === "object" &&
|
|
@@ -2991,6 +3038,9 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2991
3038
|
|
|
2992
3039
|
pi.on("turn_end", async (event, ctx) => {
|
|
2993
3040
|
await activeRun?.recorder?.handleTurnEnd(event, ctx).catch(() => undefined);
|
|
3041
|
+
// turn_end observes the message after message_end replacement handlers,
|
|
3042
|
+
// so this is the exact assistant message that remains visible.
|
|
3043
|
+
activeRun?.executor.handleMessageEnd(event.message);
|
|
2994
3044
|
});
|
|
2995
3045
|
|
|
2996
3046
|
pi.on("message_start", async (event, ctx) => {
|
|
@@ -3002,14 +3052,6 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
3002
3052
|
});
|
|
3003
3053
|
|
|
3004
3054
|
pi.on("message_end", (event) => {
|
|
3005
|
-
if (suppressWorkflowAssistantTail && event.message.role === "assistant") {
|
|
3006
|
-
const message = {
|
|
3007
|
-
...event.message,
|
|
3008
|
-
content: event.message.content.filter((part) => part.type !== "text"),
|
|
3009
|
-
};
|
|
3010
|
-
activeRun?.recorder?.handleMessageEnd({ ...event, message });
|
|
3011
|
-
return { message };
|
|
3012
|
-
}
|
|
3013
3055
|
activeRun?.recorder?.handleMessageEnd(event);
|
|
3014
3056
|
});
|
|
3015
3057
|
|
|
@@ -3045,11 +3087,9 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
3045
3087
|
return;
|
|
3046
3088
|
}
|
|
3047
3089
|
}
|
|
3048
|
-
const flushedNatural = turnCoordinator.flushNatural(
|
|
3049
|
-
ctx.isIdle() && !sessionClosed && !runHeld(),
|
|
3050
|
-
);
|
|
3051
3090
|
const run = activeRun;
|
|
3052
3091
|
if (!run) {
|
|
3092
|
+
turnCoordinator.flushNatural(ctx.isIdle() && !sessionClosed && !runHeld());
|
|
3053
3093
|
presentationPending = null;
|
|
3054
3094
|
runSyncPass(ctx);
|
|
3055
3095
|
return;
|
|
@@ -3057,7 +3097,16 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
3057
3097
|
await run.recorder?.synchronize(ctx).catch(() => undefined);
|
|
3058
3098
|
run.recorder?.settleAttempt();
|
|
3059
3099
|
run.executor.setStreaming(false);
|
|
3060
|
-
if (
|
|
3100
|
+
if (run.executor.pendingCompletion === "assistant") {
|
|
3101
|
+
// The visible response is the step output. Resolve it before any
|
|
3102
|
+
// unrelated deferred turn can start and replace the captured message.
|
|
3103
|
+
run.executor.handleAgentSettled();
|
|
3104
|
+
} else {
|
|
3105
|
+
const flushedNatural = turnCoordinator.flushNatural(
|
|
3106
|
+
ctx.isIdle() && !sessionClosed && !runHeld(),
|
|
3107
|
+
);
|
|
3108
|
+
if (flushedNatural === 0) run.executor.handleAgentSettled();
|
|
3109
|
+
}
|
|
3061
3110
|
runSyncPass(ctx);
|
|
3062
3111
|
});
|
|
3063
3112
|
|
|
@@ -3065,7 +3114,6 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
3065
3114
|
sessionClosed = true;
|
|
3066
3115
|
herdrProbeGeneration += 1;
|
|
3067
3116
|
systemTurnAbort = null;
|
|
3068
|
-
suppressWorkflowAssistantTail = false;
|
|
3069
3117
|
turnCoordinator.clearDeferred();
|
|
3070
3118
|
supersedePresentation();
|
|
3071
3119
|
const run = activeRun;
|