@osolmaz/pi-workflows 0.11.0 → 0.11.2
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 +14 -2
- package/dist/builtins/autoimplement.workflow.d.ts +2 -0
- package/dist/builtins/autoimplement.workflow.js +186 -9
- package/dist/builtins/autoimplement.workflow.js.map +1 -1
- package/dist/controllers/sqlite.d.ts +55 -7
- package/dist/controllers/sqlite.js +195 -48
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/extension/index.js +246 -54
- package/dist/extension/index.js.map +1 -1
- package/dist/herdr/setup.d.ts +13 -1
- package/dist/herdr/setup.js +349 -36
- package/dist/herdr/setup.js.map +1 -1
- package/dist/host/runner.js +3 -0
- package/dist/host/runner.js.map +1 -1
- package/dist/viewer/cli.d.ts +1 -0
- package/dist/viewer/cli.js +21 -10
- package/dist/viewer/cli.js.map +1 -1
- package/dist/workflows/migrate-sources.d.ts +1 -1
- package/dist/workflows/migrate-sources.js.map +1 -1
- package/dist/workflows/tool-input.d.ts +1 -0
- package/dist/workflows/tool-input.js +2 -2
- package/dist/workflows/tool-input.js.map +1 -1
- package/docs/2026-08-20-durable-workflow-launch-plan.md +449 -0
- package/docs/plans/2026-08-20-autoimplement-blocker-challenge-plan.md +138 -0
- package/docs/plans/2026-08-20-herdr-plugin-sync-plan.md +104 -0
- package/docs/workflows.md +11 -0
- package/herdr-plugin.toml +1 -1
- package/package.json +1 -1
- package/src/builtins/autoimplement.workflow.ts +212 -9
- package/src/controllers/sqlite.ts +308 -54
- package/src/extension/index.ts +303 -58
- package/src/herdr/setup.ts +429 -39
- package/src/host/runner.ts +3 -0
- package/src/viewer/cli.ts +22 -10
- package/src/workflows/migrate-sources.ts +5 -1
- package/src/workflows/tool-input.ts +5 -2
package/src/extension/index.ts
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { isDeepStrictEqual } from "node:util";
|
|
4
4
|
import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { builtinWorkflowCatalog } from "../builtins/catalog.js";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
projectControllerStorePath,
|
|
8
|
+
SqliteControllerStore,
|
|
9
|
+
type WorkflowRunQueueRecord,
|
|
10
|
+
} from "../controllers/index.js";
|
|
7
11
|
import type { JsonObject } from "../controllers/types.js";
|
|
8
12
|
import type { WorkflowSchedulerResult } from "../controllers/workflows.js";
|
|
13
|
+
import { compositionMetadata } from "../workflows/composition.js";
|
|
9
14
|
import { humanDecisionChannelRequest } from "../workflows/decision-presentation.js";
|
|
10
15
|
import { WorkflowEngine } from "../workflows/engine.js";
|
|
11
16
|
import {
|
|
@@ -170,6 +175,12 @@ function humanDecisionRequest(value: unknown): HumanDecisionRequest | null {
|
|
|
170
175
|
return value as HumanDecisionRequest;
|
|
171
176
|
}
|
|
172
177
|
|
|
178
|
+
type PreparedLaunchOptions = {
|
|
179
|
+
presentation?: boolean;
|
|
180
|
+
parentRunId?: string;
|
|
181
|
+
humanDecision?: AcceptedHumanDecision;
|
|
182
|
+
};
|
|
183
|
+
|
|
173
184
|
type StartRunOptions = {
|
|
174
185
|
runId?: string;
|
|
175
186
|
childKey?: string;
|
|
@@ -187,6 +198,49 @@ type StartRunOptions = {
|
|
|
187
198
|
claimToken?: string;
|
|
188
199
|
};
|
|
189
200
|
|
|
201
|
+
function definitionDigest(snapshot: WorkflowDefinitionSnapshot): string {
|
|
202
|
+
return `sha256:${createHash("sha256").update(JSON.stringify(snapshot)).digest("hex")}`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function launchSourceIdentity(workflow: WorkflowDefinition, root: unknown): unknown {
|
|
206
|
+
return {
|
|
207
|
+
root,
|
|
208
|
+
mounted: compositionMetadata(workflow)?.sources ?? [],
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function preparedLaunchOptions(options: StartRunOptions): PreparedLaunchOptions {
|
|
213
|
+
return {
|
|
214
|
+
...(options.presentation !== undefined ? { presentation: options.presentation } : {}),
|
|
215
|
+
...(options.parentRunId !== undefined ? { parentRunId: options.parentRunId } : {}),
|
|
216
|
+
...(options.humanDecision !== undefined ? { humanDecision: options.humanDecision } : {}),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function parsePreparedLaunchOptions(value: unknown): PreparedLaunchOptions {
|
|
221
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
222
|
+
throw new Error("Stored workflow launch options are invalid");
|
|
223
|
+
}
|
|
224
|
+
return value as PreparedLaunchOptions;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function safeLaunchError(error: unknown): { code: string; message: string } {
|
|
228
|
+
const raw = errorMessage(error)
|
|
229
|
+
.replace(/Bearer\s+\S+/giu, "Bearer [redacted]")
|
|
230
|
+
.replace(/(token|api[_-]?key|secret|password)(\s*[:=]\s*)\S+/giu, "$1$2[redacted]")
|
|
231
|
+
.replaceAll("\n", " ")
|
|
232
|
+
.trim();
|
|
233
|
+
const code = /not found|cannot find|unknown workflow/iu.test(raw)
|
|
234
|
+
? "workflow_not_found"
|
|
235
|
+
: /source changed|source mismatch/iu.test(raw)
|
|
236
|
+
? "source_changed"
|
|
237
|
+
: /invalid|must be/iu.test(raw)
|
|
238
|
+
? "workflow_invalid"
|
|
239
|
+
: "activation_failed";
|
|
240
|
+
const message = raw.length <= 500 ? raw : `${raw.slice(0, 480)}… [error truncated]`;
|
|
241
|
+
return { code, message: message || "The deferred workflow could not start" };
|
|
242
|
+
}
|
|
243
|
+
|
|
190
244
|
export type ParsedWorkflowArgs =
|
|
191
245
|
| { kind: "list" }
|
|
192
246
|
| { kind: "cancel" }
|
|
@@ -343,6 +397,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
343
397
|
// Run events remain an audit feed and never enter a conversation.
|
|
344
398
|
let syncArmed = false;
|
|
345
399
|
let runSyncTimer: ReturnType<typeof setInterval> | null = null;
|
|
400
|
+
let activationRecovery: ((ctx: ExtensionContext) => void) | undefined;
|
|
346
401
|
let decisionRecoveryTimer: ReturnType<typeof setInterval> | null = null;
|
|
347
402
|
let decisionRecoveryActive = false;
|
|
348
403
|
|
|
@@ -377,6 +432,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
377
432
|
const runSyncPass = (ctx: ExtensionContext): void => {
|
|
378
433
|
if (runQueueStore === null || !syncArmed) return;
|
|
379
434
|
try {
|
|
435
|
+
activationRecovery?.(ctx);
|
|
380
436
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
381
437
|
const alreadyDelivered = deliveredNotificationIds(ctx);
|
|
382
438
|
const claimToken = randomUUID();
|
|
@@ -397,7 +453,9 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
397
453
|
kind: notification.kind,
|
|
398
454
|
},
|
|
399
455
|
},
|
|
400
|
-
|
|
456
|
+
notification.kind === "launch_failure"
|
|
457
|
+
? { triggerTurn: true, deliverAs: "followUp" }
|
|
458
|
+
: { triggerTurn: false },
|
|
401
459
|
);
|
|
402
460
|
alreadyDelivered.add(notification.notificationId);
|
|
403
461
|
}
|
|
@@ -425,12 +483,6 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
425
483
|
let systemTurnAbort: AgentStepContract | null = null;
|
|
426
484
|
let suppressWorkflowAssistantTail = false;
|
|
427
485
|
let lastExpiredAttempt: { contract: AgentStepContract; reason: string } | null = null;
|
|
428
|
-
let pendingToolLaunch: {
|
|
429
|
-
ctx: ExtensionContext;
|
|
430
|
-
ref: string;
|
|
431
|
-
input: unknown;
|
|
432
|
-
options?: StartRunOptions;
|
|
433
|
-
} | null = null;
|
|
434
486
|
// The interactive run currently parked at a checkpoint, if any.
|
|
435
487
|
let lastWaitingRunId: string | null = null;
|
|
436
488
|
let widgetTimer: NodeJS.Timeout | null = null;
|
|
@@ -927,7 +979,10 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
927
979
|
workflowSource.kind === "builtin"
|
|
928
980
|
? `builtin:${workflowSource.id}`
|
|
929
981
|
: workflowSource.path,
|
|
982
|
+
workflowSource: launchSourceIdentity(workflow, workflowSource),
|
|
983
|
+
definitionDigest: definitionDigest(snapshot),
|
|
930
984
|
input,
|
|
985
|
+
launchOptions: preparedLaunchOptions(options),
|
|
931
986
|
runnerId,
|
|
932
987
|
claimToken: token,
|
|
933
988
|
leaseMs: RUN_CLAIM_LEASE_MS,
|
|
@@ -1063,6 +1118,13 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1063
1118
|
...(options.resume === true ? { resume: true } : {}),
|
|
1064
1119
|
...(options.parentRunId !== undefined ? { parentRunId: options.parentRunId } : {}),
|
|
1065
1120
|
};
|
|
1121
|
+
if (
|
|
1122
|
+
queueStore !== null &&
|
|
1123
|
+
claimToken !== undefined &&
|
|
1124
|
+
!queueStore.markWorkflowRunRunning({ runId, claimToken })
|
|
1125
|
+
) {
|
|
1126
|
+
throw new ClaimLostError(runId);
|
|
1127
|
+
}
|
|
1066
1128
|
activeRun = run;
|
|
1067
1129
|
if (queueStore !== null && claimToken !== undefined) {
|
|
1068
1130
|
const store = queueStore;
|
|
@@ -1224,8 +1286,10 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1224
1286
|
: bundle.state.workflowSource.kind === "builtin"
|
|
1225
1287
|
? `builtin:${bundle.state.workflowSource.id}`
|
|
1226
1288
|
: bundle.state.workflowSource.path;
|
|
1289
|
+
const launchOptions = parsePreparedLaunchOptions(claimed.launchOptions);
|
|
1227
1290
|
started = await startRun(ctx, sourceRef, claimed.input, {
|
|
1228
|
-
|
|
1291
|
+
...launchOptions,
|
|
1292
|
+
resume: bundle !== null,
|
|
1229
1293
|
runId: claimed.runId,
|
|
1230
1294
|
claimToken,
|
|
1231
1295
|
});
|
|
@@ -1412,8 +1476,11 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1412
1476
|
return null;
|
|
1413
1477
|
};
|
|
1414
1478
|
|
|
1415
|
-
const cancelWorkflowControl = async (
|
|
1416
|
-
|
|
1479
|
+
const cancelWorkflowControl = async (
|
|
1480
|
+
ctx: ExtensionContext,
|
|
1481
|
+
requestedRunId?: string,
|
|
1482
|
+
): Promise<WorkflowControlResult> => {
|
|
1483
|
+
if (activeRun && (requestedRunId === undefined || requestedRunId === activeRun.runId)) {
|
|
1417
1484
|
const workflowName = activeRun.workflowName;
|
|
1418
1485
|
const runId = activeRun.runId;
|
|
1419
1486
|
activeRun.engine.cancel();
|
|
@@ -1422,12 +1489,35 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1422
1489
|
details: { action: "cancel", workflowName, runId },
|
|
1423
1490
|
};
|
|
1424
1491
|
}
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1492
|
+
const queue = ensureRunQueueStore(ctx.cwd);
|
|
1493
|
+
const queued =
|
|
1494
|
+
requestedRunId === undefined
|
|
1495
|
+
? queue.findSessionReservation(ctx.sessionManager.getSessionId())
|
|
1496
|
+
: queue.getWorkflowRun(requestedRunId);
|
|
1497
|
+
if (
|
|
1498
|
+
queued !== undefined &&
|
|
1499
|
+
["queued", "starting"].includes(queued.status) &&
|
|
1500
|
+
(queued.originSessionId === null ||
|
|
1501
|
+
queued.originSessionId === ctx.sessionManager.getSessionId())
|
|
1502
|
+
) {
|
|
1503
|
+
if (!queue.cancelWorkflowRun({ runId: queued.runId })) {
|
|
1504
|
+
throw new Error(
|
|
1505
|
+
`Workflow ${queued.runId} could not be cancelled because its state changed.`,
|
|
1506
|
+
);
|
|
1507
|
+
}
|
|
1508
|
+
recordRunEvent({
|
|
1509
|
+
runId: queued.runId,
|
|
1510
|
+
workflowRef: queued.workflowName,
|
|
1511
|
+
type: "cancelled",
|
|
1512
|
+
});
|
|
1428
1513
|
return {
|
|
1429
|
-
message: `Cancelled
|
|
1430
|
-
details: {
|
|
1514
|
+
message: `Cancelled queued workflow ${queued.workflowName} (run ${queued.runId}).`,
|
|
1515
|
+
details: {
|
|
1516
|
+
action: "cancel",
|
|
1517
|
+
workflow: queued.workflowName,
|
|
1518
|
+
runId: queued.runId,
|
|
1519
|
+
queued: false,
|
|
1520
|
+
},
|
|
1431
1521
|
};
|
|
1432
1522
|
}
|
|
1433
1523
|
if (widgetSource) {
|
|
@@ -1538,6 +1628,21 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1538
1628
|
};
|
|
1539
1629
|
};
|
|
1540
1630
|
|
|
1631
|
+
const workflowLaunchStatus = (record: WorkflowRunQueueRecord): WorkflowControlResult => ({
|
|
1632
|
+
message: `Workflow ${record.workflowName} is ${record.status} (run ${record.runId}).`,
|
|
1633
|
+
details: {
|
|
1634
|
+
action: "status",
|
|
1635
|
+
active: ["starting", "running"].includes(record.status),
|
|
1636
|
+
queued: record.status === "queued",
|
|
1637
|
+
workflowName: record.workflowName,
|
|
1638
|
+
runId: record.runId,
|
|
1639
|
+
status: record.status,
|
|
1640
|
+
...(record.errorCode === null ? {} : { errorCode: record.errorCode }),
|
|
1641
|
+
...(record.errorMessage === null ? {} : { error: record.errorMessage }),
|
|
1642
|
+
},
|
|
1643
|
+
...(["failed", "cancelled"].includes(record.status) ? { level: "warning" as const } : {}),
|
|
1644
|
+
});
|
|
1645
|
+
|
|
1541
1646
|
const statusWorkflowControl = async (
|
|
1542
1647
|
ctx: ExtensionContext,
|
|
1543
1648
|
runId?: string,
|
|
@@ -1545,7 +1650,9 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1545
1650
|
if (runId !== undefined) {
|
|
1546
1651
|
const bundle = await readRunBundle(new WorkflowRunStore().runDirFor(runId));
|
|
1547
1652
|
if (bundle === null) {
|
|
1548
|
-
|
|
1653
|
+
const launch = ensureRunQueueStore(ctx.cwd).getWorkflowRun(runId);
|
|
1654
|
+
if (launch === undefined) throw new Error(`Workflow run not found: ${runId}`);
|
|
1655
|
+
return workflowLaunchStatus(launch);
|
|
1549
1656
|
}
|
|
1550
1657
|
const { state } = bundle;
|
|
1551
1658
|
return {
|
|
@@ -1554,11 +1661,11 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1554
1661
|
};
|
|
1555
1662
|
}
|
|
1556
1663
|
const state = activeRun?.lastState ?? widgetSource?.state;
|
|
1557
|
-
if (
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1664
|
+
if (state === undefined || state === null) {
|
|
1665
|
+
const queued = ensureRunQueueStore(ctx.cwd).findSessionReservation(
|
|
1666
|
+
ctx.sessionManager.getSessionId(),
|
|
1667
|
+
);
|
|
1668
|
+
if (queued !== undefined) return workflowLaunchStatus(queued);
|
|
1562
1669
|
}
|
|
1563
1670
|
if (state === undefined || state === null) {
|
|
1564
1671
|
return {
|
|
@@ -1903,8 +2010,13 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1903
2010
|
if (activeRun !== null) {
|
|
1904
2011
|
throw new Error(`A workflow is already running: ${activeRun.workflowName}.`);
|
|
1905
2012
|
}
|
|
1906
|
-
|
|
1907
|
-
|
|
2013
|
+
const reserved = ensureRunQueueStore(ctx.cwd).findSessionReservation(
|
|
2014
|
+
ctx.sessionManager.getSessionId(),
|
|
2015
|
+
);
|
|
2016
|
+
if (reserved !== undefined) {
|
|
2017
|
+
throw new Error(
|
|
2018
|
+
`Workflow ${reserved.workflowName} is already ${reserved.status} (run ${reserved.runId}).`,
|
|
2019
|
+
);
|
|
1908
2020
|
}
|
|
1909
2021
|
if (presentationPending !== null) {
|
|
1910
2022
|
throw new Error("The previous workflow result is still being presented.");
|
|
@@ -1930,35 +2042,159 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1930
2042
|
`A workflow is already running: ${activeRun.workflowName}. Cancel it before starting another.`,
|
|
1931
2043
|
);
|
|
1932
2044
|
}
|
|
1933
|
-
|
|
1934
|
-
|
|
2045
|
+
const queue = ensureRunQueueStore(ctx.cwd);
|
|
2046
|
+
const existing = queue.findSessionReservation(ctx.sessionManager.getSessionId());
|
|
2047
|
+
if (existing !== undefined) {
|
|
2048
|
+
throw new Error(
|
|
2049
|
+
`Workflow ${existing.workflowName} is already ${existing.status} (run ${existing.runId}).`,
|
|
2050
|
+
);
|
|
1935
2051
|
}
|
|
1936
2052
|
if (presentationPending !== null) {
|
|
1937
2053
|
throw new Error("The previous workflow result is still being presented.");
|
|
1938
2054
|
}
|
|
1939
|
-
const
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
const
|
|
1944
|
-
if (
|
|
1945
|
-
throw new Error(
|
|
2055
|
+
const resolved = await resolveWorkflowRef(ref, { cwd: ctx.cwd }, builtinWorkflowCatalog);
|
|
2056
|
+
const workflow = resolved.definition;
|
|
2057
|
+
const workflowSource = resolved.source;
|
|
2058
|
+
if (options.parentRunId !== undefined) {
|
|
2059
|
+
const parent = await readRunBundle(new WorkflowRunStore().runDirFor(options.parentRunId));
|
|
2060
|
+
if (parent === null || parent.state.status !== "waiting") {
|
|
2061
|
+
throw new Error(`Workflow run ${options.parentRunId} is not waiting at a checkpoint`);
|
|
1946
2062
|
}
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
source
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
2063
|
+
if (
|
|
2064
|
+
parent.state.workflowSource !== undefined &&
|
|
2065
|
+
!isDeepStrictEqual(parent.state.workflowSource, workflowSource)
|
|
2066
|
+
) {
|
|
2067
|
+
throw new Error(
|
|
2068
|
+
`Workflow source changed since run ${options.parentRunId} started; revert the edit to answer its checkpoint`,
|
|
2069
|
+
);
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
const snapshot = createDefinitionSnapshot(workflow);
|
|
2073
|
+
const runId = createRunId(workflow.name);
|
|
2074
|
+
try {
|
|
2075
|
+
queue.reserveWorkflowRun({
|
|
2076
|
+
runId,
|
|
2077
|
+
workflowName: workflow.name,
|
|
2078
|
+
workflowSourceRef:
|
|
2079
|
+
workflowSource.kind === "builtin" ? `builtin:${workflowSource.id}` : workflowSource.path,
|
|
2080
|
+
workflowSource: launchSourceIdentity(workflow, workflowSource),
|
|
2081
|
+
definitionDigest: definitionDigest(snapshot),
|
|
2082
|
+
input,
|
|
2083
|
+
launchOptions: preparedLaunchOptions(options),
|
|
2084
|
+
runnerId,
|
|
2085
|
+
originSessionId: ctx.sessionManager.getSessionId(),
|
|
2086
|
+
...(options.parentRunId !== undefined ? { parentRunId: options.parentRunId } : {}),
|
|
2087
|
+
});
|
|
1956
2088
|
} catch (error) {
|
|
1957
|
-
|
|
1958
|
-
|
|
2089
|
+
const reserved = queue.findSessionReservation(ctx.sessionManager.getSessionId());
|
|
2090
|
+
if (reserved !== undefined) {
|
|
2091
|
+
throw new Error(
|
|
2092
|
+
`A workflow launch is already waiting: ${reserved.workflowName} (run ${reserved.runId}).`,
|
|
2093
|
+
{ cause: error },
|
|
2094
|
+
);
|
|
1959
2095
|
}
|
|
1960
2096
|
throw error;
|
|
1961
2097
|
}
|
|
2098
|
+
recordRunEvent({
|
|
2099
|
+
runId,
|
|
2100
|
+
workflowRef: workflow.name,
|
|
2101
|
+
type: "queued",
|
|
2102
|
+
payload: options.parentRunId === undefined ? {} : { parentRunId: options.parentRunId },
|
|
2103
|
+
});
|
|
2104
|
+
syncArmed = true;
|
|
2105
|
+
return {
|
|
2106
|
+
message: `Workflow ${workflow.name} queued (run ${runId}).`,
|
|
2107
|
+
details: {
|
|
2108
|
+
action: "start",
|
|
2109
|
+
workflow: workflow.name,
|
|
2110
|
+
runId,
|
|
2111
|
+
source: workflowSource,
|
|
2112
|
+
queued: true,
|
|
2113
|
+
},
|
|
2114
|
+
};
|
|
2115
|
+
};
|
|
2116
|
+
|
|
2117
|
+
const activatePreparedLaunch = async (
|
|
2118
|
+
ctx: ExtensionContext,
|
|
2119
|
+
prepared: WorkflowRunQueueRecord,
|
|
2120
|
+
): Promise<boolean> => {
|
|
2121
|
+
const queue = ensureRunQueueStore(ctx.cwd);
|
|
2122
|
+
const claimToken = randomUUID();
|
|
2123
|
+
const claimed = queue.claimWorkflowRun({
|
|
2124
|
+
runId: prepared.runId,
|
|
2125
|
+
runnerId,
|
|
2126
|
+
claimToken,
|
|
2127
|
+
leaseMs: RUN_CLAIM_LEASE_MS,
|
|
2128
|
+
});
|
|
2129
|
+
if (claimed === undefined) return false;
|
|
2130
|
+
try {
|
|
2131
|
+
const resolved = await resolveWorkflowRef(
|
|
2132
|
+
claimed.workflowSourceRef,
|
|
2133
|
+
{ cwd: ctx.cwd },
|
|
2134
|
+
builtinWorkflowCatalog,
|
|
2135
|
+
);
|
|
2136
|
+
const snapshot = createDefinitionSnapshot(resolved.definition);
|
|
2137
|
+
if (
|
|
2138
|
+
!isDeepStrictEqual(
|
|
2139
|
+
launchSourceIdentity(resolved.definition, resolved.source),
|
|
2140
|
+
claimed.workflowSource,
|
|
2141
|
+
) ||
|
|
2142
|
+
definitionDigest(snapshot) !== claimed.definitionDigest
|
|
2143
|
+
) {
|
|
2144
|
+
throw new Error("Workflow source changed after the launch was queued");
|
|
2145
|
+
}
|
|
2146
|
+
const launchOptions = parsePreparedLaunchOptions(claimed.launchOptions);
|
|
2147
|
+
const started = await startRun(ctx, claimed.workflowSourceRef, claimed.input, {
|
|
2148
|
+
...launchOptions,
|
|
2149
|
+
runId: claimed.runId,
|
|
2150
|
+
claimToken,
|
|
2151
|
+
});
|
|
2152
|
+
if (started === undefined) throw new Error("The queued workflow could not start");
|
|
2153
|
+
if (launchOptions.parentRunId !== undefined) lastWaitingRunId = null;
|
|
2154
|
+
return true;
|
|
2155
|
+
} catch (error) {
|
|
2156
|
+
const safe = safeLaunchError(error);
|
|
2157
|
+
queue.failWorkflowRun({
|
|
2158
|
+
runId: claimed.runId,
|
|
2159
|
+
claimToken,
|
|
2160
|
+
errorCode: safe.code,
|
|
2161
|
+
errorMessage: safe.message,
|
|
2162
|
+
});
|
|
2163
|
+
recordRunEvent({
|
|
2164
|
+
runId: claimed.runId,
|
|
2165
|
+
workflowRef: claimed.workflowName,
|
|
2166
|
+
type: "launch_failed",
|
|
2167
|
+
payload: { errorCode: safe.code, error: safe.message },
|
|
2168
|
+
});
|
|
2169
|
+
const content = `Workflow ${claimed.workflowName} failed to start (run ${claimed.runId}): ${safe.message}. Inspect the error and call workflow start again only after you correct the cause.`;
|
|
2170
|
+
try {
|
|
2171
|
+
queue.enqueueWorkflowNotification({
|
|
2172
|
+
runId: claimed.runId,
|
|
2173
|
+
nodeId: "$launch",
|
|
2174
|
+
attemptId: claimed.runId,
|
|
2175
|
+
notificationIndex: 1,
|
|
2176
|
+
targetSessionId: claimed.originSessionId ?? ctx.sessionManager.getSessionId(),
|
|
2177
|
+
kind: "launch_failure",
|
|
2178
|
+
content,
|
|
2179
|
+
notificationId: `launch-failure:${claimed.runId}`,
|
|
2180
|
+
});
|
|
2181
|
+
} catch {
|
|
2182
|
+
// A deterministic notification id makes duplicate insertion harmless.
|
|
2183
|
+
}
|
|
2184
|
+
notify(ctx, content, "error");
|
|
2185
|
+
runSyncPass(ctx);
|
|
2186
|
+
return false;
|
|
2187
|
+
}
|
|
2188
|
+
};
|
|
2189
|
+
|
|
2190
|
+
activationRecovery = (ctx) => {
|
|
2191
|
+
if (activeRun !== null) return;
|
|
2192
|
+
const prepared = ensureRunQueueStore(ctx.cwd).findSessionReservation(
|
|
2193
|
+
ctx.sessionManager.getSessionId(),
|
|
2194
|
+
);
|
|
2195
|
+
if (prepared !== undefined && ["queued", "starting"].includes(prepared.status)) {
|
|
2196
|
+
void activatePreparedLaunch(ctx, prepared).catch(() => undefined);
|
|
2197
|
+
}
|
|
1962
2198
|
};
|
|
1963
2199
|
|
|
1964
2200
|
pi.registerCommand("piw", {
|
|
@@ -2218,7 +2454,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2218
2454
|
control = resumeWorkflowControl(ctx);
|
|
2219
2455
|
break;
|
|
2220
2456
|
case "cancel":
|
|
2221
|
-
control = await cancelWorkflowControl(ctx);
|
|
2457
|
+
control = await cancelWorkflowControl(ctx, params.runId);
|
|
2222
2458
|
break;
|
|
2223
2459
|
case "answer": {
|
|
2224
2460
|
const waiting = await resolveWaitingWorkflow(ctx, params.runId);
|
|
@@ -2348,7 +2584,14 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2348
2584
|
notify(ctx, `Could not recover human decisions: ${errorMessage(error)}`, "warning");
|
|
2349
2585
|
}
|
|
2350
2586
|
try {
|
|
2351
|
-
|
|
2587
|
+
const prepared = ensureRunQueueStore(ctx.cwd).findSessionReservation(
|
|
2588
|
+
ctx.sessionManager.getSessionId(),
|
|
2589
|
+
);
|
|
2590
|
+
if (prepared !== undefined && ["queued", "starting"].includes(prepared.status)) {
|
|
2591
|
+
await activatePreparedLaunch(ctx, prepared);
|
|
2592
|
+
} else {
|
|
2593
|
+
await resumeParkedRun(ctx);
|
|
2594
|
+
}
|
|
2352
2595
|
} catch (error) {
|
|
2353
2596
|
notify(ctx, `Could not resume a parked workflow: ${errorMessage(error)}`, "warning");
|
|
2354
2597
|
}
|
|
@@ -2443,20 +2686,23 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2443
2686
|
});
|
|
2444
2687
|
|
|
2445
2688
|
pi.on("agent_settled", async (_event, ctx) => {
|
|
2446
|
-
if (activeRun === null
|
|
2447
|
-
const launch = pendingToolLaunch;
|
|
2448
|
-
pendingToolLaunch = null;
|
|
2689
|
+
if (activeRun === null) {
|
|
2449
2690
|
try {
|
|
2450
|
-
const
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2691
|
+
const prepared = ensureRunQueueStore(ctx.cwd).findSessionReservation(
|
|
2692
|
+
ctx.sessionManager.getSessionId(),
|
|
2693
|
+
);
|
|
2694
|
+
if (prepared !== undefined && ["queued", "starting"].includes(prepared.status)) {
|
|
2695
|
+
await activatePreparedLaunch(ctx, prepared);
|
|
2696
|
+
return;
|
|
2455
2697
|
}
|
|
2456
2698
|
} catch (error) {
|
|
2457
|
-
notify(
|
|
2699
|
+
notify(
|
|
2700
|
+
ctx,
|
|
2701
|
+
`Could not activate a queued workflow: ${safeLaunchError(error).message}`,
|
|
2702
|
+
"warning",
|
|
2703
|
+
);
|
|
2704
|
+
return;
|
|
2458
2705
|
}
|
|
2459
|
-
return;
|
|
2460
2706
|
}
|
|
2461
2707
|
const run = activeRun;
|
|
2462
2708
|
if (!run) {
|
|
@@ -2486,7 +2732,6 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2486
2732
|
await run?.recorder?.stop().catch(() => undefined);
|
|
2487
2733
|
await run?.completion?.catch(() => undefined);
|
|
2488
2734
|
activeRun = null;
|
|
2489
|
-
pendingToolLaunch = null;
|
|
2490
2735
|
lastWaitingRunId = null;
|
|
2491
2736
|
if (runSyncTimer !== null) {
|
|
2492
2737
|
clearInterval(runSyncTimer);
|