@bastani/atomic 0.9.11-alpha.9 → 0.9.11
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 +134 -0
- package/README.md +1 -1
- package/dist/builtin/intercom/CHANGELOG.md +6 -0
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/intercom/subagent-relay.ts +8 -5
- package/dist/builtin/mcp/CHANGELOG.md +10 -0
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/CHANGELOG.md +20 -0
- package/dist/builtin/subagents/agents/code-simplifier.md +2 -2
- package/dist/builtin/subagents/agents/codebase-analyzer.md +2 -2
- package/dist/builtin/subagents/agents/codebase-locator.md +2 -2
- package/dist/builtin/subagents/agents/codebase-online-researcher.md +2 -2
- package/dist/builtin/subagents/agents/codebase-pattern-finder.md +2 -2
- package/dist/builtin/subagents/agents/codebase-research-analyzer.md +2 -2
- package/dist/builtin/subagents/agents/codebase-research-locator.md +2 -2
- package/dist/builtin/subagents/agents/worker.md +2 -2
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/subagents/src/extension/notification-content.ts +14 -9
- package/dist/builtin/subagents/src/runs/background/notify.ts +20 -4
- package/dist/builtin/web-access/CHANGELOG.md +20 -0
- package/dist/builtin/web-access/content-tools.ts +1 -0
- package/dist/builtin/web-access/flat-string.ts +37 -0
- package/dist/builtin/web-access/github-api.ts +2 -1
- package/dist/builtin/web-access/github-extract.ts +3 -2
- package/dist/builtin/web-access/package.json +2 -2
- package/dist/builtin/workflows/CHANGELOG.md +57 -0
- package/dist/builtin/workflows/builtin/goal-ledger.ts +6 -4
- package/dist/builtin/workflows/builtin/goal-runner.ts +2 -1
- package/dist/builtin/workflows/builtin/goal.ts +1 -1
- package/dist/builtin/workflows/builtin/ralph-core.ts +8 -9
- package/dist/builtin/workflows/builtin/ralph-forked-prompts.ts +2 -4
- package/dist/builtin/workflows/builtin/ralph-models.ts +4 -4
- package/dist/builtin/workflows/builtin/ralph-runner.ts +5 -8
- package/dist/builtin/workflows/builtin/ralph.ts +2 -1
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/src/durable/completed-catalog.ts +14 -8
- package/dist/builtin/workflows/src/durable/completed-inspection.ts +10 -11
- package/dist/builtin/workflows/src/durable/resume-catalog.ts +1 -1
- package/dist/builtin/workflows/src/durable/resume-eligibility.ts +42 -0
- package/dist/builtin/workflows/src/durable/resume-runtime.ts +36 -37
- package/dist/builtin/workflows/src/durable/tool-primitive.ts +10 -2
- package/dist/builtin/workflows/src/engine/run.ts +1 -0
- package/dist/builtin/workflows/src/extension/extension-runtime-state.ts +12 -8
- package/dist/builtin/workflows/src/extension/hil-answer-notifications.ts +5 -2
- package/dist/builtin/workflows/src/extension/index.bundle.mjs +1966 -880
- package/dist/builtin/workflows/src/extension/lifecycle-notifications.ts +6 -3
- package/dist/builtin/workflows/src/extension/postmortem-deps.ts +3 -2
- package/dist/builtin/workflows/src/extension/render-result.ts +4 -11
- package/dist/builtin/workflows/src/extension/runtime-durable-resume.ts +17 -20
- package/dist/builtin/workflows/src/extension/runtime.ts +3 -3
- package/dist/builtin/workflows/src/extension/workflow-command-completions.ts +1 -1
- package/dist/builtin/workflows/src/extension/workflow-command-registration.ts +3 -7
- package/dist/builtin/workflows/src/extension/workflow-durable-resume-command.ts +21 -24
- package/dist/builtin/workflows/src/extension/workflow-resume-picker-rows.ts +38 -26
- package/dist/builtin/workflows/src/extension/workflow-resume-shadow.ts +2 -1
- package/dist/builtin/workflows/src/extension/workflow-run-control-command.ts +56 -64
- package/dist/builtin/workflows/src/extension/workflow-schema.ts +2 -2
- package/dist/builtin/workflows/src/extension/workflow-status-summary.ts +0 -5
- package/dist/builtin/workflows/src/extension/workflow-targets.ts +17 -20
- package/dist/builtin/workflows/src/extension/workflow-tool-content.ts +1 -1
- package/dist/builtin/workflows/src/extension/workflow-tool-control.ts +24 -51
- package/dist/builtin/workflows/src/extension/workflow-tool-inspection.ts +7 -29
- package/dist/builtin/workflows/src/extension/workflow-tool-send.ts +5 -13
- package/dist/builtin/workflows/src/extension/workflow-tool.ts +4 -13
- package/dist/builtin/workflows/src/runs/background/quit.ts +4 -3
- package/dist/builtin/workflows/src/runs/background/run-inspect.ts +8 -8
- package/dist/builtin/workflows/src/runs/background/status.ts +2 -1
- package/dist/builtin/workflows/src/runs/background/workflow-lifecycle-aggregate.ts +2 -1
- package/dist/builtin/workflows/src/runs/foreground/executor-direct-helpers.ts +5 -5
- package/dist/builtin/workflows/src/runs/foreground/executor-lifecycle.ts +4 -0
- package/dist/builtin/workflows/src/runs/foreground/executor-scheduler.ts +2 -0
- package/dist/builtin/workflows/src/runs/foreground/executor-stage-call.ts +23 -8
- package/dist/builtin/workflows/src/runs/foreground/executor-stage-factory.ts +6 -2
- package/dist/builtin/workflows/src/runs/foreground/executor-task-prompts.ts +5 -0
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-context.ts +56 -10
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-controller.ts +6 -0
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-output.ts +137 -6
- package/dist/builtin/workflows/src/shared/authoring-contract-ui.d.ts +2 -0
- package/dist/builtin/workflows/src/shared/authoring-contract-ui.ts +2 -0
- package/dist/builtin/workflows/src/shared/expanded-workflow-graph.ts +159 -10
- package/dist/builtin/workflows/src/shared/flat-string.ts +33 -0
- package/dist/builtin/workflows/src/shared/graph-store-snapshot.ts +144 -0
- package/dist/builtin/workflows/src/shared/persistence-session-entries.ts +2 -3
- package/dist/builtin/workflows/src/shared/run-id.ts +36 -0
- package/dist/builtin/workflows/src/shared/store-internal.ts +32 -3
- package/dist/builtin/workflows/src/shared/store-observation.ts +10 -0
- package/dist/builtin/workflows/src/shared/store-public-types.ts +8 -0
- package/dist/builtin/workflows/src/shared/store-run-methods.ts +14 -0
- package/dist/builtin/workflows/src/shared/store-types.ts +4 -0
- package/dist/builtin/workflows/src/shared/types.ts +2 -0
- package/dist/builtin/workflows/src/shared/workflow-artifacts.ts +261 -0
- package/dist/builtin/workflows/src/tui/dispatch-confirm.ts +39 -23
- package/dist/builtin/workflows/src/tui/graph-canvas.ts +26 -9
- package/dist/builtin/workflows/src/tui/graph-view-constants.ts +3 -0
- package/dist/builtin/workflows/src/tui/graph-view-graph-render.ts +95 -98
- package/dist/builtin/workflows/src/tui/graph-view-render.ts +7 -20
- package/dist/builtin/workflows/src/tui/graph-view-state.ts +160 -13
- package/dist/builtin/workflows/src/tui/layout.ts +8 -2
- package/dist/builtin/workflows/src/tui/node-card.ts +57 -45
- package/dist/builtin/workflows/src/tui/overlay-adapter.ts +4 -2
- package/dist/builtin/workflows/src/tui/prompt-card-render.ts +291 -21
- package/dist/builtin/workflows/src/tui/prompt-card.ts +1 -1
- package/dist/builtin/workflows/src/tui/renderers.ts +0 -1
- package/dist/builtin/workflows/src/tui/run-detail.ts +52 -35
- package/dist/builtin/workflows/src/tui/run-identity-rows.ts +97 -0
- package/dist/builtin/workflows/src/tui/session-overlays.ts +24 -3
- package/dist/builtin/workflows/src/tui/session-picker.ts +94 -41
- package/dist/builtin/workflows/src/tui/stage-chat-view-archive-history.ts +190 -26
- package/dist/builtin/workflows/src/tui/stage-chat-view-footer-status.ts +52 -18
- package/dist/builtin/workflows/src/tui/stage-chat-view-input.ts +9 -5
- package/dist/builtin/workflows/src/tui/stage-chat-view-state.ts +44 -4
- package/dist/builtin/workflows/src/tui/stage-chat-view-types.ts +3 -1
- package/dist/builtin/workflows/src/tui/stage-chat-view.ts +3 -2
- package/dist/builtin/workflows/src/tui/status-list.ts +67 -45
- package/dist/builtin/workflows/src/tui/store-widget-installer.ts +7 -6
- package/dist/builtin/workflows/src/tui/widget.ts +49 -40
- package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +15 -10
- package/dist/builtin/workflows/src/tui/workflow-resume-selector.ts +21 -8
- package/dist/core/agent-session-accessors.d.ts.map +1 -1
- package/dist/core/agent-session-accessors.js +5 -0
- package/dist/core/agent-session-accessors.js.map +1 -1
- package/dist/core/agent-session-auto-compaction.d.ts.map +1 -1
- package/dist/core/agent-session-auto-compaction.js +5 -0
- package/dist/core/agent-session-auto-compaction.js.map +1 -1
- package/dist/core/agent-session-compaction.d.ts.map +1 -1
- package/dist/core/agent-session-compaction.js +3 -0
- package/dist/core/agent-session-compaction.js.map +1 -1
- package/dist/core/agent-session-events.d.ts.map +1 -1
- package/dist/core/agent-session-events.js +2 -1
- package/dist/core/agent-session-events.js.map +1 -1
- package/dist/core/agent-session-message-queue.d.ts +3 -7
- package/dist/core/agent-session-message-queue.d.ts.map +1 -1
- package/dist/core/agent-session-message-queue.js +27 -13
- package/dist/core/agent-session-message-queue.js.map +1 -1
- package/dist/core/agent-session-methods.d.ts +5 -3
- package/dist/core/agent-session-methods.d.ts.map +1 -1
- package/dist/core/agent-session-methods.js.map +1 -1
- package/dist/core/agent-session-persistent-custom-messages.d.ts.map +1 -1
- package/dist/core/agent-session-persistent-custom-messages.js +6 -3
- package/dist/core/agent-session-persistent-custom-messages.js.map +1 -1
- package/dist/core/agent-session-post-tool-compaction.d.ts.map +1 -1
- package/dist/core/agent-session-post-tool-compaction.js +11 -2
- package/dist/core/agent-session-post-tool-compaction.js.map +1 -1
- package/dist/core/agent-session-runtime.d.ts.map +1 -1
- package/dist/core/agent-session-runtime.js +5 -3
- package/dist/core/agent-session-runtime.js.map +1 -1
- package/dist/core/agent-session-tree.d.ts.map +1 -1
- package/dist/core/agent-session-tree.js +4 -0
- package/dist/core/agent-session-tree.js.map +1 -1
- package/dist/core/agent-session-types.d.ts +4 -0
- package/dist/core/agent-session-types.d.ts.map +1 -1
- package/dist/core/agent-session-types.js.map +1 -1
- package/dist/core/agent-session.d.ts +2 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +1 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/auth-storage-backends.d.ts.map +1 -1
- package/dist/core/auth-storage-backends.js +12 -1
- package/dist/core/auth-storage-backends.js.map +1 -1
- package/dist/core/bounded-model-refresh.d.ts +14 -0
- package/dist/core/bounded-model-refresh.d.ts.map +1 -0
- package/dist/core/bounded-model-refresh.js +38 -0
- package/dist/core/bounded-model-refresh.js.map +1 -0
- package/dist/core/context-tool-pairing.d.ts +4 -4
- package/dist/core/context-tool-pairing.js +4 -4
- package/dist/core/context-tool-pairing.js.map +1 -1
- package/dist/core/http-dispatcher.d.ts.map +1 -1
- package/dist/core/http-dispatcher.js +26 -1
- package/dist/core/http-dispatcher.js.map +1 -1
- package/dist/core/messages.d.ts +18 -6
- package/dist/core/messages.d.ts.map +1 -1
- package/dist/core/messages.js +46 -18
- package/dist/core/messages.js.map +1 -1
- package/dist/core/model-refresh-timeout.d.ts +9 -0
- package/dist/core/model-refresh-timeout.d.ts.map +1 -0
- package/dist/core/model-refresh-timeout.js +9 -0
- package/dist/core/model-refresh-timeout.js.map +1 -0
- package/dist/core/model-runtime-snapshot.d.ts +3 -0
- package/dist/core/model-runtime-snapshot.d.ts.map +1 -1
- package/dist/core/model-runtime-snapshot.js +59 -0
- package/dist/core/model-runtime-snapshot.js.map +1 -1
- package/dist/core/model-runtime.d.ts +7 -1
- package/dist/core/model-runtime.d.ts.map +1 -1
- package/dist/core/model-runtime.js +72 -17
- package/dist/core/model-runtime.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +4 -3
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager-core.d.ts +1 -1
- package/dist/core/session-manager-core.d.ts.map +1 -1
- package/dist/core/session-manager-core.js +2 -2
- package/dist/core/session-manager-core.js.map +1 -1
- package/dist/core/session-manager-entries.d.ts +1 -1
- package/dist/core/session-manager-entries.d.ts.map +1 -1
- package/dist/core/session-manager-entries.js +2 -1
- 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 -2
- package/dist/core/session-manager-history.js.map +1 -1
- package/dist/core/session-manager-types.d.ts +3 -0
- package/dist/core/session-manager-types.d.ts.map +1 -1
- package/dist/core/session-manager-types.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/main-app-mode.d.ts +1 -0
- package/dist/main-app-mode.d.ts.map +1 -1
- package/dist/main-app-mode.js +3 -0
- package/dist/main-app-mode.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +8 -3
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-actions.d.ts +7 -2
- package/dist/modes/interactive/components/chat-session-host-actions.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-actions.js +13 -4
- package/dist/modes/interactive/components/chat-session-host-actions.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-editor.d.ts +3 -1
- package/dist/modes/interactive/components/chat-session-host-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-editor.js +2 -2
- package/dist/modes/interactive/components/chat-session-host-editor.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-events.d.ts +3 -0
- package/dist/modes/interactive/components/chat-session-host-events.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-events.js +5 -4
- package/dist/modes/interactive/components/chat-session-host-events.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host.d.ts +16 -2
- package/dist/modes/interactive/components/chat-session-host.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host.js +35 -5
- package/dist/modes/interactive/components/chat-session-host.js.map +1 -1
- package/dist/modes/interactive/components/index.d.ts +1 -0
- package/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/dist/modes/interactive/components/index.js.map +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts +0 -1
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +25 -35
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-editor-actions.js +2 -1
- package/dist/modes/interactive/interactive-editor-actions.js.map +1 -1
- package/dist/modes/interactive/interactive-input-handling.js +1 -1
- package/dist/modes/interactive/interactive-input-handling.js.map +1 -1
- package/dist/modes/interactive/interactive-mode-surface.d.ts +4 -1
- package/dist/modes/interactive/interactive-mode-surface.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode-surface.js.map +1 -1
- package/dist/modes/interactive/interactive-model-routing.js +4 -1
- package/dist/modes/interactive/interactive-model-routing.js.map +1 -1
- package/dist/modes/interactive/interactive-pause.d.ts +3 -1
- package/dist/modes/interactive/interactive-pause.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-pause.js +5 -1
- package/dist/modes/interactive/interactive-pause.js.map +1 -1
- package/dist/modes/interactive/interactive-queueing.js +3 -3
- package/dist/modes/interactive/interactive-queueing.js.map +1 -1
- package/dist/modes/interactive-engine/engine-health.d.ts +6 -0
- package/dist/modes/interactive-engine/engine-health.d.ts.map +1 -1
- package/dist/modes/interactive-engine/engine-health.js +15 -1
- package/dist/modes/interactive-engine/engine-health.js.map +1 -1
- package/dist/modes/interactive-engine/isolated-auth.js +1 -1
- package/dist/modes/interactive-engine/isolated-auth.js.map +1 -1
- package/dist/modes/interactive-engine/isolated-runtime.d.ts +1 -0
- package/dist/modes/interactive-engine/isolated-runtime.d.ts.map +1 -1
- package/dist/modes/interactive-engine/isolated-runtime.js +7 -1
- package/dist/modes/interactive-engine/isolated-runtime.js.map +1 -1
- package/dist/modes/interactive-engine/remote-model-catalog.d.ts.map +1 -1
- package/dist/modes/interactive-engine/remote-model-catalog.js +5 -1
- package/dist/modes/interactive-engine/remote-model-catalog.js.map +1 -1
- package/dist/modes/rpc/rpc-command-handler.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-command-handler.js +23 -7
- package/dist/modes/rpc/rpc-command-handler.js.map +1 -1
- package/dist/modes/rpc/rpc-input-scheduler.js +1 -1
- package/dist/modes/rpc/rpc-input-scheduler.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +2 -1
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/docs/custom-provider.md +3 -3
- package/docs/extensions.md +3 -1
- package/docs/index.md +10 -0
- package/docs/keybindings.md +1 -1
- package/docs/models.md +2 -0
- package/docs/providers.md +7 -3
- package/docs/quickstart.md +7 -1
- package/docs/session-format.md +2 -0
- package/docs/termux.md +3 -1
- package/docs/usage.md +3 -2
- package/docs/workflows.md +82 -46
- package/npm-shrinkwrap.json +59 -27
- package/package.json +3 -3
- package/dist/builtin/workflows/src/tui/toast.ts +0 -107
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { flattenTruncatedString } from "./flat-string.js";
|
|
2
|
+
import type {
|
|
3
|
+
PendingPrompt,
|
|
4
|
+
RunSnapshot,
|
|
5
|
+
StageInputRequest,
|
|
6
|
+
StageSnapshot,
|
|
7
|
+
StoreSnapshot,
|
|
8
|
+
ToolEvent,
|
|
9
|
+
ToolNodeSnapshot,
|
|
10
|
+
WorkflowChildReplaySnapshot,
|
|
11
|
+
WorkflowNotice,
|
|
12
|
+
} from "./store-types.js";
|
|
13
|
+
import type { WorkflowOutputValues, WorkflowSerializableValue } from "./types.js";
|
|
14
|
+
|
|
15
|
+
export const COMPACT_RESULT_FIELD_LIMIT = 1024;
|
|
16
|
+
|
|
17
|
+
function compactResultField(value: WorkflowSerializableValue | undefined): string | undefined {
|
|
18
|
+
if (typeof value !== "string") return undefined;
|
|
19
|
+
if (value.length <= COMPACT_RESULT_FIELD_LIMIT) return value;
|
|
20
|
+
// Flattened, not just sliced: `graphSnapshot()` memoizes this projection, so a
|
|
21
|
+
// SlicedString here would keep the whole run result alive inside the store.
|
|
22
|
+
return flattenTruncatedString(value.slice(0, COMPACT_RESULT_FIELD_LIMIT));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function compactRunResult(result: WorkflowOutputValues | undefined): WorkflowOutputValues | undefined {
|
|
26
|
+
if (result === undefined) return undefined;
|
|
27
|
+
const status = compactResultField(result.status);
|
|
28
|
+
const summary = compactResultField(result.summary);
|
|
29
|
+
const remainingWork = compactResultField(result.remaining_work);
|
|
30
|
+
const resultText = compactResultField(result.result);
|
|
31
|
+
const compact: WorkflowOutputValues = {
|
|
32
|
+
...(status !== undefined ? { status } : {}),
|
|
33
|
+
...(summary !== undefined ? { summary } : {}),
|
|
34
|
+
...(remainingWork !== undefined ? { remaining_work: remainingWork } : {}),
|
|
35
|
+
...(resultText !== undefined ? { result: resultText } : {}),
|
|
36
|
+
};
|
|
37
|
+
return Object.keys(compact).length === 0 ? undefined : compact;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Both cloners are called only from a `!== undefined` guard at their call sites,
|
|
41
|
+
// so they take and return a concrete value; an internal undefined branch here
|
|
42
|
+
// would be unreachable and would only weaken the type for every caller.
|
|
43
|
+
function clonePrompt(prompt: PendingPrompt): PendingPrompt {
|
|
44
|
+
return {
|
|
45
|
+
...prompt,
|
|
46
|
+
...(prompt.choices !== undefined ? { choices: [...prompt.choices] } : {}),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function cloneInputRequest(request: StageInputRequest): StageInputRequest {
|
|
51
|
+
return {
|
|
52
|
+
...request,
|
|
53
|
+
questions: request.questions.map((question) => ({
|
|
54
|
+
...question,
|
|
55
|
+
options: question.options.map((option) => ({ ...option })),
|
|
56
|
+
})),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function compactToolEvents(events: readonly ToolEvent[]): ToolEvent[] {
|
|
61
|
+
const event = events.at(-1);
|
|
62
|
+
return event === undefined ? [] : [{ name: event.name }];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function compactWorkflowChild(child: WorkflowChildReplaySnapshot): WorkflowChildReplaySnapshot {
|
|
66
|
+
return {
|
|
67
|
+
...child,
|
|
68
|
+
outputs: {},
|
|
69
|
+
outputCount: child.outputCount ?? Object.keys(child.outputs).length,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function compactStage(stage: StageSnapshot): StageSnapshot {
|
|
74
|
+
const {
|
|
75
|
+
toolEvents,
|
|
76
|
+
workflowChild,
|
|
77
|
+
workflowChildRun,
|
|
78
|
+
pendingPrompt,
|
|
79
|
+
promptFootprint,
|
|
80
|
+
inputRequest,
|
|
81
|
+
notices,
|
|
82
|
+
mcpScope: _mcpScope,
|
|
83
|
+
attemptedModels: _attemptedModels,
|
|
84
|
+
modelAttempts: _modelAttempts,
|
|
85
|
+
// Agent-stage results can be unbounded and graph cards do not render
|
|
86
|
+
// them. Durable tool cards use ToolNodeSnapshot.resultSummary instead.
|
|
87
|
+
result: _result,
|
|
88
|
+
...metadata
|
|
89
|
+
} = stage;
|
|
90
|
+
return {
|
|
91
|
+
...metadata,
|
|
92
|
+
parentIds: [...stage.parentIds],
|
|
93
|
+
toolEvents: compactToolEvents(toolEvents),
|
|
94
|
+
...(workflowChild !== undefined ? { workflowChild: compactWorkflowChild(workflowChild) } : {}),
|
|
95
|
+
...(workflowChildRun !== undefined ? { workflowChildRun: { ...workflowChildRun } } : {}),
|
|
96
|
+
...(pendingPrompt !== undefined ? { pendingPrompt: clonePrompt(pendingPrompt) } : {}),
|
|
97
|
+
...(promptFootprint !== undefined ? { promptFootprint: clonePrompt(promptFootprint) } : {}),
|
|
98
|
+
...(inputRequest !== undefined ? { inputRequest: cloneInputRequest(inputRequest) } : {}),
|
|
99
|
+
...(notices !== undefined ? { notices: notices.map((notice) => ({ ...notice })) } : {}),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function compactToolNode(node: ToolNodeSnapshot): ToolNodeSnapshot {
|
|
104
|
+
return { ...node, parentIds: [...node.parentIds] };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function compactRun(run: RunSnapshot): RunSnapshot {
|
|
108
|
+
const { inputs: _inputs, result: sourceResult, stages, toolNodes, pendingPrompt, ...metadata } = run;
|
|
109
|
+
const result = compactRunResult(sourceResult);
|
|
110
|
+
return {
|
|
111
|
+
...metadata,
|
|
112
|
+
inputs: {},
|
|
113
|
+
stages: stages.map(compactStage),
|
|
114
|
+
...(toolNodes !== undefined ? { toolNodes: toolNodes.map(compactToolNode) } : {}),
|
|
115
|
+
...(pendingPrompt !== undefined ? { pendingPrompt: clonePrompt(pendingPrompt) } : {}),
|
|
116
|
+
...(result !== undefined ? { result } : {}),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function createGraphStoreSnapshot(
|
|
121
|
+
runs: readonly RunSnapshot[],
|
|
122
|
+
notices: readonly WorkflowNotice[],
|
|
123
|
+
version: number,
|
|
124
|
+
): StoreSnapshot {
|
|
125
|
+
const snapshot: StoreSnapshot = {
|
|
126
|
+
runs: runs.map(compactRun),
|
|
127
|
+
notices: notices.map((notice) => ({ ...notice })),
|
|
128
|
+
version,
|
|
129
|
+
};
|
|
130
|
+
deepFreezeGraphValue(snapshot);
|
|
131
|
+
return snapshot;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function deepFreezeGraphValue(value: unknown): void {
|
|
135
|
+
if (value === null || typeof value !== "object" || Object.isFrozen(value)) return;
|
|
136
|
+
Object.freeze(value);
|
|
137
|
+
if (Array.isArray(value)) {
|
|
138
|
+
for (const item of value) deepFreezeGraphValue(item);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
for (const key of Object.keys(value as Record<string, unknown>)) {
|
|
142
|
+
deepFreezeGraphValue((value as Record<string, unknown>)[key]);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -139,7 +139,7 @@ export interface RunBlockedPayload {
|
|
|
139
139
|
|
|
140
140
|
/**
|
|
141
141
|
* Appends a `workflow.run.start` entry and labels it for /tree filtering.
|
|
142
|
-
* Label format: `wf:<name>:<
|
|
142
|
+
* Label format: `wf:<name>:<full-run-id>`.
|
|
143
143
|
*/
|
|
144
144
|
export function appendRunStart(api: PersistenceAPI, payload: RunStartPayload): void {
|
|
145
145
|
if (typeof api.appendEntry !== "function") return;
|
|
@@ -156,8 +156,7 @@ export function appendRunStart(api: PersistenceAPI, payload: RunStartPayload): v
|
|
|
156
156
|
ts: payload.ts,
|
|
157
157
|
});
|
|
158
158
|
if (entryId && typeof api.setLabel === "function") {
|
|
159
|
-
|
|
160
|
-
api.setLabel(entryId, `wf:${payload.name}:${shortId}`);
|
|
159
|
+
api.setLabel(entryId, `wf:${payload.name}:${payload.runId}`);
|
|
161
160
|
}
|
|
162
161
|
}
|
|
163
162
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The run identifier contract.
|
|
3
|
+
*
|
|
4
|
+
* A run id is a bare `crypto.randomUUID()` value (see `extension/dispatcher.ts`),
|
|
5
|
+
* and the durable DBOS `workflowId` is that same value. Every user-facing surface
|
|
6
|
+
* renders it in full, so every resolver accepts it only in full: there is no
|
|
7
|
+
* unique-prefix fallback and no truncated form that addresses a run.
|
|
8
|
+
*
|
|
9
|
+
* This lives in `shared/` because both the extension resolvers and the durable
|
|
10
|
+
* catalog need it, and `durable/` must not import from `extension/`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Canonical rendered length of a run id, dashes included. */
|
|
14
|
+
export const RUN_ID_LENGTH = 36;
|
|
15
|
+
|
|
16
|
+
const RUN_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* True only for a full 8-4-4-4-12 hex UUID.
|
|
20
|
+
*
|
|
21
|
+
* Rejects a prefix, a 32-character dashless form, and a 36-character string that
|
|
22
|
+
* is the right length but not hex — a transposed or truncated paste should fail
|
|
23
|
+
* loudly here rather than silently miss during lookup.
|
|
24
|
+
*/
|
|
25
|
+
export function isFullRunId(value: string): boolean {
|
|
26
|
+
return RUN_ID_PATTERN.test(value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Reported instead of "not found" so a truncated paste is diagnosable as
|
|
31
|
+
* truncated. A well-formed id that no run happens to carry is a different
|
|
32
|
+
* failure and keeps the not-found message.
|
|
33
|
+
*/
|
|
34
|
+
export function malformedRunIdMessage(target: string): string {
|
|
35
|
+
return `Run id must be a full ${RUN_ID_LENGTH}-character UUID; got "${target}" (${target.length} chars).`;
|
|
36
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createGraphStoreSnapshot } from "./graph-store-snapshot.js";
|
|
1
2
|
import type { PromptAnswerRecord, RunEndMetadata } from "./store-public-types.js";
|
|
2
3
|
import type {
|
|
3
4
|
PendingPrompt,
|
|
@@ -42,6 +43,7 @@ export interface StoreState {
|
|
|
42
43
|
readonly runs: RunSnapshot[];
|
|
43
44
|
readonly notices: WorkflowNotice[];
|
|
44
45
|
readonly listeners: Set<(snap: StoreSnapshot) => void>;
|
|
46
|
+
readonly invalidationListeners: Set<() => void>;
|
|
45
47
|
readonly resolvers: Map<string, ResolverEntry>;
|
|
46
48
|
readonly stagePromptAnswers: Map<string, PromptAnswerRecord>;
|
|
47
49
|
readonly stagePromptDrafts: Map<string, string>;
|
|
@@ -51,6 +53,7 @@ export interface StoreState {
|
|
|
51
53
|
export interface StoreContext {
|
|
52
54
|
readonly state: StoreState;
|
|
53
55
|
snapshot(): StoreSnapshot;
|
|
56
|
+
graphSnapshot(): StoreSnapshot;
|
|
54
57
|
notify(): void;
|
|
55
58
|
bumpAndNotify(): void;
|
|
56
59
|
findRun(runId: string): RunSnapshot | undefined;
|
|
@@ -128,6 +131,7 @@ export function createStoreState(): StoreState {
|
|
|
128
131
|
runs: [],
|
|
129
132
|
notices: [],
|
|
130
133
|
listeners: new Set(),
|
|
134
|
+
invalidationListeners: new Set(),
|
|
131
135
|
resolvers: new Map(),
|
|
132
136
|
stagePromptAnswers: new Map(),
|
|
133
137
|
stagePromptDrafts: new Map(),
|
|
@@ -136,16 +140,40 @@ export function createStoreState(): StoreState {
|
|
|
136
140
|
}
|
|
137
141
|
|
|
138
142
|
export function createStoreContext(state: StoreState = createStoreState()): StoreContext {
|
|
143
|
+
let cachedGraphVersion = -1;
|
|
144
|
+
let cachedGraphSnapshot: StoreSnapshot | undefined;
|
|
145
|
+
|
|
139
146
|
function snapshot(): StoreSnapshot {
|
|
140
147
|
return JSON.parse(
|
|
141
148
|
JSON.stringify({ runs: state.runs, notices: state.notices, version: state.version }),
|
|
142
149
|
) as StoreSnapshot;
|
|
143
150
|
}
|
|
144
151
|
|
|
152
|
+
function graphSnapshot(): StoreSnapshot {
|
|
153
|
+
if (cachedGraphSnapshot === undefined || cachedGraphVersion !== state.version) {
|
|
154
|
+
cachedGraphSnapshot = createGraphStoreSnapshot(state.runs, state.notices, state.version);
|
|
155
|
+
cachedGraphVersion = state.version;
|
|
156
|
+
}
|
|
157
|
+
return cachedGraphSnapshot;
|
|
158
|
+
}
|
|
159
|
+
|
|
145
160
|
function notify(): void {
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
161
|
+
for (const fn of state.invalidationListeners) {
|
|
162
|
+
try {
|
|
163
|
+
fn();
|
|
164
|
+
} catch {
|
|
165
|
+
// One observer must not starve later observers or the snapshot channel.
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (state.listeners.size > 0) {
|
|
169
|
+
const snap = snapshot();
|
|
170
|
+
for (const fn of state.listeners) {
|
|
171
|
+
try {
|
|
172
|
+
fn(snap);
|
|
173
|
+
} catch {
|
|
174
|
+
// Store mutations remain authoritative when a consumer fails.
|
|
175
|
+
}
|
|
176
|
+
}
|
|
149
177
|
}
|
|
150
178
|
}
|
|
151
179
|
|
|
@@ -209,6 +237,7 @@ export function createStoreContext(state: StoreState = createStoreState()): Stor
|
|
|
209
237
|
return {
|
|
210
238
|
state,
|
|
211
239
|
snapshot,
|
|
240
|
+
graphSnapshot,
|
|
212
241
|
notify,
|
|
213
242
|
bumpAndNotify,
|
|
214
243
|
findRun,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Store } from "./store-public-types.js";
|
|
2
|
+
import type { StoreSnapshot } from "./store-types.js";
|
|
3
|
+
|
|
4
|
+
export function readGraphStoreSnapshot(store: Store): StoreSnapshot {
|
|
5
|
+
return store.graphSnapshot();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function subscribeStoreInvalidation(store: Store, listener: () => void): () => void {
|
|
9
|
+
return store.subscribeInvalidation(listener);
|
|
10
|
+
}
|
|
@@ -228,5 +228,13 @@ export interface Store {
|
|
|
228
228
|
/** Drop every run and notice. */
|
|
229
229
|
clear(): void;
|
|
230
230
|
snapshot(): StoreSnapshot;
|
|
231
|
+
/**
|
|
232
|
+
* Return one immutable payload-free graph projection per store version.
|
|
233
|
+
* Graph-visible state may change only through a version-bumping store method;
|
|
234
|
+
* direct mutation of values returned by `runs()` would leave this cache stale.
|
|
235
|
+
*/
|
|
236
|
+
graphSnapshot(): StoreSnapshot;
|
|
231
237
|
subscribe(fn: (snap: StoreSnapshot) => void): () => void;
|
|
238
|
+
/** Subscribe synchronously to store invalidation without constructing a full snapshot. */
|
|
239
|
+
subscribeInvalidation(fn: () => void): () => void;
|
|
232
240
|
}
|
|
@@ -27,7 +27,9 @@ type RunStoreMethods = Pick<
|
|
|
27
27
|
| "recordRunResumed"
|
|
28
28
|
| "clear"
|
|
29
29
|
| "snapshot"
|
|
30
|
+
| "graphSnapshot"
|
|
30
31
|
| "subscribe"
|
|
32
|
+
| "subscribeInvalidation"
|
|
31
33
|
>;
|
|
32
34
|
|
|
33
35
|
export function createRunStoreMethods(context: StoreContext): RunStoreMethods {
|
|
@@ -179,6 +181,7 @@ export function createRunStoreMethods(context: StoreContext): RunStoreMethods {
|
|
|
179
181
|
if (!run) return false;
|
|
180
182
|
if (TERMINAL_STATUSES.has(run.status)) return false;
|
|
181
183
|
const wasPaused = run.status === "paused";
|
|
184
|
+
const enteringQuit = metadata?.exitReason === "quit" && run.exitReason !== "quit";
|
|
182
185
|
if (!wasPaused) {
|
|
183
186
|
run.status = "paused";
|
|
184
187
|
run.pausedAt = pausedAt ?? Date.now();
|
|
@@ -186,6 +189,7 @@ export function createRunStoreMethods(context: StoreContext): RunStoreMethods {
|
|
|
186
189
|
}
|
|
187
190
|
if (metadata?.resumable !== undefined) run.resumable = metadata.resumable;
|
|
188
191
|
if (metadata?.exitReason !== undefined) run.exitReason = metadata.exitReason;
|
|
192
|
+
if (enteringQuit) run.quitAt = Date.now();
|
|
189
193
|
if (wasPaused && metadata === undefined) return false;
|
|
190
194
|
context.bumpAndNotify();
|
|
191
195
|
return true;
|
|
@@ -201,6 +205,7 @@ export function createRunStoreMethods(context: StoreContext): RunStoreMethods {
|
|
|
201
205
|
run.pausedDurationMs = accumulatePausedDurationMs(run.pausedDurationMs, run.pausedAt, resumedTs);
|
|
202
206
|
run.resumedAt = resumedTs;
|
|
203
207
|
run.pausedAt = undefined;
|
|
208
|
+
delete run.quitAt;
|
|
204
209
|
delete run.exitReason;
|
|
205
210
|
context.bumpAndNotify();
|
|
206
211
|
return true;
|
|
@@ -230,12 +235,21 @@ export function createRunStoreMethods(context: StoreContext): RunStoreMethods {
|
|
|
230
235
|
return context.snapshot();
|
|
231
236
|
},
|
|
232
237
|
|
|
238
|
+
graphSnapshot(): StoreSnapshot {
|
|
239
|
+
return context.graphSnapshot();
|
|
240
|
+
},
|
|
241
|
+
|
|
233
242
|
subscribe(fn: (snap: StoreSnapshot) => void): () => void {
|
|
234
243
|
state.listeners.add(fn);
|
|
235
244
|
return () => {
|
|
236
245
|
state.listeners.delete(fn);
|
|
237
246
|
};
|
|
238
247
|
},
|
|
248
|
+
|
|
249
|
+
subscribeInvalidation(fn: () => void): () => void {
|
|
250
|
+
state.invalidationListeners.add(fn);
|
|
251
|
+
return () => state.invalidationListeners.delete(fn);
|
|
252
|
+
},
|
|
239
253
|
};
|
|
240
254
|
}
|
|
241
255
|
|
|
@@ -153,6 +153,8 @@ export interface WorkflowChildReplaySnapshot {
|
|
|
153
153
|
/** True when the child reached this terminal status through ctx.exit(). */
|
|
154
154
|
readonly exited?: boolean;
|
|
155
155
|
readonly outputs: WorkflowOutputValues;
|
|
156
|
+
/** Payload-free output count used by compact graph projections. */
|
|
157
|
+
readonly outputCount?: number;
|
|
156
158
|
readonly exitReason?: string;
|
|
157
159
|
}
|
|
158
160
|
|
|
@@ -282,6 +284,8 @@ export interface RunSnapshot {
|
|
|
282
284
|
pausedDurationMs?: number;
|
|
283
285
|
/** Timestamp set when a controlled pause begins; cleared on resume. */
|
|
284
286
|
pausedAt?: number;
|
|
287
|
+
/** Timestamp when the run entered resumable quit state; display-only expiry marker. */
|
|
288
|
+
quitAt?: number;
|
|
285
289
|
/** Timestamp recorded on the most recent resume from a paused state. */
|
|
286
290
|
resumedAt?: number;
|
|
287
291
|
result?: WorkflowOutputValues;
|
|
@@ -392,6 +392,8 @@ export interface WorkflowRunContext<
|
|
|
392
392
|
> {
|
|
393
393
|
/** Typed inputs provided by the caller, validated against the input schema. */
|
|
394
394
|
readonly inputs: TInputs;
|
|
395
|
+
/** Stable owning workflow-run id for durable run-scoped artifacts. */
|
|
396
|
+
readonly runId?: string;
|
|
395
397
|
/** Invocation working directory for workflow-owned artifacts. Defaults to the host process cwd when omitted. */
|
|
396
398
|
readonly cwd?: string;
|
|
397
399
|
/** Intentionally end this workflow run from any call depth. */
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { mkdir, readdir, rm, stat } from "node:fs/promises";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { getAgentDir, getEnvValue } from "@bastani/atomic";
|
|
6
|
+
import type { DurableWorkflowBackend } from "../durable/backend.js";
|
|
7
|
+
import { getDurableBackend } from "../durable/factory.js";
|
|
8
|
+
import { type WorkflowRunResumeCandidate, workflowRunHasPausedState } from "../durable/resume-eligibility.js";
|
|
9
|
+
import { store } from "./store.js";
|
|
10
|
+
import type { RunSnapshot } from "./store-types.js";
|
|
11
|
+
|
|
12
|
+
/** Maximum age of durable workflow run artifacts before the next workflow write prunes them. */
|
|
13
|
+
export const WORKFLOW_ARTIFACT_RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
|
|
14
|
+
|
|
15
|
+
/** Overrides the durable workflow-artifact root, mirroring the agent-directory override convention. */
|
|
16
|
+
export const ENV_WORKFLOW_ARTIFACT_DIR = "ATOMIC_WORKFLOW_ARTIFACT_DIR";
|
|
17
|
+
|
|
18
|
+
export type WorkflowArtifactRunState = "protected" | "terminal" | "orphan";
|
|
19
|
+
export type WorkflowArtifactRunStateResolver = (runId: string) => WorkflowArtifactRunState | undefined;
|
|
20
|
+
|
|
21
|
+
const ARTIFACT_PRUNE_RECHECK_MS = 60 * 60 * 1000;
|
|
22
|
+
const lastArtifactPruneAt = new Map<string, number>();
|
|
23
|
+
const pendingArtifactPrunes = new Map<string, Promise<void>>();
|
|
24
|
+
|
|
25
|
+
function workflowArtifactRoot(): string {
|
|
26
|
+
const override = getEnvValue(ENV_WORKFLOW_ARTIFACT_DIR);
|
|
27
|
+
if (override !== undefined && override.length > 0) return override;
|
|
28
|
+
return join(dirname(getAgentDir()), "workflows");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function workflowArtifactRunsRoot(): string {
|
|
32
|
+
return join(workflowArtifactRoot(), "runs");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function safeRunId(runId: string): string {
|
|
36
|
+
const safe = runId.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
37
|
+
return safe.length > 0 ? safe : "run";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function workflowArtifactRunPath(runId: string): string {
|
|
41
|
+
return join(workflowArtifactRunsRoot(), safeRunId(runId));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Detect a run-scoped artifact path anywhere in a run's result or stages.
|
|
46
|
+
*
|
|
47
|
+
* `JSON.stringify` escapes each Windows separator, so `…\runs\<id>\…` serializes
|
|
48
|
+
* as `…\\runs\\<id>\\…` and a single backslash-to-slash pass leaves `//runs//`.
|
|
49
|
+
* Collapsing repeated separators after that pass makes the probe behave the same
|
|
50
|
+
* on both platforms; without it every Windows artifact reference was invisible,
|
|
51
|
+
* so no run was ever reported as having lost its artifacts.
|
|
52
|
+
*/
|
|
53
|
+
export function workflowRunHasArtifactReference(run: Pick<RunSnapshot, "id" | "result" | "stages">): boolean {
|
|
54
|
+
const serialized = JSON.stringify({ result: run.result, stages: run.stages });
|
|
55
|
+
if (serialized === undefined) return false;
|
|
56
|
+
const normalized = serialized.replaceAll("\\", "/").replace(/\/{2,}/g, "/");
|
|
57
|
+
return normalized.includes(`/runs/${safeRunId(run.id)}/`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Return artifact integrity only for runs whose snapshot names a run-scoped artifact. */
|
|
61
|
+
export function workflowRunArtifactsIntact(run: Pick<RunSnapshot, "id" | "result" | "stages">): boolean | undefined {
|
|
62
|
+
if (!workflowRunHasArtifactReference(run)) return undefined;
|
|
63
|
+
return existsSync(workflowArtifactRunPath(run.id));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Build the one resume candidate shape used by all live-run resume surfaces. */
|
|
67
|
+
export function workflowRunResumeCandidate(run: RunSnapshot): WorkflowRunResumeCandidate {
|
|
68
|
+
const artifactsIntact = workflowRunArtifactsIntact(run);
|
|
69
|
+
let hasDurableCheckpoint: boolean | undefined;
|
|
70
|
+
try {
|
|
71
|
+
hasDurableCheckpoint = getDurableBackend().isWorkflowLoadable(run.id);
|
|
72
|
+
} catch {
|
|
73
|
+
// A backend that is not ready cannot prove a missing checkpoint.
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
...run,
|
|
77
|
+
hasPausedState: workflowRunHasPausedState(run),
|
|
78
|
+
...(hasDurableCheckpoint === undefined ? {} : { hasDurableCheckpoint }),
|
|
79
|
+
...(artifactsIntact === undefined ? {} : { artifactsIntact }),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Protection means live work, not "could theoretically be resumed".
|
|
85
|
+
*
|
|
86
|
+
* A run still in flight, holding a pending prompt, or explicitly quit keeps its
|
|
87
|
+
* artifacts indefinitely, because a resume is expected and those files are its
|
|
88
|
+
* inputs. A run that has FAILED is terminal: it is retryable, but the retention
|
|
89
|
+
* window is precisely the grace period it gets. Treating `resumable === true` as
|
|
90
|
+
* protection made every failed run permanent, so repeated recoverable failures
|
|
91
|
+
* accumulated run-scoped artifacts that the advertised window never reclaimed.
|
|
92
|
+
*/
|
|
93
|
+
function storeRunState(run: RunSnapshot): WorkflowArtifactRunState {
|
|
94
|
+
const hasPendingInput =
|
|
95
|
+
run.pendingPrompt !== undefined ||
|
|
96
|
+
run.stages.some(
|
|
97
|
+
(stage) => stage.status === "awaiting_input" || stage.status === "paused" || stage.status === "blocked",
|
|
98
|
+
);
|
|
99
|
+
if (
|
|
100
|
+
hasPendingInput ||
|
|
101
|
+
run.status === "pending" ||
|
|
102
|
+
run.status === "running" ||
|
|
103
|
+
run.status === "paused" ||
|
|
104
|
+
run.status === "blocked" ||
|
|
105
|
+
run.exitReason === "quit"
|
|
106
|
+
) {
|
|
107
|
+
return "protected";
|
|
108
|
+
}
|
|
109
|
+
return "terminal";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function durableRunState(backend: DurableWorkflowBackend, runId: string): WorkflowArtifactRunState | undefined {
|
|
113
|
+
const handle = backend.getLoadableWorkflow(runId);
|
|
114
|
+
if (handle === undefined) return undefined;
|
|
115
|
+
// `isDurableWorkflowResumable` deliberately accepts `failed`, so it cannot be
|
|
116
|
+
// used here: it would pin a failed run's artifacts forever.
|
|
117
|
+
if (
|
|
118
|
+
handle.status === "running" ||
|
|
119
|
+
handle.status === "paused" ||
|
|
120
|
+
handle.status === "blocked" ||
|
|
121
|
+
handle.pendingPrompts > 0
|
|
122
|
+
) {
|
|
123
|
+
return "protected";
|
|
124
|
+
}
|
|
125
|
+
return "terminal";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Snapshot the live store and durable backend once for a pruning pass. This
|
|
130
|
+
* keeps the transcript write path from querying durable state once per stage.
|
|
131
|
+
*/
|
|
132
|
+
export function createWorkflowArtifactRunStateResolver(): WorkflowArtifactRunStateResolver {
|
|
133
|
+
const localRuns = new Map(store.runs().map((run) => [run.id, run]));
|
|
134
|
+
let backend: DurableWorkflowBackend | undefined;
|
|
135
|
+
let backendReady = true;
|
|
136
|
+
try {
|
|
137
|
+
backend = getDurableBackend();
|
|
138
|
+
} catch {
|
|
139
|
+
backendReady = false;
|
|
140
|
+
}
|
|
141
|
+
return (runId: string): WorkflowArtifactRunState | undefined => {
|
|
142
|
+
const local = localRuns.get(runId) ?? [...localRuns.values()].find((run) => safeRunId(run.id) === runId);
|
|
143
|
+
const localState = local === undefined ? undefined : storeRunState(local);
|
|
144
|
+
if (!backendReady) {
|
|
145
|
+
// An unavailable durable backend cannot prove that even a terminal-looking
|
|
146
|
+
// local snapshot has no resumable durable counterpart.
|
|
147
|
+
return localState === "protected" ? "protected" : undefined;
|
|
148
|
+
}
|
|
149
|
+
let durable: WorkflowArtifactRunState | undefined;
|
|
150
|
+
if (backend !== undefined) {
|
|
151
|
+
try {
|
|
152
|
+
durable = durableRunState(backend, runId);
|
|
153
|
+
} catch {
|
|
154
|
+
backendReady = false;
|
|
155
|
+
return localState === "protected" ? "protected" : undefined;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (durable === "protected" || localState === "protected") return "protected";
|
|
159
|
+
if (durable === "terminal" || localState === "terminal") return "terminal";
|
|
160
|
+
return "orphan";
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Resolve one run's artifact ownership from a fresh durable/store snapshot. */
|
|
165
|
+
export function resolveWorkflowArtifactRunState(runId: string): WorkflowArtifactRunState | undefined {
|
|
166
|
+
return createWorkflowArtifactRunStateResolver()(runId);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Delete the durable owner of a terminal run before its artifacts are removed. */
|
|
170
|
+
async function deleteTerminalDurableEntry(runId: string): Promise<boolean> {
|
|
171
|
+
let backend: DurableWorkflowBackend;
|
|
172
|
+
try {
|
|
173
|
+
backend = getDurableBackend();
|
|
174
|
+
} catch {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
const result = await backend.deleteWorkflowIfInactive(runId);
|
|
179
|
+
return result.ok || result.reason === "not_found";
|
|
180
|
+
} catch {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Remove stale run directories, retaining artifacts for resumable/non-terminal runs. */
|
|
186
|
+
export async function pruneWorkflowArtifactRuns(
|
|
187
|
+
root = workflowArtifactRunsRoot(),
|
|
188
|
+
now = Date.now(),
|
|
189
|
+
stateResolver?: WorkflowArtifactRunStateResolver,
|
|
190
|
+
): Promise<void> {
|
|
191
|
+
try {
|
|
192
|
+
const entries = await readdir(root, { withFileTypes: true, encoding: "utf8" });
|
|
193
|
+
for (const entry of entries) {
|
|
194
|
+
if (!entry.isDirectory()) continue;
|
|
195
|
+
const entryPath = join(root, entry.name);
|
|
196
|
+
let metadata: Awaited<ReturnType<typeof stat>>;
|
|
197
|
+
try {
|
|
198
|
+
metadata = await stat(entryPath);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") continue;
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
if (now - metadata.mtimeMs <= WORKFLOW_ARTIFACT_RETENTION_MS) continue;
|
|
204
|
+
// Without authoritative state, preserve the directory. An unknown run is
|
|
205
|
+
// not safe to classify as history merely because its mtime is old.
|
|
206
|
+
if (stateResolver === undefined) continue;
|
|
207
|
+
const state = stateResolver(entry.name);
|
|
208
|
+
if (state !== "terminal" && state !== "orphan") continue;
|
|
209
|
+
// Delete the durable owner first. If authoritative deletion is unavailable
|
|
210
|
+
// or refuses an active run, preserve the directory rather than leaving a
|
|
211
|
+
// durable entry pointing at missing artifacts.
|
|
212
|
+
if (state === "terminal" && !(await deleteTerminalDurableEntry(entry.name))) continue;
|
|
213
|
+
await rm(entryPath, { recursive: true, force: true });
|
|
214
|
+
}
|
|
215
|
+
} catch (error) {
|
|
216
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return;
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function pruneArtifactRootIfDue(root: string, now: number): Promise<void> {
|
|
222
|
+
const pending = pendingArtifactPrunes.get(root);
|
|
223
|
+
if (pending !== undefined) {
|
|
224
|
+
try {
|
|
225
|
+
await pending;
|
|
226
|
+
} catch {
|
|
227
|
+
// Pruning is opportunistic and must never block a stage artifact write.
|
|
228
|
+
}
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const previous = lastArtifactPruneAt.get(root);
|
|
232
|
+
if (previous !== undefined && now - previous < ARTIFACT_PRUNE_RECHECK_MS) return;
|
|
233
|
+
const operation = pruneWorkflowArtifactRuns(root, now, createWorkflowArtifactRunStateResolver());
|
|
234
|
+
pendingArtifactPrunes.set(root, operation);
|
|
235
|
+
try {
|
|
236
|
+
await operation;
|
|
237
|
+
} catch {
|
|
238
|
+
// A stale sibling that cannot be inspected is retained; writes continue.
|
|
239
|
+
} finally {
|
|
240
|
+
lastArtifactPruneAt.set(root, now);
|
|
241
|
+
pendingArtifactPrunes.delete(root);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Ensure a run-scoped durable directory exists and perform bounded state-aware GC at most hourly. */
|
|
246
|
+
export async function ensureWorkflowArtifactRunDirectory(runId: string): Promise<string> {
|
|
247
|
+
const root = workflowArtifactRunsRoot();
|
|
248
|
+
await pruneArtifactRootIfDue(root, Date.now());
|
|
249
|
+
const directory = workflowArtifactRunPath(runId);
|
|
250
|
+
await mkdir(directory, { recursive: true });
|
|
251
|
+
return directory;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Create a unique durable artifact directory beneath the owning run and prune expired siblings. */
|
|
255
|
+
export async function createWorkflowArtifactDirectory(runId?: string): Promise<string> {
|
|
256
|
+
const effectiveRunId = runId ?? randomUUID();
|
|
257
|
+
const runDirectory = await ensureWorkflowArtifactRunDirectory(effectiveRunId);
|
|
258
|
+
const artifactDirectory = join(runDirectory, `artifact-${randomUUID()}`);
|
|
259
|
+
await mkdir(artifactDirectory, { recursive: true });
|
|
260
|
+
return artifactDirectory;
|
|
261
|
+
}
|