@signalridge/pi-workflows 1.7.0 → 1.7.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.
- package/CHANGELOG.md +36 -0
- package/README.md +0 -1
- package/package.json +3 -3
- package/skills/workflow-authoring/SKILL.md +1 -1
- package/skills/workflow-authoring/references/capabilities.md +1 -1
- package/skills/workflow-authoring/references/capability-details.md +1 -1
- package/skills/workflow-patterns/SKILL.md +1 -1
- package/skills/workflow-review/SKILL.md +1 -1
- package/src/engine.ts +176 -33
- package/src/index.ts +31 -22
- package/src/rpc-client.ts +8 -3
- package/src/runtime.ts +50 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.7.1
|
|
4
|
+
### Patch Changes
|
|
5
|
+
|
|
6
|
+
- 1f586f8: Preserve resumable cancellation when a workflow pauses during successful timeout cleanup, and retain finalized subagent output alongside observer failures. Avoid retaining descendant-owned mouse gestures in bordered components. Track successful writes using final rewritten paths, and distinguish uncertain changes observed during failed calls from confirmed agent modifications while keeping both discoverable in file browsing. Preserve exact filesystem paths and lazy-directory scan state when discovering session-observed changes, including changes injected during an active scan.
|
|
7
|
+
- 1f586f8: - Bound workflow cancellation drains and durably fence unconfirmed cleanup, aborting sibling agents and releasing foreground callers.
|
|
8
|
+
- Await managed timeout cleanup before script continuation and terminal publication.
|
|
9
|
+
- Keep failed retry text and length responses discarded by overflow-compaction retry out of successful subagent fallbacks, while retaining valid pre-compaction and non-retried length output.
|
|
10
|
+
- Account for native prompts spanning reporter activation without dropping out-of-order wait debt.
|
|
11
|
+
- Reconcile settlement after observer-triggered manual compaction with run/session ownership.
|
|
12
|
+
- Preserve clipped overlay hit ranges and native container ancestry for editor autocomplete focus.
|
|
13
|
+
- Reconcile errored writes and edits against pre/post mutation evidence, retaining invocation cwd and releasing evidence when blocked or aborted preflight bypasses tool results.
|
|
14
|
+
- d9219d4: Correct shipped documentation that contradicted the code or the manifest.
|
|
15
|
+
|
|
16
|
+
- `pi-subagents`: the summary line still advertised protocol-v3 spawning while
|
|
17
|
+
the feature list and the managed-spawn section described v4.
|
|
18
|
+
- `pi-worktree`: the workspace-switching section named `/tree`; the registered
|
|
19
|
+
command is `/worktree`.
|
|
20
|
+
- `pi-workflows`: removed an orphaned sentence fragment left behind by an edit
|
|
21
|
+
to the saved-workflow paragraph.
|
|
22
|
+
- `pi-plan-mode`, `pi-goal`, `pi-usage-extension`: the stated Pi floor
|
|
23
|
+
(`0.80.6`, `0.42.4+`) contradicted the declared `^0.84.0` peer range, which
|
|
24
|
+
resolves to `0.84.x`. `pi-usage-extension` also carried a "Last updated"
|
|
25
|
+
stamp from a version that predates the workspace baseline.
|
|
26
|
+
- `pi-welcome`: the example card was a real session capture. It is now a
|
|
27
|
+
generic sample, and Install moved above the configuration sections instead of
|
|
28
|
+
sitting below them.
|
|
29
|
+
- 1f586f8: Preserve Codex opaque checkpoints across Pi's retry-only assistant-tail removal using optional, fingerprint-verified version-1 proof plus explicit lifecycle provenance. Preserve that provenance across same-runtime reload without confusing a new identical assistant response with the persisted tail; persisted rebuilds reset it and unknown provenance fails closed. Bound BTW context and tool-argument construction, including suffix-key storage for wide JSON-shaped objects (enumeration remains linear). Balance out-of-order native UI waiting notifications without affecting manual blocked ownership. Foreground workflow cancellation waits for exact-owner quiescence even when stop is unavailable or rejects after reconciliation already stopped the child, including cancelled spawn allocation recovery; unconfirmed cleanup becomes durably non-resumable with a diagnostic retaining any stop error. Background execution remains detached.
|
|
30
|
+
|
|
31
|
+
These are corrective changes: existing checkpoint fields, exports, and cross-extension protocols remain compatible, so no protocol or major-version bump is required.
|
|
32
|
+
- 1f586f8: Expand Pi peer support to `^0.84.0 || ^0.85.0`, retaining 0.84 compatibility while admitting 0.85 releases. The previous zero-major caret range excluded Pi 0.85.1. Update existing Pi development dependency pins to 0.85.1.
|
|
33
|
+
- 1f586f8: Keep goal iteration and continuation numbering correct when Pi compacts between tool results and the next response within one run. Preserve existing response limits and manual/post-run compaction handling.
|
|
34
|
+
|
|
35
|
+
Capture subagent invocation results from finalized assistant events with bounded message retention so compaction cannot hide a final error or mix in historical output. Recognize explicitly selected PowerShell when the host provides it, without requiring its export on older Pi versions or expanding default wildcard tools.
|
|
36
|
+
|
|
37
|
+
Use real session contexts for workflow replay and live TUI progress, release widget bindings on shutdown, and allow foreground checkpoint dialogs in RPC mode while retaining print-mode defaults. Cancel unanswered dialogs with their owning execution on tool abort, pause, stop, disposal, or fatal failure without journaling cancelled answers.
|
|
38
|
+
|
|
3
39
|
## 1.7.0
|
|
4
40
|
### Minor Changes
|
|
5
41
|
|
package/README.md
CHANGED
|
@@ -208,7 +208,6 @@ sentence-ending dot is not a filename dot: "please run a workflow." arms, and
|
|
|
208
208
|
turn it off entirely with `/workflows-trigger off`.
|
|
209
209
|
|
|
210
210
|
Saved workflows use project scope first and a user scope fallback; writes are atomic and names are path-safe.
|
|
211
|
-
written atomically with a backup.
|
|
212
211
|
|
|
213
212
|
## Model routing
|
|
214
213
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@signalridge/pi-workflows",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.1",
|
|
4
4
|
"description": "Deterministic JavaScript orchestration workflows backed by pi-subagents managed spawning.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
"lifecycle": "stable"
|
|
16
16
|
},
|
|
17
17
|
"peerDependencies": {
|
|
18
|
-
"@earendil-works/pi-coding-agent": "^0.84.0",
|
|
19
|
-
"@earendil-works/pi-tui": "^0.84.0",
|
|
18
|
+
"@earendil-works/pi-coding-agent": "^0.84.0 || ^0.85.0",
|
|
19
|
+
"@earendil-works/pi-tui": "^0.84.0 || ^0.85.0",
|
|
20
20
|
"@signalridge/pi-subagents": ">=1.9.0"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: workflow-authoring
|
|
3
3
|
description: Design, write, review, and debug Pi JavaScript workflows that coordinate multiple subagents. Use when a request needs named dependencies, bounded fan-out, per-item pipelines, quality gates, saved or nested workflows, pause/resume, or workflow progress; do not use it merely to run an existing named workflow.
|
|
4
4
|
metadata:
|
|
5
|
-
version: "1.7.
|
|
5
|
+
version: "1.7.1"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Workflow authoring
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# Workflow capability index
|
|
3
3
|
|
|
4
4
|
Contract format: `2.0.0`<br>
|
|
5
|
-
Contract content / skill / extension: `1.7.
|
|
5
|
+
Contract content / skill / extension: `1.7.1`
|
|
6
6
|
|
|
7
7
|
This compact generated index covers supported runtime globals and workflow-tool inputs. For constraints, compatibility behavior, internal boundaries, and dynamic-reference ownership, follow the [exhaustive generated facts](capability-details.md).
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# Exhaustive workflow capability facts
|
|
3
3
|
|
|
4
4
|
Contract format: `2.0.0`<br>
|
|
5
|
-
Contract content / skill / extension: `1.7.
|
|
5
|
+
Contract content / skill / extension: `1.7.1`
|
|
6
6
|
|
|
7
7
|
Every exact fact below is projected from the installed extension's capability contract. Explanatory judgment belongs in the hand-written references next to this file.
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: workflow-patterns
|
|
3
3
|
description: Choose and run one of Pi's built-in multi-agent workflows for research, adversarial review, code review, multi-perspective analysis, or codebase auditing. Use when the request matches a named pattern; use workflow-authoring instead for a new script or custom topology.
|
|
4
4
|
metadata:
|
|
5
|
-
version: "1.7.
|
|
5
|
+
version: "1.7.1"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Built-in workflow patterns
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: workflow-review
|
|
3
3
|
description: Audit an existing Pi JavaScript workflow for topology, prompt/data flow, limits, failure handling, cancellation, resume safety, and publication compatibility. Use when reviewing or debugging a workflow script or saved run; do not use to run a standard named workflow.
|
|
4
4
|
metadata:
|
|
5
|
-
version: "1.7.
|
|
5
|
+
version: "1.7.1"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Workflow review
|
package/src/engine.ts
CHANGED
|
@@ -66,6 +66,9 @@ import type { WorkflowStrengthTable } from "./strengths.js";
|
|
|
66
66
|
export const MAX_ATTEMPTS_PER_NODE = 3;
|
|
67
67
|
|
|
68
68
|
const BRANCH_QUIESCE_TIMEOUT_MS = 8_000;
|
|
69
|
+
// Internal deadline, longer than exact-owner RPC cleanup. Scripts can await
|
|
70
|
+
// arbitrary promises, so cancellation cannot rely on cooperative JS unwinding.
|
|
71
|
+
export const EXECUTION_DRAIN_TIMEOUT_MS = 10_000;
|
|
69
72
|
const MAX_WAITERS = 256;
|
|
70
73
|
const JOURNAL_RETRY_INITIAL_DELAY_MS = 25;
|
|
71
74
|
const JOURNAL_RETRY_MAX_DELAY_MS = 2_000;
|
|
@@ -116,6 +119,7 @@ export interface ScriptStartOptions {
|
|
|
116
119
|
choices?: string[];
|
|
117
120
|
timeoutMs?: number;
|
|
118
121
|
},
|
|
122
|
+
signal?: AbortSignal,
|
|
119
123
|
) => Promise<unknown>;
|
|
120
124
|
/** Resolve a nested `workflow(name)` reference to the script it names. */
|
|
121
125
|
loadSavedWorkflow?: (name: string) => string | undefined;
|
|
@@ -754,6 +758,7 @@ export class WorkflowEngine {
|
|
|
754
758
|
this.setWorkflowStatus(run, "running");
|
|
755
759
|
state.executionGeneration += 1;
|
|
756
760
|
state.execution = this.execute(runId, script, startOptions, undefined, state.executionGeneration, protocol);
|
|
761
|
+
const execution = state.execution;
|
|
757
762
|
// A run-fatal abort/stop already journals its terminal state; never let a
|
|
758
763
|
// leftover rejection become an unhandled promise rejection.
|
|
759
764
|
void state.execution.catch(() => {});
|
|
@@ -771,6 +776,10 @@ export class WorkflowEngine {
|
|
|
771
776
|
};
|
|
772
777
|
} catch (error: unknown) {
|
|
773
778
|
if (!(error instanceof WorkflowWaitAbortedError)) throw error;
|
|
779
|
+
// Foreground cancellation owns execution, not just this waiter. Do not
|
|
780
|
+
// advertise resumability until runtime children and checkpoint UI unwind.
|
|
781
|
+
state.controller.abort();
|
|
782
|
+
await this.drainExecution(state, execution);
|
|
774
783
|
const current = this.runs.get(runId)?.run ?? run;
|
|
775
784
|
return { runId: current.runId, status: current.status, background: false, waitAborted: true };
|
|
776
785
|
}
|
|
@@ -795,6 +804,7 @@ export class WorkflowEngine {
|
|
|
795
804
|
if (!state) return undefined;
|
|
796
805
|
if (state.lifecycleSuspended) return undefined;
|
|
797
806
|
const run = state.run;
|
|
807
|
+
if (run.status === "running" || run.status === "pausing" || run.status === "stopping") return undefined;
|
|
798
808
|
const providerTimer = this.providerResumeTimers.get(runId);
|
|
799
809
|
if (providerTimer) {
|
|
800
810
|
clearTimeout(providerTimer);
|
|
@@ -862,13 +872,16 @@ export class WorkflowEngine {
|
|
|
862
872
|
state.executionGeneration,
|
|
863
873
|
protocol,
|
|
864
874
|
);
|
|
865
|
-
|
|
875
|
+
const execution = state.execution;
|
|
876
|
+
void execution.catch(() => {});
|
|
866
877
|
if (options.background) return { runId, status: run.status, background: true };
|
|
867
878
|
try {
|
|
868
879
|
const settled = await this.waitFor(runId, options.signal);
|
|
869
880
|
return { runId, status: settled.status, background: false };
|
|
870
881
|
} catch (error: unknown) {
|
|
871
882
|
if (!(error instanceof WorkflowWaitAbortedError)) throw error;
|
|
883
|
+
state.controller.abort();
|
|
884
|
+
await this.drainExecution(state, execution);
|
|
872
885
|
return { runId, status: run.status, background: false, waitAborted: true };
|
|
873
886
|
}
|
|
874
887
|
}
|
|
@@ -896,21 +909,7 @@ export class WorkflowEngine {
|
|
|
896
909
|
// resumed from the journal. The script's in-flight agents are stopped by
|
|
897
910
|
// the runtime's abort signal via the dispatch waiters.
|
|
898
911
|
state.controller.abort();
|
|
899
|
-
this.
|
|
900
|
-
this.write({
|
|
901
|
-
kind: "run_recovery",
|
|
902
|
-
schemaVersion: JOURNAL_SCHEMA_VERSION,
|
|
903
|
-
runId,
|
|
904
|
-
status: "interrupted",
|
|
905
|
-
branchGeneration: this.recoveryBranchGeneration,
|
|
906
|
-
rotations: [],
|
|
907
|
-
recoveryId: deriveRecoveryId({ runId, rotations: [], timestamp: Date.now() }),
|
|
908
|
-
timestamp: Date.now(),
|
|
909
|
-
}),
|
|
910
|
-
);
|
|
911
|
-
run.status = "interrupted";
|
|
912
|
-
run.updatedAt = Date.now();
|
|
913
|
-
this.settleWaiters(runId, run);
|
|
912
|
+
await this.drainExecution(state, state.execution);
|
|
914
913
|
} else if (run.status !== "paused" && run.status !== "interrupted") {
|
|
915
914
|
throw new Error(`cannot pause workflow in state ${run.status}`);
|
|
916
915
|
}
|
|
@@ -1017,11 +1016,34 @@ export class WorkflowEngine {
|
|
|
1017
1016
|
const state = this.runs.get(runId);
|
|
1018
1017
|
if (!state) throw new Error(`workflow run not found: ${runId}`);
|
|
1019
1018
|
const generation = executionGeneration ?? state.executionGeneration;
|
|
1019
|
+
const executionSignal =
|
|
1020
|
+
!options.background && options.signal
|
|
1021
|
+
? AbortSignal.any([state.controller.signal, options.signal])
|
|
1022
|
+
: state.controller.signal;
|
|
1020
1023
|
|
|
1024
|
+
const cleanupTasks = new Set<Promise<void>>();
|
|
1021
1025
|
const runner: WorkflowAgentRunner = {
|
|
1022
1026
|
run: async (prompt: string, runOptions: AgentRunOptions = {}) => {
|
|
1023
|
-
|
|
1027
|
+
if (state.executionGeneration !== generation)
|
|
1028
|
+
throw new WorkflowError("stale workflow execution", WorkflowErrorCode.WORKFLOW_ABORTED);
|
|
1029
|
+
const dispatch = this.dispatchAgent(runId, prompt, runOptions);
|
|
1030
|
+
const settled = dispatch.then(
|
|
1031
|
+
() => {},
|
|
1032
|
+
() => {},
|
|
1033
|
+
);
|
|
1034
|
+
const trackCleanup = () => {
|
|
1035
|
+
cleanupTasks.add(settled);
|
|
1036
|
+
void settled.then(() => cleanupTasks.delete(settled));
|
|
1037
|
+
};
|
|
1038
|
+
runOptions.signal?.addEventListener("abort", trackCleanup, { once: true });
|
|
1039
|
+
if (runOptions.signal?.aborted) trackCleanup();
|
|
1040
|
+
try {
|
|
1041
|
+
return await dispatch;
|
|
1042
|
+
} finally {
|
|
1043
|
+
runOptions.signal?.removeEventListener("abort", trackCleanup);
|
|
1044
|
+
}
|
|
1024
1045
|
},
|
|
1046
|
+
waitForCleanup: () => this.drainExecution(state, Promise.all([...cleanupTasks])),
|
|
1025
1047
|
};
|
|
1026
1048
|
|
|
1027
1049
|
try {
|
|
@@ -1043,7 +1065,7 @@ export class WorkflowEngine {
|
|
|
1043
1065
|
agentRetries: options.agentRetries,
|
|
1044
1066
|
tokenBudget: options.tokenBudget,
|
|
1045
1067
|
agentTimeoutMs: options.agentTimeoutMs,
|
|
1046
|
-
signal:
|
|
1068
|
+
signal: executionSignal,
|
|
1047
1069
|
maxAgents: options.maxAgents,
|
|
1048
1070
|
runId,
|
|
1049
1071
|
executionNonce: `${generation}-${randomUUID()}`,
|
|
@@ -1078,6 +1100,12 @@ export class WorkflowEngine {
|
|
|
1078
1100
|
}
|
|
1079
1101
|
},
|
|
1080
1102
|
});
|
|
1103
|
+
await runner.waitForCleanup?.();
|
|
1104
|
+
if (executionSignal.aborted) {
|
|
1105
|
+
throw new WorkflowError("workflow execution aborted", WorkflowErrorCode.WORKFLOW_ABORTED, {
|
|
1106
|
+
recoverable: true,
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1081
1109
|
const live = this.runs.get(runId);
|
|
1082
1110
|
const run = live?.run;
|
|
1083
1111
|
if (run && state.executionGeneration === generation && !isTerminalWorkflow(run.status)) {
|
|
@@ -1086,6 +1114,7 @@ export class WorkflowEngine {
|
|
|
1086
1114
|
}
|
|
1087
1115
|
return result;
|
|
1088
1116
|
} catch (error: unknown) {
|
|
1117
|
+
await runner.waitForCleanup?.();
|
|
1089
1118
|
const run = this.runs.get(runId)?.run;
|
|
1090
1119
|
if (run && state.executionGeneration === generation && !isTerminalWorkflow(run.status)) {
|
|
1091
1120
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -1094,8 +1123,21 @@ export class WorkflowEngine {
|
|
|
1094
1123
|
this.setWorkflowStatus(run, "paused", message);
|
|
1095
1124
|
this.cleanupAgents(runId);
|
|
1096
1125
|
this.scheduleProviderResume(runId, workflowError.resetHint);
|
|
1097
|
-
} else if (workflowError.code === WorkflowErrorCode.WORKFLOW_ABORTED &&
|
|
1098
|
-
this.
|
|
1126
|
+
} else if (workflowError.code === WorkflowErrorCode.WORKFLOW_ABORTED && executionSignal.aborted) {
|
|
1127
|
+
this.persist(runId, "abort-recovery", () =>
|
|
1128
|
+
this.write({
|
|
1129
|
+
kind: "run_recovery",
|
|
1130
|
+
schemaVersion: JOURNAL_SCHEMA_VERSION,
|
|
1131
|
+
runId,
|
|
1132
|
+
status: "interrupted",
|
|
1133
|
+
branchGeneration: this.recoveryBranchGeneration,
|
|
1134
|
+
rotations: [],
|
|
1135
|
+
recoveryId: deriveRecoveryId({ runId, rotations: [], timestamp: Date.now() }),
|
|
1136
|
+
timestamp: Date.now(),
|
|
1137
|
+
}),
|
|
1138
|
+
);
|
|
1139
|
+
run.status = "interrupted";
|
|
1140
|
+
run.updatedAt = Date.now();
|
|
1099
1141
|
} else {
|
|
1100
1142
|
this.setWorkflowStatus(run, "failed", message);
|
|
1101
1143
|
this.cleanupAgents(runId);
|
|
@@ -1119,7 +1161,7 @@ export class WorkflowEngine {
|
|
|
1119
1161
|
if (this.disposed) throw new WorkflowEngineDisposedError();
|
|
1120
1162
|
const state = this.runs.get(runId);
|
|
1121
1163
|
if (!state) throw new Error(`workflow run not found: ${runId}`);
|
|
1122
|
-
if (state.lifecycleSuspended || state.controller.signal.aborted) {
|
|
1164
|
+
if (state.lifecycleSuspended || state.controller.signal.aborted || runOptions.signal?.aborted) {
|
|
1123
1165
|
throw new WorkflowError("workflow branch is quiescing", WorkflowErrorCode.WORKFLOW_ABORTED, {
|
|
1124
1166
|
recoverable: true,
|
|
1125
1167
|
});
|
|
@@ -1220,6 +1262,19 @@ export class WorkflowEngine {
|
|
|
1220
1262
|
let response: ManagedSpawnResponse | string;
|
|
1221
1263
|
try {
|
|
1222
1264
|
response = await this.client.spawn(task, runId, spawnNodeId, spawnAttemptId, externalSignal);
|
|
1265
|
+
} catch (error: unknown) {
|
|
1266
|
+
if (externalSignal.aborted) {
|
|
1267
|
+
const reconciled = await this.client.reconcileManaged?.(spawnKey, owner).catch(() => undefined);
|
|
1268
|
+
const id = typeof reconciled === "string" ? reconciled : reconciled?.id;
|
|
1269
|
+
await this.stopAndQuiesce(
|
|
1270
|
+
state,
|
|
1271
|
+
id ?? spawnKey,
|
|
1272
|
+
owner,
|
|
1273
|
+
executionGeneration,
|
|
1274
|
+
id ? undefined : "cancelled spawn allocation could not be confirmed",
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1277
|
+
throw error;
|
|
1223
1278
|
} finally {
|
|
1224
1279
|
externalSignal.removeEventListener("abort", onPendingAbort);
|
|
1225
1280
|
state.pendingSpawns.delete(spawnKey);
|
|
@@ -1231,9 +1286,7 @@ export class WorkflowEngine {
|
|
|
1231
1286
|
externalSignal.aborted
|
|
1232
1287
|
) {
|
|
1233
1288
|
state.bufferedTerminals.delete(spawnKey);
|
|
1234
|
-
await this.
|
|
1235
|
-
.stopOwned?.(responseId, { extension: "pi-workflows", runId, nodeId: spawnNodeId, attemptId: spawnAttemptId })
|
|
1236
|
-
.catch(() => {});
|
|
1289
|
+
await this.stopAndQuiesce(state, responseId, owner, executionGeneration);
|
|
1237
1290
|
throw new WorkflowError(
|
|
1238
1291
|
"workflow dispatch became stale during pause/resume",
|
|
1239
1292
|
WorkflowErrorCode.WORKFLOW_ABORTED,
|
|
@@ -1294,15 +1347,12 @@ export class WorkflowEngine {
|
|
|
1294
1347
|
waiter.settled = true;
|
|
1295
1348
|
state.agentWaiters.delete(pendingId);
|
|
1296
1349
|
state.bufferedTerminals.delete(spawnKey);
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
})
|
|
1304
|
-
.catch(() => {});
|
|
1305
|
-
waiter.reject(new WorkflowError("workflow aborted", WorkflowErrorCode.WORKFLOW_ABORTED, { recoverable: true }));
|
|
1350
|
+
// Stop acknowledges cancellation, not provider/tool finally completion.
|
|
1351
|
+
void this.stopAndQuiesce(state, pendingId, owner, executionGeneration).then(() => {
|
|
1352
|
+
waiter.reject(
|
|
1353
|
+
new WorkflowError("workflow aborted", WorkflowErrorCode.WORKFLOW_ABORTED, { recoverable: true }),
|
|
1354
|
+
);
|
|
1355
|
+
});
|
|
1306
1356
|
}
|
|
1307
1357
|
};
|
|
1308
1358
|
|
|
@@ -1532,6 +1582,99 @@ export class WorkflowEngine {
|
|
|
1532
1582
|
}
|
|
1533
1583
|
}
|
|
1534
1584
|
|
|
1585
|
+
private async stopAndQuiesce(
|
|
1586
|
+
state: ScriptRunState,
|
|
1587
|
+
agentId: string,
|
|
1588
|
+
owner: WorkflowOwner,
|
|
1589
|
+
executionGeneration: number,
|
|
1590
|
+
unconfirmed?: string,
|
|
1591
|
+
): Promise<void> {
|
|
1592
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1593
|
+
let stopError: unknown;
|
|
1594
|
+
try {
|
|
1595
|
+
if (unconfirmed) throw new Error(unconfirmed);
|
|
1596
|
+
await Promise.race([
|
|
1597
|
+
(async () => {
|
|
1598
|
+
try {
|
|
1599
|
+
if (!this.client.stopOwned) throw new Error("owned stop RPC unavailable");
|
|
1600
|
+
await this.client.stopOwned(agentId, owner);
|
|
1601
|
+
} catch (error: unknown) {
|
|
1602
|
+
// Reconciliation may already have stopped the record. Only exact-owner
|
|
1603
|
+
// quiescence confirms that provider/tool cleanup has actually settled.
|
|
1604
|
+
stopError = error;
|
|
1605
|
+
}
|
|
1606
|
+
if (!this.client.quiesceOwned) throw new Error("owned cleanup RPC unavailable");
|
|
1607
|
+
const result = await this.client.quiesceOwned(owner.runId, [agentId], BRANCH_QUIESCE_TIMEOUT_MS, [owner]);
|
|
1608
|
+
if (!result?.settled || result.pending.length !== 0) throw new Error("owned agents are not quiescent");
|
|
1609
|
+
})(),
|
|
1610
|
+
new Promise<never>((_resolve, reject) => {
|
|
1611
|
+
timer = setTimeout(() => reject(new Error("owned cleanup timed out")), BRANCH_QUIESCE_TIMEOUT_MS + 1_000);
|
|
1612
|
+
}),
|
|
1613
|
+
]);
|
|
1614
|
+
} catch (error: unknown) {
|
|
1615
|
+
// Never turn uncertain cleanup into a resumable interruption. Existing
|
|
1616
|
+
// terminal recovery facts durably fence this run without a protocol change.
|
|
1617
|
+
if (this.runs.get(owner.runId) !== state || state.executionGeneration !== executionGeneration) return;
|
|
1618
|
+
const stopDiagnostic = stopError === undefined ? "" : `; stop failed: ${String(stopError)}`;
|
|
1619
|
+
const diagnostic = `Owned cleanup unconfirmed for ${agentId}: ${error instanceof Error ? error.message : String(error)}${stopDiagnostic}; run is non-resumable`;
|
|
1620
|
+
this.fenceExecution(state, executionGeneration, diagnostic);
|
|
1621
|
+
} finally {
|
|
1622
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
private async drainExecution(state: ScriptRunState, execution?: Promise<unknown>): Promise<void> {
|
|
1627
|
+
if (!execution) return;
|
|
1628
|
+
const generation = state.executionGeneration;
|
|
1629
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1630
|
+
try {
|
|
1631
|
+
await Promise.race([
|
|
1632
|
+
execution.catch(() => {}),
|
|
1633
|
+
new Promise<void>((resolve) => {
|
|
1634
|
+
timer = setTimeout(() => {
|
|
1635
|
+
this.fenceExecution(state, generation, "Workflow execution cleanup timed out; run is non-resumable");
|
|
1636
|
+
resolve();
|
|
1637
|
+
}, EXECUTION_DRAIN_TIMEOUT_MS);
|
|
1638
|
+
}),
|
|
1639
|
+
]);
|
|
1640
|
+
} finally {
|
|
1641
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
private fenceExecution(state: ScriptRunState, generation: number, diagnostic: string): void {
|
|
1646
|
+
const run = state.run;
|
|
1647
|
+
if (this.runs.get(run.runId) !== state || state.executionGeneration !== generation) return;
|
|
1648
|
+
if (run.nonResumable && run.status === "stopped") return;
|
|
1649
|
+
run.nonResumable = true;
|
|
1650
|
+
const blockedNodeIds = Object.keys(run.callStatus).filter((nodeId) => run.callStatus[nodeId] !== "completed");
|
|
1651
|
+
for (const nodeId of blockedNodeIds) run.callStatus[nodeId] = "stopped";
|
|
1652
|
+
run.status = "stopped";
|
|
1653
|
+
run.error = diagnostic;
|
|
1654
|
+
run.terminalIntent = "stop";
|
|
1655
|
+
run.updatedAt = Date.now();
|
|
1656
|
+
const event: TerminalRecoveryEvent = {
|
|
1657
|
+
kind: "terminal_recovery",
|
|
1658
|
+
schemaVersion: JOURNAL_SCHEMA_VERSION,
|
|
1659
|
+
runId: run.runId,
|
|
1660
|
+
status: "stopped",
|
|
1661
|
+
terminalIntent: "stop",
|
|
1662
|
+
branchGeneration: this.recoveryBranchGeneration,
|
|
1663
|
+
terminalResults: [],
|
|
1664
|
+
blockedNodeIds,
|
|
1665
|
+
error: diagnostic,
|
|
1666
|
+
recoveryId: deriveRecoveryId({ runId: run.runId, rotations: [], timestamp: Date.now() }),
|
|
1667
|
+
timestamp: Date.now(),
|
|
1668
|
+
};
|
|
1669
|
+
this.persist(run.runId, "cleanup-unconfirmed", () => this.write(event));
|
|
1670
|
+
// Fence before aborting: synchronous abort handlers may re-enter cleanup.
|
|
1671
|
+
// Exact-owner cleanup continues, but no later result may revive this run.
|
|
1672
|
+
state.executionGeneration += 1;
|
|
1673
|
+
state.controller.abort();
|
|
1674
|
+
this.cleanupAgents(run.runId);
|
|
1675
|
+
this.settleWaiters(run.runId, run);
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1535
1678
|
private async stop(state: ScriptRunState): Promise<void> {
|
|
1536
1679
|
const run = state.run;
|
|
1537
1680
|
if (isTerminalWorkflow(run.status)) return;
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { Text } from "@earendil-works/pi-tui";
|
|
5
5
|
import { Type } from "@sinclair/typebox";
|
|
@@ -157,8 +157,14 @@ function formatStart(result: ScriptStartResult): string {
|
|
|
157
157
|
`Workflow ${result.background ? "started in background" : result.status}: ${result.runId}`,
|
|
158
158
|
`status=${result.status}`,
|
|
159
159
|
];
|
|
160
|
-
if (result.waitAborted)
|
|
161
|
-
lines.push(
|
|
160
|
+
if (result.waitAborted) {
|
|
161
|
+
lines.push(
|
|
162
|
+
"wait_aborted=true",
|
|
163
|
+
result.background
|
|
164
|
+
? "Only the wait was cancelled; retrieve the detached run with workflow_control."
|
|
165
|
+
: "Foreground execution was cancelled; inspect its status with workflow_control before resuming.",
|
|
166
|
+
);
|
|
167
|
+
}
|
|
162
168
|
if (result.result) lines.push("", result.result.slice(0, 8_000));
|
|
163
169
|
if (result.error) lines.push("", `error=${result.error.slice(0, 2_000)}`);
|
|
164
170
|
return lines.join("\n");
|
|
@@ -337,6 +343,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
337
343
|
const MAX_PENDING_DELIVERIES = 256;
|
|
338
344
|
const PENDING_DELIVERY_TTL_MS = 7 * 24 * 60 * 60 * 1_000;
|
|
339
345
|
let widgetTimer: ReturnType<typeof setInterval> | undefined;
|
|
346
|
+
let widgetUi: ExtensionContext["ui"] | undefined;
|
|
340
347
|
|
|
341
348
|
const currentEngine = (): WorkflowEngine => {
|
|
342
349
|
if (!active || !engine || engine.isDisposed())
|
|
@@ -345,12 +352,11 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
345
352
|
};
|
|
346
353
|
|
|
347
354
|
const clearWidget = (): void => {
|
|
348
|
-
|
|
349
|
-
tui?.setWidget?.("pi-workflows", undefined);
|
|
355
|
+
widgetUi?.setWidget("pi-workflows", undefined);
|
|
350
356
|
};
|
|
351
357
|
const refreshWidget = (): void => {
|
|
352
|
-
const tui =
|
|
353
|
-
if (!tui
|
|
358
|
+
const tui = widgetUi;
|
|
359
|
+
if (!tui) return;
|
|
354
360
|
const workflowEngine = engine;
|
|
355
361
|
if (!active || !workflowEngine || workflowEngine.isDisposed()) {
|
|
356
362
|
tui.setWidget("pi-workflows", undefined);
|
|
@@ -375,15 +381,6 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
375
381
|
};
|
|
376
382
|
const notifyRunChanged = (): void => refreshWidget();
|
|
377
383
|
|
|
378
|
-
const branchEntries = (): SessionEntryLike[] => {
|
|
379
|
-
const ctx = (pi as unknown as { currentSessionManager?: { getBranch?: () => unknown } }).currentSessionManager;
|
|
380
|
-
try {
|
|
381
|
-
return (ctx?.getBranch?.() as SessionEntryLike[] | undefined) ?? [];
|
|
382
|
-
} catch {
|
|
383
|
-
return [];
|
|
384
|
-
}
|
|
385
|
-
};
|
|
386
|
-
|
|
387
384
|
const DEFAULT_EXCLUDED_TOOLS = ["workflow", "workflow_control"] as const;
|
|
388
385
|
|
|
389
386
|
/**
|
|
@@ -435,6 +432,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
435
432
|
active = true;
|
|
436
433
|
const workflowEngine = currentEngine();
|
|
437
434
|
workflowEngine.onRunSettled = notifyRunChanged;
|
|
435
|
+
workflowEngine.onRuntimeEvent = notifyRunChanged;
|
|
438
436
|
// `control("resume")` and the provider-limit retry start executions with no
|
|
439
437
|
// caller to pass options for them, and the strength table is deliberately
|
|
440
438
|
// never frozen onto a run. Without this they would resume with no table and
|
|
@@ -509,13 +507,13 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
509
507
|
// Foreground checkpoint confirmation only when the run is not
|
|
510
508
|
// background — background runs are headless by contract.
|
|
511
509
|
confirm:
|
|
512
|
-
params.background === false && ctx?.hasUI
|
|
513
|
-
? async (promptText, checkpointOptions) => {
|
|
510
|
+
params.background === false && ctx?.hasUI
|
|
511
|
+
? async (promptText, checkpointOptions, signal) => {
|
|
514
512
|
if (checkpointOptions.kind === "input")
|
|
515
|
-
return ctx.ui.input(promptText, String(checkpointOptions.default ?? ""));
|
|
513
|
+
return ctx.ui.input(promptText, String(checkpointOptions.default ?? ""), { signal });
|
|
516
514
|
if (checkpointOptions.kind === "select")
|
|
517
|
-
return ctx.ui.select(promptText, checkpointOptions.choices ?? []);
|
|
518
|
-
return ctx.ui.confirm(promptText, promptText);
|
|
515
|
+
return ctx.ui.select(promptText, checkpointOptions.choices ?? [], { signal });
|
|
516
|
+
return ctx.ui.confirm(promptText, promptText, { signal });
|
|
519
517
|
}
|
|
520
518
|
: undefined,
|
|
521
519
|
loadSavedWorkflow: resolveNestedWorkflow,
|
|
@@ -525,7 +523,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
525
523
|
const replacement = params.script !== undefined || params.name !== undefined ? script : undefined;
|
|
526
524
|
const resumed = await workflowEngine.resume(
|
|
527
525
|
params.resumeFromRunId,
|
|
528
|
-
|
|
526
|
+
ctx.sessionManager.getBranch(),
|
|
529
527
|
options,
|
|
530
528
|
replacement,
|
|
531
529
|
);
|
|
@@ -1116,6 +1114,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1116
1114
|
} finally {
|
|
1117
1115
|
workflowEngine.resumeLifecycle();
|
|
1118
1116
|
branchQuiesce = undefined;
|
|
1117
|
+
refreshWidget();
|
|
1119
1118
|
}
|
|
1120
1119
|
});
|
|
1121
1120
|
};
|
|
@@ -1257,6 +1256,10 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1257
1256
|
{
|
|
1258
1257
|
append(event) {
|
|
1259
1258
|
pi.appendEntry(JOURNAL_ENTRY_TYPE, event);
|
|
1259
|
+
// Journal writes precede some state mutations; refresh once applied.
|
|
1260
|
+
queueMicrotask(() => {
|
|
1261
|
+
if (generation === lifecycleGeneration) refreshWidget();
|
|
1262
|
+
});
|
|
1260
1263
|
},
|
|
1261
1264
|
},
|
|
1262
1265
|
() => {
|
|
@@ -1288,7 +1291,12 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1288
1291
|
return;
|
|
1289
1292
|
}
|
|
1290
1293
|
|
|
1294
|
+
if (generation !== lifecycleGeneration) {
|
|
1295
|
+
recoveredEngine.dispose();
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1291
1298
|
engine = recoveredEngine;
|
|
1299
|
+
widgetUi = ctx.mode === "tui" ? ctx.ui : undefined;
|
|
1292
1300
|
registerSurface();
|
|
1293
1301
|
|
|
1294
1302
|
// Bind per-session delivery endpoint (A10): capture the session-bound send
|
|
@@ -1382,6 +1390,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1382
1390
|
// engine quiescence; the old session must not accept another run.
|
|
1383
1391
|
active = false;
|
|
1384
1392
|
clearWidget();
|
|
1393
|
+
widgetUi = undefined;
|
|
1385
1394
|
if (widgetTimer) {
|
|
1386
1395
|
clearInterval(widgetTimer);
|
|
1387
1396
|
widgetTimer = undefined;
|
package/src/rpc-client.ts
CHANGED
|
@@ -380,7 +380,7 @@ export function createManagedSpawnClient(
|
|
|
380
380
|
agentId,
|
|
381
381
|
owner,
|
|
382
382
|
},
|
|
383
|
-
|
|
383
|
+
undefined,
|
|
384
384
|
);
|
|
385
385
|
},
|
|
386
386
|
async reconcileManaged(spawnKey, owner) {
|
|
@@ -403,10 +403,15 @@ export function createManagedSpawnClient(
|
|
|
403
403
|
...(owners ? { owners } : {}),
|
|
404
404
|
timeoutMs,
|
|
405
405
|
},
|
|
406
|
-
|
|
406
|
+
undefined,
|
|
407
407
|
timeoutMs + 1_000,
|
|
408
408
|
);
|
|
409
|
-
if (
|
|
409
|
+
if (
|
|
410
|
+
!isRecord(data) ||
|
|
411
|
+
typeof data.settled !== "boolean" ||
|
|
412
|
+
!Array.isArray(data.pending) ||
|
|
413
|
+
!data.pending.every((id) => typeof id === "string" && id.length > 0)
|
|
414
|
+
) {
|
|
410
415
|
throw new Error("subagents quiescence returned an invalid response");
|
|
411
416
|
}
|
|
412
417
|
const pending = data.pending.filter((id): id is string => typeof id === "string");
|
package/src/runtime.ts
CHANGED
|
@@ -116,6 +116,8 @@ export type Limiter = <T>(fn: () => Promise<T>) => Promise<T>;
|
|
|
116
116
|
/** The dispatch boundary the engine implements over spawn-managed RPC. */
|
|
117
117
|
export interface WorkflowAgentRunner {
|
|
118
118
|
run(prompt: string, options?: AgentRunOptions): Promise<unknown>;
|
|
119
|
+
/** Managed runners bound and confirm cleanup of aborted dispatches. */
|
|
120
|
+
waitForCleanup?(): Promise<void>;
|
|
119
121
|
}
|
|
120
122
|
|
|
121
123
|
export interface AgentRunOptions {
|
|
@@ -300,7 +302,7 @@ export interface WorkflowRunOptions {
|
|
|
300
302
|
sharedStore?: SharedStore;
|
|
301
303
|
/** Resolve a nested `workflow(name)` reference to the script it names. */
|
|
302
304
|
loadSavedWorkflow?: (name: string) => string | undefined;
|
|
303
|
-
confirm?: (promptText: string, options: CheckpointOptions) => Promise<unknown>;
|
|
305
|
+
confirm?: (promptText: string, options: CheckpointOptions, signal?: AbortSignal) => Promise<unknown>;
|
|
304
306
|
onLog?: (message: string) => void;
|
|
305
307
|
onPhase?: (title: string) => void;
|
|
306
308
|
onRuntimeEvent?: (event: WorkflowRuntimeEvent) => void;
|
|
@@ -921,6 +923,7 @@ async function withTimeout<T>(
|
|
|
921
923
|
* must degrade to the old behaviour, not to an agent that can hang forever.
|
|
922
924
|
*/
|
|
923
925
|
onArm?: (restart: () => void) => void,
|
|
926
|
+
waitForCleanup?: () => Promise<void>,
|
|
924
927
|
): Promise<T> {
|
|
925
928
|
if (ms === null) {
|
|
926
929
|
onArm?.(() => {});
|
|
@@ -934,9 +937,11 @@ async function withTimeout<T>(
|
|
|
934
937
|
// finished agent long after the fact, so settlement makes it inert here
|
|
935
938
|
// rather than relying on every holder to guard its own call.
|
|
936
939
|
let settled = false;
|
|
940
|
+
let timedOut = false;
|
|
937
941
|
|
|
938
942
|
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
939
943
|
const fire = () => {
|
|
944
|
+
timedOut = true;
|
|
940
945
|
try {
|
|
941
946
|
onTimeout?.();
|
|
942
947
|
} catch {
|
|
@@ -963,6 +968,9 @@ async function withTimeout<T>(
|
|
|
963
968
|
} finally {
|
|
964
969
|
settled = true;
|
|
965
970
|
if (timeoutId) clearTimeout(timeoutId);
|
|
971
|
+
// The timeout wins the race immediately, but managed cleanup must settle
|
|
972
|
+
// (or durably fence the run) before script continuation and journal output.
|
|
973
|
+
if (timedOut) await waitForCleanup?.();
|
|
966
974
|
}
|
|
967
975
|
}
|
|
968
976
|
|
|
@@ -1722,6 +1730,7 @@ export async function runWorkflow<T = unknown>(
|
|
|
1722
1730
|
(restart) => {
|
|
1723
1731
|
restartTimeout = restart;
|
|
1724
1732
|
},
|
|
1733
|
+
options.agent?.waitForCleanup,
|
|
1725
1734
|
);
|
|
1726
1735
|
|
|
1727
1736
|
throwIfAborted();
|
|
@@ -1789,6 +1798,18 @@ export async function runWorkflow<T = unknown>(
|
|
|
1789
1798
|
});
|
|
1790
1799
|
return result;
|
|
1791
1800
|
} catch (error) {
|
|
1801
|
+
// A timeout waits for managed cleanup before reaching this catch.
|
|
1802
|
+
// Explicit cancellation may arrive during that await; it wins only
|
|
1803
|
+
// over this recoverable timeout, never over a fatal cleanup fence.
|
|
1804
|
+
if (
|
|
1805
|
+
options.signal?.aborted &&
|
|
1806
|
+
!shared.runFatalController.signal.aborted &&
|
|
1807
|
+
error instanceof WorkflowError &&
|
|
1808
|
+
error.recoverable &&
|
|
1809
|
+
error.code === WorkflowErrorCode.AGENT_TIMEOUT
|
|
1810
|
+
) {
|
|
1811
|
+
throwIfAborted();
|
|
1812
|
+
}
|
|
1792
1813
|
if (isAborted()) throw error;
|
|
1793
1814
|
|
|
1794
1815
|
const workflowError = wrapError(error, { agentLabel: label });
|
|
@@ -2285,7 +2306,34 @@ export async function runWorkflow<T = unknown>(
|
|
|
2285
2306
|
|
|
2286
2307
|
let reply: unknown;
|
|
2287
2308
|
if (options.confirm) {
|
|
2288
|
-
|
|
2309
|
+
// Host-only cancellation must not affect author options or replay hashes.
|
|
2310
|
+
const signal = options.signal
|
|
2311
|
+
? AbortSignal.any([options.signal, shared.runFatalController.signal])
|
|
2312
|
+
: shared.runFatalController.signal;
|
|
2313
|
+
const confirm = options.confirm;
|
|
2314
|
+
let onAbort: (() => void) | undefined;
|
|
2315
|
+
try {
|
|
2316
|
+
const cancelled = new Promise<never>((_resolve, reject) => {
|
|
2317
|
+
onAbort = () => {
|
|
2318
|
+
try {
|
|
2319
|
+
throwIfAborted();
|
|
2320
|
+
} catch (error) {
|
|
2321
|
+
reject(error);
|
|
2322
|
+
}
|
|
2323
|
+
};
|
|
2324
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2325
|
+
if (signal.aborted) onAbort();
|
|
2326
|
+
});
|
|
2327
|
+
// Older host callbacks may ignore the optional signal. Still release
|
|
2328
|
+
// the runtime wait, and observe any eventual callback rejection.
|
|
2329
|
+
const answer = Promise.resolve().then(() => {
|
|
2330
|
+
throwIfAborted();
|
|
2331
|
+
return confirm(promptText, checkpointOptions, signal);
|
|
2332
|
+
});
|
|
2333
|
+
reply = await Promise.race([answer, cancelled]);
|
|
2334
|
+
} finally {
|
|
2335
|
+
if (onAbort) signal.removeEventListener("abort", onAbort);
|
|
2336
|
+
}
|
|
2289
2337
|
} else if (checkpointOptions.headless === "abort") {
|
|
2290
2338
|
throw new WorkflowError(
|
|
2291
2339
|
`checkpoint "${promptText}" needs human input but none is available (headless run)`,
|