@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
|
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.9.3-alpha.3] - 2026-06-27
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Published a synchronized Atomic 0.9.3-alpha.3 prerelease for the web-access extension; no web-access extension changes were made after 0.9.2.
|
|
12
|
+
|
|
7
13
|
## [0.9.2] - 2026-06-23
|
|
8
14
|
|
|
9
15
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bastani/web-access",
|
|
3
|
-
"version": "0.9.3-alpha.
|
|
3
|
+
"version": "0.9.3-alpha.3",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Atomic extension for web search, URL fetching, GitHub repo cloning, PDF/video extraction. Fork of: https://github.com/nicobailon/pi-web-access",
|
|
6
6
|
"contributors": [
|
|
@@ -6,10 +6,59 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.9.3-alpha.3] - 2026-06-27
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Published the Atomic 0.9.3-alpha.3 prerelease for the workflows extension from the same code as 0.9.3-alpha.2; no workflow changes were made after the previous prerelease.
|
|
14
|
+
|
|
15
|
+
## [0.9.3-alpha.2] - 2026-06-27
|
|
16
|
+
|
|
17
|
+
### Breaking Changes
|
|
18
|
+
|
|
19
|
+
- Workflow durability is now enabled by default with the zero-infrastructure per-workflow file backend rooted under `~/.atomic/workflow-durable`; the previous process-local in-memory default and `ATOMIC_WORKFLOW_DURABLE_DIR` opt-in path were removed. In-memory durability remains available for explicit test/custom backend overrides and for the privacy opt-out `ATOMIC_WORKFLOW_DURABLE=0`/`false`/`off`/`memory`. ([#1498](https://github.com/bastani-inc/atomic/issues/1498))
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- Added `StageContext.sendUserMessage(content, { deliverAs? })` so workflow authors can inject a normal follow-on user turn into a stage session after `prompt()` resolves. Idle sessions start immediately, streaming sessions queue as follow-up by default (or steering with `deliverAs: "steer"`), schema-backed stages can continue after their one allowed structured `prompt()` without tripping the one-shot prompt guard, follow-on turns preserve the stage MCP allow/deny scope, and native stage sessions accept the same string or text/image block content as `AgentSession.sendUserMessage()`. Durable replayed cached stages now reject this live-session operation instead of silently ignoring it. ([#1520](https://github.com/bastani-inc/atomic/issues/1520))
|
|
24
|
+
- Added cross-session workflow resumability via a durable workflow backend seam (`packages/workflows/src/durable/`). Workflows now persist `ctx.*` operation checkpoints (tool, ui, stage) to a durable backend so a new Atomic session can resume a workflow started in a prior session from the last completed checkpoint, without re-running completed work. Cross-session persistence is enabled by default through a lock-protected per-workflow file backend at `~/.atomic/workflow-durable`; no environment variable is required. When `DBOS_SYSTEM_DATABASE_URL` is set, Atomic lazily initializes DBOS with Postgres-backed workflow/control records and falls back to the file backend if DBOS is unavailable. Top-level workflow metadata is mirrored as `workflow.durable.checkpoint` session entries for `/workflow resume` discovery. ([#1498](https://github.com/bastani-inc/atomic/issues/1498))
|
|
25
|
+
- Introduced `ctx.tool(name, args, fn, options?)` — a durable cached tool execution primitive that runs arbitrary TypeScript code and caches the result so completed side effects are not repeated on resume. Supports optional retry with exponential backoff via `retriesAllowed`, `maxAttempts`, `intervalMs`, and `backoffRate` options. Only `ctx.*` blocks produce durable checkpoints. ([#1498](https://github.com/bastani-inc/atomic/issues/1498))
|
|
26
|
+
- Added durable `ctx.ui` checkpointing: completed `ctx.ui.input` / `confirm` / `select` / `editor` / `custom` responses are cached by prompt identity that includes method, label/message, options, and call order, so repeated prompts do not collide and a resumed workflow does not re-ask already-answered prompts. ([#1498](https://github.com/bastani-inc/atomic/issues/1498))
|
|
27
|
+
- Added durable `ctx.stage` / `ctx.task` / `ctx.chain` / `ctx.parallel` / child `ctx.workflow` checkpointing: completed stage-like outputs are recorded with stable ordinal replay keys and replayed on resume, skipping re-execution while avoiding checkpoint id collisions after redispatch. Chain and parallel items reuse durable task checkpoints, and child workflow calls checkpoint the completed child result at the parent boundary. ([#1498](https://github.com/bastani-inc/atomic/issues/1498))
|
|
28
|
+
- Wired `/workflow resume` into the TUI command surface to fall back to the cross-session durable resume catalog when a target id is not a live run, and to surface durable root workflow history in fresh sessions. It lists resumable workflows by top-level id and resumes via DBOS-style replay (re-dispatch with the original workflow id so completed checkpoints are skipped), mirroring `/resume` ergonomics. ([#1498](https://github.com/bastani-inc/atomic/issues/1498))
|
|
29
|
+
- Added `@dbos-inc/dbos-sdk` as an optional dependency of `@bastani/atomic` so DBOS-backed durable execution is available without a separate install when the SDK resolves. The workflows package remains dependency-free at runtime; the DBOS adapter is loaded lazily only when `DBOS_SYSTEM_DATABASE_URL` is set. ([#1498](https://github.com/bastani-inc/atomic/issues/1498))
|
|
30
|
+
- Added a cross-session resume catalog (`scanResumableWorkflows`) that scans session JSONL files for durable checkpoint entries to build the `/workflow resume` selector list, displaying root workflow name, status, completed checkpoint count, and pending prompts. The scanner understands Atomic's actual extension custom-entry JSONL shape (`type: "custom"`, `customType: "workflow.durable.checkpoint"`) as well as legacy direct checkpoint rows.
|
|
31
|
+
- Added cancellation, failure, and retry semantics for durable workflows: killed/cancelled workflows are marked `cancelled` and excluded from automatic resume; recoverable failures remain in the resumable list; non-recoverable failures are filtered out; `ctx.tool` retries with exponential backoff; pending `ctx.ui` prompts leave off on resume for the user to answer.
|
|
32
|
+
- Added DBOS read-side hydration: when `DBOS_SYSTEM_DATABASE_URL` is set, a fresh Atomic process now hydrates its in-memory checkpoint mirror from DBOS on `/workflow resume` so workflows persisted by a prior session can be discovered and replayed without prior in-process state. Checkpoint records are stored as structured envelopes (including kind, checkpoint id, args hash, prompt hash, and replay key) in DBOS so hydration fully reconstructs the original checkpoint metadata. Root workflow metadata is mirrored separately, so DBOS helper-workflow completion does not make Atomic roots appear completed; `ctx.tool`/`ctx.ui`/`ctx.task` await async DBOS checkpoint persistence before returning side-effect results. Legacy/simple DBOS step outputs without the envelope are handled gracefully as generic stage checkpoints. ([#1498](https://github.com/bastani-inc/atomic/issues/1498))
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- Added direct mouse-click activation for visible workflow graph nodes: clicking a node now focuses it and opens/attaches it through the same path as pressing Enter, with hit-testing based on the rendered node rectangles so overlay chrome/scroll geometry stays in sync while wheel scrolling and empty-space/chrome clicks remain safe no-ops. ([#1521](https://github.com/bastani-inc/atomic/issues/1521))
|
|
37
|
+
- Fixed attached workflow stage chats to capture mouse/trackpad wheel events by default and enable button-event SGR terminal reporting, keeping scroll gestures inside the active stage transcript or prompt instead of falling through to terminal/main-chat scrollback; `ctrl+t` now toggles true copy mode by disabling workflow-chat mouse reporting so terminal/tmux text selection works normally. ([#1519](https://github.com/bastani-inc/atomic/issues/1519))
|
|
38
|
+
- Routed idle `StageContext.sendUserMessage()` turns through the workflow stage guard so first-call injected turns record normal stage lifecycle and all idle injected turns observe abort/kill wiring, MCP scope, readiness handling, and the concurrency limiter. ([#1520](https://github.com/bastani-inc/atomic/issues/1520))
|
|
39
|
+
- Fixed attached workflow stage chats so stale terminal, paused, blocked, skipped, or otherwise no-longer-running live stage handles, terminal run/stage transitions before `agent_end`, reopened terminal chats, late post-terminal subagent tool updates, and partial subagent workflow-graph progress no longer keep the `Working...` spinner, animation tick, subagent renderer interval, renderer animation registry entry, or subagent progress spinner active, while genuine follow-up/prompt/resume `agent_start` work still shows progress on retained terminal stage chats. ([#1518](https://github.com/bastani-inc/atomic/issues/1518))
|
|
40
|
+
- Fixed `/workflow resume`'s interactive picker to reuse the same Atomic `/resume` session-selector tree chrome for live, paused, failed-resumable, and cross-session durable workflows, replacing the hand-rolled durable/combined picker so the selector is cosmetically consistent across resume flows. ([#1498](https://github.com/bastani-inc/atomic/issues/1498))
|
|
41
|
+
- Matched `/resume` empty-state behavior for interactive `/workflow resume` by opening the shared empty selector when no resumable workflow rows remain after filtering instead of printing an error. ([#1498](https://github.com/bastani-inc/atomic/issues/1498))
|
|
42
|
+
- Hid `/workflow resume` selector entries that only exist as stale session-cache metadata with no matching durable backend handle and hid durable `running`/`failed`/`blocked` rows whose workflow definition is no longer registered, filtering old pre-checkpoint-engine/test workflow runs instead of surfacing rows that can only fail as stale or missing-definition. ([#1498](https://github.com/bastani-inc/atomic/issues/1498))
|
|
43
|
+
- Persisted terminal non-resumable durable metadata during finalization so non-recoverable failed/blocked workflows do not remain visible in `/workflow resume` as resumable entries after they are completely finished. ([#1498](https://github.com/bastani-inc/atomic/issues/1498))
|
|
44
|
+
- Delayed durable `/workflow resume` discovery for `running`/`paused` workflows until the first durable checkpoint or pending checkpointable prompt exists, records resumable LM-stage session metadata as soon as a stage opens, and reopens that exact Atomic/Pi session file on durable resume so eligible stages continue mid-session while completed tool/UI/stage checkpoints still replay all-or-nothing. CLI/orchestrator quit now leaves durable-progress workflows resumable instead of treating panel close as an authoritative kill; `/workflow kill` remains the explicit non-resumable disposal path, and successful completion still marks the root workflow terminal and removes the row once the whole workflow terminates to the end. ([#1498](https://github.com/bastani-inc/atomic/issues/1498))
|
|
45
|
+
- Fixed repeated quit/resume durable LM-stage cycles by keeping in-progress stage-session metadata separate from completed stage-output checkpoints, persisting DBOS checkpoint records by checkpoint id rather than replay key, removing stale quit snapshots before reusing the original durable workflow id, and sending a single `Continue` continuation message to the recorded LM session instead of replaying the original workflow prompt. ([#1498](https://github.com/bastani-inc/atomic/issues/1498))
|
|
46
|
+
- Made `/workflow resume` list only inactive workflows: actively-running durable runs are hidden from the selector and refused on resume (in-session or cross-session) with an intuitive error pointing at `/workflow connect`/`/workflow kill`, while quitting the CLI/panel flips the durable handle from `running` to `paused` so it re-enters the resumable set. ([#1498](https://github.com/bastani-inc/atomic/issues/1498))
|
|
47
|
+
- Fixed DBOS durable metadata updates to use versioned metadata records with latest-timestamp hydration, removed invalid DBOS `duplicationPolicy` parameters, awaited completed stage checkpoint writes before returning to workflow code where practical, awaited DBOS initialization before first dispatch/list/resume in DBOS-configured runtimes, opened an interactive durable picker for no-arg `/workflow resume` when only durable entries exist (including sessions with only completed local runs), distinguished repeated same-name/same-args `ctx.tool` calls by stable ordinal identity, and made retry backoff observe cancellation before later attempts run. ([#1498](https://github.com/bastani-inc/atomic/issues/1498))
|
|
48
|
+
- Fixed durable stage lifecycle persistence so workflow completion waits for pending backend checkpoint writes (including DBOS async writes) and reports a failed run when those writes fail instead of acknowledging success before checkpoints are durable. Terminal durable status metadata is flushed before completed/failed/cancelled root runs return. Cached `ctx.task`, `ctx.chain`/`ctx.parallel` items, and child `ctx.workflow` replay now record completed replay nodes in the workflow graph/store, matching cached `ctx.stage` visibility. DBOS `recordCheckpointAsync()` updates the replay mirror only after DBOS accepts the checkpoint, so failed durable writes do not make the current process believe a side effect is safely replayable. ([#1498](https://github.com/bastani-inc/atomic/issues/1498))
|
|
49
|
+
- Fixed repeated `ctx.workflow(child)` calls to use a dedicated durable replay-key counter so cache-hit calls do not desync ordinal sequencing across resume; direct completed stages now checkpoint under the durable replay key used for lookup instead of the executor snapshot key; `ctx.exit` terminal states (`cancelled`, `blocked`, `skipped`) are now persisted to durable metadata and flushed; no-arg `/workflow resume` surfaces durable workflow history even when live local runs exist; and `durableBackend` is propagated into child workflow run options so custom backends are shared across parent and child instead of split-braining. ([#1498](https://github.com/bastani-inc/atomic/issues/1498))
|
|
50
|
+
- Fixed durable resume edge cases where workflows discovered only from session JSONL could be dropped before actual resume, schema-backed stage replay returned raw JSON text instead of structured values, parallel fail-fast skipped stages before async finalizers completed, retry backoff left abort listeners behind after normal sleeps, and durable stage checkpointing could ignore an explicit stage replay key. ([#1498](https://github.com/bastani-inc/atomic/issues/1498))
|
|
51
|
+
- Fixed durable replay robustness: child workflow internal `ctx.tool`/`ctx.ui`/`ctx.stage` side effects are now checkpointed under the parent/root durable workflow via a scoped backend keyed by the child boundary, so an interrupted child does not re-execute completed side effects on parent resume (previously child checkpoints were written under a fresh per-run UUID that was never recovered). `/workflow resume` now refuses cache-only entries as `stale` when a workflow has session-JSONL metadata but no durable checkpoint state in the backend, instead of silently re-running from scratch. Durable replay identities (`durableHash`) now use a SHA-256 digest instead of a 32-bit DJB2 hash that could collide across distinct tool/stage identities. `ctx.tool` re-checks cancellation after the tool function resolves and before recording/returning the side-effect result, so a side effect that completes concurrently with cancellation is not persisted as a replayable checkpoint. File-backed durability recovers from a stale lock directory left by a crashed process (backdated beyond a threshold) instead of wedging until the acquire timeout. ([#1498](https://github.com/bastani-inc/atomic/issues/1498))
|
|
52
|
+
- Fixed terminal durable cache suppression so stale session-JSONL entries cannot resurrect workflows the backend knows are completed/cancelled/non-resumable: the backend terminal state now takes precedence over older JSONL cache entries during listing and resume. Successful durable `/workflow resume <id>` now connects/opens the overlay to the resumed run, matching live resume ergonomics. No-arg `/workflow resume` now shows a combined selector (live runs + cross-session durable workflows) so durable workflows are directly selectable even when live runs exist, instead of only being printed as hints. Fixed `ScopedDurableBackend.listCheckpoints` to exclude sibling child scopes that shared a common id prefix (previously re-prefixing caused sibling checkpoints to leak into the wrong scope). Cleaned up a cosmetic merged import line in `run.ts`. ([#1498](https://github.com/bastani-inc/atomic/issues/1498))
|
|
53
|
+
- Fixed empty string stage outputs to be preserved durably as empty strings instead of being collapsed into status objects, so a stage completing with empty assistant text replays as empty rather than appearing to have no checkpoint. Schema-backed stage prompts with empty string results are now also checkpointed. Stage finalization failures (including durable checkpoint write errors) no longer leak the concurrency limiter/semaphore because each cleanup step is independently guarded. Mixed live+durable `/workflow resume` now uses async DBOS-hydrated durable listing (`prepareDurableResumable`) instead of a synchronous in-memory listing so cross-session entries discovered from Postgres are included in the combined selector. Replayed durable stages now register in the graph frontier tracker so parent/lineage topology is preserved for subsequent stages. Dismissing the combined resume picker returns to chat without opening a second live-only picker. ([#1498](https://github.com/bastani-inc/atomic/issues/1498))
|
|
54
|
+
- Fixed Ralph review regressions in durable resume: durable and combined pickers now settle selections before synchronous custom-UI disposal can dismiss them; headless no-arg durable resume prints the catalog instead of awaiting unavailable/no-op picker UI; failed live runs selected from the combined picker resume through the continuation path; durable resume dispatch forwards the interactive/non-interactive command policy; `ctx.ui.custom` replays cached void/`undefined` responses instead of treating them as cache misses; stage finalization errors are rethrown after releasing handles/limiters; and resume integration tests isolate the durable singleton so full integration runs remain deterministic. ([#1498](https://github.com/bastani-inc/atomic/issues/1498))
|
|
55
|
+
- Fixed default file-backed durability to write one JSON state file per root workflow instead of rewriting a shared `state.json` on every checkpoint, avoid all-files scans on point checkpoint lookups, prune completed/cancelled/non-resumable terminal workflow files while retaining running/paused/recoverable-failed files for crash recovery until resumed or killed, use restrictive `0700` durable directories and `0600` state/lock-owner files, fail closed to in-memory durability when no home directory is available, and reclaim stale locks only when their owner marker belongs to a dead process. Added `ATOMIC_WORKFLOW_DURABLE=0`/`false`/`off`/`memory` as a documented privacy opt-out for plaintext cross-session checkpoint persistence. ([#1498](https://github.com/bastani-inc/atomic/issues/1498))
|
|
56
|
+
|
|
9
57
|
## [0.9.3-alpha.1] - 2026-06-25
|
|
10
58
|
|
|
11
59
|
### Changed
|
|
12
60
|
|
|
61
|
+
- Workflow stage sessions are now marked as internal and excluded from the standard Atomic `/resume`, `atomic -r`, and `--continue` history, while remaining fully resumable/inspectable through `/workflow resume`, `/workflow attach`, and the `workflow` tool's `status`/`stages`/`stage`/`resume` actions via the run/stage store and its `sessionFile` links ([#1504](https://github.com/bastani-inc/atomic/issues/1504)).
|
|
13
62
|
- Revised the builtin `goal` worker/continuation prompts and the builtin `ralph` orchestrator prompts to emphasize implementing the requested task through full completion ("do not stop until the objective is complete") instead of preparing a partial implementation for incremental review.
|
|
14
63
|
- Removed model-visible current-iteration/turn/loop information from the builtin `goal`, `ralph`, and `open-claude-design` workflows so models are not biased by which iteration they are on. This drops the worker `Turn: N/M` banner, `Implement iteration N/M` / `Research iteration N/M` framing, "first iteration/worker turn" phrasing, and the live-preview `iteration N/M` label; switches model-relayed artifacts to stable non-ordinal names (`worker-receipt.md`, `review-<reviewer>.json`, `review-round-latest.json`, `orchestrator-report.md`); and strips turn fields from the model-facing goal ledger artifact. Internal UI stage names (e.g. `work-turn-2`, `orchestrator-2`) are unchanged, and the `max_turns`/`max_loops` inputs and `turns_completed`/`iterations_completed` outputs are retained.
|
|
15
64
|
- Raised workflow-stage subagent recursion to the shared five-level nesting budget so workflow stages match main-chat delegation depth.
|
|
@@ -540,7 +540,7 @@ Tradeoff: `Type.Unsafe<T>()` does not deeply validate at runtime — it trusts t
|
|
|
540
540
|
|
|
541
541
|
Input overrides are bare `key=value` tokens (no leading `--`). Values are JSON-parsed when possible, so numbers, booleans, and quoted strings work as expected (e.g. `count=3`, `flag=true`, `prompt="multi word value"`). A whole-object override can be passed as a single JSON token (e.g. `{"prompt":"...","count":3}`). Runtime validation is strict: unknown input keys, missing required values, type mismatches, and invalid `select` choices fail before a named workflow run starts.
|
|
542
542
|
|
|
543
|
-
Workflows always run as **background tasks** in interactive sessions — the chat editor stays free while a run executes. Press **F2** (or `/workflow connect <run-id>`) to attach to the live graph viewer; HIL prompts (`ctx.ui.input/confirm/select/editor/custom`) appear as awaiting-input graph nodes. Press Enter on
|
|
543
|
+
Workflows always run as **background tasks** in interactive sessions — the chat editor stays free while a run executes. Press **F2** (or `/workflow connect <run-id>`) to attach to the live graph viewer; HIL prompts (`ctx.ui.input/confirm/select/editor/custom`) appear as awaiting-input graph nodes. Press Enter on a focused node, or click a visible graph node directly, to open that stage and answer locally, never as a modal dialog over the chat. Attached stage chats capture mouse/trackpad wheel events by default so scrolling stays inside the active stage transcript or prompt instead of falling through to terminal/main-chat scrollback. Press `ctrl+t` to toggle **copy mode**: copy mode disables workflow-chat mouse reporting so normal terminal/tmux text selection can work; press `ctrl+t` again to leave copy mode and restore workflow-chat scrolling. While copy mode is on, wheel/trackpad gestures are handled by the terminal/tmux and may scroll terminal scrollback, so leave copy mode before using the wheel again. Human input is detected when those runtime `ctx.ui.*` calls execute; workflows no longer have a declaration-time HIL flag.
|
|
544
544
|
|
|
545
545
|
Nested `ctx.workflow(...)` calls are displayed as an expanded graph within the top-level run. `/workflow status` and run pickers list only top-level user-launched workflows, not implementation-owned child runs. The `workflow` tool's `stages`, `stage`, `transcript`, `send`, `pause`, `interrupt`, and `resume` actions can still target visible child stage ids, prefixes, or names from the expanded graph; Atomic routes the control action to the owning nested run internally. (`stages`, `stage`, `transcript`, and `send` are `workflow` tool actions, not `/workflow` slash subcommands; the slash command exposes `connect`, `attach`, `pause`, `list`, `status`, `interrupt`, `kill`, `resume`, `reload`, and `inputs`.)
|
|
546
546
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { TSchema } from "typebox";
|
|
2
2
|
export type { Static, TSchema } from "typebox";
|
|
3
|
-
export type { AgentSessionAdapter, CompleteAdapter, CompleteStageOpts, GitWorktreeSetupOptions, GitWorktreeSetupResult, PromptAdapter, PromptOptions, ResolvedInputs, RunResult, RunStatus, StageAdapters, StageStatus, StageOptions, StageContext, StageSnapshot, StageExecutionMeta, StageMcpOptions, StageOutputOptions, StagePromptOptions, StageSessionCreateOptions, StageSessionCreateResult, StageSessionRuntime, WorkflowAction, WorkflowArtifact, WorkflowChainOptions, WorkflowChainStep, WorkflowChildResult, WorkflowContextMode, WorkflowControlEvent, WorkflowCustomToolDefinition, WorkflowCustomUiComponent, WorkflowCustomUiFactory, WorkflowCustomUiKeybindings, WorkflowCustomUiOptions, WorkflowCustomUiOverlayHandle, WorkflowCustomUiOverlayOptions, WorkflowCustomUiTheme, WorkflowCustomUiTui, WorkflowDetails, WorkflowDetailsMode, WorkflowDetailsStatus, WorkflowDirectOptions, WorkflowDirectTaskItem, WorkflowExecutionMode, WorkflowExecutionPolicy, WorkflowExitOptions, WorkflowExitStatus, WorkflowInputBindings, WorkflowInputSchema, WorkflowInputSchemaMap, WorkflowInputValues, WorkflowIntercomSummary, WorkflowMaxOutput, WorkflowMcpPort, WorkflowModelAttempt, WorkflowModelCatalogPort, WorkflowModelFallbackFields, WorkflowModelInfo, WorkflowModelUsage, WorkflowModelValue, WorkflowOutputMode, WorkflowOutputSchema, WorkflowOutputSchemaMap, WorkflowOutputValues, WorkflowParallelChainStep, WorkflowParallelOptions, WorkflowPersistencePort, WorkflowProgressSummary, WorkflowRunChildArgs, WorkflowRunChildOptions, WorkflowRunChildOptionsArgument, WorkflowRunOutput, WorkflowRuntimeConfig, WorkflowSerializableObject, WorkflowSerializablePrimitive, WorkflowSerializableValue, WorkflowSharedTaskDefaults, WorkflowTaskContext, WorkflowTaskContextInput, WorkflowTaskOptions, WorkflowTaskResult, WorkflowTaskSessionFields, WorkflowTaskSessionOptions, WorkflowTaskStep, WorkflowThinkingLevel, WorkflowUIAdapter, WorkflowUIContext, WorkflowWorktreeInputBinding, } from "./shared/authoring-contract.js";
|
|
3
|
+
export type { AgentSessionAdapter, CompleteAdapter, CompleteStageOpts, GitWorktreeSetupOptions, GitWorktreeSetupResult, PromptAdapter, PromptOptions, ResolvedInputs, RunResult, RunStatus, StageAdapters, StageStatus, StageOptions, StageContext, StageSnapshot, StageExecutionMeta, StageMcpOptions, StageOutputOptions, StagePromptOptions, StageSendUserMessageOptions, StageUserMessageContent, StageUserMessageDelivery, StageSessionCreateOptions, StageSessionCreateResult, StageSessionRuntime, WorkflowAction, WorkflowArtifact, WorkflowChainOptions, WorkflowChainStep, WorkflowChildResult, WorkflowContextMode, WorkflowControlEvent, WorkflowCustomToolDefinition, WorkflowCustomUiComponent, WorkflowCustomUiFactory, WorkflowCustomUiKeybindings, WorkflowCustomUiOptions, WorkflowCustomUiOverlayHandle, WorkflowCustomUiOverlayOptions, WorkflowCustomUiTheme, WorkflowCustomUiTui, WorkflowDetails, WorkflowDetailsMode, WorkflowDetailsStatus, WorkflowDirectOptions, WorkflowDirectTaskItem, WorkflowExecutionMode, WorkflowExecutionPolicy, WorkflowExitOptions, WorkflowExitStatus, WorkflowInputBindings, WorkflowInputSchema, WorkflowInputSchemaMap, WorkflowInputValues, WorkflowIntercomSummary, WorkflowMaxOutput, WorkflowMcpPort, WorkflowModelAttempt, WorkflowModelCatalogPort, WorkflowModelFallbackFields, WorkflowModelInfo, WorkflowModelUsage, WorkflowModelValue, WorkflowOutputMode, WorkflowOutputSchema, WorkflowOutputSchemaMap, WorkflowOutputValues, WorkflowParallelChainStep, WorkflowParallelOptions, WorkflowPersistencePort, WorkflowProgressSummary, WorkflowRunChildArgs, WorkflowRunChildOptions, WorkflowRunChildOptionsArgument, WorkflowRunOutput, WorkflowRuntimeConfig, WorkflowSerializableObject, WorkflowSerializablePrimitive, WorkflowSerializableValue, WorkflowSharedTaskDefaults, WorkflowTaskContext, WorkflowTaskContextInput, WorkflowTaskOptions, WorkflowTaskResult, WorkflowTaskSessionFields, WorkflowTaskSessionOptions, WorkflowTaskStep, WorkflowThinkingLevel, WorkflowUIAdapter, WorkflowUIContext, WorkflowWorktreeInputBinding, } from "./shared/authoring-contract.js";
|
|
4
4
|
import type * as AuthoringContract from "./shared/authoring-contract.js";
|
|
5
5
|
import type { AuthoredWorkflowSpec as SharedAuthoredWorkflowSpec, WorkflowInputsFromSchemas, WorkflowOutputsFromSchemas, WorkflowProvidedInputsFromSchemas } from "./shared/workflow-authoring-types.js";
|
|
6
6
|
export type { WorkflowInputsFromSchemas, WorkflowOutputsFromSchemas, WorkflowProvidedInputsFromSchemas, } from "./shared/workflow-authoring-types.js";
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable workflow backend seam.
|
|
3
|
+
*
|
|
4
|
+
* This interface abstracts durable checkpoint storage so the workflow engine
|
|
5
|
+
* can persist `ctx.*` operation results without coupling to a specific storage
|
|
6
|
+
* backend. The default implementation is the zero-infrastructure
|
|
7
|
+
* per-workflow file backend, rooted under `~/.atomic/workflow-durable`, so
|
|
8
|
+
* cross-session resume works without user setup. {@link InMemoryDurableBackend}
|
|
9
|
+
* remains available for isolated tests and explicit custom overrides. The
|
|
10
|
+
* {@link DbosDurableBackend} adapter (in dbos-backend.ts) wraps the real
|
|
11
|
+
* `@dbos-inc/dbos-sdk` when configured.
|
|
12
|
+
*
|
|
13
|
+
* Design:
|
|
14
|
+
* - Only `ctx.*` blocks write checkpoints (tool, ui, stage).
|
|
15
|
+
* - Checkpoints are keyed by (workflowId, kind, checkpointId) for uniqueness.
|
|
16
|
+
* - Tool/ui checkpoints also key on a content hash so identical calls are
|
|
17
|
+
* idempotent (completed side effects are not repeated on resume).
|
|
18
|
+
* - The backend is the checkpoint source of truth; session JSONL entries are
|
|
19
|
+
* a discovery cache.
|
|
20
|
+
*
|
|
21
|
+
* cross-ref: issue #1498 — DBOS integration behind a backend seam.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type { WorkflowSerializableValue } from "../shared/types.js";
|
|
25
|
+
import { createHash } from "node:crypto";
|
|
26
|
+
import type {
|
|
27
|
+
DurableCheckpoint,
|
|
28
|
+
DurableCheckpointEntry,
|
|
29
|
+
DurableToolCheckpoint,
|
|
30
|
+
DurableUiCheckpoint,
|
|
31
|
+
DurableStageCheckpoint,
|
|
32
|
+
DurableWorkflowHandle,
|
|
33
|
+
DurableWorkflowStatus,
|
|
34
|
+
ResumableWorkflowEntry,
|
|
35
|
+
WorkflowSerializableObject,
|
|
36
|
+
} from "./types.js";
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Backend interface
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Input for registering/updating a workflow in the durable backend.
|
|
44
|
+
* Optional fields default to existing values or zero.
|
|
45
|
+
*/
|
|
46
|
+
export type WorkflowRegistrationInput =
|
|
47
|
+
Omit<DurableWorkflowHandle, "completedCheckpoints" | "pendingPrompts" | "updatedAt">
|
|
48
|
+
& Partial<Pick<DurableWorkflowHandle, "completedCheckpoints" | "pendingPrompts" | "updatedAt">>;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Abstract durable checkpoint store. Implementations:
|
|
52
|
+
* - per-workflow file backend — JSON-file-backed; default cross-process resume.
|
|
53
|
+
* - {@link DbosDurableBackend} — wraps `@dbos-inc/dbos-sdk` + Postgres.
|
|
54
|
+
* - {@link InMemoryDurableBackend} — process-local; explicit test/custom use.
|
|
55
|
+
*/
|
|
56
|
+
export interface DurableWorkflowBackend {
|
|
57
|
+
/**
|
|
58
|
+
* True when the backend persists across processes (file/DBOS). When false
|
|
59
|
+
* (in-memory), the engine skips session-cache persistence since there is no
|
|
60
|
+
* cross-session state to discover. This keeps in-process test runs from
|
|
61
|
+
* polluting session JSONL with discovery cache entries.
|
|
62
|
+
*/
|
|
63
|
+
readonly persistent: boolean;
|
|
64
|
+
/** Register or update a workflow's top-level metadata. */
|
|
65
|
+
registerWorkflow(handle: WorkflowRegistrationInput): void;
|
|
66
|
+
|
|
67
|
+
/** Record a completed checkpoint. Idempotent: same (kind, checkpointId) is a no-op. */
|
|
68
|
+
recordCheckpoint(checkpoint: DurableCheckpoint): void;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Optional async checkpoint persistence. DBOS-backed implementations use this
|
|
72
|
+
* so ctx.tool/ctx.ui can await durable writes before returning side-effect
|
|
73
|
+
* results to workflow code.
|
|
74
|
+
*/
|
|
75
|
+
recordCheckpointAsync?(checkpoint: DurableCheckpoint): Promise<void>;
|
|
76
|
+
|
|
77
|
+
/** Optional: wait for serialized durable writes to settle. */
|
|
78
|
+
flush?(): Promise<void>;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Look up a cached tool output by content hash. Returns `undefined` if the
|
|
82
|
+
* tool has not completed. This is how `ctx.tool` avoids re-executing side
|
|
83
|
+
* effects on resume.
|
|
84
|
+
*/
|
|
85
|
+
getToolOutput(workflowId: string, argsHash: string): WorkflowSerializableValue | undefined;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Look up a cached UI response by prompt hash. Returns `undefined` if the
|
|
89
|
+
* prompt has not been answered. This is how `ctx.ui.*` resumes at pending
|
|
90
|
+
* prompts instead of re-asking completed ones.
|
|
91
|
+
*/
|
|
92
|
+
getUiResponse(workflowId: string, promptHash: string): WorkflowSerializableValue | undefined;
|
|
93
|
+
|
|
94
|
+
/** Look up a cached stage output by replay key. */
|
|
95
|
+
getStageOutput(workflowId: string, replayKey: string): WorkflowSerializableValue | undefined;
|
|
96
|
+
|
|
97
|
+
/** Look up resumable stage session metadata by replay key, when available. */
|
|
98
|
+
getStageSession(workflowId: string, replayKey: string): { sessionId?: string; sessionFile?: string } | undefined;
|
|
99
|
+
|
|
100
|
+
/** List all checkpoints for a workflow (in completion order). */
|
|
101
|
+
listCheckpoints(workflowId: string): readonly DurableCheckpoint[];
|
|
102
|
+
|
|
103
|
+
/** Get the top-level workflow handle, or `undefined` if not registered. */
|
|
104
|
+
getWorkflow(workflowId: string): DurableWorkflowHandle | undefined;
|
|
105
|
+
|
|
106
|
+
/** Update workflow status (running/paused/completed/failed/cancelled/blocked). */
|
|
107
|
+
setWorkflowStatus(workflowId: string, status: DurableWorkflowStatus, pendingPrompts?: number, resumable?: boolean): void;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* List resumable workflows (not completed/cancelled, or explicitly marked
|
|
111
|
+
* resumable after failure). Used by the `/workflow resume` selector.
|
|
112
|
+
*/
|
|
113
|
+
listResumableWorkflows(): readonly ResumableWorkflowEntry[];
|
|
114
|
+
|
|
115
|
+
/** Export a session-cache entry for the given workflow (for JSONL persistence). */
|
|
116
|
+
toCacheEntry(workflowId: string): DurableCheckpointEntry | undefined;
|
|
117
|
+
|
|
118
|
+
/** Clear all state (for tests). */
|
|
119
|
+
reset(): void;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Optional: hydrate a single workflow's checkpoints from the persistent
|
|
123
|
+
* store (DBOS) into the in-memory mirror. Implementations that do not need
|
|
124
|
+
* async hydration (in-memory, file) omit this method.
|
|
125
|
+
*/
|
|
126
|
+
hydrateWorkflow?(workflowId: string): Promise<void>;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Optional: hydrate all resumable workflows from the persistent store (DBOS)
|
|
130
|
+
* into the in-memory mirror. Called before listing/resuming in a fresh
|
|
131
|
+
* process. Implementations that do not need async hydration omit this.
|
|
132
|
+
*/
|
|
133
|
+
hydrateResumableWorkflows?(): Promise<void>;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// Hashing helpers
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Compute a deterministic content digest for a JSON-serializable value.
|
|
142
|
+
* Uses canonical JSON stringification (sorted keys) for stability and SHA-256
|
|
143
|
+
* for collision resistance. The earlier 32-bit DJB2 hash demonstrably
|
|
144
|
+
* collided across distinct tool/stage identities and could cause completed
|
|
145
|
+
* side effects to be skipped (or merged) incorrectly on resume.
|
|
146
|
+
*
|
|
147
|
+
* cross-ref: issue #1498 — collision-resistant durable replay identities.
|
|
148
|
+
*/
|
|
149
|
+
export function durableHash(value: WorkflowSerializableValue | WorkflowSerializableObject): string {
|
|
150
|
+
const canonical = canonicalJsonString(value);
|
|
151
|
+
const digest = createHash("sha256").update(canonical).digest("hex");
|
|
152
|
+
return `h${digest.slice(0, 32)}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function canonicalJsonString(value: unknown): string {
|
|
156
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
157
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJsonString).join(",")}]`;
|
|
158
|
+
const obj = value as Record<string, unknown>;
|
|
159
|
+
const keys = Object.keys(obj).sort();
|
|
160
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalJsonString(obj[k])}`).join(",")}}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
// In-memory backend
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
interface InMemoryWorkflowRecord {
|
|
168
|
+
handle: DurableWorkflowHandle;
|
|
169
|
+
checkpoints: Map<string, DurableCheckpoint>;
|
|
170
|
+
toolByHash: Map<string, DurableToolCheckpoint>;
|
|
171
|
+
uiByHash: Map<string, DurableUiCheckpoint>;
|
|
172
|
+
stageOutputByReplayKey: Map<string, DurableStageCheckpoint>;
|
|
173
|
+
stageSessionByReplayKey: Map<string, DurableStageCheckpoint>;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function checkpointKey(c: DurableCheckpoint): string {
|
|
177
|
+
return `${c.kind}:${c.checkpointId}`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Process-local durable backend for tests and explicit custom overrides.
|
|
182
|
+
* Checkpoints live in memory for the lifetime of the process; the production
|
|
183
|
+
* default is the cross-process per-workflow file backend.
|
|
184
|
+
*/
|
|
185
|
+
export class InMemoryDurableBackend implements DurableWorkflowBackend {
|
|
186
|
+
public readonly persistent = false;
|
|
187
|
+
private readonly workflows = new Map<string, InMemoryWorkflowRecord>();
|
|
188
|
+
|
|
189
|
+
registerWorkflow(handle: WorkflowRegistrationInput): void {
|
|
190
|
+
const existing = this.workflows.get(handle.workflowId);
|
|
191
|
+
const completedCheckpoints = handle.completedCheckpoints ?? existing?.handle.completedCheckpoints ?? 0;
|
|
192
|
+
const pendingPrompts = handle.pendingPrompts ?? existing?.handle.pendingPrompts ?? 0;
|
|
193
|
+
const updatedAt = handle.updatedAt ?? Date.now();
|
|
194
|
+
const full: DurableWorkflowHandle = {
|
|
195
|
+
workflowId: handle.workflowId,
|
|
196
|
+
name: handle.name,
|
|
197
|
+
inputs: handle.inputs,
|
|
198
|
+
createdAt: handle.createdAt,
|
|
199
|
+
status: handle.status,
|
|
200
|
+
...(handle.sessionFile !== undefined ? { sessionFile: handle.sessionFile } : {}),
|
|
201
|
+
completedCheckpoints,
|
|
202
|
+
pendingPrompts,
|
|
203
|
+
updatedAt,
|
|
204
|
+
...(handle.label !== undefined ? { label: handle.label } : {}),
|
|
205
|
+
...(handle.rootWorkflowId !== undefined ? { rootWorkflowId: handle.rootWorkflowId } : {}),
|
|
206
|
+
...(handle.resumable !== undefined ? { resumable: handle.resumable } : {}),
|
|
207
|
+
};
|
|
208
|
+
if (existing) existing.handle = full;
|
|
209
|
+
else this.workflows.set(handle.workflowId, { handle: full, checkpoints: new Map(), toolByHash: new Map(), uiByHash: new Map(), stageOutputByReplayKey: new Map(), stageSessionByReplayKey: new Map() });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
recordCheckpoint(checkpoint: DurableCheckpoint): void {
|
|
213
|
+
const rec = this.workflows.get(checkpoint.workflowId);
|
|
214
|
+
if (!rec) return;
|
|
215
|
+
const key = checkpointKey(checkpoint);
|
|
216
|
+
if (rec.checkpoints.has(key)) return; // idempotent
|
|
217
|
+
rec.checkpoints.set(key, checkpoint);
|
|
218
|
+
if (checkpoint.kind === "tool") rec.toolByHash.set(checkpoint.argsHash, checkpoint);
|
|
219
|
+
else if (checkpoint.kind === "ui") rec.uiByHash.set(checkpoint.promptHash, checkpoint);
|
|
220
|
+
else {
|
|
221
|
+
if ("output" in checkpoint) rec.stageOutputByReplayKey.set(checkpoint.replayKey, checkpoint);
|
|
222
|
+
if (checkpoint.sessionId !== undefined || checkpoint.sessionFile !== undefined) {
|
|
223
|
+
const existing = rec.stageSessionByReplayKey.get(checkpoint.replayKey);
|
|
224
|
+
if (existing === undefined || checkpoint.completedAt >= existing.completedAt) {
|
|
225
|
+
rec.stageSessionByReplayKey.set(checkpoint.replayKey, checkpoint);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
rec.handle = { ...rec.handle, completedCheckpoints: rec.checkpoints.size, updatedAt: checkpoint.completedAt };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
getToolOutput(workflowId: string, argsHash: string): WorkflowSerializableValue | undefined {
|
|
233
|
+
return this.workflows.get(workflowId)?.toolByHash.get(argsHash)?.output;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
getUiResponse(workflowId: string, promptHash: string): WorkflowSerializableValue | undefined {
|
|
237
|
+
return this.workflows.get(workflowId)?.uiByHash.get(promptHash)?.response;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
getStageOutput(workflowId: string, replayKey: string): WorkflowSerializableValue | undefined {
|
|
241
|
+
const checkpoint = this.workflows.get(workflowId)?.stageOutputByReplayKey.get(replayKey);
|
|
242
|
+
return checkpoint !== undefined && "output" in checkpoint ? checkpoint.output : undefined;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
getStageSession(workflowId: string, replayKey: string): { sessionId?: string; sessionFile?: string } | undefined {
|
|
246
|
+
const checkpoint = this.workflows.get(workflowId)?.stageSessionByReplayKey.get(replayKey);
|
|
247
|
+
if (checkpoint?.sessionId === undefined && checkpoint?.sessionFile === undefined) return undefined;
|
|
248
|
+
return { ...(checkpoint.sessionId !== undefined ? { sessionId: checkpoint.sessionId } : {}), ...(checkpoint.sessionFile !== undefined ? { sessionFile: checkpoint.sessionFile } : {}) };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
listCheckpoints(workflowId: string): readonly DurableCheckpoint[] {
|
|
252
|
+
const rec = this.workflows.get(workflowId);
|
|
253
|
+
if (!rec) return [];
|
|
254
|
+
return [...rec.checkpoints.values()].sort((a, b) => a.completedAt - b.completedAt);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
getWorkflow(workflowId: string): DurableWorkflowHandle | undefined {
|
|
258
|
+
return this.workflows.get(workflowId)?.handle;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
setWorkflowStatus(workflowId: string, status: DurableWorkflowStatus, pendingPrompts?: number, resumable?: boolean): void {
|
|
262
|
+
const rec = this.workflows.get(workflowId);
|
|
263
|
+
if (!rec) return;
|
|
264
|
+
rec.handle = { ...rec.handle, status, updatedAt: Date.now(), ...(pendingPrompts !== undefined ? { pendingPrompts } : {}), ...(resumable !== undefined ? { resumable } : {}) };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
listResumableWorkflows(): readonly ResumableWorkflowEntry[] {
|
|
268
|
+
return [...this.workflows.values()]
|
|
269
|
+
.filter((rec) => isRootWorkflow(rec.handle) && isResumableHandle(rec.handle))
|
|
270
|
+
.map((rec) => toResumableEntry(rec.handle));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
toCacheEntry(workflowId: string): DurableCheckpointEntry | undefined {
|
|
274
|
+
const rec = this.workflows.get(workflowId);
|
|
275
|
+
if (!rec) return undefined;
|
|
276
|
+
const h = rec.handle;
|
|
277
|
+
return {
|
|
278
|
+
type: "workflow.durable.checkpoint",
|
|
279
|
+
workflowId: h.workflowId,
|
|
280
|
+
name: h.name,
|
|
281
|
+
inputs: h.inputs,
|
|
282
|
+
status: h.status,
|
|
283
|
+
completedCheckpoints: h.completedCheckpoints,
|
|
284
|
+
pendingPrompts: h.pendingPrompts,
|
|
285
|
+
...(h.label !== undefined ? { label: h.label } : {}),
|
|
286
|
+
...(h.rootWorkflowId !== undefined ? { rootWorkflowId: h.rootWorkflowId } : {}),
|
|
287
|
+
...(h.resumable !== undefined ? { resumable: h.resumable } : {}),
|
|
288
|
+
ts: h.updatedAt,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
reset(): void {
|
|
293
|
+
this.workflows.clear();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Export all records (for FileDurableBackend serialization or debugging). */
|
|
297
|
+
exportAll(): readonly { readonly handle: DurableWorkflowHandle; readonly checkpoints: readonly DurableCheckpoint[] }[] {
|
|
298
|
+
return [...this.workflows.values()].map((rec) => ({ handle: rec.handle, checkpoints: [...rec.checkpoints.values()] }));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Import records (for FileDurableBackend deserialization). */
|
|
302
|
+
importAll(records: readonly { readonly handle: DurableWorkflowHandle; readonly checkpoints: readonly DurableCheckpoint[] }[]): void {
|
|
303
|
+
for (const rec of records) {
|
|
304
|
+
this.registerWorkflow(rec.handle);
|
|
305
|
+
for (const cp of rec.checkpoints) this.recordCheckpoint(cp);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function isRootWorkflow(handle: DurableWorkflowHandle): boolean {
|
|
311
|
+
return handle.rootWorkflowId === undefined || handle.rootWorkflowId === handle.workflowId;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function hasResumeProgress(handle: DurableWorkflowHandle): boolean {
|
|
315
|
+
return handle.completedCheckpoints > 0 || handle.pendingPrompts > 0;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function isResumableHandle(handle: DurableWorkflowHandle): boolean {
|
|
319
|
+
if (handle.status === "failed" || handle.status === "blocked") return handle.resumable !== false;
|
|
320
|
+
// `running`/`paused` are both resumable at the backend level: a `running`
|
|
321
|
+
// durable handle may belong to a crashed process (no live executor), which
|
|
322
|
+
// is exactly the cross-session crash-recovery case. Same-session
|
|
323
|
+
// double-resume is prevented by the command layer, which hides durable
|
|
324
|
+
// entries that match an active live run.
|
|
325
|
+
return (handle.status === "running" || handle.status === "paused") && hasResumeProgress(handle);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function toResumableEntry(handle: DurableWorkflowHandle): ResumableWorkflowEntry {
|
|
329
|
+
return {
|
|
330
|
+
workflowId: handle.workflowId,
|
|
331
|
+
name: handle.name,
|
|
332
|
+
inputs: handle.inputs,
|
|
333
|
+
status: handle.status,
|
|
334
|
+
completedCheckpoints: handle.completedCheckpoints,
|
|
335
|
+
pendingPrompts: handle.pendingPrompts,
|
|
336
|
+
...(handle.sessionFile !== undefined ? { sessionFile: handle.sessionFile } : {}),
|
|
337
|
+
...(handle.label !== undefined ? { label: handle.label } : {}),
|
|
338
|
+
...(handle.rootWorkflowId !== undefined ? { rootWorkflowId: handle.rootWorkflowId } : {}),
|
|
339
|
+
...(handle.resumable !== undefined ? { resumable: handle.resumable } : {}),
|
|
340
|
+
createdAt: handle.createdAt,
|
|
341
|
+
updatedAt: handle.updatedAt,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
WorkflowChildResult,
|
|
3
|
+
WorkflowDefinition,
|
|
4
|
+
WorkflowInputValues,
|
|
5
|
+
WorkflowOutputValues,
|
|
6
|
+
WorkflowRunChildArgs,
|
|
7
|
+
WorkflowSerializableValue,
|
|
8
|
+
} from "../shared/types.js";
|
|
9
|
+
import { isWorkflowDefinition, workflowDefinitionRequirementMessage } from "../runs/foreground/executor-child-helpers.js";
|
|
10
|
+
import type { DurableWorkflowBackend } from "./backend.js";
|
|
11
|
+
import type { DurableScope } from "./scoped-backend.js";
|
|
12
|
+
import { recordCheckpointDurably } from "./tool-primitive.js";
|
|
13
|
+
|
|
14
|
+
export function createDurableChildWorkflowPrimitive(input: {
|
|
15
|
+
readonly workflowId: string;
|
|
16
|
+
readonly rootWorkflowId: string;
|
|
17
|
+
readonly backend: DurableWorkflowBackend;
|
|
18
|
+
readonly nextReplayKey: (name: string) => string;
|
|
19
|
+
readonly recordCachedStage: (name: string, replayKey: string, output: WorkflowSerializableValue) => void;
|
|
20
|
+
/**
|
|
21
|
+
* Publish the durable scope computed for the next child invocation so the
|
|
22
|
+
* child runner can consume it and route its internal side-effect checkpoints
|
|
23
|
+
* under the root workflow. Avoids re-deriving the ordinal independently.
|
|
24
|
+
*/
|
|
25
|
+
readonly setChildDurableScope: (scope: DurableScope) => void;
|
|
26
|
+
readonly workflow: <
|
|
27
|
+
TChildInputs extends WorkflowInputValues,
|
|
28
|
+
TChildOutputs extends WorkflowOutputValues,
|
|
29
|
+
TChildRunInputs extends WorkflowInputValues = TChildInputs,
|
|
30
|
+
>(
|
|
31
|
+
child: WorkflowDefinition<TChildInputs, TChildOutputs, TChildRunInputs>,
|
|
32
|
+
...args: WorkflowRunChildArgs<TChildRunInputs>
|
|
33
|
+
) => Promise<WorkflowChildResult<TChildOutputs>>;
|
|
34
|
+
}) {
|
|
35
|
+
return async <
|
|
36
|
+
TChildInputs extends WorkflowInputValues,
|
|
37
|
+
TChildOutputs extends WorkflowOutputValues,
|
|
38
|
+
TChildRunInputs extends WorkflowInputValues = TChildInputs,
|
|
39
|
+
>(
|
|
40
|
+
child: WorkflowDefinition<TChildInputs, TChildOutputs, TChildRunInputs>,
|
|
41
|
+
...args: WorkflowRunChildArgs<TChildRunInputs>
|
|
42
|
+
): Promise<WorkflowChildResult<TChildOutputs>> => {
|
|
43
|
+
if (!isWorkflowDefinition(child)) throw new Error(workflowDefinitionRequirementMessage("ctx.workflow(definition)", child));
|
|
44
|
+
const options = args[0] as { readonly stageName?: string } | undefined;
|
|
45
|
+
const boundaryName = options?.stageName ?? `workflow:${child.normalizedName}`;
|
|
46
|
+
const replayKey = input.nextReplayKey(boundaryName);
|
|
47
|
+
// Route this child's internal side-effect checkpoints under the root
|
|
48
|
+
// workflow with a stable boundary key, so an interrupted child does not
|
|
49
|
+
// re-execute completed side effects on parent resume.
|
|
50
|
+
// cross-ref: issue #1498.
|
|
51
|
+
input.setChildDurableScope({ rootWorkflowId: input.rootWorkflowId, scopePrefix: replayKey });
|
|
52
|
+
const cached = input.backend.getStageOutput(input.workflowId, replayKey);
|
|
53
|
+
if (cached !== undefined && isWorkflowChildResult(cached)) {
|
|
54
|
+
input.recordCachedStage(boundaryName, replayKey, cached);
|
|
55
|
+
return cached as WorkflowChildResult<TChildOutputs>;
|
|
56
|
+
}
|
|
57
|
+
const result = await input.workflow(child, ...args);
|
|
58
|
+
await recordCheckpointDurably(input.backend, {
|
|
59
|
+
kind: "stage",
|
|
60
|
+
workflowId: input.workflowId,
|
|
61
|
+
checkpointId: `workflow:${replayKey}`,
|
|
62
|
+
name: boundaryName,
|
|
63
|
+
replayKey,
|
|
64
|
+
output: result as WorkflowSerializableValue,
|
|
65
|
+
completedAt: Date.now(),
|
|
66
|
+
});
|
|
67
|
+
return result as WorkflowChildResult<TChildOutputs>;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isWorkflowChildResult(value: WorkflowSerializableValue): value is WorkflowChildResult<WorkflowOutputValues> {
|
|
72
|
+
return typeof value === "object"
|
|
73
|
+
&& value !== null
|
|
74
|
+
&& !Array.isArray(value)
|
|
75
|
+
&& typeof (value as { readonly workflow?: WorkflowSerializableValue }).workflow === "string"
|
|
76
|
+
&& typeof (value as { readonly runId?: WorkflowSerializableValue }).runId === "string"
|
|
77
|
+
&& typeof (value as { readonly status?: WorkflowSerializableValue }).status === "string"
|
|
78
|
+
&& typeof (value as { readonly outputs?: WorkflowSerializableValue }).outputs === "object";
|
|
79
|
+
}
|