@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,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 { projectControllerStorePath, SqliteControllerStore } from "../controllers/index.js";
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
- { triggerTurn: false },
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
- resume: true,
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 (ctx: ExtensionContext): Promise<WorkflowControlResult> => {
1416
- if (activeRun) {
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
- if (pendingToolLaunch !== null) {
1426
- const ref = pendingToolLaunch.ref;
1427
- pendingToolLaunch = null;
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 the queued workflow launch for ${ref}.`,
1430
- details: { action: "cancel", workflow: ref, queued: false },
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
- throw new Error(`Workflow run not found: ${runId}`);
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 ((state === undefined || state === null) && pendingToolLaunch !== null) {
1558
- return {
1559
- message: `Workflow ${pendingToolLaunch.ref} is queued until the current turn finishes.`,
1560
- details: { active: false, queued: true, workflow: pendingToolLaunch.ref },
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
- if (pendingToolLaunch !== null) {
1907
- throw new Error("A workflow launch is already waiting for the current turn to finish.");
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
- if (pendingToolLaunch !== null) {
1934
- throw new Error("A workflow launch is already waiting for the current turn to finish.");
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 reservation = { ctx, ref, input, options };
1940
- pendingToolLaunch = reservation;
1941
- try {
1942
- const resolved = await resolveWorkflowRef(ref, { cwd: ctx.cwd }, builtinWorkflowCatalog);
1943
- const workflow = resolved.definition;
1944
- if (pendingToolLaunch !== reservation) {
1945
- throw new Error("The queued workflow launch was cancelled before validation finished.");
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
- return {
1948
- message: `Workflow ${workflow.name} will start after this turn finishes.`,
1949
- details: {
1950
- action: "start",
1951
- workflow: workflow.name,
1952
- source: resolved.source,
1953
- queued: true,
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
- if (pendingToolLaunch === reservation) {
1958
- pendingToolLaunch = null;
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
- await resumeParkedRun(ctx);
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 && pendingToolLaunch !== null) {
2447
- const launch = pendingToolLaunch;
2448
- pendingToolLaunch = null;
2689
+ if (activeRun === null) {
2449
2690
  try {
2450
- const runId = await startRun(launch.ctx, launch.ref, launch.input, launch.options);
2451
- if (runId === undefined) {
2452
- notify(launch.ctx, "The queued workflow could not start.", "error");
2453
- } else if (launch.options?.parentRunId !== undefined) {
2454
- lastWaitingRunId = null;
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(launch.ctx, `Could not start queued workflow: ${errorMessage(error)}`, "error");
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);