@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/dist/extension/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
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 { builtinWorkflowCatalog } from "../builtins/catalog.js";
|
|
5
|
-
import { projectControllerStorePath, SqliteControllerStore } from "../controllers/index.js";
|
|
5
|
+
import { projectControllerStorePath, SqliteControllerStore, } from "../controllers/index.js";
|
|
6
|
+
import { compositionMetadata } from "../workflows/composition.js";
|
|
6
7
|
import { humanDecisionChannelRequest } from "../workflows/decision-presentation.js";
|
|
7
8
|
import { WorkflowEngine } from "../workflows/engine.js";
|
|
8
9
|
import { ClaimLostError, errorMessage, isClaimLostError, TimeoutError, } from "../workflows/errors.js";
|
|
@@ -56,6 +57,44 @@ function humanDecisionRequest(value) {
|
|
|
56
57
|
}
|
|
57
58
|
return value;
|
|
58
59
|
}
|
|
60
|
+
function definitionDigest(snapshot) {
|
|
61
|
+
return `sha256:${createHash("sha256").update(JSON.stringify(snapshot)).digest("hex")}`;
|
|
62
|
+
}
|
|
63
|
+
function launchSourceIdentity(workflow, root) {
|
|
64
|
+
return {
|
|
65
|
+
root,
|
|
66
|
+
mounted: compositionMetadata(workflow)?.sources ?? [],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function preparedLaunchOptions(options) {
|
|
70
|
+
return {
|
|
71
|
+
...(options.presentation !== undefined ? { presentation: options.presentation } : {}),
|
|
72
|
+
...(options.parentRunId !== undefined ? { parentRunId: options.parentRunId } : {}),
|
|
73
|
+
...(options.humanDecision !== undefined ? { humanDecision: options.humanDecision } : {}),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function parsePreparedLaunchOptions(value) {
|
|
77
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
78
|
+
throw new Error("Stored workflow launch options are invalid");
|
|
79
|
+
}
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
function safeLaunchError(error) {
|
|
83
|
+
const raw = errorMessage(error)
|
|
84
|
+
.replace(/Bearer\s+\S+/giu, "Bearer [redacted]")
|
|
85
|
+
.replace(/(token|api[_-]?key|secret|password)(\s*[:=]\s*)\S+/giu, "$1$2[redacted]")
|
|
86
|
+
.replaceAll("\n", " ")
|
|
87
|
+
.trim();
|
|
88
|
+
const code = /not found|cannot find|unknown workflow/iu.test(raw)
|
|
89
|
+
? "workflow_not_found"
|
|
90
|
+
: /source changed|source mismatch/iu.test(raw)
|
|
91
|
+
? "source_changed"
|
|
92
|
+
: /invalid|must be/iu.test(raw)
|
|
93
|
+
? "workflow_invalid"
|
|
94
|
+
: "activation_failed";
|
|
95
|
+
const message = raw.length <= 500 ? raw : `${raw.slice(0, 480)}… [error truncated]`;
|
|
96
|
+
return { code, message: message || "The deferred workflow could not start" };
|
|
97
|
+
}
|
|
59
98
|
/** Parse `/workflow` arguments. Exported for tests. */
|
|
60
99
|
export function parseWorkflowArgs(args) {
|
|
61
100
|
const trimmed = args.trim();
|
|
@@ -178,6 +217,7 @@ export default function piWorkflows(pi) {
|
|
|
178
217
|
// Run events remain an audit feed and never enter a conversation.
|
|
179
218
|
let syncArmed = false;
|
|
180
219
|
let runSyncTimer = null;
|
|
220
|
+
let activationRecovery;
|
|
181
221
|
let decisionRecoveryTimer = null;
|
|
182
222
|
let decisionRecoveryActive = false;
|
|
183
223
|
const recordRunEvent = (event) => {
|
|
@@ -207,6 +247,7 @@ export default function piWorkflows(pi) {
|
|
|
207
247
|
if (runQueueStore === null || !syncArmed)
|
|
208
248
|
return;
|
|
209
249
|
try {
|
|
250
|
+
activationRecovery?.(ctx);
|
|
210
251
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
211
252
|
const alreadyDelivered = deliveredNotificationIds(ctx);
|
|
212
253
|
const claimToken = randomUUID();
|
|
@@ -225,7 +266,9 @@ export default function piWorkflows(pi) {
|
|
|
225
266
|
runId: notification.runId,
|
|
226
267
|
kind: notification.kind,
|
|
227
268
|
},
|
|
228
|
-
},
|
|
269
|
+
}, notification.kind === "launch_failure"
|
|
270
|
+
? { triggerTurn: true, deliverAs: "followUp" }
|
|
271
|
+
: { triggerTurn: false });
|
|
229
272
|
alreadyDelivered.add(notification.notificationId);
|
|
230
273
|
}
|
|
231
274
|
runQueueStore.markWorkflowNotificationDelivered({
|
|
@@ -252,7 +295,6 @@ export default function piWorkflows(pi) {
|
|
|
252
295
|
let systemTurnAbort = null;
|
|
253
296
|
let suppressWorkflowAssistantTail = false;
|
|
254
297
|
let lastExpiredAttempt = null;
|
|
255
|
-
let pendingToolLaunch = null;
|
|
256
298
|
// The interactive run currently parked at a checkpoint, if any.
|
|
257
299
|
let lastWaitingRunId = null;
|
|
258
300
|
let widgetTimer = null;
|
|
@@ -671,7 +713,10 @@ export default function piWorkflows(pi) {
|
|
|
671
713
|
workflowSourceRef: workflowSource.kind === "builtin"
|
|
672
714
|
? `builtin:${workflowSource.id}`
|
|
673
715
|
: workflowSource.path,
|
|
716
|
+
workflowSource: launchSourceIdentity(workflow, workflowSource),
|
|
717
|
+
definitionDigest: definitionDigest(snapshot),
|
|
674
718
|
input,
|
|
719
|
+
launchOptions: preparedLaunchOptions(options),
|
|
675
720
|
runnerId,
|
|
676
721
|
claimToken: token,
|
|
677
722
|
leaseMs: RUN_CLAIM_LEASE_MS,
|
|
@@ -799,6 +844,11 @@ export default function piWorkflows(pi) {
|
|
|
799
844
|
...(options.resume === true ? { resume: true } : {}),
|
|
800
845
|
...(options.parentRunId !== undefined ? { parentRunId: options.parentRunId } : {}),
|
|
801
846
|
};
|
|
847
|
+
if (queueStore !== null &&
|
|
848
|
+
claimToken !== undefined &&
|
|
849
|
+
!queueStore.markWorkflowRunRunning({ runId, claimToken })) {
|
|
850
|
+
throw new ClaimLostError(runId);
|
|
851
|
+
}
|
|
802
852
|
activeRun = run;
|
|
803
853
|
if (queueStore !== null && claimToken !== undefined) {
|
|
804
854
|
const store = queueStore;
|
|
@@ -948,8 +998,10 @@ export default function piWorkflows(pi) {
|
|
|
948
998
|
: bundle.state.workflowSource.kind === "builtin"
|
|
949
999
|
? `builtin:${bundle.state.workflowSource.id}`
|
|
950
1000
|
: bundle.state.workflowSource.path;
|
|
1001
|
+
const launchOptions = parsePreparedLaunchOptions(claimed.launchOptions);
|
|
951
1002
|
started = await startRun(ctx, sourceRef, claimed.input, {
|
|
952
|
-
|
|
1003
|
+
...launchOptions,
|
|
1004
|
+
resume: bundle !== null,
|
|
953
1005
|
runId: claimed.runId,
|
|
954
1006
|
claimToken,
|
|
955
1007
|
});
|
|
@@ -1103,8 +1155,8 @@ export default function piWorkflows(pi) {
|
|
|
1103
1155
|
}
|
|
1104
1156
|
return null;
|
|
1105
1157
|
};
|
|
1106
|
-
const cancelWorkflowControl = async (ctx) => {
|
|
1107
|
-
if (activeRun) {
|
|
1158
|
+
const cancelWorkflowControl = async (ctx, requestedRunId) => {
|
|
1159
|
+
if (activeRun && (requestedRunId === undefined || requestedRunId === activeRun.runId)) {
|
|
1108
1160
|
const workflowName = activeRun.workflowName;
|
|
1109
1161
|
const runId = activeRun.runId;
|
|
1110
1162
|
activeRun.engine.cancel();
|
|
@@ -1113,12 +1165,30 @@ export default function piWorkflows(pi) {
|
|
|
1113
1165
|
details: { action: "cancel", workflowName, runId },
|
|
1114
1166
|
};
|
|
1115
1167
|
}
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1168
|
+
const queue = ensureRunQueueStore(ctx.cwd);
|
|
1169
|
+
const queued = requestedRunId === undefined
|
|
1170
|
+
? queue.findSessionReservation(ctx.sessionManager.getSessionId())
|
|
1171
|
+
: queue.getWorkflowRun(requestedRunId);
|
|
1172
|
+
if (queued !== undefined &&
|
|
1173
|
+
["queued", "starting"].includes(queued.status) &&
|
|
1174
|
+
(queued.originSessionId === null ||
|
|
1175
|
+
queued.originSessionId === ctx.sessionManager.getSessionId())) {
|
|
1176
|
+
if (!queue.cancelWorkflowRun({ runId: queued.runId })) {
|
|
1177
|
+
throw new Error(`Workflow ${queued.runId} could not be cancelled because its state changed.`);
|
|
1178
|
+
}
|
|
1179
|
+
recordRunEvent({
|
|
1180
|
+
runId: queued.runId,
|
|
1181
|
+
workflowRef: queued.workflowName,
|
|
1182
|
+
type: "cancelled",
|
|
1183
|
+
});
|
|
1119
1184
|
return {
|
|
1120
|
-
message: `Cancelled
|
|
1121
|
-
details: {
|
|
1185
|
+
message: `Cancelled queued workflow ${queued.workflowName} (run ${queued.runId}).`,
|
|
1186
|
+
details: {
|
|
1187
|
+
action: "cancel",
|
|
1188
|
+
workflow: queued.workflowName,
|
|
1189
|
+
runId: queued.runId,
|
|
1190
|
+
queued: false,
|
|
1191
|
+
},
|
|
1122
1192
|
};
|
|
1123
1193
|
}
|
|
1124
1194
|
if (widgetSource) {
|
|
@@ -1225,11 +1295,28 @@ export default function piWorkflows(pi) {
|
|
|
1225
1295
|
},
|
|
1226
1296
|
};
|
|
1227
1297
|
};
|
|
1298
|
+
const workflowLaunchStatus = (record) => ({
|
|
1299
|
+
message: `Workflow ${record.workflowName} is ${record.status} (run ${record.runId}).`,
|
|
1300
|
+
details: {
|
|
1301
|
+
action: "status",
|
|
1302
|
+
active: ["starting", "running"].includes(record.status),
|
|
1303
|
+
queued: record.status === "queued",
|
|
1304
|
+
workflowName: record.workflowName,
|
|
1305
|
+
runId: record.runId,
|
|
1306
|
+
status: record.status,
|
|
1307
|
+
...(record.errorCode === null ? {} : { errorCode: record.errorCode }),
|
|
1308
|
+
...(record.errorMessage === null ? {} : { error: record.errorMessage }),
|
|
1309
|
+
},
|
|
1310
|
+
...(["failed", "cancelled"].includes(record.status) ? { level: "warning" } : {}),
|
|
1311
|
+
});
|
|
1228
1312
|
const statusWorkflowControl = async (ctx, runId) => {
|
|
1229
1313
|
if (runId !== undefined) {
|
|
1230
1314
|
const bundle = await readRunBundle(new WorkflowRunStore().runDirFor(runId));
|
|
1231
1315
|
if (bundle === null) {
|
|
1232
|
-
|
|
1316
|
+
const launch = ensureRunQueueStore(ctx.cwd).getWorkflowRun(runId);
|
|
1317
|
+
if (launch === undefined)
|
|
1318
|
+
throw new Error(`Workflow run not found: ${runId}`);
|
|
1319
|
+
return workflowLaunchStatus(launch);
|
|
1233
1320
|
}
|
|
1234
1321
|
const { state } = bundle;
|
|
1235
1322
|
return {
|
|
@@ -1238,11 +1325,10 @@ export default function piWorkflows(pi) {
|
|
|
1238
1325
|
};
|
|
1239
1326
|
}
|
|
1240
1327
|
const state = activeRun?.lastState ?? widgetSource?.state;
|
|
1241
|
-
if (
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
};
|
|
1328
|
+
if (state === undefined || state === null) {
|
|
1329
|
+
const queued = ensureRunQueueStore(ctx.cwd).findSessionReservation(ctx.sessionManager.getSessionId());
|
|
1330
|
+
if (queued !== undefined)
|
|
1331
|
+
return workflowLaunchStatus(queued);
|
|
1246
1332
|
}
|
|
1247
1333
|
if (state === undefined || state === null) {
|
|
1248
1334
|
return {
|
|
@@ -1545,8 +1631,9 @@ export default function piWorkflows(pi) {
|
|
|
1545
1631
|
if (activeRun !== null) {
|
|
1546
1632
|
throw new Error(`A workflow is already running: ${activeRun.workflowName}.`);
|
|
1547
1633
|
}
|
|
1548
|
-
|
|
1549
|
-
|
|
1634
|
+
const reserved = ensureRunQueueStore(ctx.cwd).findSessionReservation(ctx.sessionManager.getSessionId());
|
|
1635
|
+
if (reserved !== undefined) {
|
|
1636
|
+
throw new Error(`Workflow ${reserved.workflowName} is already ${reserved.status} (run ${reserved.runId}).`);
|
|
1550
1637
|
}
|
|
1551
1638
|
if (presentationPending !== null) {
|
|
1552
1639
|
throw new Error("The previous workflow result is still being presented.");
|
|
@@ -1564,36 +1651,140 @@ export default function piWorkflows(pi) {
|
|
|
1564
1651
|
if (activeRun !== null) {
|
|
1565
1652
|
throw new Error(`A workflow is already running: ${activeRun.workflowName}. Cancel it before starting another.`);
|
|
1566
1653
|
}
|
|
1567
|
-
|
|
1568
|
-
|
|
1654
|
+
const queue = ensureRunQueueStore(ctx.cwd);
|
|
1655
|
+
const existing = queue.findSessionReservation(ctx.sessionManager.getSessionId());
|
|
1656
|
+
if (existing !== undefined) {
|
|
1657
|
+
throw new Error(`Workflow ${existing.workflowName} is already ${existing.status} (run ${existing.runId}).`);
|
|
1569
1658
|
}
|
|
1570
1659
|
if (presentationPending !== null) {
|
|
1571
1660
|
throw new Error("The previous workflow result is still being presented.");
|
|
1572
1661
|
}
|
|
1573
|
-
const
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
const
|
|
1578
|
-
if (
|
|
1579
|
-
throw new Error(
|
|
1662
|
+
const resolved = await resolveWorkflowRef(ref, { cwd: ctx.cwd }, builtinWorkflowCatalog);
|
|
1663
|
+
const workflow = resolved.definition;
|
|
1664
|
+
const workflowSource = resolved.source;
|
|
1665
|
+
if (options.parentRunId !== undefined) {
|
|
1666
|
+
const parent = await readRunBundle(new WorkflowRunStore().runDirFor(options.parentRunId));
|
|
1667
|
+
if (parent === null || parent.state.status !== "waiting") {
|
|
1668
|
+
throw new Error(`Workflow run ${options.parentRunId} is not waiting at a checkpoint`);
|
|
1580
1669
|
}
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1670
|
+
if (parent.state.workflowSource !== undefined &&
|
|
1671
|
+
!isDeepStrictEqual(parent.state.workflowSource, workflowSource)) {
|
|
1672
|
+
throw new Error(`Workflow source changed since run ${options.parentRunId} started; revert the edit to answer its checkpoint`);
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
const snapshot = createDefinitionSnapshot(workflow);
|
|
1676
|
+
const runId = createRunId(workflow.name);
|
|
1677
|
+
try {
|
|
1678
|
+
queue.reserveWorkflowRun({
|
|
1679
|
+
runId,
|
|
1680
|
+
workflowName: workflow.name,
|
|
1681
|
+
workflowSourceRef: workflowSource.kind === "builtin" ? `builtin:${workflowSource.id}` : workflowSource.path,
|
|
1682
|
+
workflowSource: launchSourceIdentity(workflow, workflowSource),
|
|
1683
|
+
definitionDigest: definitionDigest(snapshot),
|
|
1684
|
+
input,
|
|
1685
|
+
launchOptions: preparedLaunchOptions(options),
|
|
1686
|
+
runnerId,
|
|
1687
|
+
originSessionId: ctx.sessionManager.getSessionId(),
|
|
1688
|
+
...(options.parentRunId !== undefined ? { parentRunId: options.parentRunId } : {}),
|
|
1689
|
+
});
|
|
1590
1690
|
}
|
|
1591
1691
|
catch (error) {
|
|
1592
|
-
|
|
1593
|
-
|
|
1692
|
+
const reserved = queue.findSessionReservation(ctx.sessionManager.getSessionId());
|
|
1693
|
+
if (reserved !== undefined) {
|
|
1694
|
+
throw new Error(`A workflow launch is already waiting: ${reserved.workflowName} (run ${reserved.runId}).`, { cause: error });
|
|
1594
1695
|
}
|
|
1595
1696
|
throw error;
|
|
1596
1697
|
}
|
|
1698
|
+
recordRunEvent({
|
|
1699
|
+
runId,
|
|
1700
|
+
workflowRef: workflow.name,
|
|
1701
|
+
type: "queued",
|
|
1702
|
+
payload: options.parentRunId === undefined ? {} : { parentRunId: options.parentRunId },
|
|
1703
|
+
});
|
|
1704
|
+
syncArmed = true;
|
|
1705
|
+
return {
|
|
1706
|
+
message: `Workflow ${workflow.name} queued (run ${runId}).`,
|
|
1707
|
+
details: {
|
|
1708
|
+
action: "start",
|
|
1709
|
+
workflow: workflow.name,
|
|
1710
|
+
runId,
|
|
1711
|
+
source: workflowSource,
|
|
1712
|
+
queued: true,
|
|
1713
|
+
},
|
|
1714
|
+
};
|
|
1715
|
+
};
|
|
1716
|
+
const activatePreparedLaunch = async (ctx, prepared) => {
|
|
1717
|
+
const queue = ensureRunQueueStore(ctx.cwd);
|
|
1718
|
+
const claimToken = randomUUID();
|
|
1719
|
+
const claimed = queue.claimWorkflowRun({
|
|
1720
|
+
runId: prepared.runId,
|
|
1721
|
+
runnerId,
|
|
1722
|
+
claimToken,
|
|
1723
|
+
leaseMs: RUN_CLAIM_LEASE_MS,
|
|
1724
|
+
});
|
|
1725
|
+
if (claimed === undefined)
|
|
1726
|
+
return false;
|
|
1727
|
+
try {
|
|
1728
|
+
const resolved = await resolveWorkflowRef(claimed.workflowSourceRef, { cwd: ctx.cwd }, builtinWorkflowCatalog);
|
|
1729
|
+
const snapshot = createDefinitionSnapshot(resolved.definition);
|
|
1730
|
+
if (!isDeepStrictEqual(launchSourceIdentity(resolved.definition, resolved.source), claimed.workflowSource) ||
|
|
1731
|
+
definitionDigest(snapshot) !== claimed.definitionDigest) {
|
|
1732
|
+
throw new Error("Workflow source changed after the launch was queued");
|
|
1733
|
+
}
|
|
1734
|
+
const launchOptions = parsePreparedLaunchOptions(claimed.launchOptions);
|
|
1735
|
+
const started = await startRun(ctx, claimed.workflowSourceRef, claimed.input, {
|
|
1736
|
+
...launchOptions,
|
|
1737
|
+
runId: claimed.runId,
|
|
1738
|
+
claimToken,
|
|
1739
|
+
});
|
|
1740
|
+
if (started === undefined)
|
|
1741
|
+
throw new Error("The queued workflow could not start");
|
|
1742
|
+
if (launchOptions.parentRunId !== undefined)
|
|
1743
|
+
lastWaitingRunId = null;
|
|
1744
|
+
return true;
|
|
1745
|
+
}
|
|
1746
|
+
catch (error) {
|
|
1747
|
+
const safe = safeLaunchError(error);
|
|
1748
|
+
queue.failWorkflowRun({
|
|
1749
|
+
runId: claimed.runId,
|
|
1750
|
+
claimToken,
|
|
1751
|
+
errorCode: safe.code,
|
|
1752
|
+
errorMessage: safe.message,
|
|
1753
|
+
});
|
|
1754
|
+
recordRunEvent({
|
|
1755
|
+
runId: claimed.runId,
|
|
1756
|
+
workflowRef: claimed.workflowName,
|
|
1757
|
+
type: "launch_failed",
|
|
1758
|
+
payload: { errorCode: safe.code, error: safe.message },
|
|
1759
|
+
});
|
|
1760
|
+
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.`;
|
|
1761
|
+
try {
|
|
1762
|
+
queue.enqueueWorkflowNotification({
|
|
1763
|
+
runId: claimed.runId,
|
|
1764
|
+
nodeId: "$launch",
|
|
1765
|
+
attemptId: claimed.runId,
|
|
1766
|
+
notificationIndex: 1,
|
|
1767
|
+
targetSessionId: claimed.originSessionId ?? ctx.sessionManager.getSessionId(),
|
|
1768
|
+
kind: "launch_failure",
|
|
1769
|
+
content,
|
|
1770
|
+
notificationId: `launch-failure:${claimed.runId}`,
|
|
1771
|
+
});
|
|
1772
|
+
}
|
|
1773
|
+
catch {
|
|
1774
|
+
// A deterministic notification id makes duplicate insertion harmless.
|
|
1775
|
+
}
|
|
1776
|
+
notify(ctx, content, "error");
|
|
1777
|
+
runSyncPass(ctx);
|
|
1778
|
+
return false;
|
|
1779
|
+
}
|
|
1780
|
+
};
|
|
1781
|
+
activationRecovery = (ctx) => {
|
|
1782
|
+
if (activeRun !== null)
|
|
1783
|
+
return;
|
|
1784
|
+
const prepared = ensureRunQueueStore(ctx.cwd).findSessionReservation(ctx.sessionManager.getSessionId());
|
|
1785
|
+
if (prepared !== undefined && ["queued", "starting"].includes(prepared.status)) {
|
|
1786
|
+
void activatePreparedLaunch(ctx, prepared).catch(() => undefined);
|
|
1787
|
+
}
|
|
1597
1788
|
};
|
|
1598
1789
|
pi.registerCommand("piw", {
|
|
1599
1790
|
description: "Open the current workflow run in piw through Herdr",
|
|
@@ -1841,7 +2032,7 @@ export default function piWorkflows(pi) {
|
|
|
1841
2032
|
control = resumeWorkflowControl(ctx);
|
|
1842
2033
|
break;
|
|
1843
2034
|
case "cancel":
|
|
1844
|
-
control = await cancelWorkflowControl(ctx);
|
|
2035
|
+
control = await cancelWorkflowControl(ctx, params.runId);
|
|
1845
2036
|
break;
|
|
1846
2037
|
case "answer": {
|
|
1847
2038
|
const waiting = await resolveWaitingWorkflow(ctx, params.runId);
|
|
@@ -1951,7 +2142,13 @@ export default function piWorkflows(pi) {
|
|
|
1951
2142
|
notify(ctx, `Could not recover human decisions: ${errorMessage(error)}`, "warning");
|
|
1952
2143
|
}
|
|
1953
2144
|
try {
|
|
1954
|
-
|
|
2145
|
+
const prepared = ensureRunQueueStore(ctx.cwd).findSessionReservation(ctx.sessionManager.getSessionId());
|
|
2146
|
+
if (prepared !== undefined && ["queued", "starting"].includes(prepared.status)) {
|
|
2147
|
+
await activatePreparedLaunch(ctx, prepared);
|
|
2148
|
+
}
|
|
2149
|
+
else {
|
|
2150
|
+
await resumeParkedRun(ctx);
|
|
2151
|
+
}
|
|
1955
2152
|
}
|
|
1956
2153
|
catch (error) {
|
|
1957
2154
|
notify(ctx, `Could not resume a parked workflow: ${errorMessage(error)}`, "warning");
|
|
@@ -2031,22 +2228,18 @@ export default function piWorkflows(pi) {
|
|
|
2031
2228
|
activeRun?.recorder?.handleToolEnd(event);
|
|
2032
2229
|
});
|
|
2033
2230
|
pi.on("agent_settled", async (_event, ctx) => {
|
|
2034
|
-
if (activeRun === null
|
|
2035
|
-
const launch = pendingToolLaunch;
|
|
2036
|
-
pendingToolLaunch = null;
|
|
2231
|
+
if (activeRun === null) {
|
|
2037
2232
|
try {
|
|
2038
|
-
const
|
|
2039
|
-
if (
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
else if (launch.options?.parentRunId !== undefined) {
|
|
2043
|
-
lastWaitingRunId = null;
|
|
2233
|
+
const prepared = ensureRunQueueStore(ctx.cwd).findSessionReservation(ctx.sessionManager.getSessionId());
|
|
2234
|
+
if (prepared !== undefined && ["queued", "starting"].includes(prepared.status)) {
|
|
2235
|
+
await activatePreparedLaunch(ctx, prepared);
|
|
2236
|
+
return;
|
|
2044
2237
|
}
|
|
2045
2238
|
}
|
|
2046
2239
|
catch (error) {
|
|
2047
|
-
notify(
|
|
2240
|
+
notify(ctx, `Could not activate a queued workflow: ${safeLaunchError(error).message}`, "warning");
|
|
2241
|
+
return;
|
|
2048
2242
|
}
|
|
2049
|
-
return;
|
|
2050
2243
|
}
|
|
2051
2244
|
const run = activeRun;
|
|
2052
2245
|
if (!run) {
|
|
@@ -2076,7 +2269,6 @@ export default function piWorkflows(pi) {
|
|
|
2076
2269
|
await run?.recorder?.stop().catch(() => undefined);
|
|
2077
2270
|
await run?.completion?.catch(() => undefined);
|
|
2078
2271
|
activeRun = null;
|
|
2079
|
-
pendingToolLaunch = null;
|
|
2080
2272
|
lastWaitingRunId = null;
|
|
2081
2273
|
if (runSyncTimer !== null) {
|
|
2082
2274
|
clearInterval(runSyncTimer);
|