@marcoscale98/pi-extensible-workflows 5.14.1-fork.1

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 (204) hide show
  1. package/CHANGELOG.md +536 -0
  2. package/README.md +64 -0
  3. package/dist/src/agent-execution.d.ts +338 -0
  4. package/dist/src/agent-execution.js +1633 -0
  5. package/dist/src/background-widget.d.ts +74 -0
  6. package/dist/src/background-widget.js +1340 -0
  7. package/dist/src/budget.d.ts +38 -0
  8. package/dist/src/budget.js +188 -0
  9. package/dist/src/changelog.d.ts +6 -0
  10. package/dist/src/changelog.js +112 -0
  11. package/dist/src/decoders.d.ts +96 -0
  12. package/dist/src/decoders.js +629 -0
  13. package/dist/src/execution.d.ts +19 -0
  14. package/dist/src/execution.js +749 -0
  15. package/dist/src/herdr.d.ts +53 -0
  16. package/dist/src/herdr.js +270 -0
  17. package/dist/src/host-delivery.d.ts +95 -0
  18. package/dist/src/host-delivery.js +376 -0
  19. package/dist/src/host-navigator.d.ts +83 -0
  20. package/dist/src/host-navigator.js +1087 -0
  21. package/dist/src/host-phases.d.ts +89 -0
  22. package/dist/src/host-phases.js +320 -0
  23. package/dist/src/host-recovery.d.ts +141 -0
  24. package/dist/src/host-recovery.js +356 -0
  25. package/dist/src/host-runtime.d.ts +78 -0
  26. package/dist/src/host-runtime.js +378 -0
  27. package/dist/src/host-view.d.ts +116 -0
  28. package/dist/src/host-view.js +868 -0
  29. package/dist/src/host.d.ts +40 -0
  30. package/dist/src/host.js +1641 -0
  31. package/dist/src/index.d.ts +29 -0
  32. package/dist/src/index.js +13725 -0
  33. package/dist/src/index.js.map +6 -0
  34. package/dist/src/io.d.ts +14 -0
  35. package/dist/src/io.js +82 -0
  36. package/dist/src/package-identity.d.ts +4 -0
  37. package/dist/src/package-identity.js +6 -0
  38. package/dist/src/paths.d.ts +14 -0
  39. package/dist/src/paths.js +51 -0
  40. package/dist/src/persistence.d.ts +5 -0
  41. package/dist/src/persistence.js +5 -0
  42. package/dist/src/pi-runtime-adapter.d.ts +27 -0
  43. package/dist/src/pi-runtime-adapter.js +243 -0
  44. package/dist/src/pi-runtime-runner.d.ts +42 -0
  45. package/dist/src/pi-runtime-runner.js +505 -0
  46. package/dist/src/registry.d.ts +48 -0
  47. package/dist/src/registry.js +379 -0
  48. package/dist/src/retention.d.ts +17 -0
  49. package/dist/src/retention.js +195 -0
  50. package/dist/src/runtime/agent-runner.d.ts +143 -0
  51. package/dist/src/runtime/agent-runner.js +12 -0
  52. package/dist/src/runtime/index.d.ts +2 -0
  53. package/dist/src/runtime/index.js +2 -0
  54. package/dist/src/runtime/namespace.d.ts +14 -0
  55. package/dist/src/runtime/namespace.js +1 -0
  56. package/dist/src/runtime/workflow-result.d.ts +2 -0
  57. package/dist/src/runtime/workflow-result.js +6 -0
  58. package/dist/src/session-handoff.d.ts +2 -0
  59. package/dist/src/session-handoff.js +82 -0
  60. package/dist/src/session-lease.d.ts +12 -0
  61. package/dist/src/session-lease.js +180 -0
  62. package/dist/src/store.d.ts +95 -0
  63. package/dist/src/store.js +951 -0
  64. package/dist/src/subagent-manager-handle.d.ts +4 -0
  65. package/dist/src/subagent-manager-handle.js +6 -0
  66. package/dist/src/tool-timing.d.ts +12 -0
  67. package/dist/src/tool-timing.js +33 -0
  68. package/dist/src/trajectory-contracts.d.ts +8 -0
  69. package/dist/src/trajectory-contracts.js +9 -0
  70. package/dist/src/trajectory-host-handle.d.ts +12 -0
  71. package/dist/src/trajectory-host-handle.js +6 -0
  72. package/dist/src/trajectory.d.ts +128 -0
  73. package/dist/src/trajectory.js +594 -0
  74. package/dist/src/types.d.ts +960 -0
  75. package/dist/src/types.js +60 -0
  76. package/dist/src/utils.d.ts +57 -0
  77. package/dist/src/utils.js +225 -0
  78. package/dist/src/validation.d.ts +29 -0
  79. package/dist/src/validation.js +1100 -0
  80. package/dist/src/workflow-artifacts.d.ts +14 -0
  81. package/dist/src/workflow-artifacts.js +40 -0
  82. package/dist/starter/index.d.ts +1 -0
  83. package/dist/starter/index.js +103 -0
  84. package/dist/starter/index.js.map +6 -0
  85. package/dist/starter/review-loop.d.ts +72 -0
  86. package/dist/starter/review-loop.js +43 -0
  87. package/dist/starter/roles/developer.md +6 -0
  88. package/dist/starter/roles/oracle.md +18 -0
  89. package/dist/starter/roles/researcher.md +20 -0
  90. package/dist/starter/roles/reviewer.md +19 -0
  91. package/dist/starter/roles/scout.md +18 -0
  92. package/dist/subagents/index.d.ts +8 -0
  93. package/dist/subagents/index.js +8021 -0
  94. package/dist/subagents/index.js.map +6 -0
  95. package/dist/subagents/src/contracts.d.ts +173 -0
  96. package/dist/subagents/src/contracts.js +68 -0
  97. package/dist/subagents/src/decode.d.ts +15 -0
  98. package/dist/subagents/src/decode.js +182 -0
  99. package/dist/subagents/src/index.d.ts +12 -0
  100. package/dist/subagents/src/index.js +180 -0
  101. package/dist/subagents/src/manager.d.ts +3 -0
  102. package/dist/subagents/src/manager.js +1636 -0
  103. package/dist/subagents/src/navigator.d.ts +5 -0
  104. package/dist/subagents/src/navigator.js +627 -0
  105. package/dist/subagents/src/view.d.ts +70 -0
  106. package/dist/subagents/src/view.js +387 -0
  107. package/dist/subagents/src/worktree.d.ts +34 -0
  108. package/dist/subagents/src/worktree.js +68 -0
  109. package/dist/trajectory/assets/favicon.png +0 -0
  110. package/dist/trajectory/assets/index.html +772 -0
  111. package/dist/trajectory/assets/marked.min.js +6 -0
  112. package/dist/trajectory/assets/morphdom.min.js +1 -0
  113. package/dist/trajectory/index.d.ts +5 -0
  114. package/dist/trajectory/index.js +3459 -0
  115. package/dist/trajectory/index.js.map +6 -0
  116. package/dist/trajectory/src/assets/favicon.png +0 -0
  117. package/dist/trajectory/src/assets/index.html +772 -0
  118. package/dist/trajectory/src/assets/marked.min.js +6 -0
  119. package/dist/trajectory/src/assets/morphdom.min.js +1 -0
  120. package/dist/trajectory/src/export.d.ts +25 -0
  121. package/dist/trajectory/src/export.js +80 -0
  122. package/dist/trajectory/src/index.d.ts +26 -0
  123. package/dist/trajectory/src/index.js +732 -0
  124. package/dist/trajectory/src/server.d.ts +7 -0
  125. package/dist/trajectory/src/server.js +2534 -0
  126. package/dist/trajectory/src/server.js.map +6 -0
  127. package/examples/advanced-patterns/auto-model-router.mjs +78 -0
  128. package/examples/workflow-extension-template/README.md +37 -0
  129. package/examples/workflow-extension-template/extension.test.mjs +59 -0
  130. package/examples/workflow-extension-template/index.js +51 -0
  131. package/examples/workflow-extension-template/roles/reviewer.md +4 -0
  132. package/package.json +105 -0
  133. package/skills/pi-extensible-workflows/SKILL.md +160 -0
  134. package/src/agent-execution.ts +1533 -0
  135. package/src/background-widget.ts +1496 -0
  136. package/src/budget.ts +109 -0
  137. package/src/changelog.ts +114 -0
  138. package/src/decoders.ts +567 -0
  139. package/src/execution.ts +652 -0
  140. package/src/herdr.ts +240 -0
  141. package/src/host-delivery.ts +346 -0
  142. package/src/host-navigator.ts +871 -0
  143. package/src/host-phases.ts +264 -0
  144. package/src/host-recovery.ts +333 -0
  145. package/src/host-runtime.ts +373 -0
  146. package/src/host-view.ts +828 -0
  147. package/src/host.ts +1502 -0
  148. package/src/index.ts +29 -0
  149. package/src/io.ts +73 -0
  150. package/src/package-identity.ts +7 -0
  151. package/src/paths.ts +49 -0
  152. package/src/persistence.ts +11 -0
  153. package/src/pi-runtime-adapter.ts +247 -0
  154. package/src/pi-runtime-runner.ts +436 -0
  155. package/src/registry.ts +321 -0
  156. package/src/retention.ts +178 -0
  157. package/src/runtime/agent-runner.ts +158 -0
  158. package/src/runtime/index.ts +2 -0
  159. package/src/runtime/namespace.ts +16 -0
  160. package/src/runtime/workflow-result.ts +8 -0
  161. package/src/session-handoff.ts +78 -0
  162. package/src/session-lease.ts +125 -0
  163. package/src/store.ts +832 -0
  164. package/src/subagent-manager-handle.ts +8 -0
  165. package/src/tool-timing.ts +47 -0
  166. package/src/trajectory-contracts.ts +13 -0
  167. package/src/trajectory-host-handle.ts +16 -0
  168. package/src/trajectory.ts +543 -0
  169. package/src/types.ts +374 -0
  170. package/src/utils.ts +192 -0
  171. package/src/validation.ts +910 -0
  172. package/src/workflow-artifacts.ts +35 -0
  173. package/starter/index.ts +35 -0
  174. package/starter/prompts/council.md +16 -0
  175. package/starter/prompts/deep-research.md +16 -0
  176. package/starter/prompts/oracle.md +14 -0
  177. package/starter/prompts/parallel-review.md +22 -0
  178. package/starter/prompts/parallel-scout.md +16 -0
  179. package/starter/prompts/review-loop.md +14 -0
  180. package/starter/prompts/review.md +14 -0
  181. package/starter/prompts/scout.md +14 -0
  182. package/starter/review-loop.ts +73 -0
  183. package/starter/roles/developer.md +6 -0
  184. package/starter/roles/oracle.md +18 -0
  185. package/starter/roles/researcher.md +20 -0
  186. package/starter/roles/reviewer.md +19 -0
  187. package/starter/roles/scout.md +18 -0
  188. package/subagents/README.md +145 -0
  189. package/subagents/index.ts +8 -0
  190. package/subagents/src/contracts.ts +176 -0
  191. package/subagents/src/decode.ts +158 -0
  192. package/subagents/src/index.ts +194 -0
  193. package/subagents/src/manager.ts +1560 -0
  194. package/subagents/src/navigator.ts +488 -0
  195. package/subagents/src/view.ts +376 -0
  196. package/subagents/src/worktree.ts +99 -0
  197. package/trajectory/index.ts +5 -0
  198. package/trajectory/src/assets/favicon.png +0 -0
  199. package/trajectory/src/assets/index.html +772 -0
  200. package/trajectory/src/assets/marked.min.js +6 -0
  201. package/trajectory/src/assets/morphdom.min.js +1 -0
  202. package/trajectory/src/export.ts +85 -0
  203. package/trajectory/src/index.ts +604 -0
  204. package/trajectory/src/server.ts +447 -0
@@ -0,0 +1,264 @@
1
+ import { type PersistedRun } from "./persistence.js";
2
+ import { type AgentRecord, type LaunchSnapshot, type RunState, type WorkflowPhaseShellActivity } from "./types.js";
3
+ import { object } from "./utils.js";
4
+
5
+ export const WORKFLOW_PHASE_STATES = ["not started", "running", "completed", "failed", "cancelled", "interrupted", "budget_exhausted"] as const;
6
+ export type WorkflowPhaseState = (typeof WORKFLOW_PHASE_STATES)[number];
7
+ export interface WorkflowPhaseAgentCounts { total: number; completed: number; running: number; failed: number; cancelled: number; pending: number }
8
+ export interface WorkflowPhaseView { id: string; name: string; occurrence: number; state: WorkflowPhaseState; observed: boolean; afterAgent?: number; agents: readonly AgentRecord[]; counts: WorkflowPhaseAgentCounts; shellActivity?: WorkflowPhaseShellActivity }
9
+ export interface WorkflowPhaseModel { runState: RunState; declaredPhases: readonly string[]; phases: readonly WorkflowPhaseView[]; currentPhaseIndex?: number; currentPhaseId?: string; counts: Readonly<Partial<Record<WorkflowPhaseState, number>>>; unassignedAgents?: readonly AgentRecord[] }
10
+ type WorkflowPhaseSource = readonly string[] | Pick<LaunchSnapshot, "phases"> | undefined;
11
+ function phaseNames(source: WorkflowPhaseSource): string[] {
12
+ const phases: readonly unknown[] = source === undefined ? [] : Array.isArray(source) ? source : object(source) ? source.phases ?? [] : [];
13
+ return phases.filter((phase): phase is string => typeof phase === "string" && phase.trim() !== "").map((phase) => phase.trim());
14
+ }
15
+ export function phaseAgentCounts(agents: readonly AgentRecord[]): WorkflowPhaseAgentCounts {
16
+ const counts: WorkflowPhaseAgentCounts = { total: agents.length, completed: 0, running: 0, failed: 0, cancelled: 0, pending: 0 };
17
+ for (const agent of agents) {
18
+ if (agent.state === "completed") counts.completed += 1;
19
+ else if (agent.state === "running") counts.running += 1;
20
+ else if (agent.state === "failed") counts.failed += 1;
21
+ else if (agent.state === "cancelled") counts.cancelled += 1;
22
+ else counts.pending += 1;
23
+ }
24
+ return counts;
25
+ }
26
+ /** The live shell activity recorded for one phase index; `-1` is the preflight scope before any phase. */
27
+ export function shellActivityFor(run: Pick<PersistedRun, "activeShellsByPhase">, phaseIndex: number): WorkflowPhaseShellActivity | undefined {
28
+ return run.activeShellsByPhase?.find((activity) => activity.phaseIndex === phaseIndex && activity.active > 0);
29
+ }
30
+ function phaseState(runState: RunState, counts: WorkflowPhaseAgentCounts, isLatest: boolean, shellActive = false): WorkflowPhaseState {
31
+ if (!isLatest && !shellActive) return "completed";
32
+ if (runState === "failed") return "failed";
33
+ if (runState === "stopped") return "cancelled";
34
+ if (runState === "interrupted") return "interrupted";
35
+ if (runState === "budget_exhausted") return "budget_exhausted";
36
+ if (counts.failed > 0) return "failed";
37
+ if (shellActive) return "running";
38
+ if (counts.cancelled > 0) return "cancelled";
39
+ if (counts.running > 0 || counts.pending > 0) return "running";
40
+ return runState === "completed" ? "completed" : "running";
41
+ }
42
+ type WorkflowPhaseRun = Omit<Pick<PersistedRun, "state" | "phase" | "phaseHistory" | "agents" | "activeShellsByPhase">, "phaseHistory"> & { phaseHistory?: unknown };
43
+ export function buildWorkflowPhaseModel(run: WorkflowPhaseRun, source?: WorkflowPhaseSource): WorkflowPhaseModel {
44
+ const declaredPhases = phaseNames(source);
45
+ const rawHistory: readonly unknown[] = Array.isArray(run.phaseHistory) ? run.phaseHistory : [];
46
+ const observed: Array<{ name: string; afterAgent: number }> = [];
47
+ let boundary = 0;
48
+ for (const record of rawHistory) {
49
+ if (!object(record) || typeof record.phase !== "string" || !record.phase.trim() || typeof record.afterAgent !== "number" || !Number.isSafeInteger(record.afterAgent)) continue;
50
+ boundary = Math.max(boundary, Math.min(run.agents.length, Math.max(0, record.afterAgent)));
51
+ observed.push({ name: record.phase.trim(), afterAgent: boundary });
52
+ }
53
+ if (!observed.length && typeof run.phase === "string" && run.phase.trim()) observed.push({ name: run.phase.trim(), afterAgent: 0 });
54
+ const observedEntries = observed.map((entry, index) => ({ ...entry, index, agents: run.agents.slice(entry.afterAgent, observed[index + 1]?.afterAgent ?? run.agents.length) }));
55
+ const matchedDeclarations = new Set<number>();
56
+ const declarationIndices = observedEntries.map((entry) => {
57
+ const index = declaredPhases.findIndex((name, candidate) => !matchedDeclarations.has(candidate) && name === entry.name);
58
+ if (index >= 0) matchedDeclarations.add(index);
59
+ return index >= 0 ? index : undefined;
60
+ });
61
+ const entries: Array<{ name: string; observedIndex?: number; declarationIndex?: number }> = observedEntries.map((entry, index) => ({ name: entry.name, observedIndex: index, ...(declarationIndices[index] === undefined ? {} : { declarationIndex: declarationIndices[index] }) }));
62
+ for (const [declarationIndex, name] of declaredPhases.entries()) {
63
+ if (matchedDeclarations.has(declarationIndex)) continue;
64
+ const insertion = entries.findIndex((entry) => entry.declarationIndex !== undefined && entry.declarationIndex > declarationIndex);
65
+ const pending = { name };
66
+ if (insertion < 0) entries.push(pending); else entries.splice(insertion, 0, pending);
67
+ }
68
+ const occurrences = new Map<string, number>();
69
+ const phases = entries.map((entry) => {
70
+ const occurrence = (occurrences.get(entry.name) ?? 0) + 1;
71
+ occurrences.set(entry.name, occurrence);
72
+ const observation = entry.observedIndex === undefined ? undefined : observedEntries[entry.observedIndex];
73
+ const agents = observation?.agents ?? [];
74
+ const shellActivity = observation ? shellActivityFor(run, observation.index) : undefined;
75
+ const counts = phaseAgentCounts(agents);
76
+ const state = observation ? phaseState(run.state, counts, entry.observedIndex === observedEntries.length - 1, shellActivity !== undefined) : "not started";
77
+ return { id: `${entry.name}#${String(occurrence)}`, name: entry.name, occurrence, state, observed: observation !== undefined, ...(observation ? { afterAgent: observation.afterAgent } : {}), agents, counts, ...(shellActivity ? { shellActivity } : {}) };
78
+ });
79
+ const preflightActivity = shellActivityFor(run, -1);
80
+ const phasesWithPreflight = preflightActivity ? [{ id: "preflight", name: "Preflight", occurrence: 1, state: phaseState(run.state, phaseAgentCounts([]), true, true), observed: true, afterAgent: 0, agents: [], counts: phaseAgentCounts([]), shellActivity: preflightActivity }, ...phases] : phases;
81
+ let currentPhaseIndex: number | undefined;
82
+ for (let index = phasesWithPreflight.length - 1; index >= 0; index -= 1) { if (phasesWithPreflight[index]?.observed) { currentPhaseIndex = index; break; } }
83
+ const counts: Partial<Record<WorkflowPhaseState, number>> = {};
84
+ for (const phase of phasesWithPreflight) counts[phase.state] = (counts[phase.state] ?? 0) + 1;
85
+ const current = currentPhaseIndex === undefined ? undefined : phasesWithPreflight[currentPhaseIndex];
86
+ const assigned = new Set(observedEntries.flatMap(({ agents }) => agents.map((agent) => agent.id)));
87
+ const unassignedAgents = run.agents.filter((agent) => !assigned.has(agent.id));
88
+ const result: WorkflowPhaseModel = { runState: run.state, declaredPhases, phases: phasesWithPreflight, counts };
89
+ if (current !== undefined && currentPhaseIndex !== undefined) { result.currentPhaseIndex = currentPhaseIndex; result.currentPhaseId = current.id; }
90
+ if (unassignedAgents.length) result.unassignedAgents = unassignedAgents;
91
+ return result;
92
+ }
93
+ export interface WorkflowPhaseSelection { phaseId?: string | undefined; agentId?: string | undefined; nodeId?: string | undefined; expandedNodeIds?: readonly string[] | undefined; treeOnly?: boolean | undefined; detailsOnly?: boolean | undefined; actions?: { title: string; options: readonly string[]; index: number } | undefined }
94
+ export type WorkflowPhaseTreeNodeKind = "workflow" | "phase" | "operation" | "agent" | "shell";
95
+ export interface WorkflowPhaseTreeNode { id: string; kind: WorkflowPhaseTreeNodeKind; label: string; depth: number; phaseId: string; operationPath: readonly string[]; parentId?: string; children: readonly string[]; state: WorkflowPhaseState | RunState | AgentRecord["state"]; agentId?: string; agent?: AgentRecord; phase?: WorkflowPhaseView; shellActivity?: WorkflowPhaseShellActivity }
96
+ export interface WorkflowPhaseTree { roots: readonly string[]; nodes: readonly WorkflowPhaseTreeNode[]; byId: ReadonlyMap<string, WorkflowPhaseTreeNode> }
97
+ export interface WorkflowPhaseTreeSelection { nodeId?: string | undefined }
98
+ export type WorkflowPhaseTreeDirection = "up" | "down" | "left" | "right";
99
+ export function workflowPhaseTreePath(kind: WorkflowPhaseTreeNodeKind, phaseId: string, operationPath: readonly string[], agentId?: string): string {
100
+ if (kind === "workflow") return "workflow";
101
+ const root = `phase/${encodeURIComponent(phaseId)}`;
102
+ if (kind === "phase") return root;
103
+ if (kind === "shell") return `${root}/shell`;
104
+ const operation = operationPath.map((part) => encodeURIComponent(part)).join("/");
105
+ if (kind === "operation") return `${root}/operation/${operation}`;
106
+ return operation ? `${root}/operation/${operation}/agent/${encodeURIComponent(agentId ?? "")}` : `${root}/agent/${encodeURIComponent(agentId ?? "")}`;
107
+ }
108
+ function workflowPhaseTreeAggregateState(states: readonly AgentRecord["state"][]): AgentRecord["state"] {
109
+ if (!states.length || states.every((state) => state === "completed")) return "completed";
110
+ if (states.some((state) => state === "failed")) return "failed";
111
+ if (states.some((state) => state === "cancelled")) return "cancelled";
112
+ if (states.some((state) => state === "running")) return "running";
113
+ return "queued";
114
+ }
115
+ export function buildWorkflowPhaseTree(model: WorkflowPhaseModel): WorkflowPhaseTree {
116
+ type Draft = Omit<WorkflowPhaseTreeNode, "children"> & { children: string[] };
117
+ type AgentEntry = { agent: AgentRecord; node?: Draft; structuralPath: readonly string[]; operationPath: readonly string[]; defaultParentId: string };
118
+ const drafts = new Map<string, Draft>();
119
+ const roots: string[] = [];
120
+ const add = (node: Omit<Draft, "children">, parentId?: string): Draft => {
121
+ const existing = drafts.get(node.id);
122
+ if (existing) return existing;
123
+ const draft: Draft = { ...node, ...(parentId === undefined ? {} : { parentId }), children: [] };
124
+ drafts.set(draft.id, draft);
125
+ if (parentId === undefined) roots.push(draft.id); else drafts.get(parentId)?.children.push(draft.id);
126
+ return draft;
127
+ };
128
+ const samePath = (left: readonly string[], right: readonly string[]): boolean => left.length === right.length && left.every((part, index) => part === right[index]);
129
+ const addPhase = (phaseId: string, label: string, agents: readonly AgentRecord[], phase?: WorkflowPhaseView): void => {
130
+ const phaseNode = add({ id: workflowPhaseTreePath("phase", phaseId, []), kind: "phase", label, depth: 0, phaseId, operationPath: [], state: phase?.state ?? workflowPhaseTreeAggregateState(agents.map((agent) => agent.state)), ...(phase ? { phase } : {}) }, workflowNode.id);
131
+ if (phase?.shellActivity) add({ id: workflowPhaseTreePath("shell", phaseId, []), kind: "shell", label: `shell [running] (${String(phase.shellActivity.active)} active)`, depth: 0, phaseId, operationPath: [], state: "running", phase, shellActivity: phase.shellActivity }, phaseNode.id);
132
+ const operationNodes = new Map<string, Draft>();
133
+ const entries: AgentEntry[] = agents.map((agent) => {
134
+ const structuralPath = [...(agent.structuralPath ?? [])];
135
+ return { agent, structuralPath, operationPath: [...structuralPath, ...(agent.parentBreadcrumb ? [agent.parentBreadcrumb] : [])], defaultParentId: phaseNode.id };
136
+ });
137
+ const agentEntries = new Map(entries.map((entry) => [entry.agent.id, entry]));
138
+ const acceptedParents = new Map<string, string>();
139
+ const wouldCycle = (childId: string, parentId: string): boolean => {
140
+ const seen = new Set<string>([childId]);
141
+ let current: string | undefined = parentId;
142
+ while (current) {
143
+ if (seen.has(current)) return true;
144
+ seen.add(current);
145
+ current = acceptedParents.get(current);
146
+ }
147
+ return false;
148
+ };
149
+ for (const entry of entries) {
150
+ const parent = entry.agent.parentId ? agentEntries.get(entry.agent.parentId) : undefined;
151
+ if (parent && !wouldCycle(entry.agent.id, parent.agent.id)) acceptedParents.set(entry.agent.id, parent.agent.id);
152
+ }
153
+ const operationChain = (path: readonly string[], owner: Draft, startIndex = 0): Draft => {
154
+ let parent = owner;
155
+ for (let index = startIndex; index < path.length; index += 1) {
156
+ const prefix = path.slice(0, index + 1);
157
+ const key = `${owner.id}:${JSON.stringify(prefix)}`;
158
+ const existing = operationNodes.get(key);
159
+ if (existing) { parent = existing; continue; }
160
+ const suffix = path.slice(startIndex, index + 1).map((part) => encodeURIComponent(part)).join("/");
161
+ const id = owner.id === phaseNode.id ? workflowPhaseTreePath("operation", phaseId, prefix) : `${owner.id}/operation/${suffix}`;
162
+ const operation = add({ id, kind: "operation", label: prefix.at(-1) ?? "", depth: 0, phaseId, operationPath: prefix, state: "queued", ...(phase ? { phase } : {}) }, parent.id);
163
+ operationNodes.set(key, operation);
164
+ parent = operation;
165
+ }
166
+ return parent;
167
+ };
168
+ for (const entry of entries) {
169
+ if (!acceptedParents.has(entry.agent.id)) entry.defaultParentId = operationChain(entry.operationPath, phaseNode).id;
170
+ }
171
+ for (const entry of entries) {
172
+ entry.node = add({ id: workflowPhaseTreePath("agent", phaseId, entry.structuralPath, entry.agent.id), kind: "agent", label: entry.agent.label ?? entry.agent.name, depth: 0, phaseId, operationPath: entry.structuralPath, state: entry.agent.state, agentId: entry.agent.id, agent: entry.agent }, phaseNode.id);
173
+ }
174
+ const attach = (node: Draft, parentId: string): void => {
175
+ const previous = node.parentId ? drafts.get(node.parentId) : undefined;
176
+ if (previous) previous.children = previous.children.filter((childId) => childId !== node.id);
177
+ node.parentId = parentId;
178
+ const parent = drafts.get(parentId);
179
+ if (parent && !parent.children.includes(node.id)) parent.children.push(node.id);
180
+ };
181
+ for (const entry of entries) {
182
+ const node = entry.node;
183
+ if (!node) continue;
184
+ const parentId = acceptedParents.get(entry.agent.id);
185
+ const parent = parentId ? agentEntries.get(parentId) : undefined;
186
+ const parentNode = parent?.node;
187
+ if (parent && parentNode) {
188
+ if (samePath(entry.operationPath, parent.operationPath)) entry.defaultParentId = parentNode.id;
189
+ else {
190
+ const commonLength = entry.operationPath.findIndex((part, index) => parent.operationPath[index] !== part);
191
+ const startIndex = commonLength < 0 ? Math.min(entry.operationPath.length, parent.operationPath.length) : commonLength;
192
+ entry.defaultParentId = operationChain(entry.operationPath, parentNode, startIndex).id;
193
+ }
194
+ }
195
+ attach(node, entry.defaultParentId);
196
+ }
197
+ const setDepth = (node: Draft, depth: number, seen = new Set<string>()): void => {
198
+ if (seen.has(node.id)) return;
199
+ seen.add(node.id);
200
+ node.depth = depth;
201
+ for (const childId of node.children) { const child = drafts.get(childId); if (child) setDepth(child, depth + 1, seen); }
202
+ };
203
+ setDepth(phaseNode, 0);
204
+ const statesFor = (node: Draft, seen = new Set<string>()): AgentRecord["state"][] => {
205
+ if (seen.has(node.id)) return [];
206
+ const nextSeen = new Set(seen).add(node.id);
207
+ return node.children.flatMap((childId) => {
208
+ const child = drafts.get(childId);
209
+ return child?.kind === "agent" ? [child.agent?.state ?? "queued", ...statesFor(child, nextSeen)] : child ? statesFor(child, nextSeen) : [];
210
+ });
211
+ };
212
+ for (const operation of operationNodes.values()) operation.state = workflowPhaseTreeAggregateState(statesFor(operation));
213
+ };
214
+ const workflowNode = add({ id: "workflow", kind: "workflow", label: "Workflow", depth: 0, phaseId: "", operationPath: [], state: model.runState });
215
+ for (const phase of model.phases) addPhase(phase.id, `${phase.name}${phase.occurrence > 1 ? ` #${String(phase.occurrence)}` : ""}`, phase.agents, phase);
216
+ if (model.unassignedAgents?.length) addPhase("unassigned", "Unassigned", model.unassignedAgents);
217
+ const nodes = [...drafts.values()].map((node) => ({ ...node, children: [...node.children] }));
218
+ return { roots, nodes, byId: new Map(nodes.map((node) => [node.id, node])) };
219
+ }
220
+ export function workflowPhaseTreeVisibleNodes(tree: WorkflowPhaseTree, expanded: ReadonlySet<string> = new Set()): readonly WorkflowPhaseTreeNode[] {
221
+ const visible: WorkflowPhaseTreeNode[] = [];
222
+ const visit = (id: string): void => {
223
+ const node = tree.byId.get(id);
224
+ if (!node) return;
225
+ visible.push(node);
226
+ if (expanded.has(node.id)) for (const childId of node.children) visit(childId);
227
+ };
228
+ for (const root of tree.roots) visit(root);
229
+ return visible;
230
+ }
231
+ export function workflowPhaseTreeInitialExpanded(tree: WorkflowPhaseTree): ReadonlySet<string> {
232
+ return new Set(tree.nodes.filter((node) => node.children.length > 0).map((node) => node.id));
233
+ }
234
+ export function preserveWorkflowPhaseTreeSelection(tree: WorkflowPhaseTree, selection: WorkflowPhaseTreeSelection): WorkflowPhaseTreeSelection {
235
+ const node = (selection.nodeId ? tree.byId.get(selection.nodeId) : undefined) ?? tree.nodes[0];
236
+ return node ? { nodeId: node.id } : {};
237
+ }
238
+ export function navigateWorkflowPhaseTree(tree: WorkflowPhaseTree, selectedNodeId: string | undefined, expandedNodeIds: ReadonlySet<string>, direction: WorkflowPhaseTreeDirection): { nodeId?: string; expandedNodeIds: ReadonlySet<string> } {
239
+ const expanded = new Set(expandedNodeIds);
240
+ const current = (selectedNodeId ? tree.byId.get(selectedNodeId) : undefined) ?? tree.nodes[0];
241
+ if (!current) return { expandedNodeIds: expanded };
242
+ if (direction === "left") {
243
+ if (current.children.length && expanded.delete(current.id)) return { nodeId: current.id, expandedNodeIds: expanded };
244
+ return { nodeId: current.parentId ?? current.id, expandedNodeIds: expanded };
245
+ }
246
+ if (direction === "right") {
247
+ if (current.children.length && !expanded.has(current.id)) { expanded.add(current.id); return { nodeId: current.id, expandedNodeIds: expanded }; }
248
+ return { nodeId: current.children[0] ?? current.id, expandedNodeIds: expanded };
249
+ }
250
+ const visible = workflowPhaseTreeVisibleNodes(tree, expanded);
251
+ const index = Math.max(0, visible.findIndex((node) => node.id === current.id));
252
+ const next = visible[(index + (direction === "up" ? visible.length - 1 : 1)) % visible.length];
253
+ return { nodeId: next?.id ?? current.id, expandedNodeIds: expanded };
254
+ }
255
+ export function preserveWorkflowPhaseSelection(model: WorkflowPhaseModel, selection: WorkflowPhaseSelection): WorkflowPhaseSelection {
256
+ const phase = model.phases.find((candidate) => candidate.id === selection.phaseId) ?? (model.currentPhaseIndex === undefined ? undefined : model.phases[model.currentPhaseIndex]) ?? model.phases[0];
257
+ if (!phase) return model.unassignedAgents?.length ? { nodeId: workflowPhaseTreePath("phase", "unassigned", []) } : {};
258
+ const tree = buildWorkflowPhaseTree(model);
259
+ const selectedAgent = selection.agentId ? phase.agents.find((candidate) => candidate.id === selection.agentId) : undefined;
260
+ const selectedCandidate = selection.nodeId ? tree.byId.get(selection.nodeId) : undefined;
261
+ const selected = selectedCandidate?.phaseId === phase.id ? selectedCandidate : selectedAgent ? tree.byId.get(workflowPhaseTreePath("agent", phase.id, selectedAgent.structuralPath ?? [], selectedAgent.id)) : undefined;
262
+ const nodeId = selected?.id ?? tree.byId.get(workflowPhaseTreePath("phase", phase.id, []))?.id;
263
+ return { phaseId: phase.id, ...(selection.agentId && phase.agents.some((agent) => agent.id === selection.agentId) ? { agentId: selection.agentId } : phase.agents[0] ? { agentId: phase.agents[0].id } : {}), ...(nodeId ? { nodeId } : {}), ...(selection.expandedNodeIds ? { expandedNodeIds: selection.expandedNodeIds } : {}) };
264
+ }
@@ -0,0 +1,333 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import type { Api, Model } from "@earendil-works/pi-ai";
3
+ import { type ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
+ import { FairAgentScheduler, WorkflowAgentExecutor, type AgentDefinition, type AgentExecutionRoot, type AgentProviderFailure, type AgentProviderRecovery } from "./agent-execution.js";
5
+ import { listRunIds, RunStore, structuralPath as operationPath, type PersistedRun } from "./persistence.js";
6
+ import { budgetUsage, budgetRelaxed, mergeBudget, resumeBudgetAllowed, validateBudget, validateBudgetPatch, WorkflowBudgetRuntime } from "./budget.js";
7
+ import { aliasDrift, createLaunchSnapshot, errorCode, errorText, jsonValue, object } from "./utils.js";
8
+ import { LAUNCH_SNAPSHOT_IDENTITY_VERSION, WorkflowError, isExternallyEndedRunState, type AgentIdentity, type AgentResourcePolicy, type BudgetApprovalRequest, type JsonValue, type LaunchSnapshot, type ModelSpec, type RunState, type ShellIdentity, type ShellOptions, type ShellResult, type WorkflowExtensionSettings, type WorkflowMetadata, type WorkflowRetryProvenance, type WorkflowWorktreeReference } from "./types.js";
9
+ import { type WorkflowRegistryApi } from "./registry.js";
10
+ import { RunLifecycle, WorkflowEventPublisher, hostSessionContext, withWorkflowFunctions, withoutActiveShells, workflowRunContext, type WorkflowRunRecord, type WorkflowToolUpdate } from "./host-runtime.js";
11
+ import { runWorkflow } from "./execution.js";
12
+ import { createWorkflowFailureDiagnostics, formatWorkflowFailureDelivery, formatWorkflowFailureDeliveryFallback, failureDiagnosticsFrom, completionDeliveryFromStore, incompleteRetryPaths, markWorkflowFailureDiagnostics, workflowFailedAt, type CompletionDeliveryContext, type CompletionDeliveryResult } from "./host-delivery.js";
13
+
14
+ /** The subset of Pi's model registry the host relies on; every member is optional because older or headless hosts expose fewer of them. */
15
+ export type ModelRegistryCapability = { getAll?: () => readonly Model<Api>[]; getAvailable?: () => readonly Model<Api>[]; find?: (provider: string, model: string) => Model<Api> | undefined; refresh?: () => Promise<void>; getError?: () => string | undefined };
16
+ export type WorkflowRecoveryContext = { model: { provider: string; id: string } | undefined; modelRegistry: ModelRegistryCapability | undefined; deliveryContext: CompletionDeliveryContext; signal?: AbortSignal; resolvedAliases?: Readonly<Record<string, string>>; blockedAliases?: ReadonlySet<string>; blockedAliasTargets?: Readonly<Record<string, string>> };
17
+ export type WorkflowRecoveryDependencies = {
18
+ pi: Pick<ExtensionAPI, "getThinkingLevel">;
19
+ home: string | undefined;
20
+ runs: Map<string, WorkflowRunRecord>;
21
+ scheduler: FairAgentScheduler;
22
+ eventPublisher: WorkflowEventPublisher;
23
+ persistRunState: (store: RunStore, metadata: WorkflowMetadata, update: (run: PersistedRun) => PersistedRun | Promise<PersistedRun>) => Promise<PersistedRun>;
24
+ projectTrusted: (context: unknown) => boolean;
25
+ resumeHostContext: (context: unknown) => WorkflowRecoveryContext;
26
+ ensureSessionLease: (cwd: string, sessionId: string) => Promise<void>;
27
+ coordinateRunMutation?: <T>(task: () => Promise<T>) => Promise<T>;
28
+ createAgentExecutor: (root: Omit<AgentExecutionRoot, "agentDir" | "agentSetupHooks">) => WorkflowAgentExecutor;
29
+ activeSnapshotTools: (tools: readonly string[], active: ReadonlySet<string> | "session") => Set<string>;
30
+ frozenResourcePolicy: (policy: AgentResourcePolicy) => () => AgentResourcePolicy;
31
+ resolveLaunchPrologue: (input: { snapshot: Readonly<LaunchSnapshot>; cwd: string; trustedProject: boolean; rootModel: ModelSpec; modelRegistry?: WorkflowRecoveryContext["modelRegistry"]; signal: AbortSignal; resolvedAliases?: Readonly<Record<string, string>>; blockedAliases?: ReadonlySet<string>; blockedAliasTargets?: Readonly<Record<string, string>>; withPreflight: boolean }) => Promise<{ active: Set<string>; settingsPath: string; currentPolicy: AgentResourcePolicy; previousAliases: Readonly<Record<string, string>>; knownModels: ReadonlySet<string>; availableModels: ReadonlySet<string>; currentAliases: Readonly<Record<string, string>>; blockedAliases: ReadonlySet<string>; blockedAliasTargets: Readonly<Record<string, string>>; snapshot: Readonly<LaunchSnapshot>; script: string | undefined }>;
32
+ resumeRoles: (store: RunStore, snapshot: Readonly<LaunchSnapshot>, cwd: string, trustedProject: boolean) => { definitions: Readonly<Record<string, AgentDefinition>>; capture: (role: string, model: ModelSpec) => Promise<void> };
33
+ workflowAgentHandler: (store: RunStore, metadata: WorkflowMetadata, lifecycle: RunLifecycle, executor: WorkflowAgentExecutor, cwd: string, runId: string, captureRole?: (role: string, model: ModelSpec) => Promise<void>) => (prompt: string, options: Readonly<Record<string, JsonValue>>, signal: AbortSignal, identity: AgentIdentity) => Promise<JsonValue>;
34
+ shellForRun: (store: RunStore, metadata: WorkflowMetadata, lifecycle: RunLifecycle, command: string, options: ShellOptions, signal: AbortSignal, identity: ShellIdentity) => Promise<ShellResult>;
35
+ resolveWorktree: (store: RunStore, metadata: WorkflowMetadata, owner: string) => Promise<Readonly<WorkflowWorktreeReference>>;
36
+ checkpointBridge: (runId: string, store: RunStore, metadata: WorkflowMetadata, foreground: boolean, ui?: { select?: (prompt: string, options: string[]) => Promise<string | undefined> }, headless?: boolean) => (raw: Readonly<Record<string, JsonValue>>, signal: AbortSignal) => Promise<boolean>;
37
+ phaseBridge: (store: RunStore, metadata: WorkflowMetadata, lifecycle: RunLifecycle) => (phase: string) => Promise<void>;
38
+ logBridge: (store: RunStore, lifecycle: RunLifecycle, workflowName: string) => (message: string) => Promise<void>;
39
+ lifecycleFor: (store: RunStore, state: RunState, metadata: WorkflowMetadata) => RunLifecycle;
40
+ createProviderErrorRecovery: (host: unknown, fallbackModels: ReadonlySet<string>, abort: () => void) => ((failure: AgentProviderFailure) => Promise<AgentProviderRecovery>) | undefined;
41
+ cleanupTerminalRun: (runId: string) => Promise<void>;
42
+ deliver: (content: string) => void;
43
+ deliverTerminal: (store: RunStore, content: string | (() => string | Promise<string>), failure?: boolean) => Promise<void>;
44
+ workflowToolUpdate: (run: PersistedRun) => WorkflowToolUpdate;
45
+ registry: WorkflowRegistryApi;
46
+ modelSpec: (value: string, fallback: ModelSpec) => ModelSpec;
47
+ };
48
+
49
+ function workflowRecoveryGuidance(action: "resume" | "retry", state: RunState): string {
50
+ if (action === "resume") {
51
+ if (state === "failed") return "Failed workflow runs must use workflow_retry({ runId })";
52
+ if (state === "completed") return "Completed workflow runs have no recovery action";
53
+ if (state === "stopped") return "Stopped workflow runs have no recovery action; launch a new workflow";
54
+ if (state === "interrupted") return "Interrupted workflow runs must be resumed from the interactive /workflow picker";
55
+ return `Only budget-exhausted runs can be resumed with workflow_resume; source is ${state}`;
56
+ }
57
+ if (state === "budget_exhausted") return "Budget-exhausted workflow runs must use workflow_resume({ runId, budget? })";
58
+ if (state === "completed") return "Completed workflow runs have no recovery action";
59
+ if (state === "stopped") return "Stopped workflow runs cannot be retried; launch a new workflow";
60
+ if (state === "interrupted") return "Interrupted workflow runs must be resumed from the interactive /workflow picker";
61
+ return `Only failed workflow runs can be retried; source is ${state}`;
62
+ }
63
+ /** A snapshot can only be re-executed when it carries the current identity scheme and every role its script references. */
64
+ function assertResumableSnapshot(snapshot: Readonly<LaunchSnapshot>, trustedProject: boolean): void {
65
+ if (snapshot.identityVersion !== LAUNCH_SNAPSHOT_IDENTITY_VERSION) throw new WorkflowError("RESUME_INCOMPATIBLE", "Workflow launch snapshot identity version is incompatible");
66
+ if (snapshot.roles === undefined) throw new WorkflowError("RESUME_INCOMPATIBLE", "Workflow role definitions are missing from the launch snapshot");
67
+ if ((snapshot.projectRoles?.length ?? 0) > 0 && !trustedProject) throw new WorkflowError("RESUME_INCOMPATIBLE", "Cannot restore project roles in an untrusted project");
68
+ const missingRole = snapshot.agentTypes.find((role) => !snapshot.roles?.[role]);
69
+ if (missingRole) throw new WorkflowError("RESUME_INCOMPATIBLE", `Role definition is missing from the launch snapshot: ${missingRole}`);
70
+ }
71
+ function assertExpectedWorkflowState(expectedState: string | undefined, actualState: RunState): void {
72
+ if (expectedState !== undefined && expectedState !== actualState) throw new WorkflowError("RESUME_INCOMPATIBLE", `Workflow run state changed: expected state ${expectedState}, actual state ${actualState}`);
73
+ }
74
+ export function persistedFailure(run: PersistedRun, error: WorkflowError): PersistedRun { const failedAt = workflowFailedAt(error); return { ...run, error: { code: error.code, message: error.message, ...(failedAt ? { failedAt } : {}) }, ...(failedAt ? { failedAt } : {}) }; }
75
+
76
+ export function createWorkflowRecovery(deps: WorkflowRecoveryDependencies) {
77
+ const { pi, home, runs, scheduler, eventPublisher, persistRunState, projectTrusted, resumeHostContext, ensureSessionLease, createAgentExecutor, activeSnapshotTools, frozenResourcePolicy, resolveLaunchPrologue, resumeRoles, workflowAgentHandler, shellForRun, resolveWorktree, checkpointBridge, phaseBridge, logBridge, lifecycleFor, createProviderErrorRecovery, cleanupTerminalRun, deliver, deliverTerminal, workflowToolUpdate, registry, modelSpec } = deps;
78
+ const coordinateRunMutation = deps.coordinateRunMutation ?? (<T>(task: () => Promise<T>): Promise<T> => task());
79
+ type BudgetDecisionResult = { state: "running" | "completed" | "budget_exhausted"; approved: boolean; value?: JsonValue; run?: PersistedRun; completion?: CompletionDeliveryResult };
80
+ const budgetDecisionDelivery = (metadata: WorkflowMetadata, request: BudgetApprovalRequest) => `Workflow ${metadata.name} budget adjustment ${request.proposalId} for run ${request.runId} requires approval. Consumed usage: ${JSON.stringify(request.consumed)}. Previous limits: ${JSON.stringify(request.previous)}. Proposed limits: ${JSON.stringify(request.proposed)}. Respond with workflow_respond using proposalId ${request.proposalId}.`;
81
+ const appendBudgetDecisionEvent = async (run: WorkflowRunRecord, request: BudgetApprovalRequest, type: "adjustment_requested" | "adjustment_approved" | "adjustment_rejected") => {
82
+ run.budget.recordEvent({ type, budgetVersion: request.budgetVersion, dimensions: [], usage: structuredClone(request.consumed), limits: structuredClone(request.proposed), at: Date.now(), proposalId: request.proposalId, previous: structuredClone(request.previous), proposed: structuredClone(request.proposed) });
83
+ await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot() }));
84
+ };
85
+ const answerBudgetDecision = async (runId: string, proposalId: string, approved: boolean, silent = false, context?: unknown, signal?: AbortSignal, waitForCompletion = true): Promise<BudgetDecisionResult | undefined> => {
86
+ const run = runs.get(runId);
87
+ if (!run) return undefined;
88
+ const request = await run.store.answerWorkflowDecision(proposalId, approved);
89
+ if (!request) return undefined;
90
+ await appendBudgetDecisionEvent(run, request, approved ? "adjustment_approved" : "adjustment_rejected");
91
+ const result = await applyBudgetDecision(request, approved, context, signal, waitForCompletion);
92
+ if (!silent) deliver(`Workflow ${run.metadata.name} budget adjustment ${proposalId}: ${approved ? "Approved" : "Rejected"}.`);
93
+ return result;
94
+ };
95
+ const refreshPausedRunAliases = async (run: WorkflowRunRecord, context?: { model: { provider: string; id: string } | undefined; modelRegistry: WorkflowRecoveryContext["modelRegistry"]; projectTrusted?: boolean }) => {
96
+ const loaded = await run.store.load();
97
+ const trustedProject = context?.projectTrusted ?? run.projectTrusted();
98
+ const rootModel = context?.model ? { ...run.model, provider: context.model.provider, model: context.model.id } : run.model;
99
+ const { settingsPath, currentPolicy, previousAliases, knownModels, availableModels, currentAliases, blockedAliases, blockedAliasTargets, snapshot } = await resolveLaunchPrologue({ snapshot: loaded.snapshot, cwd: run.store.cwd, trustedProject, rootModel, ...(context?.modelRegistry ? { modelRegistry: context.modelRegistry } : {}), signal: run.abortController.signal, withPreflight: false });
100
+ await run.store.saveSnapshot(snapshot);
101
+ scheduler.updateRunLimit(run.store.runId, snapshot.settings.concurrency);
102
+ scheduler.setRunExtensionSettings(run.store.runId, snapshot.settings.extensionSettings);
103
+ run.executor = createAgentExecutor({ cwd: run.store.cwd, model: run.model, tools: activeSnapshotTools(snapshot.tools, "session"), resourceSelectors: currentPolicy.effective, extensionSettings: snapshot.settings.extensionSettings, availableModels, knownModels, modelAliases: currentAliases, blockedAliases, blockedAliasTargets, settingsPath, agentDefinitions: resumeRoles(run.store, snapshot, run.store.cwd, trustedProject).definitions, runStore: run.store, providerPause: async () => { deliver(`Workflow ${snapshot.metadata.name} paused: provider limit.`); await run.lifecycle.providerPause(); }, agentResourcePolicy: frozenResourcePolicy(currentPolicy) });
104
+ run.executor.setRunContext(workflowRunContext(run.store.cwd, run.store.sessionId, run.store.runId, loaded.snapshot.metadata, loaded.snapshot.args, run.abortController.signal));
105
+ const drift = aliasDrift(previousAliases, currentAliases);
106
+ if (drift.length) await run.store.appendEvent({ type: "warning", message: `Model alias mappings changed on resume: ${drift.join("; ")}` });
107
+ };
108
+ const recoveryUi = (context: unknown): { hasUI: boolean; ui: { select?: (prompt: string, options: string[]) => Promise<string | undefined> } } => {
109
+ const host = object(context) ? context : undefined;
110
+ const ui = host && object(host.ui) ? host.ui as { select?: (prompt: string, options: string[]) => Promise<string | undefined> } : {};
111
+ return { hasUI: host?.hasUI === true, ui };
112
+ };
113
+ type ColdResumeResult = { value: JsonValue; resultPath: string; resultBytes: number; completion: CompletionDeliveryResult };
114
+ const coldResumeRun = async (run: WorkflowRunRecord, hasUI: boolean, ui: { select?: (prompt: string, options: string[]) => Promise<string | undefined> }, trustedProject: boolean, context?: { model: { provider: string; id: string } | undefined; modelRegistry: WorkflowRecoveryContext["modelRegistry"]; deliveryContext: CompletionDeliveryContext; signal?: AbortSignal | undefined; resolvedAliases?: Readonly<Record<string, string>>; blockedAliases?: ReadonlySet<string>; blockedAliasTargets?: Readonly<Record<string, string>> }, modeOverride?: boolean, waitForCompletion = true): Promise<ColdResumeResult | undefined> => {
115
+ const loaded = await run.store.load();
116
+ const foreground = modeOverride ?? (loaded.run.delivery?.mode === "foreground" || (loaded.run.delivery?.mode === "background" && loaded.run.delivery.toolCallId !== undefined) || (loaded.run.delivery === undefined && loaded.snapshot.launchMode === "foreground"));
117
+ if (loaded.run.activeShells !== undefined || loaded.run.activeShellStartedAt !== undefined || loaded.run.activeShellsByPhase !== undefined) await persistRunState(run.store, run.metadata, withoutActiveShells);
118
+ await run.store.validateRetrySource();
119
+ await run.store.validateBorrowedWorktrees();
120
+ assertResumableSnapshot(loaded.snapshot, trustedProject);
121
+ const rootModel = context?.model ? { ...run.model, provider: context.model.provider, model: context.model.id } : run.model;
122
+ const controller = new AbortController();
123
+ if (context?.signal?.aborted) controller.abort(); else { context?.signal?.addEventListener("abort", () => { controller.abort(); }, { once: true }); }
124
+ run.abortController = controller;
125
+ const { settingsPath, currentPolicy, previousAliases, knownModels, availableModels, currentAliases, blockedAliases, blockedAliasTargets, snapshot, script } = await resolveLaunchPrologue({ snapshot: loaded.snapshot, cwd: run.store.cwd, trustedProject, rootModel, ...(context?.modelRegistry ? { modelRegistry: context.modelRegistry } : {}), signal: controller.signal, ...(context?.resolvedAliases ? { resolvedAliases: context.resolvedAliases } : {}), ...(context?.blockedAliases ? { blockedAliases: context.blockedAliases } : {}), ...(context?.blockedAliasTargets ? { blockedAliasTargets: context.blockedAliasTargets } : {}), withPreflight: true });
126
+ if (!script) throw new WorkflowError("INTERNAL_ERROR", "Resume preflight did not produce a launch script");
127
+ const persistedSnapshot = modeOverride === undefined ? snapshot : createLaunchSnapshot({ ...snapshot, launchMode: foreground ? "foreground" : "background" });
128
+ await run.store.saveSnapshot(persistedSnapshot);
129
+ if (modeOverride !== undefined) await persistRunState(run.store, run.metadata, (current) => ({ ...current, delivery: { ...(current.delivery ?? {}), mode: foreground ? "foreground" : "background", state: foreground ? "attached" : "pending" } }));
130
+ scheduler.updateRunLimit(run.store.runId, snapshot.settings.concurrency);
131
+ scheduler.setRunExtensionSettings(run.store.runId, snapshot.settings.extensionSettings);
132
+ const roles = resumeRoles(run.store, persistedSnapshot, run.store.cwd, trustedProject);
133
+ run.executor = createAgentExecutor({ cwd: run.store.cwd, model: rootModel, tools: activeSnapshotTools(snapshot.tools, "session"), resourceSelectors: currentPolicy.effective, extensionSettings: snapshot.settings.extensionSettings, availableModels, knownModels, modelAliases: currentAliases, blockedAliases, blockedAliasTargets, settingsPath, agentDefinitions: roles.definitions, runStore: run.store, providerPause: async () => { deliver(`Workflow ${snapshot.metadata.name} paused: provider limit.`); await run.lifecycle.providerPause(); }, agentResourcePolicy: frozenResourcePolicy(currentPolicy) });
134
+ const drift = aliasDrift(previousAliases, currentAliases);
135
+ if (drift.length) await run.store.appendEvent({ type: "warning", message: `Model alias mappings changed on resume: ${drift.join("; ")}` });
136
+ const runContext = workflowRunContext(run.store.cwd, run.store.sessionId, run.store.runId, loaded.snapshot.metadata, loaded.snapshot.args, controller.signal);
137
+ run.executor.setRunContext(runContext);
138
+ await scheduler.cancelRun(run.store.runId);
139
+ await run.lifecycle.resume();
140
+ const execution = runWorkflow(script, loaded.snapshot.args, withWorkflowFunctions({ shell: (command, options, signal, identity) => shellForRun(run.store, run.metadata, run.lifecycle, command, options, signal, identity), agent: workflowAgentHandler(run.store, run.metadata, run.lifecycle, run.executor, run.store.cwd, run.store.runId, roles.capture), worktree: async (owner) => resolveWorktree(run.store, run.metadata, owner), checkpoint: checkpointBridge(run.store.runId, run.store, run.metadata, foreground, hasUI ? ui : undefined), phase: phaseBridge(run.store, run.metadata, run.lifecycle), log: logBridge(run.store, run.lifecycle, run.metadata.name) }, run.store, runContext, registry, snapshot.settings.extensionSettings), controller.signal);
141
+ run.execution = execution;
142
+ const completion = execution.result.then(async (value) => {
143
+ await scheduler.flush(run.store.runId);
144
+ if (run.budget.hardExhausted) throw new WorkflowError("BUDGET_EXHAUSTED", "Budgeted work was attempted after hard exhaustion");
145
+ const resultPath = await run.store.saveResult(value);
146
+ const resultBytes = await run.store.resultBytes();
147
+ await run.lifecycle.terminal("completed", "completed");
148
+ await eventPublisher.runCompleted(run.store, run.metadata, resultPath);
149
+ return { value, resultPath, resultBytes };
150
+ }).catch(async (error: unknown) => {
151
+ await scheduler.flush(run.store.runId);
152
+ const typed = error instanceof WorkflowError ? error : new WorkflowError(errorCode(error) ?? "INTERNAL_ERROR", errorText(error));
153
+ if (!isExternallyEndedRunState(run.lifecycle.state)) await run.lifecycle.terminal(typed.code === "BUDGET_EXHAUSTED" ? "budget_exhausted" : "failed", typed.code);
154
+ const persisted = await persistRunState(run.store, run.metadata, (current) => persistedFailure({ ...current, ...run.budget.snapshot() }, typed));
155
+ const ended = run.lifecycle.state;
156
+ const state = isExternallyEndedRunState(ended) ? ended : "failed";
157
+ if (state === "failed") retryReservations.delete(persisted.retry?.lineageRootRunId ?? run.store.runId);
158
+ await eventPublisher.runFailed(run.store, run.metadata, typed, state);
159
+ run.update?.(workflowToolUpdate(persisted));
160
+ if (state === "failed") { const diagnostic = await createWorkflowFailureDiagnostics(run.store, run.metadata, typed, persisted); markWorkflowFailureDiagnostics(typed, diagnostic); }
161
+ throw typed;
162
+ }).finally(() => cleanupTerminalRun(run.store.runId));
163
+ run.completion = completion;
164
+ if (!foreground || !waitForCompletion) {
165
+ void completion.then(async (result) => {
166
+ await deliverTerminal(run.store, async () => (await completionDeliveryFromStore({ mode: "background", name: run.metadata.name, runId: run.store.runId, value: result.value, resultPath: result.resultPath, resultBytes: result.resultBytes, store: run.store, ...(context === undefined ? {} : { context: context.deliveryContext }) })).content);
167
+ }, async (error: unknown) => {
168
+ const diagnostic = failureDiagnosticsFrom(error);
169
+ await deliverTerminal(run.store, diagnostic ? formatWorkflowFailureDelivery(diagnostic) : formatWorkflowFailureDeliveryFallback(run.metadata.name, run.store.runId, run.store.directory, error), true);
170
+ });
171
+ return undefined;
172
+ }
173
+ const markDelivered = () => run.store.updateState((current) => current.delivery?.mode === "foreground" && (current.delivery.state === "attached" || current.delivery.state === "pending") ? { ...current, delivery: { ...current.delivery, state: "delivered" } } : current);
174
+ try {
175
+ const result = await completion;
176
+ await markDelivered();
177
+ const completionResult = await completionDeliveryFromStore({ mode: "foreground", name: run.metadata.name, runId: run.store.runId, value: result.value, resultPath: result.resultPath, resultBytes: result.resultBytes, store: run.store, ...(context === undefined ? {} : { context: context.deliveryContext }) });
178
+ return { ...result, completion: completionResult };
179
+ } catch (error) {
180
+ await markDelivered();
181
+ throw error;
182
+ }
183
+ };
184
+ const applyBudgetDecision = async (request: BudgetApprovalRequest, approved: boolean, context?: unknown, signal?: AbortSignal, waitForCompletion = true): Promise<BudgetDecisionResult> => {
185
+ const run = runs.get(request.runId);
186
+ if (!run) throw new WorkflowError("RESUME_INCOMPATIBLE", `Unknown workflow run: ${request.runId}`);
187
+ if (!approved) return { state: "budget_exhausted", approved: false };
188
+ const nextBudget = validateBudget(request.proposed);
189
+ const nextVersion = request.budgetVersion + 1;
190
+ const runtime = new WorkflowBudgetRuntime(nextBudget, nextVersion, request.consumed, run.budget.events, { active: false });
191
+ run.budget = runtime;
192
+ await persistRunState(run.store, run.metadata, (current) => { const next = { ...current, ...runtime.snapshot(), budgetVersion: nextVersion }; if (nextBudget) next.budget = nextBudget; else delete next.budget; return next; });
193
+ const { hasUI, ui } = recoveryUi(context);
194
+ const completed = await coldResumeRun(run, hasUI, ui, projectTrusted(context), { ...resumeHostContext(context), ...(signal ? { signal } : {}) }, request.foreground, waitForCompletion);
195
+ if (completed) return { state: "completed", approved: true, value: completed.value, run: (await run.store.load()).run, completion: completed.completion };
196
+ return { state: "running", approved: true };
197
+ };
198
+ const resumeWorkflowRun = async (runId: string, rawPatch?: unknown, context?: unknown, signal?: AbortSignal, modeOverride?: boolean, waitForCompletion = true, expectedState?: string): Promise<Record<string, JsonValue>> => {
199
+ const run = runs.get(runId);
200
+ if (!run) {
201
+ const { cwd, sessionId } = hostSessionContext(context);
202
+ if (cwd && sessionId) {
203
+ try {
204
+ const state = (await new RunStore(cwd, sessionId, runId, home).load()).run.state;
205
+ assertExpectedWorkflowState(expectedState, state);
206
+ throw new WorkflowError("RESUME_INCOMPATIBLE", workflowRecoveryGuidance("resume", state));
207
+ } catch (error) {
208
+ if (error instanceof WorkflowError) throw error;
209
+ }
210
+ }
211
+ throw new WorkflowError("RESUME_INCOMPATIBLE", `Unknown workflow run ${runId} in the current project and Pi session`);
212
+ }
213
+ const loaded = await run.store.load();
214
+ assertExpectedWorkflowState(expectedState, loaded.run.state);
215
+ if (loaded.run.state !== "budget_exhausted") throw new WorkflowError("RESUME_INCOMPATIBLE", workflowRecoveryGuidance("resume", loaded.run.state));
216
+ const currentBudget = validateBudget(loaded.run.budget ?? loaded.snapshot.budget);
217
+ const patch = rawPatch === undefined ? {} : validateBudgetPatch(rawPatch);
218
+ const nextBudget = mergeBudget(currentBudget, patch);
219
+ const usage = budgetUsage(loaded.run.usage);
220
+ if (!resumeBudgetAllowed(nextBudget, usage)) throw new WorkflowError("RESUME_INCOMPATIBLE", "Every exhausted hard budget must be raised above retained usage or removed");
221
+ if (budgetRelaxed(currentBudget, nextBudget)) {
222
+ const proposalId = randomUUID();
223
+ const request: BudgetApprovalRequest = { kind: "budget", proposalId, runId, consumed: usage, previous: currentBudget ?? {}, proposed: nextBudget ?? {}, budgetVersion: loaded.run.budgetVersion ?? 1, ...(modeOverride === undefined ? {} : { foreground: modeOverride }) };
224
+ await run.store.requestWorkflowDecision(request);
225
+ await appendBudgetDecisionEvent(run, request, "adjustment_requested");
226
+ deliver(budgetDecisionDelivery(run.metadata, request));
227
+ return { state: "awaiting_approval", proposalId };
228
+ }
229
+ const changed = JSON.stringify(currentBudget ?? {}) !== JSON.stringify(nextBudget ?? {});
230
+ if (changed) {
231
+ const nextVersion = (loaded.run.budgetVersion ?? 1) + 1;
232
+ const runtime = new WorkflowBudgetRuntime(nextBudget, nextVersion, usage, loaded.run.budgetEvents, { active: false });
233
+ run.budget = runtime;
234
+ await persistRunState(run.store, run.metadata, (current) => { const next = { ...current, ...runtime.snapshot(), budgetVersion: nextVersion }; if (nextBudget) next.budget = nextBudget; else delete next.budget; return next; });
235
+ }
236
+ const { hasUI, ui } = recoveryUi(context);
237
+ const completed = await coldResumeRun(run, hasUI, ui, projectTrusted(context), { ...resumeHostContext(context), ...(signal ? { signal } : {}) }, modeOverride, waitForCompletion);
238
+ if (completed) {
239
+ const persistedRun = structuredClone((await run.store.load()).run);
240
+ if (!jsonValue(persistedRun)) throw new WorkflowError("RESUME_INCOMPATIBLE", "Persisted run is not JSON-compatible");
241
+ return { state: "completed", runId, value: completed.value, run: persistedRun, completion: { content: completed.completion.content, inlined: completed.completion.inlined } };
242
+ }
243
+ return { state: "running" };
244
+ };
245
+ const retryReservations = new Set<string>();
246
+ type PreparedRetryWorkflowRun = { childRun: WorkflowRunRecord; parentRunId: string; lineageRootRunId: string; concurrency: number; extensionSettings?: Readonly<WorkflowExtensionSettings>; hostModel: { provider: string; id: string }; modelRegistry: WorkflowRecoveryContext["modelRegistry"]; currentAliases: Readonly<Record<string, string>>; blockedAliases: ReadonlySet<string>; blockedAliasTargets: Readonly<Record<string, string>> };
247
+ const retryWorkflowRunUnlocked = async (runId: string, context: unknown, signal?: AbortSignal, expectedState?: string): Promise<PreparedRetryWorkflowRun> => {
248
+ if (typeof runId !== "string" || !runId.trim()) throw new WorkflowError("RESUME_INCOMPATIBLE", "workflow_retry requires an explicit run ID");
249
+ const host = object(context) ? context : {};
250
+ const { cwd, sessionId } = hostSessionContext(context);
251
+ if (!cwd || !sessionId) throw new WorkflowError("RESUME_INCOMPATIBLE", "workflow_retry requires the current project and Pi session");
252
+ await ensureSessionLease(cwd, sessionId);
253
+ const sourceStore = new RunStore(cwd, sessionId, runId, home);
254
+ let loaded: { run: PersistedRun; snapshot: Readonly<LaunchSnapshot> };
255
+ try { loaded = await sourceStore.load(); } catch (error) { throw new WorkflowError("RESUME_INCOMPATIBLE", `Unknown workflow run ${runId} in the current project and Pi session: ${errorText(error)}`); }
256
+ assertExpectedWorkflowState(expectedState, loaded.run.state);
257
+ if (loaded.run.state !== "failed") throw new WorkflowError("RESUME_INCOMPATIBLE", workflowRecoveryGuidance("retry", loaded.run.state));
258
+ // decodeRetry() already guarantees the field shapes; only the semantic constraints remain to check here.
259
+ if (loaded.run.retry && (!loaded.run.retry.sourceRunId || !loaded.run.retry.lineageRootRunId)) throw new WorkflowError("RESUME_INCOMPATIBLE", "The source retry provenance is incomplete");
260
+ const lineageRootRunId = loaded.run.retry?.lineageRootRunId ?? loaded.run.id;
261
+ if (retryReservations.has(lineageRootRunId)) throw new WorkflowError("RESUME_INCOMPATIBLE", `An active retry already owns lineage ${lineageRootRunId}`);
262
+ const activeStates = new Set<RunState>(["queued", "running", "pausing", "paused", "awaiting_input", "interrupted", "budget_exhausted"]);
263
+ for (const candidateId of await listRunIds(cwd, sessionId, home)) {
264
+ if (candidateId === runId) continue;
265
+ const candidate = new RunStore(cwd, sessionId, candidateId, home);
266
+ try {
267
+ const candidateRun = (await candidate.load()).run;
268
+ if (activeStates.has(candidateRun.state) && candidateRun.retry?.lineageRootRunId === lineageRootRunId) throw new WorkflowError("RESUME_INCOMPATIBLE", `An active retry child already exists for source lineage ${lineageRootRunId}`);
269
+ } catch (error) {
270
+ if (error instanceof WorkflowError && error.code === "RESUME_INCOMPATIBLE") throw error;
271
+ }
272
+ }
273
+ retryReservations.add(lineageRootRunId);
274
+ let childCreated = false;
275
+ try {
276
+ const trustedProject = projectTrusted(context);
277
+ await sourceStore.validateRetrySource();
278
+ await sourceStore.validateBorrowedWorktrees();
279
+ assertResumableSnapshot(loaded.snapshot, trustedProject);
280
+ const modelRegistry = resumeHostContext(context).modelRegistry;
281
+ const hostModel = object(host.model) && typeof host.model.provider === "string" && typeof host.model.id === "string" ? { provider: host.model.provider, id: host.model.id } : { provider: "", id: "" };
282
+ const rootModel: ModelSpec = { provider: hostModel.provider, model: hostModel.id, thinking: pi.getThinkingLevel() };
283
+ const { active, settingsPath, currentPolicy, knownModels, availableModels, currentAliases, blockedAliases, blockedAliasTargets, snapshot: childBaseSnapshot } = await resolveLaunchPrologue({ snapshot: loaded.snapshot, cwd, trustedProject, rootModel, ...(modelRegistry ? { modelRegistry } : {}), signal: signal ?? new AbortController().signal, withPreflight: true });
284
+ await sourceStore.validateNamedWorktrees();
285
+ for (const name of loaded.run.retry?.namedWorktrees ?? []) await sourceStore.resolveNamedWorktree(name);
286
+ const completedPaths = (await sourceStore.replayableOperations()).map(({ path }) => path);
287
+ const incompletePaths = incompleteRetryPaths([...(loaded.run.retry?.incompletePaths ?? []), ...loaded.run.agents.filter((agent) => agent.state !== "completed").map((agent) => operationPath("agent", ...(agent.structuralPath ?? [])))], completedPaths);
288
+ const namedWorktrees = [...new Set([...(loaded.run.retry?.namedWorktrees ?? []), ...(await sourceStore.worktrees()).filter(({ owner }) => owner.startsWith(`${operationPath("worktree", "named")}/`)).map(({ owner }) => decodeURIComponent(owner.split("/").at(-1) ?? owner))])];
289
+ const budget = validateBudget(loaded.run.budget ?? loaded.snapshot.budget);
290
+ const childRunId = randomUUID();
291
+ const childStore = new RunStore(cwd, sessionId, childRunId, home);
292
+ const childBudget = new WorkflowBudgetRuntime(budget, loaded.run.budgetVersion ?? 1, loaded.run.usage, loaded.run.budgetEvents);
293
+ const childInitialBudget = childBudget.snapshot();
294
+ const retry: WorkflowRetryProvenance = { sourceRunId: loaded.run.id, lineageRootRunId, completedPaths, incompletePaths, namedWorktrees };
295
+ await childStore.create({ id: childRunId, workflowName: loaded.snapshot.metadata.name, cwd, sessionId, state: "interrupted", parentRunId: loaded.run.id, retry, agents: [], agentSessions: [], ...(budget ? { budget } : {}), budgetVersion: loaded.run.budgetVersion ?? 1, ...childInitialBudget }, childBaseSnapshot);
296
+ const model = modelSpec(loaded.snapshot.models[0] ?? "", rootModel);
297
+ const lifecycle = lifecycleFor(childStore, "interrupted", loaded.snapshot.metadata);
298
+ const abortController = new AbortController();
299
+ const providerErrorRecovery = createProviderErrorRecovery(context, availableModels, () => { abortController.abort(); });
300
+ const providerPause = async () => { deliver(`Workflow ${loaded.snapshot.metadata.name} paused: provider limit.`); await lifecycle.providerPause(); };
301
+ const childRun: WorkflowRunRecord = { executor: createAgentExecutor({ cwd, model, tools: activeSnapshotTools(loaded.snapshot.tools, active), resourceSelectors: currentPolicy.effective, extensionSettings: childBaseSnapshot.settings.extensionSettings, availableModels, knownModels, modelAliases: currentAliases, blockedAliases, blockedAliasTargets, settingsPath, agentDefinitions: loaded.snapshot.roles ?? {}, runStore: childStore, providerPause, agentResourcePolicy: frozenResourcePolicy(currentPolicy) }), store: childStore, metadata: loaded.snapshot.metadata, model, lifecycle, budget: childBudget, abortController, projectTrusted: () => projectTrusted(context), checkpointResolvers: new Map(), ...(providerErrorRecovery ? { providerErrorRecovery } : {}) };
302
+ runs.set(childRunId, childRun);
303
+ childCreated = true;
304
+ return { childRun, parentRunId: loaded.run.id, lineageRootRunId, concurrency: loaded.snapshot.settings.concurrency, ...(childBaseSnapshot.settings.extensionSettings === undefined ? {} : { extensionSettings: childBaseSnapshot.settings.extensionSettings }), hostModel, modelRegistry, currentAliases, blockedAliases, blockedAliasTargets };
305
+ } finally {
306
+ if (!childCreated) retryReservations.delete(lineageRootRunId);
307
+ }
308
+ };
309
+ const retryWorkflowRun = async (runId: string, context: unknown, signal?: AbortSignal, modeOverride?: boolean, expectedState?: string): Promise<{ runId: string; parentRunId: string; state: "running" | "completed"; value?: JsonValue; run?: PersistedRun; completion?: CompletionDeliveryResult }> => {
310
+ const prepared = await coordinateRunMutation(() => retryWorkflowRunUnlocked(runId, context, signal, expectedState));
311
+ let childStarted = false;
312
+ try {
313
+ scheduler.addRun(prepared.childRun.store.runId, prepared.concurrency, () => { prepared.childRun.budget.checkAgentLaunch(); }, prepared.extensionSettings);
314
+ await eventPublisher.runStarted(prepared.childRun.store, prepared.childRun.metadata);
315
+ const { hasUI, ui } = recoveryUi(context);
316
+ const recoveryContext = resumeHostContext(context);
317
+ const completed = await coldResumeRun(prepared.childRun, hasUI, ui, projectTrusted(context), { model: prepared.hostModel, modelRegistry: prepared.modelRegistry, deliveryContext: recoveryContext.deliveryContext, resolvedAliases: prepared.currentAliases, blockedAliases: prepared.blockedAliases, blockedAliasTargets: prepared.blockedAliasTargets, ...(signal ? { signal } : {}) }, modeOverride);
318
+ const completion = runs.get(prepared.childRun.store.runId)?.completion;
319
+ if (completion) {
320
+ childStarted = true;
321
+ void completion.then(() => { retryReservations.delete(prepared.lineageRootRunId); }, () => { retryReservations.delete(prepared.lineageRootRunId); });
322
+ } else if (completed) {
323
+ childStarted = true;
324
+ retryReservations.delete(prepared.lineageRootRunId);
325
+ }
326
+ if (completed) return { runId: prepared.childRun.store.runId, parentRunId: prepared.parentRunId, state: "completed", value: completed.value, run: (await prepared.childRun.store.load()).run, completion: completed.completion };
327
+ return { runId: prepared.childRun.store.runId, parentRunId: prepared.parentRunId, state: "running" };
328
+ } finally {
329
+ if (!childStarted) retryReservations.delete(prepared.lineageRootRunId);
330
+ }
331
+ };
332
+ return { refreshPausedRunAliases, coldResumeRun, applyBudgetDecision, answerBudgetDecision, budgetDecisionDelivery, resumeWorkflowRun, retryWorkflowRun };
333
+ }