@bastani/atomic 0.9.3-alpha.1 → 0.9.3-alpha.3
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 +15 -0
- package/dist/builtin/cursor/CHANGELOG.md +21 -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/CHANGELOG.md +6 -0
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/CHANGELOG.md +6 -0
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/CHANGELOG.md +15 -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/CHANGELOG.md +6 -0
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +49 -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
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/** Durable `ctx.ui` wrapper with collision-resistant prompt identities. */
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
WorkflowCustomUiFactory,
|
|
5
|
+
WorkflowCustomUiOptions,
|
|
6
|
+
WorkflowUIContext,
|
|
7
|
+
} from "../shared/authoring-contract-ui.js";
|
|
8
|
+
import type { WorkflowSerializableValue } from "../shared/types.js";
|
|
9
|
+
import type { DurableWorkflowBackend } from "./backend.js";
|
|
10
|
+
import { durableHash } from "./backend.js";
|
|
11
|
+
import type { DurableUiCheckpoint, UiPromptKind } from "./types.js";
|
|
12
|
+
import { recordCheckpointDurably } from "./tool-primitive.js";
|
|
13
|
+
|
|
14
|
+
export interface DurableUiDeps {
|
|
15
|
+
readonly workflowId: string;
|
|
16
|
+
readonly backend: DurableWorkflowBackend;
|
|
17
|
+
readonly nextCheckpointId: () => string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function wrapUiWithDurable(base: WorkflowUIContext, deps: DurableUiDeps): WorkflowUIContext {
|
|
21
|
+
const ordinals = new Map<string, number>();
|
|
22
|
+
|
|
23
|
+
const nextIdentity = (kind: UiPromptKind, message: string, details?: WorkflowSerializableValue): { key: string; hash: string } => {
|
|
24
|
+
const baseKey = durableHash({ kind, message, details: details ?? null });
|
|
25
|
+
const ordinal = (ordinals.get(baseKey) ?? 0) + 1;
|
|
26
|
+
ordinals.set(baseKey, ordinal);
|
|
27
|
+
const identity = { kind, message, details: details ?? null, ordinal };
|
|
28
|
+
return { key: JSON.stringify(identity), hash: durableHash(identity) };
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const record = async (kind: UiPromptKind, identity: { key: string; hash: string }, response: WorkflowSerializableValue): Promise<void> => {
|
|
32
|
+
const checkpoint: DurableUiCheckpoint = {
|
|
33
|
+
kind: "ui",
|
|
34
|
+
workflowId: deps.workflowId,
|
|
35
|
+
checkpointId: `ui:${identity.hash}`,
|
|
36
|
+
promptKind: kind,
|
|
37
|
+
message: identity.key,
|
|
38
|
+
promptHash: identity.hash,
|
|
39
|
+
response,
|
|
40
|
+
completedAt: Date.now(),
|
|
41
|
+
};
|
|
42
|
+
await recordCheckpointDurably(deps.backend, checkpoint);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const cached = (identity: { readonly hash: string }): WorkflowSerializableValue | undefined => deps.backend.getUiResponse(deps.workflowId, identity.hash);
|
|
46
|
+
|
|
47
|
+
const cachedCustom = (identity: { readonly hash: string }): { readonly found: boolean; readonly response?: WorkflowSerializableValue } => {
|
|
48
|
+
const hit = deps.backend.listCheckpoints(deps.workflowId)
|
|
49
|
+
.find((checkpoint) => checkpoint.kind === "ui" && checkpoint.promptHash === identity.hash);
|
|
50
|
+
return hit?.kind === "ui" ? { found: true, response: hit.response } : { found: false };
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
async input(promptText: string): Promise<string> {
|
|
55
|
+
const identity = nextIdentity("input", promptText);
|
|
56
|
+
const hit = cached(identity);
|
|
57
|
+
if (typeof hit === "string") return hit;
|
|
58
|
+
const response = await base.input(promptText);
|
|
59
|
+
await record("input", identity, response);
|
|
60
|
+
return response;
|
|
61
|
+
},
|
|
62
|
+
async confirm(message: string): Promise<boolean> {
|
|
63
|
+
const identity = nextIdentity("confirm", message);
|
|
64
|
+
const hit = cached(identity);
|
|
65
|
+
if (typeof hit === "boolean") return hit;
|
|
66
|
+
const response = await base.confirm(message);
|
|
67
|
+
await record("confirm", identity, response);
|
|
68
|
+
return response;
|
|
69
|
+
},
|
|
70
|
+
async select<T extends string>(message: string, options: readonly T[]): Promise<T> {
|
|
71
|
+
const identity = nextIdentity("select", message, [...options]);
|
|
72
|
+
const hit = cached(identity);
|
|
73
|
+
if (typeof hit === "string") return hit as T;
|
|
74
|
+
const response = await base.select<T>(message, options);
|
|
75
|
+
await record("select", identity, response);
|
|
76
|
+
return response;
|
|
77
|
+
},
|
|
78
|
+
async editor(initial?: string): Promise<string> {
|
|
79
|
+
const identity = nextIdentity("editor", initial ?? "", initial ?? null);
|
|
80
|
+
const hit = cached(identity);
|
|
81
|
+
if (typeof hit === "string") return hit;
|
|
82
|
+
const response = await base.editor(initial);
|
|
83
|
+
await record("editor", identity, response);
|
|
84
|
+
return response;
|
|
85
|
+
},
|
|
86
|
+
async custom<T>(factory: WorkflowCustomUiFactory<T>, options?: WorkflowCustomUiOptions): Promise<T> {
|
|
87
|
+
const replayIdentity = options?.replayIdentity ?? factory?.name ?? "custom";
|
|
88
|
+
const identity = nextIdentity("custom", replayIdentity, { replayIdentity });
|
|
89
|
+
const hit = cachedCustom(identity);
|
|
90
|
+
if (hit.found) return hit.response as T;
|
|
91
|
+
const response = await base.custom<T>(factory, options);
|
|
92
|
+
await record("custom", identity, response as WorkflowSerializableValue);
|
|
93
|
+
return response;
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -9,6 +9,7 @@ export type EngineStageRuntimeOptions = Pick<
|
|
|
9
9
|
| "persistence"
|
|
10
10
|
| "onStageStart"
|
|
11
11
|
| "onStageEnd"
|
|
12
|
+
| "onStageSession"
|
|
12
13
|
| "confirmStageReadiness"
|
|
13
14
|
| "usePromptNodesForUi"
|
|
14
15
|
>;
|
|
@@ -37,4 +38,6 @@ export type EngineChildRunOptions = Pick<
|
|
|
37
38
|
| "stageControlRegistry"
|
|
38
39
|
| "onStageStart"
|
|
39
40
|
| "onStageEnd"
|
|
41
|
+
| "onStageSession"
|
|
42
|
+
| "durableBackend"
|
|
40
43
|
>;
|
|
@@ -33,11 +33,11 @@ export function createParallelPrimitive(input: {
|
|
|
33
33
|
taskWithSharedDefaults(taskOptionsFromStep(step, prompt, taskPrevious(step)), options),
|
|
34
34
|
parallelScope,
|
|
35
35
|
);
|
|
36
|
-
}, (error) => {
|
|
36
|
+
}, async (error) => {
|
|
37
37
|
if (!failFastEnabled) return;
|
|
38
38
|
parallelScope.failed = true;
|
|
39
39
|
parallelScope.firstFailure = error;
|
|
40
|
-
|
|
40
|
+
await Promise.all([...parallelScope.activeStages.values()].map((stage) => stage.skip()));
|
|
41
41
|
}, {
|
|
42
42
|
beforeDequeue: input.runtime.exit.throwIfWorkflowExitSelected,
|
|
43
43
|
beforeMap: input.runtime.exit.throwIfWorkflowExitSelected,
|
|
@@ -49,10 +49,10 @@ function createTaskPrimitive(runtime: EngineRuntime): WorkflowTaskPrimitive {
|
|
|
49
49
|
...(stageFailFastScope !== undefined ? { failFastScope: stageFailFastScope } : {}),
|
|
50
50
|
});
|
|
51
51
|
const stage = stageHandle.context;
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
);
|
|
52
|
+
const promptText = resolvedTaskOptions.resumeFromSessionFile !== undefined
|
|
53
|
+
? "Continue"
|
|
54
|
+
: applyTaskContext(`${taskReadInstruction(resolvedTaskOptions)}${taskPrompt(resolvedTaskOptions)}`, taskPrevious(resolvedTaskOptions));
|
|
55
|
+
const rawOutput = await stage.prompt(promptText, taskPromptOptions(resolvedTaskOptions));
|
|
56
56
|
const structured = typeof rawOutput === "string" ? undefined : rawOutput;
|
|
57
57
|
const text = truncateTaskOutput(structuredTaskOutputText(rawOutput), resolvedTaskOptions.maxOutput);
|
|
58
58
|
const sessionId = (() => {
|
|
@@ -5,37 +5,51 @@ import type {
|
|
|
5
5
|
} from "../../shared/types.js";
|
|
6
6
|
import type { RunOpts } from "../../runs/foreground/executor-types.js";
|
|
7
7
|
import { makeHeadlessUnavailableUIContext, normalizeUIContext } from "../../runs/foreground/executor-hil.js";
|
|
8
|
+
import type { WorkflowUIContext as AuthoringWorkflowUIContext } from "../../shared/authoring-contract-ui.js";
|
|
9
|
+
import { wrapUiWithDurable, type DurableUiDeps } from "../../durable/ui-primitive.js";
|
|
8
10
|
|
|
9
|
-
export
|
|
11
|
+
export interface BuildExitGatedUiContextInput {
|
|
10
12
|
readonly opts: RunOpts;
|
|
11
|
-
readonly baseFromPromptNodes: () =>
|
|
13
|
+
readonly baseFromPromptNodes: () => AuthoringWorkflowUIContext;
|
|
12
14
|
readonly throwIfWorkflowExitSelected: () => void;
|
|
13
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Optional durable UI deps. When provided, completed ctx.ui responses are
|
|
17
|
+
* cached durably and replayed on resume instead of re-asking the user.
|
|
18
|
+
*
|
|
19
|
+
* cross-ref: issue #1498 — durable ctx.ui response/pending prompt state.
|
|
20
|
+
*/
|
|
21
|
+
readonly durableUi?: DurableUiDeps;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function buildExitGatedUiContext(input: BuildExitGatedUiContextInput): WorkflowUIContext {
|
|
14
25
|
const base = input.opts.usePromptNodesForUi === true
|
|
15
26
|
? input.baseFromPromptNodes()
|
|
16
27
|
: input.opts.executionMode === "non_interactive" && input.opts.ui === undefined
|
|
17
28
|
? makeHeadlessUnavailableUIContext()
|
|
18
29
|
: normalizeUIContext(input.opts.ui);
|
|
30
|
+
// Wrap the resolved base UI with durable caching when deps are supplied.
|
|
31
|
+
// The durable wrapper is transparent when no cached response exists.
|
|
32
|
+
const durableBase = input.durableUi !== undefined ? wrapUiWithDurable(base, input.durableUi) : base;
|
|
19
33
|
return {
|
|
20
34
|
async input(promptText: string): Promise<string> {
|
|
21
35
|
input.throwIfWorkflowExitSelected();
|
|
22
|
-
return await
|
|
36
|
+
return await durableBase.input(promptText);
|
|
23
37
|
},
|
|
24
38
|
async confirm(message: string): Promise<boolean> {
|
|
25
39
|
input.throwIfWorkflowExitSelected();
|
|
26
|
-
return await
|
|
40
|
+
return await durableBase.confirm(message);
|
|
27
41
|
},
|
|
28
42
|
async select<T extends string>(message: string, options: readonly T[]): Promise<T> {
|
|
29
43
|
input.throwIfWorkflowExitSelected();
|
|
30
|
-
return await
|
|
44
|
+
return await durableBase.select(message, options);
|
|
31
45
|
},
|
|
32
46
|
async editor(initial?: string): Promise<string> {
|
|
33
47
|
input.throwIfWorkflowExitSelected();
|
|
34
|
-
return await
|
|
48
|
+
return await durableBase.editor(initial);
|
|
35
49
|
},
|
|
36
50
|
async custom<T>(factory: WorkflowCustomUiFactory<T>, options?: WorkflowCustomUiOptions): Promise<T> {
|
|
37
51
|
input.throwIfWorkflowExitSelected();
|
|
38
|
-
return await
|
|
52
|
+
return await durableBase.custom(factory, options);
|
|
39
53
|
},
|
|
40
54
|
};
|
|
41
55
|
}
|
|
@@ -17,11 +17,18 @@ import {
|
|
|
17
17
|
workflowChildReplaySnapshot,
|
|
18
18
|
workflowDefinitionRequirementMessage,
|
|
19
19
|
} from "../../runs/foreground/executor-child-helpers.js";
|
|
20
|
+
import type { DurableScope } from "../../durable/scoped-backend.js";
|
|
20
21
|
|
|
21
22
|
export function createChildWorkflowRunner(input: {
|
|
22
23
|
readonly runtime: EngineRuntime;
|
|
23
24
|
readonly resolveWorkflowCwd: () => string;
|
|
24
25
|
readonly nextWorkflowBoundaryReplayKey: (name: string) => string;
|
|
26
|
+
/**
|
|
27
|
+
* Consume the durable scope published by the durable child primitive for the
|
|
28
|
+
* current invocation, if any. Routes child internal side-effect checkpoints
|
|
29
|
+
* under the root workflow.
|
|
30
|
+
*/
|
|
31
|
+
readonly consumeDurableScope?: () => DurableScope | undefined;
|
|
25
32
|
readonly runWorkflow: <
|
|
26
33
|
TInputs extends WorkflowInputValues,
|
|
27
34
|
TRunInputs extends WorkflowInputValues = TInputs,
|
|
@@ -100,6 +107,7 @@ export function createChildWorkflowRunner(input: {
|
|
|
100
107
|
},
|
|
101
108
|
signal: childController.signal,
|
|
102
109
|
deferWorkflowStart: false,
|
|
110
|
+
...(input.consumeDurableScope !== undefined ? { durableScope: input.consumeDurableScope() } : {}),
|
|
103
111
|
});
|
|
104
112
|
boundary.observeChildRun(childRunPromise);
|
|
105
113
|
const childRun = await childRunPromise;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable terminal-status finalization for workflow runs.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `run()` to keep the engine entrypoint under the file-length
|
|
5
|
+
* gate. Persists the final durable status (cancelled/blocked/skipped/failed/
|
|
6
|
+
* killed) for cross-session resume discovery when the run did not complete
|
|
7
|
+
* normally (normal completion is handled in the run try-block).
|
|
8
|
+
*
|
|
9
|
+
* cross-ref: issue #1498.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { RunSnapshot } from "../shared/store-types.js";
|
|
13
|
+
import type { WorkflowPersistencePort } from "../shared/types.js";
|
|
14
|
+
import type { DurableWorkflowBackend } from "../durable/backend.js";
|
|
15
|
+
import { persistDurableCacheEntry } from "../durable/resume-catalog.js";
|
|
16
|
+
import type { DurableWorkflowStatus } from "../durable/types.js";
|
|
17
|
+
|
|
18
|
+
export interface DurableTerminalFinalizeInput {
|
|
19
|
+
readonly runId: string;
|
|
20
|
+
readonly runSnapshot: RunSnapshot;
|
|
21
|
+
readonly isRoot: boolean;
|
|
22
|
+
readonly durableBackend: DurableWorkflowBackend;
|
|
23
|
+
readonly persistence?: WorkflowPersistencePort;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Map and persist the terminal durable status for a root workflow run when the
|
|
28
|
+
* run did not complete normally. Safe to call from a `finally` block: flush
|
|
29
|
+
* failures are logged but never rethrown so they do not mask the original
|
|
30
|
+
* failure/exit status.
|
|
31
|
+
*/
|
|
32
|
+
export async function finalizeDurableTerminalStatus(input: DurableTerminalFinalizeInput): Promise<void> {
|
|
33
|
+
if (!input.isRoot) return;
|
|
34
|
+
const status = input.runSnapshot.status;
|
|
35
|
+
const isExitTerminal = input.runSnapshot.exited === true && status !== "running";
|
|
36
|
+
if (status !== "failed" && status !== "killed" && !isExitTerminal) return;
|
|
37
|
+
|
|
38
|
+
const durableStatus = toDurableStatus(status);
|
|
39
|
+
if (durableStatus !== undefined) {
|
|
40
|
+
input.durableBackend.setWorkflowStatus(input.runId, durableStatus, undefined, input.runSnapshot.resumable);
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
await input.durableBackend.flush?.();
|
|
44
|
+
} catch (flushErr) {
|
|
45
|
+
const msg = flushErr instanceof Error ? flushErr.message : String(flushErr);
|
|
46
|
+
console.warn(`atomic-workflows: durable terminal status flush failed: ${msg}`);
|
|
47
|
+
}
|
|
48
|
+
if (input.persistence !== undefined && input.durableBackend.persistent) {
|
|
49
|
+
const cacheEntry = input.durableBackend.toCacheEntry(input.runId);
|
|
50
|
+
if (cacheEntry) persistDurableCacheEntry(input.persistence, cacheEntry);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function toDurableStatus(status: RunSnapshot["status"]): DurableWorkflowStatus | undefined {
|
|
55
|
+
switch (status) {
|
|
56
|
+
case "completed":
|
|
57
|
+
case "skipped":
|
|
58
|
+
return "completed";
|
|
59
|
+
case "cancelled":
|
|
60
|
+
case "killed":
|
|
61
|
+
return "cancelled";
|
|
62
|
+
case "failed":
|
|
63
|
+
return "failed";
|
|
64
|
+
case "blocked":
|
|
65
|
+
return "blocked";
|
|
66
|
+
default:
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { StageSnapshot } from "../shared/store-types.js";
|
|
2
|
+
import type { WorkflowPersistencePort } from "../shared/types.js";
|
|
3
|
+
import type { DurableWorkflowBackend } from "../durable/backend.js";
|
|
4
|
+
import { persistDurableCacheEntry } from "../durable/resume-catalog.js";
|
|
5
|
+
import { recordStageSessionCheckpoint, type DurableStageDeps } from "../durable/stage-primitive.js";
|
|
6
|
+
|
|
7
|
+
export interface DurableStageSessionRecorderInput {
|
|
8
|
+
readonly runId: string;
|
|
9
|
+
readonly deps: DurableStageDeps;
|
|
10
|
+
readonly backend: DurableWorkflowBackend;
|
|
11
|
+
readonly persistence?: WorkflowPersistencePort;
|
|
12
|
+
readonly onStageSession?: (runId: string, snapshot: StageSnapshot) => unknown;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function createDurableStageSessionRecorder(
|
|
16
|
+
input: DurableStageSessionRecorderInput,
|
|
17
|
+
): (stageRunId: string, snapshot: StageSnapshot) => void {
|
|
18
|
+
return (stageRunId, snapshot) => {
|
|
19
|
+
if (stageRunId === input.runId) {
|
|
20
|
+
void recordStageSessionCheckpoint(input.deps, snapshot).then((recorded) => {
|
|
21
|
+
if (!recorded || !input.persistence || !input.backend.persistent) return;
|
|
22
|
+
const cacheEntry = input.backend.toCacheEntry(input.runId);
|
|
23
|
+
if (cacheEntry) persistDurableCacheEntry(input.persistence, cacheEntry);
|
|
24
|
+
}).catch((error) => {
|
|
25
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
26
|
+
console.warn(`atomic-workflows: durable stage session checkpoint failed: ${message}`);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
void input.onStageSession?.(stageRunId, snapshot);
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
WorkflowInputValues,
|
|
5
5
|
WorkflowOutputValues,
|
|
6
6
|
WorkflowRunContext,
|
|
7
|
+
WorkflowSerializableValue,
|
|
7
8
|
} from "../shared/types.js";
|
|
8
9
|
import type { WorkflowFailure } from "../shared/workflow-failures.js";
|
|
9
10
|
import { classifyWorkflowFailure } from "../shared/workflow-failures.js";
|
|
@@ -17,6 +18,8 @@ import { buildExitGatedUiContext } from "./primitives/ui.js";
|
|
|
17
18
|
import { createWorkflowExitManager } from "./primitives/exit.js";
|
|
18
19
|
import { createWorkflowTaskRunners } from "./primitives/task.js";
|
|
19
20
|
import { createChildWorkflowRunner } from "./primitives/workflow.js";
|
|
21
|
+
import { createChainPrimitive } from "./primitives/chain.js";
|
|
22
|
+
import { createParallelPrimitive } from "./primitives/parallel.js";
|
|
20
23
|
import { createRunLimiter } from "../runs/shared/concurrency.js";
|
|
21
24
|
import { stageControlRegistry as defaultStageControlRegistry } from "../runs/foreground/stage-control-registry.js";
|
|
22
25
|
import type { RunOpts, RunResult } from "../runs/foreground/executor-types.js";
|
|
@@ -37,6 +40,16 @@ import {
|
|
|
37
40
|
} from "../runs/foreground/executor-lifecycle.js";
|
|
38
41
|
import { assertWorkflowRunOutputs, normalizeWorkflowRunOutput } from "../runs/foreground/executor-outputs.js";
|
|
39
42
|
import { isWorkflowDefinition, workflowDefinitionRequirementMessage } from "../runs/foreground/executor-child-helpers.js";
|
|
43
|
+
import { getDurableBackend } from "../durable/factory.js";
|
|
44
|
+
import { createToolPrimitive, createCheckpointIdGenerator } from "../durable/tool-primitive.js";
|
|
45
|
+
import { persistDurableCacheEntry } from "../durable/resume-catalog.js";
|
|
46
|
+
import { createDurableStagePrimitive, createDurableTaskPrimitive, recordStageCheckpoint, createStageReplayKeyGenerator, recordCachedStageWithTracker } from "../durable/stage-primitive.js";
|
|
47
|
+
import { createDurableChildWorkflowPrimitive } from "../durable/child-primitive.js";
|
|
48
|
+
import { ScopedDurableBackend, type DurableScope } from "../durable/scoped-backend.js";
|
|
49
|
+
import { finalizeDurableTerminalStatus } from "./run-durable-finalize.js";
|
|
50
|
+
import { createDurableStageSessionRecorder } from "./run-durable-stage-session.js";
|
|
51
|
+
import type { DurableWorkflowBackend } from "../durable/backend.js";
|
|
52
|
+
import type { StageSnapshot } from "../shared/store-types.js";
|
|
40
53
|
|
|
41
54
|
function nextEventLoopTurn(): Promise<void> {
|
|
42
55
|
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
@@ -176,6 +189,38 @@ export async function run<
|
|
|
176
189
|
classifyExecutorFailure,
|
|
177
190
|
drainWorkflowExitCleanups: exit.drainWorkflowExitCleanups,
|
|
178
191
|
});
|
|
192
|
+
// Durable workflow backend — registers this run and wires ctx.tool/ui/stage.
|
|
193
|
+
// Declared early so the stage-end recorder can attach to stageOptions.
|
|
194
|
+
// cross-ref: issue #1498 — DBOS-backed cross-session resumability.
|
|
195
|
+
// Child runs with a durable scope route their internal side-effect
|
|
196
|
+
// checkpoints under the root workflow so interrupted children do not
|
|
197
|
+
// re-execute completed side effects on parent resume.
|
|
198
|
+
const rootBackend: DurableWorkflowBackend = opts.durableBackend ?? getDurableBackend();
|
|
199
|
+
const durableBackend: DurableWorkflowBackend = opts.durableScope !== undefined
|
|
200
|
+
? new ScopedDurableBackend(rootBackend, opts.durableScope)
|
|
201
|
+
: rootBackend;
|
|
202
|
+
const checkpointIdGenerator = createCheckpointIdGenerator();
|
|
203
|
+
const stageReplayKeyGenerator = createStageReplayKeyGenerator(runId);
|
|
204
|
+
const completedStageReplayKeys = new Map<string, string>();
|
|
205
|
+
const durableStageDeps = {
|
|
206
|
+
workflowId: runId,
|
|
207
|
+
backend: durableBackend,
|
|
208
|
+
nextCheckpointId: checkpointIdGenerator,
|
|
209
|
+
nextReplayKey: stageReplayKeyGenerator,
|
|
210
|
+
replayKeyForCompletedStage: (stage: StageSnapshot) => completedStageReplayKeys.get(stage.id),
|
|
211
|
+
};
|
|
212
|
+
const userOnStageEnd = opts.onStageEnd;
|
|
213
|
+
const durableOnStageEnd = async (stageRunId: string, snapshot: StageSnapshot): Promise<void> => {
|
|
214
|
+
if (stageRunId === runId && snapshot.status === "completed") {
|
|
215
|
+
await recordStageCheckpoint(durableStageDeps, snapshot);
|
|
216
|
+
if (opts.persistence && durableBackend.persistent) {
|
|
217
|
+
const cacheEntry = durableBackend.toCacheEntry(runId);
|
|
218
|
+
if (cacheEntry) persistDurableCacheEntry(opts.persistence, cacheEntry);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
await userOnStageEnd?.(stageRunId, snapshot);
|
|
222
|
+
};
|
|
223
|
+
const durableOnStageSession = createDurableStageSessionRecorder({ runId, deps: durableStageDeps, backend: durableBackend, persistence: opts.persistence, onStageSession: opts.onStageSession });
|
|
179
224
|
const stageOptions: EngineStageRuntimeOptions = {
|
|
180
225
|
continuation: opts.continuation,
|
|
181
226
|
models: opts.models,
|
|
@@ -183,7 +228,8 @@ export async function run<
|
|
|
183
228
|
defaultSessionDir: opts.defaultSessionDir,
|
|
184
229
|
persistence: opts.persistence,
|
|
185
230
|
onStageStart: opts.onStageStart,
|
|
186
|
-
onStageEnd:
|
|
231
|
+
onStageEnd: durableOnStageEnd,
|
|
232
|
+
onStageSession: durableOnStageSession,
|
|
187
233
|
confirmStageReadiness: opts.confirmStageReadiness,
|
|
188
234
|
usePromptNodesForUi: opts.usePromptNodesForUi,
|
|
189
235
|
};
|
|
@@ -210,6 +256,8 @@ export async function run<
|
|
|
210
256
|
stageControlRegistry: opts.stageControlRegistry,
|
|
211
257
|
onStageStart: opts.onStageStart,
|
|
212
258
|
onStageEnd: opts.onStageEnd,
|
|
259
|
+
onStageSession: opts.onStageSession,
|
|
260
|
+
durableBackend,
|
|
213
261
|
};
|
|
214
262
|
const runtime = new EngineRuntime({
|
|
215
263
|
runId,
|
|
@@ -238,14 +286,77 @@ export async function run<
|
|
|
238
286
|
workflowBoundaryReplayCounts.set(name, next);
|
|
239
287
|
return `workflow:${name}:${next}`;
|
|
240
288
|
};
|
|
289
|
+
// Durable child workflow replay keys use a SEPARATE counter so that cache
|
|
290
|
+
// hits (which do not invoke the inner workflow runner) do not desync the
|
|
291
|
+
// ordinal sequence. Without this, repeated ctx.workflow(child) calls would
|
|
292
|
+
// shift replay keys on resume and re-execute completed children.
|
|
293
|
+
// cross-ref: issue #1498.
|
|
294
|
+
const durableChildReplayCounts = new Map<string, number>();
|
|
295
|
+
const nextDurableChildReplayKey = (name: string): string => {
|
|
296
|
+
const next = (durableChildReplayCounts.get(name) ?? 0) + 1;
|
|
297
|
+
durableChildReplayCounts.set(name, next);
|
|
298
|
+
return `workflow:${name}:${next}`;
|
|
299
|
+
};
|
|
241
300
|
const taskRunners = createWorkflowTaskRunners({ runtime });
|
|
301
|
+
// Durable scope holder: the durable child primitive publishes the scope for
|
|
302
|
+
// the next child invocation; the child runner consumes it when launching the
|
|
303
|
+
// run. This routes child internal side-effect checkpoints under the root.
|
|
304
|
+
let pendingChildDurableScope: DurableScope | undefined;
|
|
242
305
|
const workflow = createChildWorkflowRunner({
|
|
243
306
|
runtime,
|
|
244
307
|
resolveWorkflowCwd,
|
|
245
308
|
nextWorkflowBoundaryReplayKey,
|
|
309
|
+
consumeDurableScope: () => {
|
|
310
|
+
const scope = pendingChildDurableScope;
|
|
311
|
+
pendingChildDurableScope = undefined;
|
|
312
|
+
return scope;
|
|
313
|
+
},
|
|
246
314
|
runWorkflow: run,
|
|
247
315
|
});
|
|
248
316
|
|
|
317
|
+
// Durable workflow registration — register this run for cross-session discovery.
|
|
318
|
+
if (opts.continuation === undefined && opts.parentRun === undefined) {
|
|
319
|
+
// New root run: register it durably so a future session can discover it.
|
|
320
|
+
durableBackend.registerWorkflow({
|
|
321
|
+
workflowId: runId,
|
|
322
|
+
name: def.name,
|
|
323
|
+
inputs: resolvedInputs as Record<string, import("../shared/types.js").WorkflowSerializableValue>,
|
|
324
|
+
createdAt: runSnapshot.startedAt,
|
|
325
|
+
status: "running",
|
|
326
|
+
rootWorkflowId: runId,
|
|
327
|
+
resumable: true,
|
|
328
|
+
...(opts.persistence !== undefined ? { sessionFile: undefined } : {}),
|
|
329
|
+
});
|
|
330
|
+
} else if (opts.parentRun === undefined) {
|
|
331
|
+
// Resuming a root workflow: mark it as running again in the backend.
|
|
332
|
+
durableBackend.setWorkflowStatus(runId, "running");
|
|
333
|
+
}
|
|
334
|
+
const tool = createToolPrimitive({
|
|
335
|
+
workflowId: runId,
|
|
336
|
+
backend: durableBackend,
|
|
337
|
+
nextCheckpointId: checkpointIdGenerator,
|
|
338
|
+
throwIfCancelled: () => {
|
|
339
|
+
if (ownController.signal.aborted) {
|
|
340
|
+
throw new Error("atomic-workflows: workflow cancelled");
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
signal: ownController.signal,
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// Durable ctx.ui wrapper — caches completed user responses so a resumed workflow does not re-ask answered prompts.
|
|
347
|
+
const durableUiDeps = { workflowId: runId, backend: durableBackend, nextCheckpointId: checkpointIdGenerator };
|
|
348
|
+
const recordCachedStage = (name: string, replayKey: string, output: WorkflowSerializableValue): void =>
|
|
349
|
+
recordCachedStageWithTracker(activeStore, tracker, runId, name, replayKey, output, completedStageReplayKeys);
|
|
350
|
+
const durableTask = createDurableTaskPrimitive({
|
|
351
|
+
workflowId: runId, backend: durableBackend,
|
|
352
|
+
nextReplayKey: (stageName) => stageReplayKeyGenerator(stageName), task: taskRunners.task,
|
|
353
|
+
recordCachedTask: (name, replayKey, output) => recordCachedStage(name, replayKey, output),
|
|
354
|
+
});
|
|
355
|
+
const durableWorkflow = createDurableChildWorkflowPrimitive({
|
|
356
|
+
workflowId: runId, rootWorkflowId: opts.parentRun?.rootRunId ?? runId, backend: durableBackend,
|
|
357
|
+
nextReplayKey: nextDurableChildReplayKey, setChildDurableScope: (scope) => { pendingChildDurableScope = scope; },
|
|
358
|
+
recordCachedStage, workflow,
|
|
359
|
+
});
|
|
249
360
|
const ctx: WorkflowRunContext<TInputs> = {
|
|
250
361
|
inputs: resolvedInputs as TInputs,
|
|
251
362
|
get cwd() { return resolveWorkflowCwd(); },
|
|
@@ -253,6 +364,7 @@ export async function run<
|
|
|
253
364
|
ui: buildExitGatedUiContext({
|
|
254
365
|
opts,
|
|
255
366
|
throwIfWorkflowExitSelected: exit.throwIfWorkflowExitSelected,
|
|
367
|
+
durableUi: durableUiDeps,
|
|
256
368
|
baseFromPromptNodes: () => buildPromptNodeUiAdapter({
|
|
257
369
|
runId,
|
|
258
370
|
activeStore,
|
|
@@ -267,11 +379,23 @@ export async function run<
|
|
|
267
379
|
classifyExecutorFailure,
|
|
268
380
|
}),
|
|
269
381
|
}),
|
|
270
|
-
stage:
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
382
|
+
stage: createDurableStagePrimitive({
|
|
383
|
+
workflowId: runId,
|
|
384
|
+
backend: durableBackend,
|
|
385
|
+
nextReplayKey: (stageName) => stageReplayKeyGenerator(stageName),
|
|
386
|
+
recordCachedStage,
|
|
387
|
+
stage: (name, options, replayKey) => {
|
|
388
|
+
const stage = runtime.stage(name, options);
|
|
389
|
+
const stageId = activeStore.runs().find((r) => r.id === runId)?.stages.at(-1)?.id;
|
|
390
|
+
if (stageId !== undefined) completedStageReplayKeys.set(stageId, replayKey);
|
|
391
|
+
return stage;
|
|
392
|
+
},
|
|
393
|
+
}),
|
|
394
|
+
task: durableTask,
|
|
395
|
+
chain: createChainPrimitive({ runtime, task: durableTask }),
|
|
396
|
+
parallel: createParallelPrimitive({ runtime, task: durableTask }),
|
|
397
|
+
workflow: durableWorkflow,
|
|
398
|
+
tool,
|
|
275
399
|
};
|
|
276
400
|
|
|
277
401
|
try {
|
|
@@ -298,8 +422,15 @@ export async function run<
|
|
|
298
422
|
const result = normalizeWorkflowRunOutput(def.name, rawResult);
|
|
299
423
|
assertWorkflowRunOutputs(def.name, result, def.outputs);
|
|
300
424
|
assertWorkflowCreatedStage(runSnapshot);
|
|
425
|
+
await durableBackend.flush?.();
|
|
301
426
|
const recorded = activeStore.recordRunEnd(runId, "completed", result);
|
|
302
427
|
appendRunEndWhenRecorded(opts.persistence, recorded, { runId, status: "completed", result, ts: Date.now() });
|
|
428
|
+
durableBackend.setWorkflowStatus(runId, "completed");
|
|
429
|
+
await durableBackend.flush?.();
|
|
430
|
+
if (opts.persistence && durableBackend.persistent) {
|
|
431
|
+
const cacheEntry = durableBackend.toCacheEntry(runId);
|
|
432
|
+
if (cacheEntry) persistDurableCacheEntry(opts.persistence, cacheEntry);
|
|
433
|
+
}
|
|
303
434
|
return reconcileTerminalRunResult(runId, runSnapshot, activeStore, { status: "completed", result }, opts.onRunEnd);
|
|
304
435
|
} catch (err) {
|
|
305
436
|
const selectedExit = findWorkflowExitSignal(err, exitScope) ?? findWorkflowExitSignal(ownController.signal.reason, exitScope);
|
|
@@ -351,6 +482,17 @@ export async function run<
|
|
|
351
482
|
});
|
|
352
483
|
return reconcileTerminalRunResult(runId, runSnapshot, activeStore, { status: "failed", error: metadata.errorMessage }, opts.onRunEnd);
|
|
353
484
|
} finally {
|
|
485
|
+
// Persist final durable status for cross-session resume discovery.
|
|
486
|
+
// Covers ctx.exit terminal states and failure/kill paths; normal
|
|
487
|
+
// completion is handled in the try block.
|
|
488
|
+
// cross-ref: issue #1498
|
|
489
|
+
await finalizeDurableTerminalStatus({
|
|
490
|
+
runId,
|
|
491
|
+
runSnapshot,
|
|
492
|
+
isRoot: opts.parentRun === undefined,
|
|
493
|
+
durableBackend,
|
|
494
|
+
persistence: opts.persistence,
|
|
495
|
+
});
|
|
354
496
|
opts.cancellation?.unregister(runId);
|
|
355
497
|
}
|
|
356
498
|
}
|
|
@@ -148,12 +148,18 @@ export class EngineRuntime {
|
|
|
148
148
|
const allow = options?.mcp?.allow ?? null;
|
|
149
149
|
const deny = options?.mcp?.deny ?? null;
|
|
150
150
|
const hasScope = allow !== null || deny !== null;
|
|
151
|
+
let depth = 0;
|
|
151
152
|
return {
|
|
152
153
|
apply: () => {
|
|
153
|
-
if (this.childRunOptions.mcp
|
|
154
|
+
if (!this.childRunOptions.mcp || !hasScope) return;
|
|
155
|
+
if (depth === 0) this.childRunOptions.mcp.setScope(stageId, allow, deny);
|
|
156
|
+
depth += 1;
|
|
154
157
|
},
|
|
155
158
|
clear: () => {
|
|
156
|
-
if (this.childRunOptions.mcp
|
|
159
|
+
if (!this.childRunOptions.mcp || !hasScope) return;
|
|
160
|
+
if (depth === 0) return;
|
|
161
|
+
depth -= 1;
|
|
162
|
+
if (depth === 0) this.childRunOptions.mcp.clearScope(stageId);
|
|
157
163
|
},
|
|
158
164
|
};
|
|
159
165
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { cancellationRegistry } from "../runs/background/cancellation-registry.js";
|
|
1
|
+
import { quitRun } from "../runs/background/quit.js";
|
|
3
2
|
import { store } from "../shared/store.js";
|
|
4
3
|
import { subscribeIntercomControl } from "../intercom/result-intercom.js";
|
|
5
4
|
import { buildIntercomCallbacks } from "../intercom/intercom-routing.js";
|
|
@@ -7,9 +6,8 @@ import { installStoreWidget, installToolExecutionHooks } from "../tui/store-widg
|
|
|
7
6
|
import { buildGraphOverlayAdapter } from "../tui/overlay-adapter.js";
|
|
8
7
|
import type { GraphOverlayPort } from "../tui/overlay-adapter.js";
|
|
9
8
|
import { registerInlineFormRenderer } from "../tui/inline-form-overlay.js";
|
|
10
|
-
import { registerChatSurfaceRenderer
|
|
9
|
+
import { registerChatSurfaceRenderer } from "../tui/chat-surface-message.js";
|
|
11
10
|
import { deriveGraphTheme } from "../tui/graph-theme.js";
|
|
12
|
-
import type { WorkflowPersistencePort } from "../shared/types.js";
|
|
13
11
|
import { renderRunBanner, renderRunSummary, type RunEndPayload, type RunStartPayload } from "./renderers.js";
|
|
14
12
|
import { buildRuntimeAdapters } from "./wiring.js";
|
|
15
13
|
import type { ExtensionAPI, PiCommandContext } from "./public-types.js";
|
|
@@ -36,15 +34,11 @@ function registerWorkflowMessageRenderers(pi: ExtensionAPI): void {
|
|
|
36
34
|
|
|
37
35
|
function buildWorkflowOverlay(
|
|
38
36
|
pi: ExtensionAPI,
|
|
39
|
-
getPersistence: () => WorkflowPersistencePort | undefined,
|
|
40
37
|
): GraphOverlayPort {
|
|
41
38
|
return buildGraphOverlayAdapter(pi, store, {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (run && result.ok) {
|
|
46
|
-
emitChatSurface(pi, { kind: "killed", run, previousStatus: result.previousStatus });
|
|
47
|
-
}
|
|
39
|
+
onQuitRun: (runId) => {
|
|
40
|
+
quitRun(runId, { store });
|
|
41
|
+
pi.ui?.notify?.(`Workflow quit; resume with /workflow resume.`, "info");
|
|
48
42
|
},
|
|
49
43
|
});
|
|
50
44
|
}
|
|
@@ -83,7 +77,7 @@ function registerIntercomControl(
|
|
|
83
77
|
function factory(pi: ExtensionAPI): void {
|
|
84
78
|
const adapters = buildRuntimeAdapters(pi);
|
|
85
79
|
const runtimeState = createWorkflowExtensionRuntimeState(pi, adapters);
|
|
86
|
-
const overlay = buildWorkflowOverlay(pi
|
|
80
|
+
const overlay = buildWorkflowOverlay(pi);
|
|
87
81
|
const workflowCommands = new Map<string, WorkflowCommandHandler>();
|
|
88
82
|
const storeWidgetRef: { current: (() => void) | null } = { current: null };
|
|
89
83
|
const intercomControlRef: { current: (() => void) | null } = { current: null };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { killAllRuns } from "../runs/background/status.js";
|
|
2
|
+
import { quitAllRuns } from "../runs/background/quit.js";
|
|
2
3
|
import { cancellationRegistry } from "../runs/background/cancellation-registry.js";
|
|
3
4
|
import { stageControlRegistry } from "../runs/foreground/stage-control-registry.js";
|
|
4
5
|
import { store } from "../shared/store.js";
|
|
@@ -99,7 +100,10 @@ export function registerWorkflowLifecycleHandlers(
|
|
|
99
100
|
deps.intercomControlRef.current?.();
|
|
100
101
|
deps.intercomControlRef.current = null;
|
|
101
102
|
if (reason === "quit") {
|
|
102
|
-
|
|
103
|
+
// CLI/orchestrator quit is a resumable process boundary, not explicit
|
|
104
|
+
// `/workflow kill`. Durable-progress workflows stay available through
|
|
105
|
+
// `/workflow resume`; stage handles are disposed after being paused.
|
|
106
|
+
quitAllRuns({ store, stageControlRegistry });
|
|
103
107
|
stageControlRegistry.clear();
|
|
104
108
|
}
|
|
105
109
|
deps.storeWidgetRef.current?.();
|