@bastani/atomic 0.9.3-alpha.1 → 0.9.3-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/dist/builtin/cursor/CHANGELOG.md +15 -0
- package/dist/builtin/cursor/README.md +2 -1
- package/dist/builtin/cursor/package.json +2 -2
- package/dist/builtin/cursor/src/cursor-models-raw.json +2 -9
- package/dist/builtin/cursor/src/model-mapper.ts +14 -3
- package/dist/builtin/cursor/src/proto/protobuf-codec-base64.ts +22 -0
- package/dist/builtin/cursor/src/proto/protobuf-codec-request.ts +53 -13
- package/dist/builtin/cursor/src/proto/protobuf-codec-wire.ts +24 -7
- package/dist/builtin/cursor/src/proto/protobuf-codec.ts +3 -2
- package/dist/builtin/cursor/src/stream.ts +5 -11
- package/dist/builtin/cursor/src/transport-types.ts +3 -0
- package/dist/builtin/cursor/src/transport.ts +1 -0
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/CHANGELOG.md +9 -0
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/subagents/src/extension/fanout-child.ts +1 -0
- package/dist/builtin/subagents/src/extension/index.ts +6 -3
- package/dist/builtin/subagents/src/extension/schemas.ts +0 -5
- package/dist/builtin/subagents/src/runs/background/async-job-tracker.ts +1 -4
- package/dist/builtin/subagents/src/runs/foreground/subagent-executor-single.ts +15 -1
- package/dist/builtin/subagents/src/runs/foreground/subagent-executor.ts +35 -1
- package/dist/builtin/subagents/src/runs/shared/subagent-prompt-runtime.ts +4 -2
- package/dist/builtin/subagents/src/shared/types-async.ts +1 -0
- package/dist/builtin/subagents/src/slash/prompt-template-bridge.ts +27 -5
- package/dist/builtin/subagents/src/tui/render-layout.ts +27 -4
- package/dist/builtin/subagents/src/tui/render-result-animation.ts +22 -31
- package/dist/builtin/subagents/src/tui/render-result-compact.ts +6 -6
- package/dist/builtin/subagents/src/tui/render-result.ts +20 -19
- package/dist/builtin/subagents/src/tui/render-status-progress.ts +3 -3
- package/dist/builtin/subagents/src/tui/render-widget.ts +46 -7
- package/dist/builtin/subagents/src/tui/render.ts +2 -2
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +43 -0
- package/dist/builtin/workflows/README.md +1 -1
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/src/authoring.d.ts +1 -1
- package/dist/builtin/workflows/src/durable/backend.ts +343 -0
- package/dist/builtin/workflows/src/durable/child-primitive.ts +79 -0
- package/dist/builtin/workflows/src/durable/dbos-backend.ts +421 -0
- package/dist/builtin/workflows/src/durable/dbos-envelope.ts +171 -0
- package/dist/builtin/workflows/src/durable/factory.ts +96 -0
- package/dist/builtin/workflows/src/durable/file-backend.ts +433 -0
- package/dist/builtin/workflows/src/durable/index.ts +73 -0
- package/dist/builtin/workflows/src/durable/resume-catalog.ts +217 -0
- package/dist/builtin/workflows/src/durable/resume-runtime.ts +299 -0
- package/dist/builtin/workflows/src/durable/scoped-backend.ts +171 -0
- package/dist/builtin/workflows/src/durable/stage-primitive.ts +284 -0
- package/dist/builtin/workflows/src/durable/tool-primitive.ts +180 -0
- package/dist/builtin/workflows/src/durable/types.ts +168 -0
- package/dist/builtin/workflows/src/durable/ui-primitive.ts +96 -0
- package/dist/builtin/workflows/src/engine/options.ts +3 -0
- package/dist/builtin/workflows/src/engine/primitives/parallel.ts +2 -2
- package/dist/builtin/workflows/src/engine/primitives/task.ts +4 -4
- package/dist/builtin/workflows/src/engine/primitives/ui.ts +22 -8
- package/dist/builtin/workflows/src/engine/primitives/workflow.ts +8 -0
- package/dist/builtin/workflows/src/engine/run-durable-finalize.ts +69 -0
- package/dist/builtin/workflows/src/engine/run-durable-stage-session.ts +31 -0
- package/dist/builtin/workflows/src/engine/run.ts +148 -6
- package/dist/builtin/workflows/src/engine/runtime.ts +8 -2
- package/dist/builtin/workflows/src/extension/extension-factory.ts +6 -12
- package/dist/builtin/workflows/src/extension/extension-lifecycle.ts +5 -1
- package/dist/builtin/workflows/src/extension/extension-runtime-state.ts +3 -0
- package/dist/builtin/workflows/src/extension/runtime.ts +48 -9
- package/dist/builtin/workflows/src/extension/workflow-run-control-command.ts +143 -4
- package/dist/builtin/workflows/src/runs/background/quit.ts +61 -0
- package/dist/builtin/workflows/src/runs/background/status.ts +1 -0
- package/dist/builtin/workflows/src/runs/foreground/executor-direct-helpers.ts +5 -5
- package/dist/builtin/workflows/src/runs/foreground/executor-stage-call.ts +74 -33
- package/dist/builtin/workflows/src/runs/foreground/executor-stage-context.ts +20 -1
- package/dist/builtin/workflows/src/runs/foreground/executor-stage-factory.ts +8 -7
- package/dist/builtin/workflows/src/runs/foreground/executor-stage-replay.ts +1 -0
- package/dist/builtin/workflows/src/runs/foreground/executor-stage-types.ts +1 -1
- package/dist/builtin/workflows/src/runs/foreground/executor-types.ts +19 -2
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-context.ts +4 -0
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-controller.ts +10 -10
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-options.ts +5 -1
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-send-user-message.ts +25 -0
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-types.ts +3 -0
- package/dist/builtin/workflows/src/shared/authoring-contract-stage.d.ts +16 -0
- package/dist/builtin/workflows/src/shared/authoring-contract-stage.ts +20 -0
- package/dist/builtin/workflows/src/shared/authoring-contract-ui.d.ts +23 -1
- package/dist/builtin/workflows/src/shared/authoring-contract-ui.ts +30 -1
- package/dist/builtin/workflows/src/shared/store-public-types.ts +6 -2
- package/dist/builtin/workflows/src/shared/store-run-methods.ts +12 -6
- package/dist/builtin/workflows/src/shared/types.ts +55 -0
- package/dist/builtin/workflows/src/tui/graph-view-constants.ts +1 -1
- package/dist/builtin/workflows/src/tui/graph-view-graph-render.ts +41 -0
- package/dist/builtin/workflows/src/tui/graph-view-input.ts +82 -24
- package/dist/builtin/workflows/src/tui/graph-view-render.ts +7 -0
- package/dist/builtin/workflows/src/tui/graph-view-state.ts +22 -2
- package/dist/builtin/workflows/src/tui/graph-view-types.ts +4 -5
- package/dist/builtin/workflows/src/tui/overlay-adapter.ts +9 -11
- package/dist/builtin/workflows/src/tui/stage-chat-view-footer-status.ts +9 -3
- package/dist/builtin/workflows/src/tui/stage-chat-view-input.ts +11 -2
- package/dist/builtin/workflows/src/tui/stage-chat-view-live-events.ts +35 -0
- package/dist/builtin/workflows/src/tui/stage-chat-view-state.ts +51 -17
- package/dist/builtin/workflows/src/tui/stage-chat-view-status.ts +36 -0
- package/dist/builtin/workflows/src/tui/stage-chat-view-types.ts +5 -1
- package/dist/builtin/workflows/src/tui/stage-chat-view.ts +3 -1
- package/dist/builtin/workflows/src/tui/status-list.ts +14 -2
- package/dist/builtin/workflows/src/tui/widget.ts +23 -8
- package/dist/builtin/workflows/src/tui/workflow-attach-pane-types.ts +5 -4
- package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +8 -8
- package/dist/builtin/workflows/src/tui/workflow-resume-selector.ts +151 -0
- package/dist/core/extensions/loader-virtual-modules.d.ts.map +1 -1
- package/dist/core/extensions/loader-virtual-modules.js +47 -30
- package/dist/core/extensions/loader-virtual-modules.js.map +1 -1
- package/dist/core/messages.d.ts +1 -0
- package/dist/core/messages.d.ts.map +1 -1
- package/dist/core/messages.js +46 -1
- package/dist/core/messages.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +12 -0
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager-core.d.ts +15 -7
- package/dist/core/session-manager-core.d.ts.map +1 -1
- package/dist/core/session-manager-core.js +20 -9
- package/dist/core/session-manager-core.js.map +1 -1
- package/dist/core/session-manager-entries.d.ts +2 -2
- package/dist/core/session-manager-entries.d.ts.map +1 -1
- package/dist/core/session-manager-entries.js +9 -3
- package/dist/core/session-manager-entries.js.map +1 -1
- package/dist/core/session-manager-history.d.ts.map +1 -1
- package/dist/core/session-manager-history.js +2 -1
- package/dist/core/session-manager-history.js.map +1 -1
- package/dist/core/session-manager-list.d.ts +3 -3
- package/dist/core/session-manager-list.d.ts.map +1 -1
- package/dist/core/session-manager-list.js +27 -8
- package/dist/core/session-manager-list.js.map +1 -1
- package/dist/core/session-manager-storage.d.ts +3 -1
- package/dist/core/session-manager-storage.d.ts.map +1 -1
- package/dist/core/session-manager-storage.js +55 -12
- package/dist/core/session-manager-storage.js.map +1 -1
- package/dist/core/session-manager-tool-dependencies.d.ts +10 -0
- package/dist/core/session-manager-tool-dependencies.d.ts.map +1 -0
- package/dist/core/session-manager-tool-dependencies.js +133 -0
- package/dist/core/session-manager-tool-dependencies.js.map +1 -0
- package/dist/core/session-manager-types.d.ts +22 -0
- package/dist/core/session-manager-types.d.ts.map +1 -1
- package/dist/core/session-manager-types.js.map +1 -1
- package/dist/core/session-manager.d.ts +2 -2
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +1 -1
- package/dist/core/session-manager.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-runtime.d.ts +1 -0
- package/dist/modes/interactive/components/chat-session-host-runtime.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-runtime.js +12 -0
- package/dist/modes/interactive/components/chat-session-host-runtime.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-terminal-cleanup.d.ts +4 -0
- package/dist/modes/interactive/components/chat-session-host-terminal-cleanup.d.ts.map +1 -0
- package/dist/modes/interactive/components/chat-session-host-terminal-cleanup.js +131 -0
- package/dist/modes/interactive/components/chat-session-host-terminal-cleanup.js.map +1 -0
- package/dist/modes/interactive/components/chat-session-host.d.ts +2 -0
- package/dist/modes/interactive/components/chat-session-host.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host.js +7 -1
- package/dist/modes/interactive/components/chat-session-host.js.map +1 -1
- package/dist/modes/interactive/components/chat-transcript.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-transcript.js +15 -4
- package/dist/modes/interactive/components/chat-transcript.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +3 -0
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +26 -0
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/docs/compaction.md +2 -0
- package/docs/models.md +1 -1
- package/docs/providers.md +2 -1
- package/docs/session-format.md +6 -0
- package/docs/sessions.md +6 -0
- package/docs/workflows.md +105 -3
- package/package.json +4 -3
|
@@ -141,6 +141,9 @@ export function createWorkflowExtensionRuntimeState(
|
|
|
141
141
|
dispatch(args, options) { return runtimeRef.current.dispatch(args, options); },
|
|
142
142
|
runDirect(args, options) { return runtimeRef.current.runDirect(args, options); },
|
|
143
143
|
resumeFailedRun(sourceRunId, stageId, options) { return runtimeRef.current.resumeFailedRun(sourceRunId, stageId, options); },
|
|
144
|
+
resumeDurableWorkflow(workflowIdOrPrefix, options) { return runtimeRef.current.resumeDurableWorkflow(workflowIdOrPrefix, options); },
|
|
145
|
+
listDurableResumable(sessionDir) { return runtimeRef.current.listDurableResumable(sessionDir); },
|
|
146
|
+
prepareDurableResumable(workflowIdOrPrefix, sessionDir) { return runtimeRef.current.prepareDurableResumable(workflowIdOrPrefix, sessionDir); },
|
|
144
147
|
};
|
|
145
148
|
|
|
146
149
|
function workflowModelCatalogFromContext(ctx?: PiModelContext): WorkflowModelCatalogPort | undefined {
|
|
@@ -43,6 +43,10 @@ import { runDetached } from "../runs/background/runner.js";
|
|
|
43
43
|
import type { JobTracker } from "../runs/background/job-tracker.js";
|
|
44
44
|
import { appendRunEnd } from "../shared/persistence-session-entries.js";
|
|
45
45
|
import { classifyWorkflowFailure } from "../shared/workflow-failures.js";
|
|
46
|
+
import { resumeDurableWorkflow as resumeDurableWorkflowAdapter, prepareRuntimeDurableResumable, isBackendTerminal, type ResumeDurableDeps, type ResumeDurableResult } from "../durable/resume-runtime.js";
|
|
47
|
+
import { getDurableBackend, initializeDbosDurableBackendFromEnv } from "../durable/factory.js";
|
|
48
|
+
import { scanResumableWorkflows } from "../durable/resume-catalog.js";
|
|
49
|
+
import type { ResumableWorkflowEntry } from "../durable/types.js";
|
|
46
50
|
import { directMode, directModelRequests, directOptions, directProgressTotal } from "./runtime-direct.js";
|
|
47
51
|
|
|
48
52
|
// ---------------------------------------------------------------------------
|
|
@@ -87,11 +91,9 @@ export interface ExtensionRuntimeOpts {
|
|
|
87
91
|
/** Resolve the host's non-default session directory for workflow stage transcripts. */
|
|
88
92
|
resolveDefaultStageSessionDir?: () => string | undefined;
|
|
89
93
|
}
|
|
90
|
-
|
|
91
94
|
// ---------------------------------------------------------------------------
|
|
92
95
|
// Public interface
|
|
93
96
|
// ---------------------------------------------------------------------------
|
|
94
|
-
|
|
95
97
|
export type ResumeFailedRunResult =
|
|
96
98
|
| { ok: true; runId: string; sourceRunId: string; resumeFromStageId: string; message: string }
|
|
97
99
|
| { ok: false; reason: "run_not_found" | "not_resumable" | "workflow_not_found" | "insufficient_state"; message: string };
|
|
@@ -114,12 +116,21 @@ export interface ExtensionRuntime {
|
|
|
114
116
|
|
|
115
117
|
/** Start a linked continuation for a failed resumable named workflow run. */
|
|
116
118
|
resumeFailedRun(sourceRunId: string, stageId?: string, options?: RuntimeDispatchOptions): ResumeFailedRunResult;
|
|
117
|
-
}
|
|
118
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Resume a durable workflow by top-level workflow id when no live run exists.
|
|
122
|
+
* Re-dispatches the workflow with the cached inputs and original workflow id
|
|
123
|
+
* so durable checkpoints replay (skipping completed side effects).
|
|
124
|
+
*
|
|
125
|
+
* cross-ref: issue #1498 — cross-session /workflow resume selector.
|
|
126
|
+
*/
|
|
127
|
+
resumeDurableWorkflow(workflowIdOrPrefix: string, options?: RuntimeDispatchOptions): import("../durable/resume-runtime.js").ResumeDurableResult;
|
|
128
|
+
listDurableResumable(sessionDir?: string): readonly import("../durable/types.js").ResumableWorkflowEntry[];
|
|
129
|
+
prepareDurableResumable(workflowIdOrPrefix?: string, sessionDir?: string): Promise<readonly import("../durable/types.js").ResumableWorkflowEntry[]>;
|
|
130
|
+
}
|
|
119
131
|
export interface RuntimeDispatchOptions {
|
|
120
132
|
readonly policy?: WorkflowExecutionPolicy;
|
|
121
133
|
}
|
|
122
|
-
|
|
123
134
|
// ---------------------------------------------------------------------------
|
|
124
135
|
// Factory
|
|
125
136
|
// ---------------------------------------------------------------------------
|
|
@@ -150,6 +161,9 @@ export function createExtensionRuntime(opts: ExtensionRuntimeOpts = {}): Extensi
|
|
|
150
161
|
const jobs = opts.jobs;
|
|
151
162
|
const runtimeCwd = opts.cwd ?? process.cwd();
|
|
152
163
|
const resolveDefaultStageSessionDir = opts.resolveDefaultStageSessionDir;
|
|
164
|
+
const dbosReady = initializeDbosDurableBackendFromEnv().catch((err) => process.emitWarning(`Atomic workflow DBOS durability unavailable; using file-backed durability: ${err instanceof Error ? err.message : String(err)}`));
|
|
165
|
+
const ensureDbosReady = async (): Promise<void> => { await dbosReady; };
|
|
166
|
+
let preparedDurableCatalog: readonly ResumableWorkflowEntry[] = [];
|
|
153
167
|
|
|
154
168
|
function runOptions(args: WorkflowToolArgs, policy?: WorkflowExecutionPolicy): RunOpts {
|
|
155
169
|
const argConcurrency =
|
|
@@ -182,14 +196,12 @@ export function createExtensionRuntime(opts: ExtensionRuntimeOpts = {}): Extensi
|
|
|
182
196
|
cwd: runtimeCwd,
|
|
183
197
|
};
|
|
184
198
|
}
|
|
185
|
-
|
|
186
199
|
function explicitIntercomDelivery(args: WorkflowToolArgs): WorkflowIntercomDelivery | undefined {
|
|
187
200
|
if (args.intercom?.enabled === false) return "off";
|
|
188
201
|
if (args.intercom?.delivery !== undefined) return args.intercom.delivery;
|
|
189
202
|
if (args.intercom?.enabled === true) return "control-and-result";
|
|
190
203
|
return undefined;
|
|
191
204
|
}
|
|
192
|
-
|
|
193
205
|
function effectiveIntercomDelivery(args: WorkflowToolArgs, mode: WorkflowDetails["mode"]): WorkflowIntercomDelivery {
|
|
194
206
|
const explicit = explicitIntercomDelivery(args);
|
|
195
207
|
if (explicit !== undefined) return explicit;
|
|
@@ -202,7 +214,6 @@ export function createExtensionRuntime(opts: ExtensionRuntimeOpts = {}): Extensi
|
|
|
202
214
|
}
|
|
203
215
|
return "off";
|
|
204
216
|
}
|
|
205
|
-
|
|
206
217
|
function intercomParentSession(args: WorkflowToolArgs): string | undefined {
|
|
207
218
|
if (args.intercom?.parentSession !== undefined) return args.intercom.parentSession;
|
|
208
219
|
if (typeof intercom?.parentSession === "function") return intercom.parentSession();
|
|
@@ -371,6 +382,7 @@ export function createExtensionRuntime(opts: ExtensionRuntimeOpts = {}): Extensi
|
|
|
371
382
|
}
|
|
372
383
|
|
|
373
384
|
async function runDirectAsync(args: WorkflowToolArgs, policy?: WorkflowExecutionPolicy): Promise<WorkflowDetails> {
|
|
385
|
+
await ensureDbosReady();
|
|
374
386
|
const runId = crypto.randomUUID();
|
|
375
387
|
const mode = directMode(args);
|
|
376
388
|
const delivery = effectiveIntercomDelivery(args, mode);
|
|
@@ -423,7 +435,8 @@ export function createExtensionRuntime(opts: ExtensionRuntimeOpts = {}): Extensi
|
|
|
423
435
|
return registry;
|
|
424
436
|
},
|
|
425
437
|
|
|
426
|
-
dispatch(args: WorkflowToolArgs, options?: RuntimeDispatchOptions): Promise<WorkflowToolResult> {
|
|
438
|
+
async dispatch(args: WorkflowToolArgs, options?: RuntimeDispatchOptions): Promise<WorkflowToolResult> {
|
|
439
|
+
await ensureDbosReady();
|
|
427
440
|
const defaultSessionDir = resolveDefaultStageSessionDir?.();
|
|
428
441
|
return dispatch(args, {
|
|
429
442
|
registry,
|
|
@@ -441,7 +454,8 @@ export function createExtensionRuntime(opts: ExtensionRuntimeOpts = {}): Extensi
|
|
|
441
454
|
});
|
|
442
455
|
},
|
|
443
456
|
|
|
444
|
-
runDirect(args: WorkflowToolArgs, options?: RuntimeDispatchOptions): Promise<WorkflowDetails> {
|
|
457
|
+
async runDirect(args: WorkflowToolArgs, options?: RuntimeDispatchOptions): Promise<WorkflowDetails> {
|
|
458
|
+
await ensureDbosReady();
|
|
445
459
|
const policy = options?.policy ?? INTERACTIVE_WORKFLOW_POLICY;
|
|
446
460
|
if (args.async === true && policy.awaitTerminalRun !== true) {
|
|
447
461
|
return runDirectAsync(args, policy);
|
|
@@ -457,5 +471,30 @@ export function createExtensionRuntime(opts: ExtensionRuntimeOpts = {}): Extensi
|
|
|
457
471
|
},
|
|
458
472
|
|
|
459
473
|
resumeFailedRun,
|
|
474
|
+
|
|
475
|
+
resumeDurableWorkflow(workflowIdOrPrefix: string, options?: RuntimeDispatchOptions): ResumeDurableResult {
|
|
476
|
+
const adapterDeps: ResumeDurableDeps = {
|
|
477
|
+
registry,
|
|
478
|
+
baseRunOpts: runOptions({ workflow: "", inputs: {} }, options?.policy),
|
|
479
|
+
durableBackend: getDurableBackend(),
|
|
480
|
+
};
|
|
481
|
+
return resumeDurableWorkflowAdapter(workflowIdOrPrefix, adapterDeps, preparedDurableCatalog);
|
|
482
|
+
},
|
|
483
|
+
listDurableResumable(sessionDir?: string): readonly ResumableWorkflowEntry[] {
|
|
484
|
+
const backend = getDurableBackend();
|
|
485
|
+
const live = backend.listResumableWorkflows();
|
|
486
|
+
const dir = sessionDir ?? resolveDefaultStageSessionDir?.();
|
|
487
|
+
if (dir === undefined) return live;
|
|
488
|
+
const scanned = scanResumableWorkflows(dir);
|
|
489
|
+
const liveIds = new Set(live.map((e) => e.workflowId));
|
|
490
|
+
const compatible = scanned.filter((e) => !liveIds.has(e.workflowId) && backend.getWorkflow(e.workflowId) !== undefined && !isBackendTerminal(backend, e.workflowId));
|
|
491
|
+
return [...live, ...compatible];
|
|
492
|
+
},
|
|
493
|
+
|
|
494
|
+
async prepareDurableResumable(workflowIdOrPrefix?: string, sessionDir?: string): Promise<readonly ResumableWorkflowEntry[]> {
|
|
495
|
+
await ensureDbosReady();
|
|
496
|
+
preparedDurableCatalog = await prepareRuntimeDurableResumable(getDurableBackend, () => resolveDefaultStageSessionDir?.(), workflowIdOrPrefix, sessionDir);
|
|
497
|
+
return preparedDurableCatalog;
|
|
498
|
+
},
|
|
460
499
|
};
|
|
461
500
|
}
|
|
@@ -5,6 +5,7 @@ import { store } from "../shared/store.js";
|
|
|
5
5
|
import { topLevelWorkflowRuns } from "../shared/run-visibility.js";
|
|
6
6
|
import { renderSessionList } from "../tui/session-list.js";
|
|
7
7
|
import { openKillConfirm, openSessionPicker } from "../tui/session-overlays.js";
|
|
8
|
+
import { openWorkflowResumeSelector } from "../tui/workflow-resume-selector.js";
|
|
8
9
|
import { deriveGraphTheme } from "../tui/graph-theme.js";
|
|
9
10
|
import { emitChatSurface } from "../tui/chat-surface-message.js";
|
|
10
11
|
import type { GraphOverlayPort } from "../tui/overlay-adapter.js";
|
|
@@ -13,6 +14,8 @@ import type { ExtensionAPI, PiCommandContext } from "./public-types.js";
|
|
|
13
14
|
import type { WorkflowCommandReporter } from "./workflow-command-utils.js";
|
|
14
15
|
import { stripYesFlag } from "./workflow-command-utils.js";
|
|
15
16
|
import { workflowPolicyFromContext } from "./workflow-policy.js";
|
|
17
|
+
import { formatResumableWorkflowList } from "../durable/resume-catalog.js";
|
|
18
|
+
import type { ResumableWorkflowEntry } from "../durable/types.js";
|
|
16
19
|
import {
|
|
17
20
|
formatAlreadyEndedRetainedMessage,
|
|
18
21
|
overlaySurfaceFromContext,
|
|
@@ -37,6 +40,75 @@ function resolveAttachStageId(runId: string, stageTarget: string | undefined): s
|
|
|
37
40
|
return byName?.id ?? false;
|
|
38
41
|
}
|
|
39
42
|
|
|
43
|
+
function filterSelectorDurableEntries(
|
|
44
|
+
runtime: ExtensionRuntime,
|
|
45
|
+
entries: readonly ResumableWorkflowEntry[],
|
|
46
|
+
): readonly ResumableWorkflowEntry[] {
|
|
47
|
+
const registry = runtime.registry as { has(name: string): boolean } | undefined;
|
|
48
|
+
if (registry === undefined) return entries;
|
|
49
|
+
return entries.filter((entry) => {
|
|
50
|
+
const requiresCurrentDefinition = entry.status === "running" || entry.status === "failed" || entry.status === "blocked";
|
|
51
|
+
return !requiresCurrentDefinition || registry.has(entry.name);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async function handleDurableResume(
|
|
57
|
+
target: string | undefined,
|
|
58
|
+
ctx: PiCommandContext,
|
|
59
|
+
reporter: WorkflowCommandReporter,
|
|
60
|
+
deps: WorkflowRunControlDeps,
|
|
61
|
+
): Promise<boolean> {
|
|
62
|
+
const print = (msg: string): void => reporter.info(msg);
|
|
63
|
+
const fail = (msg: string): void => reporter.error(msg);
|
|
64
|
+
const runtime = deps.runtimeForContext(ctx);
|
|
65
|
+
const policy = workflowPolicyFromContext(ctx);
|
|
66
|
+
// Hydrate the durable backend from DBOS (if configured) before listing so a
|
|
67
|
+
// fresh process discovers workflows persisted by a prior session.
|
|
68
|
+
const prepared = await runtime.prepareDurableResumable(target);
|
|
69
|
+
const durable = filterSelectorDurableEntries(runtime, prepared);
|
|
70
|
+
if (target !== undefined) {
|
|
71
|
+
// Attempt resume by id/prefix against the durable catalog.
|
|
72
|
+
const result = runtime.resumeDurableWorkflow(target, { policy });
|
|
73
|
+
if (result.ok) {
|
|
74
|
+
print(result.message);
|
|
75
|
+
// Open/connect the overlay to the resumed run, analogous to live resume.
|
|
76
|
+
if (policy.allowInputPicker) deps.overlay.open(result.runId, overlaySurfaceFromContext(ctx));
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
// Not a durable workflow either — surface the catalog for discovery.
|
|
80
|
+
if (durable.length > 0) {
|
|
81
|
+
fail(`${result.message}\n\n${formatResumableWorkflowList(durable)}`);
|
|
82
|
+
} else {
|
|
83
|
+
fail(result.message);
|
|
84
|
+
}
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
// No target: show the durable selector when interactive, otherwise print.
|
|
88
|
+
if (durable.length === 0) {
|
|
89
|
+
fail("No resumable durable workflows found. Usage: /workflow resume <id> (or /resume for Atomic sessions).");
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
if (!policy.allowInputPicker) {
|
|
93
|
+
print(`${formatResumableWorkflowList(durable)}\n\nResume with: /workflow resume <id>`);
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
const picked = await openWorkflowResumeSelector(ctx.ui, [], durable);
|
|
97
|
+
if (picked.kind === "durable") {
|
|
98
|
+
const result = runtime.resumeDurableWorkflow(picked.workflowId, { policy });
|
|
99
|
+
if (result.ok) {
|
|
100
|
+
print(result.message);
|
|
101
|
+
if (policy.allowInputPicker) deps.overlay.open(result.runId, overlaySurfaceFromContext(ctx));
|
|
102
|
+
} else {
|
|
103
|
+
fail(result.message);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (picked.kind !== "durable") {
|
|
107
|
+
print(`${formatResumableWorkflowList(durable)}\n\nResume with: /workflow resume <id>`);
|
|
108
|
+
}
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
40
112
|
export async function handleRunControlCommand(
|
|
41
113
|
action: "connect" | "interrupt" | "kill" | "attach" | "pause" | "resume",
|
|
42
114
|
rest: string[],
|
|
@@ -115,7 +187,7 @@ export async function handleRunControlCommand(
|
|
|
115
187
|
}
|
|
116
188
|
if (failHeadlessAttachCommand("connect", resolved.runId)) return true;
|
|
117
189
|
if (policy.allowInputPicker) deps.overlay.open(resolved.runId, overlaySurfaceFromContext(ctx));
|
|
118
|
-
print(`Attached to ${resolved.runId.slice(0, 8)}. h/ctrl+d hide · q
|
|
190
|
+
print(`Attached to ${resolved.runId.slice(0, 8)}. h/ctrl+d hide · q quit (resumable via /workflow resume) · esc close.`);
|
|
119
191
|
return true;
|
|
120
192
|
}
|
|
121
193
|
|
|
@@ -206,10 +278,64 @@ export async function handleRunControlCommand(
|
|
|
206
278
|
if (action === "pause") {
|
|
207
279
|
const active = topLevelWorkflowRuns(store.runs()).filter((r) => r.endedAt === undefined);
|
|
208
280
|
fail(active.length === 0 ? "No active runs to pause." : `Picker requires an interactive UI surface. Active runs:\n${active.map((r) => ` ${r.id.slice(0, 8)} ${r.name}`).join("\n")}\n\nUsage: /workflow pause <runId> [stageId]`);
|
|
281
|
+
} else if (action === "attach") {
|
|
282
|
+
fail(`${renderSessionList(store.runs(), { theme, includeAll: true })}\n\nPicker requires an interactive UI surface. Pass a runId: /workflow attach <id> [stageId]`);
|
|
209
283
|
} else {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
284
|
+
// resume: show cross-session durable catalog in headless/print mode.
|
|
285
|
+
return await handleDurableResume(undefined, ctx, reporter, deps);
|
|
286
|
+
}
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
if (action === "resume") {
|
|
290
|
+
// Only inactive workflows belong in the resume selector. Live runs:
|
|
291
|
+
// show paused (quit) or recoverably-failed runs; actively-running live
|
|
292
|
+
// runs are hidden (resuming one that is executing would double-dispatch).
|
|
293
|
+
const liveRuns = topLevelWorkflowRuns(store.runs()).filter((run) =>
|
|
294
|
+
run.status === "paused" || (run.status === "failed" && run.resumable !== false),
|
|
295
|
+
);
|
|
296
|
+
// Durable entries: a `running` durable handle may be a crashed process
|
|
297
|
+
// (cross-session crash recovery), so it stays selectable UNLESS it
|
|
298
|
+
// matches an actively-executing live run in this session.
|
|
299
|
+
const activeLiveIds = new Set(
|
|
300
|
+
topLevelWorkflowRuns(store.runs())
|
|
301
|
+
.filter((run) => run.endedAt === undefined && run.status === "running" && run.exitReason !== "quit")
|
|
302
|
+
.map((run) => run.id),
|
|
303
|
+
);
|
|
304
|
+
const runtime = deps.runtimeForContext(ctx);
|
|
305
|
+
let durableEntries: readonly ResumableWorkflowEntry[] = [];
|
|
306
|
+
try {
|
|
307
|
+
const prepared = await runtime.prepareDurableResumable(undefined);
|
|
308
|
+
durableEntries = filterSelectorDurableEntries(runtime, prepared)
|
|
309
|
+
.filter((entry) => !activeLiveIds.has(entry.workflowId));
|
|
310
|
+
} catch (error) {
|
|
311
|
+
if (liveRuns.length === 0) {
|
|
312
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
313
|
+
fail(`Failed to list resumable workflows: ${message}`);
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
const picked = await openWorkflowResumeSelector(ctx.ui, liveRuns, durableEntries);
|
|
318
|
+
if (picked.kind === "durable") return await handleDurableResume(picked.workflowId, ctx, reporter, deps);
|
|
319
|
+
if (picked.kind === "live") {
|
|
320
|
+
const resolved = resolveRunIdPrefix(picked.runId);
|
|
321
|
+
if (resolved.kind !== "exact") {
|
|
322
|
+
fail(`Run not found: ${picked.runId}`);
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
const run = store.runs().find((r) => r.id === resolved.runId);
|
|
326
|
+
const isPaused = run?.status === "paused" || (run?.stages.some((s) => s.status === "paused") ?? false);
|
|
327
|
+
const isResumableContinuation = run !== undefined && !isPaused && ((run.status === "failed" && run.endedAt !== undefined && run.resumable !== false) || (run.endedAt === undefined && run.resumable === true && run.failureRecoverability === "recoverable"));
|
|
328
|
+
if (isResumableContinuation) {
|
|
329
|
+
const continuation = deps.runtimeForContext(ctx).resumeFailedRun(resolved.runId, undefined, { policy });
|
|
330
|
+
continuation.ok ? print(continuation.message) : fail(continuation.message);
|
|
331
|
+
} else {
|
|
332
|
+
const result = resumeRun(resolved.runId, {});
|
|
333
|
+
if (result.ok && result.mode === "snapshot" && run?.exitReason === "quit") {
|
|
334
|
+
return await handleDurableResume(resolved.runId, ctx, reporter, deps);
|
|
335
|
+
}
|
|
336
|
+
if (result.ok && policy.allowInputPicker) deps.overlay.open(result.runId, overlaySurfaceFromContext(ctx));
|
|
337
|
+
result.ok ? print(result.message ?? `Resumed ${result.runId.slice(0, 8)}`) : fail(`Run not found: ${picked.runId}`);
|
|
338
|
+
}
|
|
213
339
|
}
|
|
214
340
|
return true;
|
|
215
341
|
}
|
|
@@ -220,6 +346,11 @@ export async function handleRunControlCommand(
|
|
|
220
346
|
} else {
|
|
221
347
|
const resolved = resolveRunIdPrefix(target);
|
|
222
348
|
if (resolved.kind === "not_found") {
|
|
349
|
+
// Not a live run — fall back to the cross-session durable resume catalog.
|
|
350
|
+
// cross-ref: issue #1498 — /workflow resume by top-level workflow id.
|
|
351
|
+
if (action === "resume") {
|
|
352
|
+
return await handleDurableResume(target, ctx, reporter, deps);
|
|
353
|
+
}
|
|
223
354
|
fail(`Run not found: ${target}`);
|
|
224
355
|
return true;
|
|
225
356
|
}
|
|
@@ -260,6 +391,11 @@ export async function handleRunControlCommand(
|
|
|
260
391
|
const run = store.runs().find((r) => r.id === stageRunId);
|
|
261
392
|
const isPaused = run?.status === "paused" || (run?.stages.some((s) => s.status === "paused") ?? false);
|
|
262
393
|
const isResumableContinuation = run !== undefined && !isPaused && ((run.status === "failed" && run.endedAt !== undefined && run.resumable !== false) || (run.endedAt === undefined && run.resumable === true && run.failureRecoverability === "recoverable"));
|
|
394
|
+
const isActivelyRunning = run !== undefined && run.endedAt === undefined && run.status === "running" && !isPaused && run.exitReason !== "quit";
|
|
395
|
+
if (isActivelyRunning && action === "resume") {
|
|
396
|
+
fail(`Workflow ${stageRunId.slice(0, 8)} is already running in this session. Attach with \`/workflow connect ${stageRunId.slice(0, 8)}\` instead of resuming.`);
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
263
399
|
if (isResumableContinuation) {
|
|
264
400
|
const continuation = deps.runtimeForContext(ctx).resumeFailedRun(stageRunId, stageId, { policy });
|
|
265
401
|
continuation.ok ? print(continuation.message) : fail(continuation.message);
|
|
@@ -270,6 +406,9 @@ export async function handleRunControlCommand(
|
|
|
270
406
|
fail(`Run not found: ${stageRunId.slice(0, 8)}`);
|
|
271
407
|
return true;
|
|
272
408
|
}
|
|
409
|
+
if (result.mode === "snapshot" && run?.exitReason === "quit" && action === "resume") {
|
|
410
|
+
return await handleDurableResume(stageRunId, ctx, reporter, deps);
|
|
411
|
+
}
|
|
273
412
|
if (!isPaused) {
|
|
274
413
|
if (policy.allowInputPicker) deps.overlay.open(result.runId, overlaySurfaceFromContext(ctx));
|
|
275
414
|
print(result.message ?? `Snapshot available: run ${result.runId} (${result.snapshot.name}) — status: ${result.snapshot.status}, stages: ${result.snapshot.stages.length}`);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { pauseRun, type PauseResult } from "./status.js";
|
|
2
|
+
import { store as defaultStore } from "../../shared/store.js";
|
|
3
|
+
import type { Store } from "../../shared/store-public-types.js";
|
|
4
|
+
import { topLevelWorkflowRuns } from "../../shared/run-visibility.js";
|
|
5
|
+
import type { StageControlRegistry } from "../foreground/stage-control-registry.js";
|
|
6
|
+
import { getDurableBackend } from "../../durable/factory.js";
|
|
7
|
+
|
|
8
|
+
export type QuitRunResult = PauseResult;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Quit/detach a workflow UI without authoritatively killing the workflow.
|
|
12
|
+
*
|
|
13
|
+
* This is the graph-panel/orchestrator close affordance: it pauses any live
|
|
14
|
+
* stage handles when possible, annotates the run as resumable via
|
|
15
|
+
* `/workflow resume`, and deliberately does NOT abort through the cancellation
|
|
16
|
+
* registry or append a terminal `workflow.run.end` entry. `/workflow kill`
|
|
17
|
+
* remains the only explicit non-resumable manual kill path.
|
|
18
|
+
*/
|
|
19
|
+
export function quitRun(
|
|
20
|
+
runId: string,
|
|
21
|
+
opts?: {
|
|
22
|
+
store?: Store;
|
|
23
|
+
stageControlRegistry?: StageControlRegistry;
|
|
24
|
+
},
|
|
25
|
+
): QuitRunResult {
|
|
26
|
+
const activeStore = opts?.store ?? defaultStore;
|
|
27
|
+
const run = activeStore.runs().find((candidate) => candidate.id === runId);
|
|
28
|
+
if (!run) return { ok: false, runId, reason: "not_found" };
|
|
29
|
+
if (run.endedAt !== undefined) return { ok: false, runId, reason: "already_ended" };
|
|
30
|
+
|
|
31
|
+
const paused = pauseRun(runId, opts);
|
|
32
|
+
if (!paused.ok && paused.reason !== "no_active_stages") return paused;
|
|
33
|
+
activeStore.recordRunPaused(runId, undefined, { exitReason: "quit", resumable: true });
|
|
34
|
+
// Mark the durable handle inactive so `/workflow resume` in this or another
|
|
35
|
+
// session can discover it again. While the run stays `running` in the store
|
|
36
|
+
// and (after a fresh dispatch) in the durable backend, it is hidden from the
|
|
37
|
+
// resume selector and refused on resume; quitting flips durable to `paused`.
|
|
38
|
+
markDurableQuit(runId);
|
|
39
|
+
return paused.ok ? paused : { ok: true, runId, paused: [] };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function markDurableQuit(runId: string): void {
|
|
43
|
+
let backend;
|
|
44
|
+
try {
|
|
45
|
+
backend = getDurableBackend();
|
|
46
|
+
} catch {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (backend.getWorkflow(runId) !== undefined) {
|
|
50
|
+
backend.setWorkflowStatus(runId, "paused", undefined, true);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function quitAllRuns(opts?: {
|
|
55
|
+
store?: Store;
|
|
56
|
+
stageControlRegistry?: StageControlRegistry;
|
|
57
|
+
}): QuitRunResult[] {
|
|
58
|
+
const activeStore = opts?.store ?? defaultStore;
|
|
59
|
+
const inFlight = topLevelWorkflowRuns(activeStore.runs()).filter((r) => r.endedAt === undefined);
|
|
60
|
+
return inFlight.map((r) => quitRun(r.id, { store: activeStore, stageControlRegistry: opts?.stageControlRegistry }));
|
|
61
|
+
}
|
|
@@ -67,7 +67,7 @@ export async function mapParallelSteps<T>(
|
|
|
67
67
|
concurrency: number | undefined,
|
|
68
68
|
failFast: boolean | undefined,
|
|
69
69
|
mapper: (step: WorkflowTaskStep) => Promise<T>,
|
|
70
|
-
onFirstFailure?: (error: unknown) => void
|
|
70
|
+
onFirstFailure?: (error: unknown) => void | Promise<void>,
|
|
71
71
|
control?: {
|
|
72
72
|
readonly beforeDequeue?: () => void;
|
|
73
73
|
readonly beforeMap?: () => void;
|
|
@@ -92,11 +92,11 @@ export async function mapParallelSteps<T>(
|
|
|
92
92
|
controlSignal = error;
|
|
93
93
|
if (failFastEnabled) rejectFirstFailure(error);
|
|
94
94
|
};
|
|
95
|
-
const recordFailure = (index: number, error: unknown): void => {
|
|
95
|
+
const recordFailure = async (index: number, error: unknown): Promise<void> => {
|
|
96
96
|
failures.push({ index, error });
|
|
97
97
|
if (firstFailure === undefined) {
|
|
98
98
|
firstFailure = error;
|
|
99
|
-
onFirstFailure?.(error);
|
|
99
|
+
await onFirstFailure?.(error);
|
|
100
100
|
if (failFastEnabled) rejectFirstFailure(error);
|
|
101
101
|
}
|
|
102
102
|
};
|
|
@@ -112,7 +112,7 @@ export async function mapParallelSteps<T>(
|
|
|
112
112
|
selectControlSignal(err);
|
|
113
113
|
return;
|
|
114
114
|
}
|
|
115
|
-
recordFailure(nextIndex, err);
|
|
115
|
+
await recordFailure(nextIndex, err);
|
|
116
116
|
return;
|
|
117
117
|
}
|
|
118
118
|
if (controlSignal !== undefined) return;
|
|
@@ -128,7 +128,7 @@ export async function mapParallelSteps<T>(
|
|
|
128
128
|
selectControlSignal(err);
|
|
129
129
|
return;
|
|
130
130
|
}
|
|
131
|
-
recordFailure(index, err);
|
|
131
|
+
await recordFailure(index, err);
|
|
132
132
|
if (failFastEnabled) return;
|
|
133
133
|
}
|
|
134
134
|
}
|
|
@@ -8,6 +8,21 @@ import { hasExplicitFastModeCandidate } from "./executor-direct-helpers.js";
|
|
|
8
8
|
import { applyFailureToStage } from "./executor-lifecycle.js";
|
|
9
9
|
import { isTerminalStage } from "./executor-scheduler.js";
|
|
10
10
|
|
|
11
|
+
export interface TrackedStageCallOptions {
|
|
12
|
+
readonly eagerSession?: boolean;
|
|
13
|
+
readonly allowFinalized?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type TrackedStageCaller = <T>(
|
|
17
|
+
call: () => Promise<T>,
|
|
18
|
+
eagerSessionOrOptions?: boolean | TrackedStageCallOptions,
|
|
19
|
+
) => Promise<T>;
|
|
20
|
+
|
|
21
|
+
function normalizeTrackedStageCallOptions(input: boolean | TrackedStageCallOptions | undefined): Required<TrackedStageCallOptions> {
|
|
22
|
+
if (typeof input === "boolean") return { eagerSession: input, allowFinalized: false };
|
|
23
|
+
return { eagerSession: input?.eagerSession === true, allowFinalized: input?.allowFinalized === true };
|
|
24
|
+
}
|
|
25
|
+
|
|
11
26
|
export function createTrackedStageCaller(input: {
|
|
12
27
|
readonly runtime: LiveStageRuntime;
|
|
13
28
|
readonly limiter: ConcurrencyLimiter;
|
|
@@ -15,7 +30,7 @@ export function createTrackedStageCaller(input: {
|
|
|
15
30
|
readonly adapters: StageAdapters;
|
|
16
31
|
readonly hasContinuation: boolean;
|
|
17
32
|
readonly hasScopedParents: boolean;
|
|
18
|
-
}):
|
|
33
|
+
}): TrackedStageCaller {
|
|
19
34
|
const { runtime } = input;
|
|
20
35
|
const readinessGateEnabled = runtime.opts.confirmStageReadiness !== undefined || runtime.opts.usePromptNodesForUi === true;
|
|
21
36
|
const confirmReadiness = async (): Promise<"advance" | "stay"> => {
|
|
@@ -68,22 +83,24 @@ export function createTrackedStageCaller(input: {
|
|
|
68
83
|
return result;
|
|
69
84
|
};
|
|
70
85
|
|
|
71
|
-
return async <T>(call: () => Promise<T>,
|
|
86
|
+
return async <T>(call: () => Promise<T>, eagerSessionOrOptions?: boolean | TrackedStageCallOptions): Promise<T> => {
|
|
87
|
+
const callOptions = normalizeTrackedStageCallOptions(eagerSessionOrOptions);
|
|
72
88
|
runtime.exit.throwIfWorkflowExitSelected();
|
|
73
89
|
await runtime.scheduler.waitForStageRelease(runtime.stageId, runtime.releaseLiveHandle);
|
|
74
|
-
if (runtime.state.stageFinalized) throw runtime.parallelFailFastError();
|
|
90
|
+
if (runtime.state.stageFinalized && !callOptions.allowFinalized) throw runtime.parallelFailFastError();
|
|
75
91
|
|
|
76
92
|
await input.limiter.acquire();
|
|
77
93
|
try {
|
|
78
94
|
await runtime.scheduler.waitForStageRelease(runtime.stageId, runtime.releaseLiveHandle);
|
|
79
95
|
runtime.exit.throwIfWorkflowExitSelected();
|
|
80
|
-
if (runtime.state.stageFinalized) throw runtime.parallelFailFastError();
|
|
96
|
+
if (runtime.state.stageFinalized && !callOptions.allowFinalized) throw runtime.parallelFailFastError();
|
|
81
97
|
} catch (err) {
|
|
82
98
|
input.limiter.release();
|
|
83
99
|
throw err;
|
|
84
100
|
}
|
|
85
101
|
|
|
86
|
-
|
|
102
|
+
const trackStageLifecycle = !runtime.state.stageFinalized;
|
|
103
|
+
if (trackStageLifecycle && !input.hasContinuation && runtime.stageSnapshot.startedAt === undefined && !input.hasScopedParents) {
|
|
87
104
|
const actualParentIds = runtime.scheduler.tracker.currentParents();
|
|
88
105
|
const sameParents = actualParentIds.length === runtime.stageSnapshot.parentIds.length &&
|
|
89
106
|
actualParentIds.every((value) => runtime.stageSnapshot.parentIds.includes(value));
|
|
@@ -92,25 +109,29 @@ export function createTrackedStageCaller(input: {
|
|
|
92
109
|
runtime.scheduler.setStageParentIds(runtime.stageSnapshot, actualParentIds);
|
|
93
110
|
}
|
|
94
111
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
112
|
+
if (trackStageLifecycle) {
|
|
113
|
+
runtime.stageSnapshot.status = "running";
|
|
114
|
+
runtime.stageSnapshot.startedAt = Date.now();
|
|
115
|
+
const hasNoExplicitModelConfig = input.options?.model === undefined && input.options?.fallbackModels === undefined;
|
|
116
|
+
const promptAdapterHandlesInitialPrompt = input.adapters.prompt !== undefined;
|
|
117
|
+
if (callOptions.eagerSession && !promptAdapterHandlesInitialPrompt && (hasNoExplicitModelConfig || await hasExplicitFastModeCandidate({
|
|
118
|
+
model: input.options?.model,
|
|
119
|
+
fallbackModels: input.options?.fallbackModels,
|
|
120
|
+
models: runtime.opts.models,
|
|
121
|
+
}))) {
|
|
122
|
+
try {
|
|
123
|
+
await runtime.innerCtx.__ensureSession();
|
|
124
|
+
runtime.captureStageSessionMeta();
|
|
125
|
+
} catch (err) {
|
|
126
|
+
if (!(err instanceof Error && err.message.includes("prompt adapter not configured"))) throw err;
|
|
127
|
+
}
|
|
109
128
|
}
|
|
129
|
+
runtime.applyModelFallbackMeta(runtime.innerCtx.__modelFallbackMeta());
|
|
130
|
+
runtime.activeStore.recordStageStart(runtime.runId, runtime.stageSnapshot);
|
|
131
|
+
runtime.appendStageStartOnce();
|
|
132
|
+
} else {
|
|
133
|
+
runtime.applyModelFallbackMeta(runtime.innerCtx.__modelFallbackMeta());
|
|
110
134
|
}
|
|
111
|
-
runtime.applyModelFallbackMeta(runtime.innerCtx.__modelFallbackMeta());
|
|
112
|
-
runtime.activeStore.recordStageStart(runtime.runId, runtime.stageSnapshot);
|
|
113
|
-
runtime.appendStageStartOnce();
|
|
114
135
|
|
|
115
136
|
runtime.mcpScope.apply();
|
|
116
137
|
|
|
@@ -158,37 +179,57 @@ export function createTrackedStageCaller(input: {
|
|
|
158
179
|
}
|
|
159
180
|
runtime.captureStageSessionMeta();
|
|
160
181
|
runtime.applyModelFallbackMeta(runtime.innerCtx.__modelFallbackMeta());
|
|
161
|
-
if (runtime.stageFailFastScope?.failed === true && runtime.stageFailFastScope.activeStages.has(runtime.stageId)) {
|
|
182
|
+
if (trackStageLifecycle && runtime.stageFailFastScope?.failed === true && runtime.stageFailFastScope.activeStages.has(runtime.stageId)) {
|
|
162
183
|
runtime.markSkippedForParallelFailFast();
|
|
163
184
|
throw runtime.parallelFailFastError();
|
|
164
185
|
}
|
|
165
|
-
if (runtime.state.stageFinalized) throw runtime.parallelFailFastError();
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
186
|
+
if (trackStageLifecycle && runtime.state.stageFinalized) throw runtime.parallelFailFastError();
|
|
187
|
+
if (trackStageLifecycle) {
|
|
188
|
+
runtime.stageSnapshot.status = "completed";
|
|
189
|
+
const assistantText = runtime.innerCtx.__getLastAssistantText();
|
|
190
|
+
if (assistantText !== undefined) runtime.stageSnapshot.result = assistantText;
|
|
191
|
+
}
|
|
169
192
|
return result;
|
|
170
193
|
} catch (err) {
|
|
171
194
|
const workflowExitAbort = runtime.signal.aborted ? runtime.exit.currentWorkflowExitAbortReason() : undefined;
|
|
172
195
|
if (workflowExitAbort !== undefined && !runtime.state.skippedForParallelFailFast) {
|
|
173
196
|
runtime.state.stageClosedByWorkflowExit = true;
|
|
174
|
-
if (!isTerminalStage(runtime.stageSnapshot)) {
|
|
197
|
+
if (trackStageLifecycle && !isTerminalStage(runtime.stageSnapshot)) {
|
|
175
198
|
runtime.stageSnapshot.status = "skipped";
|
|
176
199
|
runtime.stageSnapshot.skippedReason = runtime.exit.workflowExitSkippedReason(workflowExitAbort.reason);
|
|
177
200
|
}
|
|
178
|
-
} else if (!runtime.signal.aborted && !runtime.state.skippedForParallelFailFast) {
|
|
201
|
+
} else if (trackStageLifecycle && !runtime.signal.aborted && !runtime.state.skippedForParallelFailFast) {
|
|
179
202
|
applyFailureToStage(runtime.stageSnapshot, runtime.classifyExecutorFailure(err));
|
|
180
203
|
}
|
|
181
204
|
throw err;
|
|
182
205
|
} finally {
|
|
206
|
+
// Finalization, handle release, and limiter release are each independent.
|
|
207
|
+
// If finalizeStageSnapshot() throws, the limiter must still be released
|
|
208
|
+
// so the concurrency semaphore is not leaked.
|
|
209
|
+
// cross-ref: issue #1498 — durable finalization failures must not leak the stage limiter.
|
|
183
210
|
runtime.mcpScope.clear();
|
|
184
211
|
runtime.captureStageSessionMeta();
|
|
185
|
-
|
|
186
|
-
if (
|
|
212
|
+
let finalizationError: { readonly thrown: true; readonly error: unknown } | undefined;
|
|
213
|
+
if (trackStageLifecycle) {
|
|
214
|
+
try {
|
|
215
|
+
await runtime.finalizeStageSnapshot();
|
|
216
|
+
} catch (err) {
|
|
217
|
+
finalizationError = { thrown: true, error: err };
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
if (runtime.state.stageClosedByWorkflowExit || runtime.exit.currentWorkflowExitAbortReason() !== undefined) {
|
|
221
|
+
await runtime.releaseLiveHandle().catch(() => {});
|
|
222
|
+
} else {
|
|
223
|
+
await runtime.dropStageControlForCompletion().catch(() => {});
|
|
224
|
+
}
|
|
225
|
+
} catch {
|
|
226
|
+
// Best-effort: handle release failure must not prevent limiter release.
|
|
227
|
+
}
|
|
228
|
+
} else if (runtime.state.stageClosedByWorkflowExit || runtime.exit.currentWorkflowExitAbortReason() !== undefined) {
|
|
187
229
|
await runtime.releaseLiveHandle().catch(() => {});
|
|
188
|
-
} else {
|
|
189
|
-
await runtime.dropStageControlForCompletion().catch(() => {});
|
|
190
230
|
}
|
|
191
231
|
input.limiter.release();
|
|
232
|
+
if (finalizationError !== undefined) throw finalizationError.error;
|
|
192
233
|
}
|
|
193
234
|
};
|
|
194
235
|
}
|