@osolmaz/pi-workflows 0.12.0 → 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 +56 -31
- package/dist/builtins/autodoc.workflow.d.ts +4 -4
- package/dist/builtins/autoimplement.workflow.d.ts +797 -142
- package/dist/builtins/autoimplement.workflow.js +17 -105
- 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/index.d.ts +4 -2
- package/dist/builtins/index.js +3 -1
- package/dist/builtins/index.js.map +1 -1
- package/dist/builtins/monitor.workflow.d.ts +56 -23
- package/dist/builtins/monitor.workflow.js +508 -329
- package/dist/builtins/monitor.workflow.js.map +1 -1
- package/dist/builtins/pi-agent-group.d.ts +72 -0
- package/dist/builtins/pi-agent-group.js +1087 -0
- package/dist/builtins/pi-agent-group.js.map +1 -0
- 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-approval.workflow.d.ts +39 -5
- package/dist/builtins/plan-approval.workflow.js +92 -14
- package/dist/builtins/plan-approval.workflow.js.map +1 -1
- package/dist/builtins/plan-change.workflow.d.ts +449 -0
- package/dist/builtins/plan-change.workflow.js +256 -0
- package/dist/builtins/plan-change.workflow.js.map +1 -0
- package/dist/builtins/plan-presentation.js +2 -2
- package/dist/builtins/plan-presentation.js.map +1 -1
- package/dist/builtins/sanity-check.workflow.d.ts +50 -6
- package/dist/builtins/sanity-check.workflow.js +150 -28
- 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 +4 -2
- package/dist/extension/decision-channels.js +333 -146
- 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 +209 -127
- 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/session-events.d.ts +2 -2
- 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 +29 -3
- 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 +15 -10
- 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 +9 -23
- 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/catalog.js +7 -2
- package/dist/workflows/catalog.js.map +1 -1
- package/dist/workflows/composition.js +20 -2
- package/dist/workflows/composition.js.map +1 -1
- package/dist/workflows/decision-presentation.d.ts +1 -1
- package/dist/workflows/decision-presentation.js +51 -38
- package/dist/workflows/decision-presentation.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 +7 -7
- package/dist/workflows/engine.js +196 -115
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/errors.d.ts +16 -3
- package/dist/workflows/errors.js +18 -3
- package/dist/workflows/errors.js.map +1 -1
- package/dist/workflows/human-decision.d.ts +45 -8
- package/dist/workflows/human-decision.js +641 -274
- package/dist/workflows/human-decision.js.map +1 -1
- package/dist/workflows/index.d.ts +5 -6
- package/dist/workflows/index.js +4 -5
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/progress.d.ts +1 -0
- package/dist/workflows/progress.js +15 -3
- package/dist/workflows/progress.js.map +1 -1
- package/dist/workflows/schema.js +27 -1
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/store.d.ts +83 -103
- package/dist/workflows/store.js +907 -951
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/types.d.ts +88 -126
- 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 +36 -47
- package/docs/HUMAN_DECISION_PRESENTATIONS.md +14 -24
- package/docs/MONITOR.md +179 -237
- package/docs/SQLITE_STATE.md +219 -0
- package/docs/WORKFLOW_COMPOSITION.md +27 -26
- 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 +322 -0
- package/docs/plans/2026-08-21-sanity-check-plan.md +217 -95
- 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 +127 -53
- package/examples/workflows/approved-plan.workflow.ts +19 -46
- package/examples/workflows/plain-summary.workflow.ts +1 -0
- package/herdr-plugin.toml +1 -1
- package/package.json +7 -7
- package/plugins/herdr/viewer.mjs +1 -11
- package/schemas/human-decision-accepted-v1.schema.json +15 -3
- package/schemas/human-decision-continuation-v1.schema.json +10 -1
- package/schemas/human-decision-delivery-v1.schema.json +8 -0
- package/schemas/human-decision-receipt-v1.schema.json +8 -0
- package/schemas/human-decision-request-v1.schema.json +24 -4
- package/skills/autoimplement/SKILL.md +27 -0
- package/skills/autoplan/SKILL.md +5 -4
- package/skills/monitor/SKILL.md +91 -135
- package/skills/pi-workflows/SKILL.md +9 -8
- package/skills/sanity-check/SKILL.md +44 -0
- package/src/builtins/autoimplement.workflow.ts +19 -118
- package/src/builtins/autoplan.workflow.ts +290 -40
- package/src/builtins/catalog.ts +7 -5
- package/src/builtins/index.ts +18 -1
- package/src/builtins/monitor.workflow.ts +673 -382
- package/src/builtins/pi-agent-group.ts +1407 -0
- package/src/builtins/plain-summary.workflow.ts +185 -0
- package/src/builtins/plan-approval.workflow.ts +157 -24
- package/src/builtins/plan-change.workflow.ts +321 -0
- package/src/builtins/plan-presentation.ts +2 -2
- package/src/builtins/sanity-check.workflow.ts +248 -48
- 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 +456 -203
- package/src/extension/executor.ts +105 -2
- package/src/extension/herdr-viewer.ts +0 -5
- package/src/extension/index.ts +251 -151
- package/src/extension/recorder.ts +13 -22
- package/src/extension/session-events.ts +2 -2
- package/src/extension/step-message.ts +106 -4
- package/src/extension/widget.ts +32 -5
- package/src/host/rpc-executor.ts +4 -0
- package/src/host/runner.ts +93 -41
- package/src/render/graph-render.ts +12 -6
- 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 +19 -30
- 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/catalog.ts +7 -2
- package/src/workflows/composition.ts +21 -3
- package/src/workflows/decision-presentation.ts +56 -43
- package/src/workflows/definition.ts +36 -3
- package/src/workflows/engine.ts +228 -130
- package/src/workflows/errors.ts +27 -3
- package/src/workflows/human-decision.ts +921 -349
- package/src/workflows/index.ts +20 -30
- package/src/workflows/progress.ts +18 -3
- package/src/workflows/schema.ts +36 -1
- package/src/workflows/store.ts +1300 -1163
- package/src/workflows/types.ts +100 -137
- package/dist/builtins/sanity-check-session.d.ts +0 -17
- package/dist/builtins/sanity-check-session.js +0 -168
- package/dist/builtins/sanity-check-session.js.map +0 -1
- 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/schemas/human-decision-accepted-v2.schema.json +0 -50
- package/schemas/human-decision-delivery-v2.schema.json +0 -36
- package/schemas/human-decision-receipt-v2.schema.json +0 -39
- package/schemas/human-decision-request-v2.schema.json +0 -69
- package/schemas/human-decision-resolution-v2.schema.json +0 -27
- package/src/builtins/sanity-check-session.ts +0 -205
- package/src/workflows/artifacts.ts +0 -188
- package/src/workflows/migrate-sources.ts +0 -178
package/dist/extension/index.js
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import path from "node:path";
|
|
3
2
|
import { isDeepStrictEqual } from "node:util";
|
|
4
3
|
import { builtinWorkflowCatalog } from "../builtins/catalog.js";
|
|
5
|
-
import {
|
|
4
|
+
import { SqliteControllerStore } from "../controllers/index.js";
|
|
6
5
|
import { compositionMetadata } from "../workflows/composition.js";
|
|
7
6
|
import { humanDecisionChannelRequest } from "../workflows/decision-presentation.js";
|
|
8
7
|
import { WorkflowEngine } from "../workflows/engine.js";
|
|
9
|
-
import { ClaimLostError, errorMessage, isClaimLostError, TimeoutError, } from "../workflows/errors.js";
|
|
8
|
+
import { BuiltinWorkflowRevisionChangedError, ClaimLostError, errorMessage, isClaimLostError, TimeoutError, } from "../workflows/errors.js";
|
|
10
9
|
import { HumanDecisionStore, createHumanDecisionAttemptId, validateHumanDecisionResponse, } from "../workflows/human-decision.js";
|
|
11
10
|
import { discoverWorkflows, resolveWorkflowRef } from "../workflows/loader.js";
|
|
12
|
-
import { migrateLegacyWorkflowSources } from "../workflows/migrate-sources.js";
|
|
13
11
|
import { appendProgressHistory, progressRecordsFromTrace } from "../workflows/progress.js";
|
|
14
|
-
import { createRunId,
|
|
12
|
+
import { createRunId, listWorkflowRuns, readWorkflowRun, WorkflowRunStore, createDefinitionSnapshot, } from "../workflows/store.js";
|
|
15
13
|
import { PiControllerHost, parseControllerArgs, } from "./controller-host.js";
|
|
16
14
|
import { PiDecisionChannel, TelegramDecisionChannel, audienceChannels, createTelegramChannels, loadDecisionChannelConfig, verifyTelegramTokenFile, writeDecisionChannelProfile, } from "./decision-channels.js";
|
|
17
15
|
import { DeferredTurnCoordinator, } from "./deferred-turn-coordinator.js";
|
|
@@ -19,7 +17,7 @@ import { buildDeferredTurnContent, createDeferredTurnDescriptor, DEFERRED_TURN_M
|
|
|
19
17
|
import { ConversationStepExecutor } from "./executor.js";
|
|
20
18
|
import { HerdrWorkflowViewer, parseViewerPlacement, PIW_SHORTCUT, PIW_SHORTCUT_HINT, VIEWER_PLACEMENTS, } from "./herdr-viewer.js";
|
|
21
19
|
import { SessionRecorder } from "./recorder.js";
|
|
22
|
-
import { registerWorkflowAgentStepMessageRenderer, WORKFLOW_AGENT_STEP_MESSAGE_SCHEMA, WORKFLOW_AGENT_STEP_MESSAGE_TYPE, } from "./step-message.js";
|
|
20
|
+
import { recoverAssistantStep, registerWorkflowAgentStepMessageRenderer, WORKFLOW_AGENT_STEP_MESSAGE_SCHEMA, WORKFLOW_AGENT_STEP_MESSAGE_TYPE, } from "./step-message.js";
|
|
23
21
|
import { buildWidgetView } from "./widget.js";
|
|
24
22
|
import { parseWorkflowToolInput, WorkflowToolParameters } from "./workflow-tool.js";
|
|
25
23
|
export { PiDecisionChannel, TelegramDecisionChannel, audienceChannels, createTelegramChannels, decisionConfigDir, loadDecisionChannelConfig, verifyTelegramTokenFile, writeDecisionChannelProfile, } from "./decision-channels.js";
|
|
@@ -55,10 +53,8 @@ function humanDecisionRequest(value) {
|
|
|
55
53
|
const schema = value !== null && typeof value === "object"
|
|
56
54
|
? value.schema
|
|
57
55
|
: undefined;
|
|
58
|
-
if (schema !== "pi-workflows.human-decision-request.v1"
|
|
59
|
-
schema !== "pi-workflows.human-decision-request.v2") {
|
|
56
|
+
if (schema !== "pi-workflows.human-decision-request.v1")
|
|
60
57
|
return null;
|
|
61
|
-
}
|
|
62
58
|
return value;
|
|
63
59
|
}
|
|
64
60
|
function definitionDigest(snapshot) {
|
|
@@ -70,6 +66,20 @@ function launchSourceIdentity(workflow, root) {
|
|
|
70
66
|
mounted: compositionMetadata(workflow)?.sources ?? [],
|
|
71
67
|
};
|
|
72
68
|
}
|
|
69
|
+
function continuationSourceChangedError(runId, previous, current) {
|
|
70
|
+
if (previous.kind === "builtin" &&
|
|
71
|
+
current.kind === "builtin" &&
|
|
72
|
+
previous.id === current.id &&
|
|
73
|
+
previous.revision !== current.revision) {
|
|
74
|
+
return new BuiltinWorkflowRevisionChangedError({
|
|
75
|
+
runId,
|
|
76
|
+
workflowId: current.id,
|
|
77
|
+
previousRevision: previous.revision,
|
|
78
|
+
currentRevision: current.revision,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return new Error(`Workflow source changed since run ${runId} started; revert the edit to answer its checkpoint`);
|
|
82
|
+
}
|
|
73
83
|
function preparedLaunchOptions(options) {
|
|
74
84
|
return {
|
|
75
85
|
...(options.presentation !== undefined ? { presentation: options.presentation } : {}),
|
|
@@ -89,7 +99,7 @@ function safeLaunchError(error) {
|
|
|
89
99
|
.replace(/(token|api[_-]?key|secret|password)(\s*[:=]\s*)\S+/giu, "$1$2[redacted]")
|
|
90
100
|
.replaceAll("\n", " ")
|
|
91
101
|
.trim();
|
|
92
|
-
const code = /not found|cannot find|unknown workflow/iu.test(raw)
|
|
102
|
+
const code = /not found|cannot find|unknown workflow|no such file or directory/iu.test(raw)
|
|
93
103
|
? "workflow_not_found"
|
|
94
104
|
: /source changed|source mismatch/iu.test(raw)
|
|
95
105
|
? "source_changed"
|
|
@@ -212,11 +222,21 @@ export default function piWorkflows(pi) {
|
|
|
212
222
|
...[...telegramDecisionChannels.values()].map(async (channel) => channel.settle(decision)),
|
|
213
223
|
]);
|
|
214
224
|
};
|
|
215
|
-
const migrationBlockedRuns = new Set();
|
|
216
225
|
const ensureRunQueueStore = (cwd) => {
|
|
217
|
-
runQueueStore ??= new SqliteControllerStore(
|
|
226
|
+
runQueueStore ??= new SqliteControllerStore(undefined, { projectPath: cwd });
|
|
218
227
|
return runQueueStore;
|
|
219
228
|
};
|
|
229
|
+
const workflowStore = (cwd, options = {}) => {
|
|
230
|
+
const queue = ensureRunQueueStore(cwd);
|
|
231
|
+
return new WorkflowRunStore(queue.filePath, { ...options, state: queue.state });
|
|
232
|
+
};
|
|
233
|
+
const humanDecisionStore = (cwd, authorityProvider) => {
|
|
234
|
+
const queue = ensureRunQueueStore(cwd);
|
|
235
|
+
return new HumanDecisionStore(queue.filePath, {
|
|
236
|
+
state: queue.state,
|
|
237
|
+
...(authorityProvider === undefined ? {} : { authorityProvider }),
|
|
238
|
+
});
|
|
239
|
+
};
|
|
220
240
|
// Session-addressed delivery: each session polls only its durable outbox.
|
|
221
241
|
// Run events remain an audit feed and never enter a conversation.
|
|
222
242
|
let syncArmed = false;
|
|
@@ -557,7 +577,6 @@ export default function piWorkflows(pi) {
|
|
|
557
577
|
workflowViewTarget = {
|
|
558
578
|
runId: state.runId,
|
|
559
579
|
workflowName: state.workflowName,
|
|
560
|
-
runDir: path.resolve(new WorkflowRunStore().runDirFor(state.runId)),
|
|
561
580
|
};
|
|
562
581
|
renderWidget(ctx);
|
|
563
582
|
};
|
|
@@ -734,14 +753,16 @@ export default function piWorkflows(pi) {
|
|
|
734
753
|
}
|
|
735
754
|
try {
|
|
736
755
|
if (outcome === "park") {
|
|
737
|
-
runQueueStore.parkWorkflowRun({ runId: run.runId, claimToken: run.claimToken })
|
|
756
|
+
if (runQueueStore.parkWorkflowRun({ runId: run.runId, claimToken: run.claimToken })) {
|
|
757
|
+
runQueueStore.settleRunEffect(run.runId, "run.park_queue");
|
|
758
|
+
}
|
|
738
759
|
}
|
|
739
|
-
else {
|
|
740
|
-
runQueueStore.
|
|
760
|
+
else if (runQueueStore.completeWorkflowRun({ runId: run.runId, claimToken: run.claimToken })) {
|
|
761
|
+
runQueueStore.settleRunEffect(run.runId, "run.settle_queue");
|
|
741
762
|
}
|
|
742
763
|
}
|
|
743
764
|
catch {
|
|
744
|
-
// Queue bookkeeping is best-effort next to the durable
|
|
765
|
+
// Queue bookkeeping is best-effort next to the durable run fact.
|
|
745
766
|
}
|
|
746
767
|
finally {
|
|
747
768
|
run.claimToken = undefined;
|
|
@@ -752,7 +773,7 @@ export default function piWorkflows(pi) {
|
|
|
752
773
|
activeRun = null;
|
|
753
774
|
}
|
|
754
775
|
if (result.state.status === "running") {
|
|
755
|
-
// The run was parked for another runner; its
|
|
776
|
+
// The run was parked for another runner; its SQLite state stays resumable
|
|
756
777
|
// and its queue row becomes claimable. The recorder drains first: its
|
|
757
778
|
// finalization writes share the fenced store, so the claim must stay
|
|
758
779
|
// valid until capture is complete. Nothing else to present.
|
|
@@ -776,7 +797,7 @@ export default function piWorkflows(pi) {
|
|
|
776
797
|
notify(ctx, `Workflow ${run.workflowName} continues under another runner (run ${run.runId}).`);
|
|
777
798
|
return;
|
|
778
799
|
}
|
|
779
|
-
releaseClaim(run, "done");
|
|
800
|
+
releaseClaim(run, result.state.status === "waiting" ? "park" : "done");
|
|
780
801
|
recordRunEvent({
|
|
781
802
|
runId: run.runId,
|
|
782
803
|
workflowRef: run.workflowName,
|
|
@@ -882,13 +903,13 @@ export default function piWorkflows(pi) {
|
|
|
882
903
|
// refused continuation (edited source, missing parent) must not consume
|
|
883
904
|
// the parent's one-continuation slot.
|
|
884
905
|
if (options.parentRunId !== undefined) {
|
|
885
|
-
const parent =
|
|
906
|
+
const parent = readWorkflowRun(options.parentRunId);
|
|
886
907
|
if (parent === null || parent.state.status !== "waiting") {
|
|
887
908
|
throw new Error(`Workflow run ${options.parentRunId} is not waiting at a checkpoint`);
|
|
888
909
|
}
|
|
889
910
|
if (parent.state.workflowSource !== undefined &&
|
|
890
911
|
!isDeepStrictEqual(parent.state.workflowSource, workflowSource)) {
|
|
891
|
-
throw
|
|
912
|
+
throw continuationSourceChangedError(options.parentRunId, parent.state.workflowSource, workflowSource);
|
|
892
913
|
}
|
|
893
914
|
}
|
|
894
915
|
// Interactive runs are queued and claimed atomically, so this session
|
|
@@ -909,6 +930,7 @@ export default function piWorkflows(pi) {
|
|
|
909
930
|
: workflowSource.path,
|
|
910
931
|
workflowSource: launchSourceIdentity(workflow, workflowSource),
|
|
911
932
|
definitionDigest: definitionDigest(snapshot),
|
|
933
|
+
definitionSnapshot: snapshot,
|
|
912
934
|
input,
|
|
913
935
|
launchOptions: preparedLaunchOptions(options),
|
|
914
936
|
runnerId,
|
|
@@ -982,14 +1004,17 @@ export default function piWorkflows(pi) {
|
|
|
982
1004
|
},
|
|
983
1005
|
conversation: {
|
|
984
1006
|
beginAttempt: (contract) => run.recorder?.beginAttempt(contract),
|
|
1007
|
+
recoverAssistant: (contract) => recoverAssistantStep(ctx.sessionManager.getBranch(), contract),
|
|
985
1008
|
mark: () => run.recorder?.mark() ?? 0,
|
|
986
1009
|
rangeSince: (mark) => run.recorder?.rangeSince(mark),
|
|
987
1010
|
},
|
|
988
1011
|
});
|
|
989
|
-
// The
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1012
|
+
// The engine and session recorder share one transactional SQLite store.
|
|
1013
|
+
const store = workflowStore(ctx.cwd, queueStore === null || claimToken === undefined
|
|
1014
|
+
? {}
|
|
1015
|
+
: {
|
|
1016
|
+
authorityProvider: () => queueStore.workflowRunAuthority(runId, claimToken),
|
|
1017
|
+
});
|
|
993
1018
|
const engine = new WorkflowEngine({
|
|
994
1019
|
executor,
|
|
995
1020
|
store,
|
|
@@ -1014,8 +1039,8 @@ export default function piWorkflows(pi) {
|
|
|
1014
1039
|
},
|
|
1015
1040
|
// Awaited by the engine after run_started is persisted, so the session
|
|
1016
1041
|
// binding and its trace event always precede node and terminal events.
|
|
1017
|
-
onRunStarted: async (
|
|
1018
|
-
const recorder = new SessionRecorder(store,
|
|
1042
|
+
onRunStarted: async (runId) => {
|
|
1043
|
+
const recorder = new SessionRecorder(store, runId);
|
|
1019
1044
|
try {
|
|
1020
1045
|
await recorder.bind(ctx);
|
|
1021
1046
|
run.recorder = recorder;
|
|
@@ -1107,31 +1132,30 @@ export default function piWorkflows(pi) {
|
|
|
1107
1132
|
// once the cause is fixed, and say so in the feed.
|
|
1108
1133
|
let resumeFailed = run.resume === true && !isClaimLostError(error);
|
|
1109
1134
|
let terminalStatus;
|
|
1110
|
-
// Re-parking only makes sense while the
|
|
1111
|
-
// A terminal or waiting
|
|
1112
|
-
//
|
|
1113
|
-
// the bundle's real state, not a bogus failure.
|
|
1135
|
+
// Re-parking only makes sense while the run is still resumable.
|
|
1136
|
+
// A terminal or waiting run closes the queue row instead, and the feed
|
|
1137
|
+
// reports the authoritative run state.
|
|
1114
1138
|
if (resumeFailed) {
|
|
1115
1139
|
try {
|
|
1116
|
-
const bundle =
|
|
1140
|
+
const bundle = readWorkflowRun(runId);
|
|
1117
1141
|
if (bundle === null || bundle.state.status !== "running") {
|
|
1118
1142
|
resumeFailed = false;
|
|
1119
1143
|
terminalStatus = bundle?.state.status;
|
|
1120
1144
|
}
|
|
1121
1145
|
}
|
|
1122
1146
|
catch {
|
|
1123
|
-
// Unreadable
|
|
1147
|
+
// Unreadable run state closes the row too.
|
|
1124
1148
|
resumeFailed = false;
|
|
1125
1149
|
}
|
|
1126
1150
|
}
|
|
1127
|
-
// A continuation that failed before its
|
|
1151
|
+
// A continuation that failed before its run row exists frees the
|
|
1128
1152
|
// parent's continuation slot instead of consuming it forever.
|
|
1129
1153
|
let continuationSlotFreed = false;
|
|
1130
1154
|
if (run.parentRunId !== undefined &&
|
|
1131
1155
|
run.claimToken !== undefined &&
|
|
1132
1156
|
!isClaimLostError(error)) {
|
|
1133
1157
|
try {
|
|
1134
|
-
const bundle =
|
|
1158
|
+
const bundle = readWorkflowRun(runId);
|
|
1135
1159
|
if (bundle === null) {
|
|
1136
1160
|
continuationSlotFreed =
|
|
1137
1161
|
runQueueStore?.deleteWorkflowRun({ runId, claimToken: run.claimToken }) === true;
|
|
@@ -1201,7 +1225,7 @@ export default function piWorkflows(pi) {
|
|
|
1201
1225
|
runnerId,
|
|
1202
1226
|
claimToken,
|
|
1203
1227
|
leaseMs: RUN_CLAIM_LEASE_MS,
|
|
1204
|
-
excludeRunIds: [
|
|
1228
|
+
excludeRunIds: [],
|
|
1205
1229
|
sessionId: ctx.sessionManager.getSessionId(),
|
|
1206
1230
|
});
|
|
1207
1231
|
if (claimed === undefined) {
|
|
@@ -1209,7 +1233,11 @@ export default function piWorkflows(pi) {
|
|
|
1209
1233
|
}
|
|
1210
1234
|
let started;
|
|
1211
1235
|
try {
|
|
1212
|
-
const bundle =
|
|
1236
|
+
const bundle = readWorkflowRun(claimed.runId);
|
|
1237
|
+
if (bundle?.state.status === "waiting") {
|
|
1238
|
+
queueStore.parkWorkflowRun({ runId: claimed.runId, claimToken });
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1213
1241
|
const sourceRef = bundle?.state.workflowSource === undefined
|
|
1214
1242
|
? claimed.workflowSourceRef
|
|
1215
1243
|
: bundle.state.workflowSource.kind === "builtin"
|
|
@@ -1218,7 +1246,7 @@ export default function piWorkflows(pi) {
|
|
|
1218
1246
|
const launchOptions = parsePreparedLaunchOptions(claimed.launchOptions);
|
|
1219
1247
|
started = await startRun(ctx, sourceRef, claimed.input, {
|
|
1220
1248
|
...launchOptions,
|
|
1221
|
-
resume:
|
|
1249
|
+
resume: claimed.initialized,
|
|
1222
1250
|
runId: claimed.runId,
|
|
1223
1251
|
claimToken,
|
|
1224
1252
|
});
|
|
@@ -1247,12 +1275,12 @@ export default function piWorkflows(pi) {
|
|
|
1247
1275
|
? { state: "running", runId: activeRun.runId }
|
|
1248
1276
|
: workflowSchedulerResult(activeRun.lastState);
|
|
1249
1277
|
}
|
|
1250
|
-
const store =
|
|
1251
|
-
const bundle =
|
|
1278
|
+
const store = workflowStore(controllerContext?.cwd ?? process.cwd());
|
|
1279
|
+
const bundle = store.readRun(request.runId);
|
|
1252
1280
|
if (bundle !== null) {
|
|
1253
1281
|
const recovered = bundle.state.status === "running" ? await store.markRunInterrupted(request.runId) : bundle;
|
|
1254
1282
|
if (recovered !== null) {
|
|
1255
|
-
const lastTraceEvent =
|
|
1283
|
+
const lastTraceEvent = recovered.traceEvents?.at(-1) ?? null;
|
|
1256
1284
|
return workflowSchedulerResult(recovered.state, lastTraceEvent?.type === "run_interrupted");
|
|
1257
1285
|
}
|
|
1258
1286
|
return { state: "pending" };
|
|
@@ -1260,7 +1288,6 @@ export default function piWorkflows(pi) {
|
|
|
1260
1288
|
if (controllerContext === null) {
|
|
1261
1289
|
return { state: "pending" };
|
|
1262
1290
|
}
|
|
1263
|
-
await store.quarantineIncompleteRun(request.runId);
|
|
1264
1291
|
const runId = await startRun(controllerContext, request.workflow, request.input, {
|
|
1265
1292
|
runId: request.runId,
|
|
1266
1293
|
childKey,
|
|
@@ -1344,12 +1371,14 @@ export default function piWorkflows(pi) {
|
|
|
1344
1371
|
const result = await listWorkflowControl(ctx);
|
|
1345
1372
|
notify(ctx, result.message, result.level);
|
|
1346
1373
|
};
|
|
1347
|
-
const cancelHumanDecision = async (request) => {
|
|
1348
|
-
const store =
|
|
1374
|
+
const cancelHumanDecision = async (ctx, request) => {
|
|
1375
|
+
const store = humanDecisionStore(ctx.cwd);
|
|
1349
1376
|
await store.cancel(request, "cancelled");
|
|
1350
1377
|
const cancellation = await store.readCancellation(request.decisionId);
|
|
1351
|
-
if (cancellation !== null)
|
|
1378
|
+
if (cancellation !== null) {
|
|
1352
1379
|
await settleHumanDecisionChannels(cancellation);
|
|
1380
|
+
store.markEffectApplied(request.decisionId, "decision.settle_presentations");
|
|
1381
|
+
}
|
|
1353
1382
|
lastWaitingRunId = null;
|
|
1354
1383
|
};
|
|
1355
1384
|
const findOwnedWaitingHumanDecision = async (ctx) => {
|
|
@@ -1359,7 +1388,7 @@ export default function piWorkflows(pi) {
|
|
|
1359
1388
|
.filter((row) => row.originSessionId === null || row.originSessionId === sessionId)
|
|
1360
1389
|
.map((row) => row.runId));
|
|
1361
1390
|
const continued = new Set(rows.map((row) => row.parentRunId).filter((parent) => parent !== null));
|
|
1362
|
-
const bundles =
|
|
1391
|
+
const bundles = listWorkflowRuns();
|
|
1363
1392
|
for (const bundle of bundles) {
|
|
1364
1393
|
if (bundle.state.status !== "waiting" ||
|
|
1365
1394
|
!owned.has(bundle.state.runId) ||
|
|
@@ -1414,7 +1443,7 @@ export default function piWorkflows(pi) {
|
|
|
1414
1443
|
const { state } = widgetSource;
|
|
1415
1444
|
const request = humanDecisionRequest(state.finalOutput);
|
|
1416
1445
|
if (state.status === "waiting" && request !== null) {
|
|
1417
|
-
await cancelHumanDecision(request);
|
|
1446
|
+
await cancelHumanDecision(ctx, request);
|
|
1418
1447
|
clearWidgetTimer();
|
|
1419
1448
|
clearWidget(ctx);
|
|
1420
1449
|
return {
|
|
@@ -1434,7 +1463,7 @@ export default function piWorkflows(pi) {
|
|
|
1434
1463
|
}
|
|
1435
1464
|
const recovered = await findOwnedWaitingHumanDecision(ctx);
|
|
1436
1465
|
if (recovered !== null) {
|
|
1437
|
-
await cancelHumanDecision(recovered.request);
|
|
1466
|
+
await cancelHumanDecision(ctx, recovered.request);
|
|
1438
1467
|
return {
|
|
1439
1468
|
message: `Cancelled the pending human decision for workflow ${recovered.state.workflowName}.`,
|
|
1440
1469
|
details: {
|
|
@@ -1532,9 +1561,13 @@ export default function piWorkflows(pi) {
|
|
|
1532
1561
|
});
|
|
1533
1562
|
const statusWorkflowControl = async (ctx, runId) => {
|
|
1534
1563
|
if (runId !== undefined) {
|
|
1535
|
-
const
|
|
1564
|
+
const launch = ensureRunQueueStore(ctx.cwd).getWorkflowRun(runId);
|
|
1565
|
+
if (launch !== undefined &&
|
|
1566
|
+
["queued", "starting", "failed", "cancelled"].includes(launch.status)) {
|
|
1567
|
+
return workflowLaunchStatus(launch);
|
|
1568
|
+
}
|
|
1569
|
+
const bundle = readWorkflowRun(runId);
|
|
1536
1570
|
if (bundle === null) {
|
|
1537
|
-
const launch = ensureRunQueueStore(ctx.cwd).getWorkflowRun(runId);
|
|
1538
1571
|
if (launch === undefined)
|
|
1539
1572
|
throw new Error(`Workflow run not found: ${runId}`);
|
|
1540
1573
|
return workflowLaunchStatus(launch);
|
|
@@ -1572,7 +1605,7 @@ export default function piWorkflows(pi) {
|
|
|
1572
1605
|
.filter((row) => row.originSessionId === null || row.originSessionId === sessionId)
|
|
1573
1606
|
.map((row) => row.runId));
|
|
1574
1607
|
const continued = new Set(rows.map((row) => row.parentRunId).filter((parent) => parent !== null));
|
|
1575
|
-
const bundles =
|
|
1608
|
+
const bundles = listWorkflowRuns();
|
|
1576
1609
|
parentRunId =
|
|
1577
1610
|
bundles.find((bundle) => bundle.state.status === "waiting" &&
|
|
1578
1611
|
known.has(bundle.state.runId) &&
|
|
@@ -1588,7 +1621,7 @@ export default function piWorkflows(pi) {
|
|
|
1588
1621
|
queueRecord.originSessionId !== ctx.sessionManager.getSessionId()) {
|
|
1589
1622
|
throw new Error(`Workflow run ${parentRunId} belongs to another Pi session.`);
|
|
1590
1623
|
}
|
|
1591
|
-
const parent =
|
|
1624
|
+
const parent = readWorkflowRun(parentRunId);
|
|
1592
1625
|
if (parent === null ||
|
|
1593
1626
|
parent.state.status !== "waiting" ||
|
|
1594
1627
|
parent.state.workflowSource === undefined) {
|
|
@@ -1606,7 +1639,7 @@ export default function piWorkflows(pi) {
|
|
|
1606
1639
|
};
|
|
1607
1640
|
const answerWorkflowControl = async (ctx, input, requestedRunId, verified) => {
|
|
1608
1641
|
const waiting = await resolveWaitingWorkflow(ctx, requestedRunId, verified !== undefined);
|
|
1609
|
-
const parent =
|
|
1642
|
+
const parent = readWorkflowRun(waiting.parentRunId);
|
|
1610
1643
|
if (parent === null)
|
|
1611
1644
|
throw new Error(`Workflow run ${waiting.parentRunId} is unreadable.`);
|
|
1612
1645
|
const request = humanDecisionRequest(parent.state.finalOutput);
|
|
@@ -1631,9 +1664,9 @@ export default function piWorkflows(pi) {
|
|
|
1631
1664
|
},
|
|
1632
1665
|
idempotencyKey: verified?.idempotencyKey ?? createHumanDecisionAttemptId(),
|
|
1633
1666
|
};
|
|
1634
|
-
const store =
|
|
1667
|
+
const store = humanDecisionStore(ctx.cwd);
|
|
1635
1668
|
const acceptance = await store.accept(request, submission);
|
|
1636
|
-
if (acceptance.status === "conflict") {
|
|
1669
|
+
if (acceptance.status === "conflict" || acceptance.decision.provenance !== "human") {
|
|
1637
1670
|
throw new Error("That human decision was already answered differently.");
|
|
1638
1671
|
}
|
|
1639
1672
|
accepted = acceptance.decision;
|
|
@@ -1657,19 +1690,9 @@ export default function piWorkflows(pi) {
|
|
|
1657
1690
|
};
|
|
1658
1691
|
}
|
|
1659
1692
|
}
|
|
1660
|
-
const decisionStore =
|
|
1661
|
-
if (request !== null && continuationRunId !== undefined && accepted !== undefined) {
|
|
1662
|
-
await decisionStore.recordContinuation(request.decisionId, {
|
|
1663
|
-
schema: "pi-workflows.human-decision-continuation.v1",
|
|
1664
|
-
decisionId: request.decisionId,
|
|
1665
|
-
requestDigest: request.requestDigest,
|
|
1666
|
-
parentRunId: waiting.parentRunId,
|
|
1667
|
-
runId: continuationRunId,
|
|
1668
|
-
createdAt: accepted.acceptedAt,
|
|
1669
|
-
});
|
|
1670
|
-
}
|
|
1693
|
+
const decisionStore = humanDecisionStore(ctx.cwd);
|
|
1671
1694
|
if (continuationRunId !== undefined) {
|
|
1672
|
-
const existingContinuation =
|
|
1695
|
+
const existingContinuation = readWorkflowRun(continuationRunId);
|
|
1673
1696
|
if (existingContinuation !== null) {
|
|
1674
1697
|
if (accepted !== undefined)
|
|
1675
1698
|
await settleHumanDecisionChannels(accepted);
|
|
@@ -1694,7 +1717,18 @@ export default function piWorkflows(pi) {
|
|
|
1694
1717
|
throw new Error("Could not start the checkpoint continuation.");
|
|
1695
1718
|
}
|
|
1696
1719
|
if (request !== null && accepted !== undefined) {
|
|
1720
|
+
await decisionStore.recordContinuation(request.decisionId, {
|
|
1721
|
+
schema: "pi-workflows.human-decision-continuation.v1",
|
|
1722
|
+
decisionId: request.decisionId,
|
|
1723
|
+
requestDigest: request.requestDigest,
|
|
1724
|
+
provenance: accepted.provenance,
|
|
1725
|
+
parentRunId: waiting.parentRunId,
|
|
1726
|
+
runId: continued,
|
|
1727
|
+
createdAt: accepted.acceptedAt,
|
|
1728
|
+
});
|
|
1729
|
+
decisionStore.markEffectApplied(request.decisionId, "decision.continue");
|
|
1697
1730
|
await settleHumanDecisionChannels(accepted);
|
|
1731
|
+
decisionStore.markEffectApplied(request.decisionId, "decision.settle_presentations");
|
|
1698
1732
|
}
|
|
1699
1733
|
lastWaitingRunId = null;
|
|
1700
1734
|
return {
|
|
@@ -1712,7 +1746,7 @@ export default function piWorkflows(pi) {
|
|
|
1712
1746
|
const channel = new PiDecisionChannel({
|
|
1713
1747
|
actorId: ctx.sessionManager.getSessionId(),
|
|
1714
1748
|
ui: ctx.ui,
|
|
1715
|
-
store:
|
|
1749
|
+
store: humanDecisionStore(ctx.cwd),
|
|
1716
1750
|
onAnswer: async (answer) => {
|
|
1717
1751
|
const result = await answerWorkflowControl(ctx, answer.response, request.runId, answer);
|
|
1718
1752
|
notify(ctx, result.message, result.level);
|
|
@@ -1750,7 +1784,7 @@ export default function piWorkflows(pi) {
|
|
|
1750
1784
|
config: loaded.channels,
|
|
1751
1785
|
credentials: loaded.credentials,
|
|
1752
1786
|
configDir: loaded.configDir,
|
|
1753
|
-
store:
|
|
1787
|
+
store: humanDecisionStore(ctx.cwd),
|
|
1754
1788
|
onAnswer: async (answer) => {
|
|
1755
1789
|
await answerWorkflowControl(ctx, answer.response, answer.request.runId, answer);
|
|
1756
1790
|
},
|
|
@@ -1758,11 +1792,11 @@ export default function piWorkflows(pi) {
|
|
|
1758
1792
|
await Promise.all([...telegramDecisionChannels.values()].map(async (channel) => channel.start()));
|
|
1759
1793
|
};
|
|
1760
1794
|
const recoverHumanDecisions = async (ctx, deliverPending = true) => {
|
|
1761
|
-
const runStore =
|
|
1762
|
-
const store =
|
|
1795
|
+
const runStore = workflowStore(ctx.cwd);
|
|
1796
|
+
const store = humanDecisionStore(ctx.cwd);
|
|
1763
1797
|
const requests = (await store.listRequests()).sort((left, right) => left.createdAt.localeCompare(right.createdAt));
|
|
1764
1798
|
for (const request of requests) {
|
|
1765
|
-
const parent =
|
|
1799
|
+
const parent = runStore.readRun(request.runId);
|
|
1766
1800
|
if (parent === null || parent.state.status !== "waiting")
|
|
1767
1801
|
continue;
|
|
1768
1802
|
const currentRequest = humanDecisionRequest(parent.state.finalOutput);
|
|
@@ -1775,60 +1809,110 @@ export default function piWorkflows(pi) {
|
|
|
1775
1809
|
const ownedBySession = queueRecord !== undefined &&
|
|
1776
1810
|
(queueRecord.originSessionId === null ||
|
|
1777
1811
|
queueRecord.originSessionId === ctx.sessionManager.getSessionId());
|
|
1778
|
-
|
|
1779
|
-
|
|
1812
|
+
if (!ownedBySession)
|
|
1813
|
+
continue;
|
|
1814
|
+
const recoveryToken = randomUUID();
|
|
1815
|
+
const recoveryQueue = ensureRunQueueStore(ctx.cwd);
|
|
1816
|
+
const claimed = recoveryQueue.claimWorkflowRun({
|
|
1817
|
+
runId: request.runId,
|
|
1818
|
+
runnerId: ctx.sessionManager.getSessionId(),
|
|
1819
|
+
claimToken: recoveryToken,
|
|
1820
|
+
leaseMs: RUN_CLAIM_LEASE_MS,
|
|
1821
|
+
});
|
|
1822
|
+
if (claimed === undefined)
|
|
1823
|
+
continue;
|
|
1824
|
+
const ownerStore = humanDecisionStore(ctx.cwd, () => recoveryQueue.workflowRunAuthority(request.runId, recoveryToken));
|
|
1825
|
+
let resolved = await store.readResolved(request.decisionId);
|
|
1826
|
+
if (resolved === null) {
|
|
1780
1827
|
let cancellation = await store.readCancellation(request.decisionId);
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1828
|
+
const expired = request.expiresAt !== undefined && Date.parse(request.expiresAt) <= Date.now();
|
|
1829
|
+
if (cancellation === null && expired) {
|
|
1830
|
+
if (request.defaultResponse === undefined) {
|
|
1831
|
+
await ownerStore.cancel(request, "expired");
|
|
1832
|
+
cancellation = await ownerStore.readCancellation(request.decisionId);
|
|
1833
|
+
}
|
|
1834
|
+
else {
|
|
1835
|
+
try {
|
|
1836
|
+
resolved = (await ownerStore.resolveTimeout(request)).decision;
|
|
1837
|
+
}
|
|
1838
|
+
catch {
|
|
1839
|
+
cancellation = await store.readCancellation(request.decisionId);
|
|
1840
|
+
resolved = await store.readResolved(request.decisionId);
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1786
1843
|
}
|
|
1787
1844
|
if (cancellation !== null) {
|
|
1845
|
+
recoveryQueue.completeWorkflowRun({ runId: request.runId, claimToken: recoveryToken });
|
|
1846
|
+
ownerStore.markEffectApplied(request.decisionId, "decision.cancel_parent");
|
|
1788
1847
|
await settleHumanDecisionChannels(cancellation);
|
|
1848
|
+
ownerStore.markEffectApplied(request.decisionId, "decision.settle_presentations");
|
|
1849
|
+
ownerStore.close();
|
|
1789
1850
|
continue;
|
|
1790
1851
|
}
|
|
1791
|
-
if (
|
|
1852
|
+
if (resolved === null) {
|
|
1853
|
+
if (!deliverPending) {
|
|
1854
|
+
recoveryQueue.parkWorkflowRun({ runId: request.runId, claimToken: recoveryToken });
|
|
1855
|
+
ownerStore.close();
|
|
1856
|
+
continue;
|
|
1857
|
+
}
|
|
1858
|
+
const channels = audienceChannels(decisionChannelConfig, request.audience);
|
|
1859
|
+
for (const channelId of channels) {
|
|
1860
|
+
const channel = telegramDecisionChannels.get(channelId);
|
|
1861
|
+
if (channel !== undefined)
|
|
1862
|
+
await channel.deliver(humanDecisionChannelRequest(request));
|
|
1863
|
+
}
|
|
1864
|
+
if (ctx.mode === "tui" && channels.includes("pi") && lastWaitingRunId === null) {
|
|
1865
|
+
lastWaitingRunId = request.runId;
|
|
1866
|
+
queueMicrotask(() => void promptHumanDecision(ctx, request));
|
|
1867
|
+
}
|
|
1868
|
+
recoveryQueue.parkWorkflowRun({ runId: request.runId, claimToken: recoveryToken });
|
|
1869
|
+
ownerStore.close();
|
|
1792
1870
|
continue;
|
|
1793
|
-
const channels = audienceChannels(decisionChannelConfig, request.audience);
|
|
1794
|
-
for (const channelId of channels) {
|
|
1795
|
-
const channel = telegramDecisionChannels.get(channelId);
|
|
1796
|
-
if (channel !== undefined)
|
|
1797
|
-
await channel.deliver(humanDecisionChannelRequest(request));
|
|
1798
1871
|
}
|
|
1799
|
-
if (ctx.mode === "tui" && channels.includes("pi") && lastWaitingRunId === null) {
|
|
1800
|
-
lastWaitingRunId = request.runId;
|
|
1801
|
-
queueMicrotask(() => void promptHumanDecision(ctx, request));
|
|
1802
|
-
}
|
|
1803
|
-
continue;
|
|
1804
1872
|
}
|
|
1805
|
-
|
|
1873
|
+
const currentParent = runStore.readRun(request.runId);
|
|
1874
|
+
if (currentParent === null || currentParent.state.status !== "waiting") {
|
|
1875
|
+
recoveryQueue.parkWorkflowRun({ runId: request.runId, claimToken: recoveryToken });
|
|
1876
|
+
ownerStore.close();
|
|
1806
1877
|
continue;
|
|
1878
|
+
}
|
|
1807
1879
|
const runId = `continuation-${request.decisionId.slice("decision-".length)}`;
|
|
1808
1880
|
const continuation = (await store.readContinuation(request.decisionId)) ?? {
|
|
1809
1881
|
schema: "pi-workflows.human-decision-continuation.v1",
|
|
1810
1882
|
decisionId: request.decisionId,
|
|
1811
1883
|
requestDigest: request.requestDigest,
|
|
1884
|
+
provenance: resolved.provenance,
|
|
1812
1885
|
parentRunId: request.runId,
|
|
1813
1886
|
runId,
|
|
1814
|
-
createdAt:
|
|
1887
|
+
createdAt: resolved.acceptedAt,
|
|
1815
1888
|
};
|
|
1816
|
-
|
|
1817
|
-
|
|
1889
|
+
recoveryQueue.parkWorkflowRun({
|
|
1890
|
+
runId: request.runId,
|
|
1891
|
+
claimToken: recoveryToken,
|
|
1892
|
+
});
|
|
1893
|
+
const existing = runStore.readRun(continuation.runId);
|
|
1818
1894
|
if (existing === null && activeRun === null) {
|
|
1819
|
-
if (
|
|
1895
|
+
if (currentParent.state.workflowSource === undefined) {
|
|
1896
|
+
ownerStore.close();
|
|
1820
1897
|
continue;
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1898
|
+
}
|
|
1899
|
+
const workflowRef = currentParent.state.workflowSource.kind === "builtin"
|
|
1900
|
+
? `builtin:${currentParent.state.workflowSource.id}`
|
|
1901
|
+
: currentParent.state.workflowSource.path;
|
|
1902
|
+
await startRun(ctx, workflowRef, currentParent.state.input, {
|
|
1825
1903
|
parentRunId: request.runId,
|
|
1826
|
-
humanDecision:
|
|
1904
|
+
humanDecision: resolved,
|
|
1827
1905
|
runId: continuation.runId,
|
|
1828
1906
|
quiet: true,
|
|
1829
1907
|
});
|
|
1830
1908
|
}
|
|
1831
|
-
|
|
1909
|
+
if (runStore.readRun(continuation.runId) !== null) {
|
|
1910
|
+
await ownerStore.recordContinuation(request.decisionId, continuation);
|
|
1911
|
+
ownerStore.markEffectApplied(request.decisionId, "decision.continue");
|
|
1912
|
+
}
|
|
1913
|
+
await settleHumanDecisionChannels(resolved);
|
|
1914
|
+
ownerStore.markEffectApplied(request.decisionId, "decision.settle_presentations");
|
|
1915
|
+
ownerStore.close();
|
|
1832
1916
|
if (activeRun !== null)
|
|
1833
1917
|
break;
|
|
1834
1918
|
}
|
|
@@ -1874,7 +1958,7 @@ export default function piWorkflows(pi) {
|
|
|
1874
1958
|
}
|
|
1875
1959
|
const queue = ensureRunQueueStore(ctx.cwd);
|
|
1876
1960
|
const existing = queue.findSessionReservation(ctx.sessionManager.getSessionId());
|
|
1877
|
-
if (existing !== undefined) {
|
|
1961
|
+
if (existing !== undefined && existing.runId !== options.parentRunId) {
|
|
1878
1962
|
throw new Error(`Workflow ${existing.workflowName} is already ${existing.status} (run ${existing.runId}).`);
|
|
1879
1963
|
}
|
|
1880
1964
|
if (presentationPending !== null) {
|
|
@@ -1884,13 +1968,13 @@ export default function piWorkflows(pi) {
|
|
|
1884
1968
|
const workflow = resolved.definition;
|
|
1885
1969
|
const workflowSource = resolved.source;
|
|
1886
1970
|
if (options.parentRunId !== undefined) {
|
|
1887
|
-
const parent =
|
|
1971
|
+
const parent = readWorkflowRun(options.parentRunId);
|
|
1888
1972
|
if (parent === null || parent.state.status !== "waiting") {
|
|
1889
1973
|
throw new Error(`Workflow run ${options.parentRunId} is not waiting at a checkpoint`);
|
|
1890
1974
|
}
|
|
1891
1975
|
if (parent.state.workflowSource !== undefined &&
|
|
1892
1976
|
!isDeepStrictEqual(parent.state.workflowSource, workflowSource)) {
|
|
1893
|
-
throw
|
|
1977
|
+
throw continuationSourceChangedError(options.parentRunId, parent.state.workflowSource, workflowSource);
|
|
1894
1978
|
}
|
|
1895
1979
|
}
|
|
1896
1980
|
const snapshot = createDefinitionSnapshot(workflow);
|
|
@@ -1902,6 +1986,7 @@ export default function piWorkflows(pi) {
|
|
|
1902
1986
|
workflowSourceRef: workflowSource.kind === "builtin" ? `builtin:${workflowSource.id}` : workflowSource.path,
|
|
1903
1987
|
workflowSource: launchSourceIdentity(workflow, workflowSource),
|
|
1904
1988
|
definitionDigest: definitionDigest(snapshot),
|
|
1989
|
+
definitionSnapshot: snapshot,
|
|
1905
1990
|
input,
|
|
1906
1991
|
launchOptions: preparedLaunchOptions(options),
|
|
1907
1992
|
runnerId,
|
|
@@ -2090,7 +2175,7 @@ export default function piWorkflows(pi) {
|
|
|
2090
2175
|
}
|
|
2091
2176
|
catch (error) {
|
|
2092
2177
|
const message = errorMessage(error);
|
|
2093
|
-
notify(ctx, /
|
|
2178
|
+
notify(ctx, /continuations\.parent_run_id/.test(message)
|
|
2094
2179
|
? "That checkpoint was already answered; see its continuation run."
|
|
2095
2180
|
: `Could not continue workflow: ${message}`, "error");
|
|
2096
2181
|
}
|
|
@@ -2237,9 +2322,10 @@ export default function piWorkflows(pi) {
|
|
|
2237
2322
|
label: "Workflow",
|
|
2238
2323
|
description: [
|
|
2239
2324
|
"List, start, inspect, pause, resume, cancel, answer, update, or complete pi-workflows runs.",
|
|
2240
|
-
"When the user asks to monitor, watch, poll, or check something repeatedly, start the built-in monitor workflow with input keys task,
|
|
2325
|
+
"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.",
|
|
2326
|
+
"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.",
|
|
2241
2327
|
"Use update or submit only when a workflow step contract asks for it, and pass the exact step and attempt ids.",
|
|
2242
|
-
"Do not start repeated work without the user's request
|
|
2328
|
+
"Do not start repeated work without the user's request.",
|
|
2243
2329
|
].join(" "),
|
|
2244
2330
|
parameters: WorkflowToolParameters,
|
|
2245
2331
|
async execute(toolCallId, rawParams, _signal, _onUpdate, ctx) {
|
|
@@ -2266,7 +2352,7 @@ export default function piWorkflows(pi) {
|
|
|
2266
2352
|
break;
|
|
2267
2353
|
case "answer": {
|
|
2268
2354
|
const waiting = await resolveWaitingWorkflow(ctx, params.runId);
|
|
2269
|
-
const parent =
|
|
2355
|
+
const parent = readWorkflowRun(waiting.parentRunId);
|
|
2270
2356
|
if (parent !== null && humanDecisionRequest(parent.state.finalOutput) !== null) {
|
|
2271
2357
|
throw new Error("This checkpoint requires a verified human answer from Pi UI or a configured decision channel.");
|
|
2272
2358
|
}
|
|
@@ -2294,7 +2380,7 @@ export default function piWorkflows(pi) {
|
|
|
2294
2380
|
}
|
|
2295
2381
|
throw new Error("No workflow step is waiting for output.");
|
|
2296
2382
|
}
|
|
2297
|
-
// Flush the conversation into
|
|
2383
|
+
// Flush the conversation into SQLite before accepting, so the
|
|
2298
2384
|
// attempt range includes the assistant message carrying this call.
|
|
2299
2385
|
await activeRun.recorder?.record(ctx).catch(() => undefined);
|
|
2300
2386
|
await activeRun.recorder?.synchronize(ctx).catch(() => undefined);
|
|
@@ -2334,22 +2420,7 @@ export default function piWorkflows(pi) {
|
|
|
2334
2420
|
controllerContext = ctx;
|
|
2335
2421
|
if (herdrEnabled)
|
|
2336
2422
|
void refreshHerdrCapability(ctx);
|
|
2337
|
-
|
|
2338
|
-
const queue = ensureRunQueueStore(ctx.cwd);
|
|
2339
|
-
const migration = await migrateLegacyWorkflowSources({
|
|
2340
|
-
catalog: builtinWorkflowCatalog,
|
|
2341
|
-
queue,
|
|
2342
|
-
});
|
|
2343
|
-
migrationBlockedRuns.clear();
|
|
2344
|
-
for (const blocked of migration.blocked)
|
|
2345
|
-
migrationBlockedRuns.add(blocked.runId);
|
|
2346
|
-
if (migration.blocked.length > 0) {
|
|
2347
|
-
notify(ctx, `Could not migrate ${migration.blocked.length} legacy workflow source(s).`, "warning");
|
|
2348
|
-
}
|
|
2349
|
-
}
|
|
2350
|
-
catch (error) {
|
|
2351
|
-
notify(ctx, `Could not migrate legacy workflow sources: ${errorMessage(error)}`, "warning");
|
|
2352
|
-
}
|
|
2423
|
+
ensureRunQueueStore(ctx.cwd);
|
|
2353
2424
|
try {
|
|
2354
2425
|
await reloadDecisionChannels(ctx);
|
|
2355
2426
|
}
|
|
@@ -2431,6 +2502,9 @@ export default function piWorkflows(pi) {
|
|
|
2431
2502
|
});
|
|
2432
2503
|
pi.on("turn_end", async (event, ctx) => {
|
|
2433
2504
|
await activeRun?.recorder?.handleTurnEnd(event, ctx).catch(() => undefined);
|
|
2505
|
+
// turn_end observes the message after message_end replacement handlers,
|
|
2506
|
+
// so this is the exact assistant message that remains visible.
|
|
2507
|
+
activeRun?.executor.handleMessageEnd(event.message);
|
|
2434
2508
|
});
|
|
2435
2509
|
pi.on("message_start", async (event, ctx) => {
|
|
2436
2510
|
await activeRun?.recorder?.handleMessageStart(event, ctx).catch(() => undefined);
|
|
@@ -2473,9 +2547,9 @@ export default function piWorkflows(pi) {
|
|
|
2473
2547
|
return;
|
|
2474
2548
|
}
|
|
2475
2549
|
}
|
|
2476
|
-
const flushedNatural = turnCoordinator.flushNatural(ctx.isIdle() && !sessionClosed && !runHeld());
|
|
2477
2550
|
const run = activeRun;
|
|
2478
2551
|
if (!run) {
|
|
2552
|
+
turnCoordinator.flushNatural(ctx.isIdle() && !sessionClosed && !runHeld());
|
|
2479
2553
|
presentationPending = null;
|
|
2480
2554
|
runSyncPass(ctx);
|
|
2481
2555
|
return;
|
|
@@ -2483,8 +2557,16 @@ export default function piWorkflows(pi) {
|
|
|
2483
2557
|
await run.recorder?.synchronize(ctx).catch(() => undefined);
|
|
2484
2558
|
run.recorder?.settleAttempt();
|
|
2485
2559
|
run.executor.setStreaming(false);
|
|
2486
|
-
if (
|
|
2560
|
+
if (run.executor.pendingCompletion === "assistant") {
|
|
2561
|
+
// The visible response is the step output. Resolve it before any
|
|
2562
|
+
// unrelated deferred turn can start and replace the captured message.
|
|
2487
2563
|
run.executor.handleAgentSettled();
|
|
2564
|
+
}
|
|
2565
|
+
else {
|
|
2566
|
+
const flushedNatural = turnCoordinator.flushNatural(ctx.isIdle() && !sessionClosed && !runHeld());
|
|
2567
|
+
if (flushedNatural === 0)
|
|
2568
|
+
run.executor.handleAgentSettled();
|
|
2569
|
+
}
|
|
2488
2570
|
runSyncPass(ctx);
|
|
2489
2571
|
});
|
|
2490
2572
|
pi.on("session_shutdown", async () => {
|