@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.
Files changed (36) hide show
  1. package/README.md +14 -2
  2. package/dist/builtins/autoimplement.workflow.d.ts +2 -0
  3. package/dist/builtins/autoimplement.workflow.js +186 -9
  4. package/dist/builtins/autoimplement.workflow.js.map +1 -1
  5. package/dist/controllers/sqlite.d.ts +55 -7
  6. package/dist/controllers/sqlite.js +195 -48
  7. package/dist/controllers/sqlite.js.map +1 -1
  8. package/dist/extension/index.js +246 -54
  9. package/dist/extension/index.js.map +1 -1
  10. package/dist/herdr/setup.d.ts +13 -1
  11. package/dist/herdr/setup.js +349 -36
  12. package/dist/herdr/setup.js.map +1 -1
  13. package/dist/host/runner.js +3 -0
  14. package/dist/host/runner.js.map +1 -1
  15. package/dist/viewer/cli.d.ts +1 -0
  16. package/dist/viewer/cli.js +21 -10
  17. package/dist/viewer/cli.js.map +1 -1
  18. package/dist/workflows/migrate-sources.d.ts +1 -1
  19. package/dist/workflows/migrate-sources.js.map +1 -1
  20. package/dist/workflows/tool-input.d.ts +1 -0
  21. package/dist/workflows/tool-input.js +2 -2
  22. package/dist/workflows/tool-input.js.map +1 -1
  23. package/docs/2026-08-20-durable-workflow-launch-plan.md +449 -0
  24. package/docs/plans/2026-08-20-autoimplement-blocker-challenge-plan.md +138 -0
  25. package/docs/plans/2026-08-20-herdr-plugin-sync-plan.md +104 -0
  26. package/docs/workflows.md +11 -0
  27. package/herdr-plugin.toml +1 -1
  28. package/package.json +1 -1
  29. package/src/builtins/autoimplement.workflow.ts +212 -9
  30. package/src/controllers/sqlite.ts +308 -54
  31. package/src/extension/index.ts +303 -58
  32. package/src/herdr/setup.ts +429 -39
  33. package/src/host/runner.ts +3 -0
  34. package/src/viewer/cli.ts +22 -10
  35. package/src/workflows/migrate-sources.ts +5 -1
  36. package/src/workflows/tool-input.ts +5 -2
@@ -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
- }, { triggerTurn: false });
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
- resume: true,
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
- if (pendingToolLaunch !== null) {
1117
- const ref = pendingToolLaunch.ref;
1118
- pendingToolLaunch = null;
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 the queued workflow launch for ${ref}.`,
1121
- details: { action: "cancel", workflow: ref, queued: false },
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
- throw new Error(`Workflow run not found: ${runId}`);
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 ((state === undefined || state === null) && pendingToolLaunch !== null) {
1242
- return {
1243
- message: `Workflow ${pendingToolLaunch.ref} is queued until the current turn finishes.`,
1244
- details: { active: false, queued: true, workflow: pendingToolLaunch.ref },
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
- if (pendingToolLaunch !== null) {
1549
- throw new Error("A workflow launch is already waiting for the current turn to finish.");
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
- if (pendingToolLaunch !== null) {
1568
- throw new Error("A workflow launch is already waiting for the current turn to finish.");
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 reservation = { ctx, ref, input, options };
1574
- pendingToolLaunch = reservation;
1575
- try {
1576
- const resolved = await resolveWorkflowRef(ref, { cwd: ctx.cwd }, builtinWorkflowCatalog);
1577
- const workflow = resolved.definition;
1578
- if (pendingToolLaunch !== reservation) {
1579
- throw new Error("The queued workflow launch was cancelled before validation finished.");
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
- return {
1582
- message: `Workflow ${workflow.name} will start after this turn finishes.`,
1583
- details: {
1584
- action: "start",
1585
- workflow: workflow.name,
1586
- source: resolved.source,
1587
- queued: true,
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
- if (pendingToolLaunch === reservation) {
1593
- pendingToolLaunch = null;
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
- await resumeParkedRun(ctx);
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 && pendingToolLaunch !== null) {
2035
- const launch = pendingToolLaunch;
2036
- pendingToolLaunch = null;
2231
+ if (activeRun === null) {
2037
2232
  try {
2038
- const runId = await startRun(launch.ctx, launch.ref, launch.input, launch.options);
2039
- if (runId === undefined) {
2040
- notify(launch.ctx, "The queued workflow could not start.", "error");
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(launch.ctx, `Could not start queued workflow: ${errorMessage(error)}`, "error");
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);