@bastani/atomic 0.9.10 → 0.9.11-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/dist/builtin/cursor/CHANGELOG.md +6 -0
- package/dist/builtin/cursor/package.json +2 -2
- package/dist/builtin/intercom/CHANGELOG.md +6 -0
- package/dist/builtin/intercom/package.json +2 -2
- package/dist/builtin/mcp/CHANGELOG.md +6 -0
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/CHANGELOG.md +6 -0
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/web-access/CHANGELOG.md +6 -0
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +17 -0
- package/dist/builtin/workflows/README.md +8 -3
- package/dist/builtin/workflows/builtin/deep-research-codebase.ts +1 -1
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/src/durable/backend.ts +2 -2
- package/dist/builtin/workflows/src/durable/dbos-embedded-postgres-root.ts +173 -0
- package/dist/builtin/workflows/src/durable/dbos-embedded-postgres.ts +36 -12
- package/dist/builtin/workflows/src/durable/dbos-lifecycle.ts +9 -3
- package/dist/builtin/workflows/src/durable/factory.ts +35 -6
- package/dist/builtin/workflows/src/durable/local-command.ts +11 -2
- package/dist/builtin/workflows/src/extension/extension-lifecycle.ts +14 -2
- package/dist/builtin/workflows/src/extension/public-types.ts +3 -4
- package/dist/builtin/workflows/src/extension/render-call.ts +4 -0
- package/dist/builtin/workflows/src/extension/render-result.ts +31 -1
- package/dist/builtin/workflows/src/extension/workflow-prompts.ts +5 -2
- package/dist/builtin/workflows/src/extension/workflow-schema.ts +3 -2
- package/dist/builtin/workflows/src/extension/workflow-tool-content.ts +1 -0
- package/dist/builtin/workflows/src/extension/workflow-tool-control.ts +88 -2
- package/dist/builtin/workflows/src/extension/workflow-tool.ts +13 -0
- 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-retry.d.ts.map +1 -1
- package/dist/core/agent-session-retry.js +7 -0
- package/dist/core/agent-session-retry.js.map +1 -1
- package/dist/core/agent-session-runtime.d.ts +13 -0
- package/dist/core/agent-session-runtime.d.ts.map +1 -1
- package/dist/core/agent-session-runtime.js +12 -0
- package/dist/core/agent-session-runtime.js.map +1 -1
- package/dist/core/atomic-guide-command.d.ts.map +1 -1
- package/dist/core/atomic-guide-command.js +2 -0
- package/dist/core/atomic-guide-command.js.map +1 -1
- package/dist/core/auth-storage-backends.d.ts +4 -0
- package/dist/core/auth-storage-backends.d.ts.map +1 -1
- package/dist/core/auth-storage-backends.js +48 -3
- package/dist/core/auth-storage-backends.js.map +1 -1
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +27 -11
- package/dist/core/auth-storage.js.map +1 -1
- package/dist/core/codex-errors.d.ts +3 -0
- package/dist/core/codex-errors.d.ts.map +1 -0
- package/dist/core/codex-errors.js +12 -0
- package/dist/core/codex-errors.js.map +1 -0
- package/dist/core/slash-commands.js +1 -1
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/modes/interactive/interactive-auth-routing.d.ts +2 -1
- package/dist/modes/interactive/interactive-auth-routing.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-auth-routing.js +10 -5
- package/dist/modes/interactive/interactive-auth-routing.js.map +1 -1
- package/dist/modes/interactive/interactive-autocomplete.js +12 -2
- package/dist/modes/interactive/interactive-autocomplete.js.map +1 -1
- package/dist/modes/interactive-engine/extension-ui-bridge.d.ts +3 -1
- package/dist/modes/interactive-engine/extension-ui-bridge.d.ts.map +1 -1
- package/dist/modes/interactive-engine/extension-ui-bridge.js +6 -0
- package/dist/modes/interactive-engine/extension-ui-bridge.js.map +1 -1
- package/dist/modes/interactive-engine/isolated-runtime.d.ts +4 -5
- package/dist/modes/interactive-engine/isolated-runtime.d.ts.map +1 -1
- package/dist/modes/interactive-engine/isolated-runtime.js +14 -24
- package/dist/modes/interactive-engine/isolated-runtime.js.map +1 -1
- package/dist/modes/interactive-engine/remote-model-catalog.d.ts +15 -0
- package/dist/modes/interactive-engine/remote-model-catalog.d.ts.map +1 -0
- package/dist/modes/interactive-engine/remote-model-catalog.js +67 -0
- package/dist/modes/interactive-engine/remote-model-catalog.js.map +1 -0
- package/dist/modes/rpc/rpc-client-api.d.ts +8 -1
- package/dist/modes/rpc/rpc-client-api.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client-api.js +9 -0
- package/dist/modes/rpc/rpc-client-api.js.map +1 -1
- package/dist/modes/rpc/rpc-command-handler.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-command-handler.js +30 -0
- package/dist/modes/rpc/rpc-command-handler.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +67 -7
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/docs/docs.json +9 -1
- package/docs/models/artificial-analysis-index.md +82 -0
- package/docs/models/model-selection.md +63 -0
- package/docs/models/pareto-efficiency.md +64 -0
- package/docs/providers.md +5 -1
- package/docs/quickstart.md +1 -1
- package/docs/rpc.md +27 -0
- package/docs/workflows.md +20 -7
- package/npm-shrinkwrap.json +24 -24
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.9.11-alpha.1] - 2026-07-20
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Clarified the bundled `deep-research-codebase` discovery guidance so agents reserve the heavy workflow for tasks requiring comprehensive whole-repository context ([#1925](https://github.com/bastani-inc/atomic/issues/1925)).
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Fixed newly authenticated providers remaining unavailable in the model selector until Atomic restarted. API-key and OAuth login now reload the isolated engine's credentials and refresh its authoritative model catalog immediately, including credential-dependent dynamic discovery ([#1918](https://github.com/bastani-inc/atomic/issues/1918)).
|
|
14
|
+
- Added actionable `/logout` and `/login` recovery guidance when OpenAI Codex invalidates or revokes a locally unexpired OAuth token, and prevented an earlier WebSocket-to-SSE transport diagnostic from making the definitive authentication rejection retryable ([#1922](https://github.com/bastani-inc/atomic/issues/1922)).
|
|
15
|
+
|
|
5
16
|
## [0.9.10] - 2026-07-20
|
|
6
17
|
|
|
7
18
|
### Breaking Changes
|
|
@@ -34,6 +45,9 @@
|
|
|
34
45
|
|
|
35
46
|
### Fixed
|
|
36
47
|
|
|
48
|
+
- Fixed `/logout` leaving GitHub Copilot and other providers authenticated in the isolated interactive engine until restart. Logout now invalidates the authoritative child immediately, refreshes the host model projection, prevents provider transport with the removed credential, and deletes effective legacy auth-file entries so credentials do not reappear after restart ([#1919](https://github.com/bastani-inc/atomic/issues/1919)).
|
|
49
|
+
- Fixed custom project, user, and package-provided workflow names being omitted from `/workflow <name>` and `/workflow inputs <name>` argument completion under the isolated interactive engine. The host now evaluates completion callbacks in the engine child against the live workflow registry, so `/workflow reload` updates the completion popup immediately while retaining bundled administrative-command fallback completion ([#1921](https://github.com/bastani-inc/atomic/issues/1921)).
|
|
50
|
+
|
|
37
51
|
- Fixed the repository `publish-release` workflow to reconcile an exact release PR merged externally while required checks are pending. It preserves identity/refs/SHA; correlates workflow-qualified Actions reruns by name plus workflow; and supports empty-workflow `StatusContext` and GitHub App `CheckRun` evidence. Linked reruns group by inferred kind/name across URL changes; linkless rows inspect both external kinds, accept all-passing candidates, block any pending/failure, and exclude nonempty-workflow Actions. Duplicate aliases reuse exact passing evidence. It rechecks after merge and validates merge/branch evidence. Tag recovery proves `verified merge → tag parent → current base`; exhaustive history avoids GitHub's 1,000-result ceiling; protected coordination retains its lock through ambiguous dispatch visibility; and recovered success requires exact-SHA integrity evidence.
|
|
38
52
|
- Fixed shared extension chat compaction rendering so manual, threshold, and overflow compaction use the animated working spinner with reason-aware copy instead of a duplicate plain status row plus generic `Working...`; successful compaction now falls back to the existing typed `✻ Context compacted` message when a refreshed live session snapshot is unavailable, while preserving durable session reconstruction and avoiding duplicate boundaries.
|
|
39
53
|
- Fixed workflow stage-chat `/compact` cancellation and planner/provider failures from escaping their fire-and-forget editor submission promise and terminating the CLI. The authoritative `compaction_end` event now owns the visible status, animation cleanup, and diagnostic path while the same stage remains usable for retry or follow-up.
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.9.11-alpha.1] - 2026-07-20
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Published a synchronized Atomic 0.9.11-alpha.1 prerelease for the Cursor provider package; no functional Cursor provider changes were made after 0.9.9.
|
|
10
|
+
|
|
5
11
|
## [0.9.10] - 2026-07-20
|
|
6
12
|
|
|
7
13
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bastani/cursor",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.11-alpha.1",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Experimental first-party Atomic extension for Cursor OAuth, model discovery, and streaming provider registration.",
|
|
6
6
|
"contributors": [
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
}
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@bastani/atomic-natives": "0.9.
|
|
43
|
+
"@bastani/atomic-natives": "0.9.11-alpha.1",
|
|
44
44
|
"@bufbuild/protobuf": "^2.12.1",
|
|
45
45
|
"@earendil-works/pi-ai": "^0.80.10"
|
|
46
46
|
}
|
|
@@ -4,6 +4,12 @@ All notable changes to the `pi-intercom` extension will be documented in this fi
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.9.11-alpha.1] - 2026-07-20
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Published a synchronized Atomic 0.9.11-alpha.1 prerelease for the intercom extension; no functional intercom changes were made after 0.9.10.
|
|
12
|
+
|
|
7
13
|
## [0.9.10] - 2026-07-20
|
|
8
14
|
|
|
9
15
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bastani/intercom",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.11-alpha.1",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Atomic extension providing a private coordination channel between parent and child agent sessions. Fork of: https://github.com/nicobailon/pi-intercom",
|
|
6
6
|
"contributors": [
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
}
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"tsx": "^4.
|
|
58
|
+
"tsx": "^4.23.1",
|
|
59
59
|
"typebox": "^1.3.6"
|
|
60
60
|
}
|
|
61
61
|
}
|
|
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.9.11-alpha.1] - 2026-07-20
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Published a synchronized Atomic 0.9.11-alpha.1 prerelease for the MCP extension; no functional MCP changes were made after 0.9.10.
|
|
15
|
+
|
|
10
16
|
## [0.9.10] - 2026-07-20
|
|
11
17
|
|
|
12
18
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bastani/mcp",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.11-alpha.1",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Atomic extension that adapts MCP (Model Context Protocol) servers into the coding agent. Fork of: https://github.com/nicobailon/pi-mcp-adapter",
|
|
6
6
|
"contributors": [
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.9.11-alpha.1] - 2026-07-20
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Published a synchronized Atomic 0.9.11-alpha.1 prerelease for the subagents extension; no functional subagent changes were made after 0.9.10.
|
|
10
|
+
|
|
5
11
|
## [0.9.10] - 2026-07-20
|
|
6
12
|
|
|
7
13
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bastani/subagents",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.11-alpha.1",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Atomic extension for delegating tasks to subagents with chains, parallel execution, and background runs. Fork of: https://github.com/nicobailon/pi-subagents",
|
|
6
6
|
"contributors": [
|
|
@@ -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.11-alpha.1] - 2026-07-20
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Published a synchronized Atomic 0.9.11-alpha.1 prerelease for the web-access extension; no functional web-access changes were made after 0.9.10.
|
|
12
|
+
|
|
7
13
|
## [0.9.10] - 2026-07-20
|
|
8
14
|
|
|
9
15
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bastani/web-access",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.11-alpha.1",
|
|
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,6 +6,23 @@ 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.11-alpha.1] - 2026-07-20
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Embedded DBOS Postgres now works when Atomic runs as root on Linux (containers, CI sandboxes, eval harnesses): PostgreSQL refuses UID 0, so Atomic resolves an unprivileged system account (`postgres`, `nobody`, or `daemon`), keeps the cluster under `/var/lib/atomic-postgres` (a root home directory is untraversable for that account), and runs `initdb`/`pg_ctl` with dropped privileges. When the embedded binaries sit under an untraversable prefix (for example a root-owned `~/.nvm` global install), the Postgres runtime is copied into the cluster directory once and reused.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Updated workflow-creation guidance to prefer `ctx.tool(...)` for workflow-owned side effects, connecting durable cached results to safe resume while distinguishing pure TypeScript computation and agent-stage internals ([#1923](https://github.com/bastani-inc/atomic/issues/1923)).
|
|
18
|
+
- Clarified the built-in `deep-research-codebase` discovery guidance so agents reserve the heavy workflow for tasks requiring comprehensive whole-repository context ([#1925](https://github.com/bastani-inc/atomic/issues/1925)).
|
|
19
|
+
- When no durable backend can be provisioned at all (no `DBOS_SYSTEM_DATABASE_URL`, embedded Postgres unavailable, and no Docker), workflows now degrade to a process-local in-memory backend with a loud non-durable warning instead of failing every workflow action. Degraded runs execute normally but do not survive the process, so `/workflow resume` after exit has nothing to restore; DBOS query and write failures on a provisioned backend still fail the workflow action.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Fixed `workflow({ action: "resume", runId })` returning `Run not found` for eligible durably paused workflows after session-local run state was cleared. Explicit tool resume now resolves exact IDs and unique or ambiguous prefixes through the authoritative DBOS catalog before refusing the target, preserves original workflow IDs and checkpoint replay, and surfaces resource or durable lookup failures without making ordinary status listing hydrate durable history. ([#1924](https://github.com/bastani-inc/atomic/issues/1924))
|
|
24
|
+
- Fixed session teardown crashing the process (exit code 1) after DBOS durability provisioning had failed: `shutdownDbos` re-awaited the memoized rejected configuration promise and rethrew the original provisioning error out of session dispose and the `beforeExit` hook, turning otherwise-successful `--print` runs into nonzero exits (surfacing as `NonZeroAgentExitCodeError` in eval harnesses). Shutdown is now a no-op for a backend that never reached readiness, and genuine teardown failures are logged instead of thrown.
|
|
25
|
+
|
|
9
26
|
## [0.9.10] - 2026-07-20
|
|
10
27
|
|
|
11
28
|
### Breaking Changes
|
|
@@ -62,6 +62,10 @@ When a stage human-in-the-loop prompt is answered from the workflow TUI/stage ch
|
|
|
62
62
|
|
|
63
63
|
## Authoring API
|
|
64
64
|
|
|
65
|
+
### Workflow-owned side effects
|
|
66
|
+
|
|
67
|
+
Prefer `ctx.tool(name, args, fn)` for workflow-owned TypeScript operations with side effects, including filesystem writes, network mutations, external API actions, and similar deterministic operations orchestrated directly by the workflow definition. Atomic durably caches a completed call's serializable result, so resume returns that result without rerunning `fn` or repeating the side effect. Keep pure computation and side-effect-free transformations as ordinary TypeScript. Do not wrap agent-stage internals or every function call indiscriminately.
|
|
68
|
+
|
|
65
69
|
### Example 1 — Single task
|
|
66
70
|
|
|
67
71
|
```typescript
|
|
@@ -573,11 +577,11 @@ Prompt answer replay is live-memory only. `StageSnapshot.promptAnswerState` repo
|
|
|
573
577
|
```json
|
|
574
578
|
{
|
|
575
579
|
"name": "workflow",
|
|
576
|
-
"description": "Run named builtin, project, user, or package workflows; custom definitions may import reusable project/package workflows or builtin definitions from @bastani/workflows/builtin and nest them with ctx.workflow(...), including deeper composition within the configured maxDepth; when workflow execution fits but another shape would better achieve the task, author a custom TypeScript workflow({...}) inline with normal coding tools, reload it, and run it; discover with list/get/inputs, list session runs with status (no runId; statusFilter narrows the list), inspect status/stages/stage details, send prompt answers or steering, pause/resume/interrupt/quit runs, and reload workflow resources. For large stage handoffs, write context to files/artifacts, pass paths via reads, and prompt downstream agents to 'Read the file at <path>...' instead of injecting large previous text. For transcripts, prefer status/stages/stage to get sessionFile/transcriptPath, quote the exact path without rewriting separators (Windows backslashes are valid), then search it with rg/grep and read small ranges; transcript is path-only by default when sessionFile/transcriptPath exists, explicit tail/limit returns bounded previews, and missing transcript paths fall back to a small preview.",
|
|
580
|
+
"description": "Run named builtin, project, user, or package workflows; custom definitions may import reusable project/package workflows or builtin definitions from @bastani/workflows/builtin and nest them with ctx.workflow(...), including deeper composition within the configured maxDepth; when workflow execution fits but another shape would better achieve the task, author a custom TypeScript workflow({...}) inline with normal coding tools, reload it, and run it; discover with list/get/inputs/models, list session runs with status (no runId; statusFilter narrows the list), inspect status/stages/stage details, send prompt answers or steering, pause/resume/interrupt/quit runs, and reload workflow resources. For large stage handoffs, write context to files/artifacts, pass paths via reads, and prompt downstream agents to 'Read the file at <path>...' instead of injecting large previous text. For transcripts, prefer status/stages/stage to get sessionFile/transcriptPath, quote the exact path without rewriting separators (Windows backslashes are valid), then search it with rg/grep and read small ranges; transcript is path-only by default when sessionFile/transcriptPath exists, explicit tail/limit returns bounded previews, and missing transcript paths fall back to a small preview. Use action 'models' to inspect models in the configured catalog; the result is a configured-auth snapshot showing what's present in the registry with configured authentication, not proof of credentials, entitlements, OAuth freshness, or live provider access. When authoring a workflow that should dynamically select a model, first call workflow({ action: 'models' }) to inspect the configured catalog, then select from the returned provider/id entries considering the isCurrent marker and available thinking levels.",
|
|
577
581
|
"parameters": {
|
|
578
582
|
"workflow": "string (optional) — workflow ID or normalized name",
|
|
579
583
|
"inputs": "object (optional) — key/value map of workflow inputs",
|
|
580
|
-
"action": "'run' | 'list' | 'get' | 'inputs' | 'status' | 'stages' | 'stage' | 'transcript' | 'send' | 'pause' | 'interrupt' | 'quit' | 'resume' | 'reload'",
|
|
584
|
+
"action": "'run' | 'list' | 'get' | 'inputs' | 'models' | 'status' | 'stages' | 'stage' | 'transcript' | 'send' | 'pause' | 'interrupt' | 'quit' | 'resume' | 'reload'",
|
|
581
585
|
"runId": "optional run id or unique prefix; control actions default to the active run where safe; use '--all' or all:true for pause/interrupt/quit all",
|
|
582
586
|
"stageId": "optional stage id, prefix, or name for stage-scoped actions; cannot be combined with all:true",
|
|
583
587
|
"statusFilter": "optional filter for stages or the no-runId status run listing: pending/running/awaiting_input/paused/blocked/completed/failed/skipped/cancelled/killed/all; for the status listing, run statuses match directly and awaiting_input selects runs with a pending human prompt",
|
|
@@ -601,6 +605,7 @@ Prompt answer replay is live-memory only. `StageSnapshot.promptAnswerState` repo
|
|
|
601
605
|
- **`transcript`** — path-only by default when a transcript file exists: use `status`, `stages`, or `stage` to identify the stage and its `sessionFile`/`transcriptPath`, quote the exact path without changing platform separators (for example, preserve Windows backslashes), then search that file with `rg`/`grep` for targeted terms and read only small surrounding ranges. Default text results include JSON-escaped `sessionFileJson`/`transcriptPathJson` lines for copy-safe path literals plus a `lazyReadPrompt`, with `entries: not inlined` so transcript bodies and tool outputs stay out of model context. Passing explicit `tail` or `limit` opts into a bounded inline preview for quick context checks. If no transcript path is available, the action falls back to a bounded preview of up to 5 recent entries with a `fallbackNote`. A registered live stage handle is used when one exists, even before live messages arrive; otherwise the action falls back to stored stage snapshots. Snapshot entries are ordered chronologically before `tail`/`limit` is applied, with terminal result/error entries kept after tool entries when timestamps are missing or tied. `includeToolOutput` applies only to inlined snapshot previews or no-path fallback previews; live session transcripts may not expose tool output.
|
|
602
606
|
- **`send`** — answers pending primitive/structured stage prompts only when `text`, `response`, or `message` is present; an explicit empty string is a valid answer, while an omitted payload is a no-op. Follow-ups to eligible terminal agent stages revive an interactive **post-mortem chat** through the shared resolver: on a live-handle miss the stage's retained `sessionFile` is validated (existing, readable, context-bearing) and reopened as a detached, single-flight handle so the message is delivered as a conversational follow-up appended in place — the same path used by `/workflow attach`, restored/replayed durable snapshots, and completed-workflow inspection — without resuming, retrying, or re-dispatching workflow execution. If no valid retained session exists, the follow-up is refused (`No live handle for stage.`) instead of silently resetting or exposing a handle-less non-terminal session. Arbitrary `ctx.ui.custom<T>` widget prompts require the interactive workflow graph and return a clear unsupported message when targeted through `send`. `delivery: "auto"` answers pending prompts first, then resumes paused stages, steers streaming stages, or queues a follow-up.
|
|
603
607
|
- **`reload`** — refreshes workflow resources directly in-process instead of queuing a literal `/workflow reload` chat follow-up.
|
|
608
|
+
- **`models`** — returns safe model-catalog metadata from the configured registry. Each entry contains `provider` (e.g. `openai`), `id` (e.g. `gpt-4`), `fullId` (e.g. `openai/gpt-4`), `isCurrent` (whether this is the active model), and `availableThinkingLevels`, canonically derived from the registry model's `reasoning` and `thinkingLevelMap` metadata. The result is a configured-auth snapshot: it shows which models are present in the registry with configured authentication, not proof of credentials, entitlements, OAuth freshness, or live provider access. No secrets, tokens, or authentication details are returned.
|
|
604
609
|
|
|
605
610
|
### F2 keyboard shortcut
|
|
606
611
|
|
|
@@ -689,7 +694,7 @@ The parent can consume `fixes.outputs`, `verification.outputs`, and `convergence
|
|
|
689
694
|
|
|
690
695
|
### `deep-research-codebase`
|
|
691
696
|
|
|
692
|
-
|
|
697
|
+
Heavy research for tasks requiring comprehensive, whole-repository context.
|
|
693
698
|
|
|
694
699
|
```text
|
|
695
700
|
/workflow deep-research-codebase prompt="How does session persistence work?"
|
|
@@ -18,7 +18,7 @@ import { runDeepResearchCodebase } from "./deep-research-codebase-runner.js";
|
|
|
18
18
|
|
|
19
19
|
export default workflow({
|
|
20
20
|
name: "deep-research-codebase",
|
|
21
|
-
description: "
|
|
21
|
+
description: "Heavy research for tasks requiring comprehensive, whole-repository context.",
|
|
22
22
|
inputs: {
|
|
23
23
|
prompt: Type.String({ description: "Research question or investigation focus for the codebase." }),
|
|
24
24
|
max_partitions: Type.Number({
|
|
@@ -37,7 +37,7 @@ export type DurableInactiveDeleteResult =
|
|
|
37
37
|
| { readonly ok: true }
|
|
38
38
|
| { readonly ok: false; readonly reason: "not_found" | "running" };
|
|
39
39
|
|
|
40
|
-
/** DBOS is the sole persistent implementation. In-memory is
|
|
40
|
+
/** DBOS is the sole persistent implementation. In-memory is a test seam and the non-durable last-resort fallback. */
|
|
41
41
|
export interface DurableWorkflowBackend {
|
|
42
42
|
/** Whether state survives the current process. */
|
|
43
43
|
readonly persistent: boolean;
|
|
@@ -156,7 +156,7 @@ function checkpointKey(c: DurableCheckpoint): string {
|
|
|
156
156
|
return `${c.kind}:${c.checkpointId}`;
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
/** Process-local
|
|
159
|
+
/** Process-local backend: explicit test injection and the loud non-durable fallback when DBOS cannot be provisioned. */
|
|
160
160
|
export class InMemoryDurableBackend implements DurableWorkflowBackend {
|
|
161
161
|
public readonly persistent: boolean = false;
|
|
162
162
|
private readonly workflows = new Map<string, InMemoryWorkflowRecord>();
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Root-execution support for the embedded DBOS Postgres.
|
|
3
|
+
*
|
|
4
|
+
* PostgreSQL categorically refuses to run `initdb`/`postgres` as UID 0, so a
|
|
5
|
+
* root Atomic process (common in containers, CI sandboxes, and eval harnesses)
|
|
6
|
+
* cannot provision the embedded cluster directly. On Linux we instead resolve
|
|
7
|
+
* an unprivileged system account, keep the cluster under a root-safe base
|
|
8
|
+
* directory (`/root` is mode 0700 and untraversable by that account), and run
|
|
9
|
+
* every Postgres command with dropped privileges.
|
|
10
|
+
*
|
|
11
|
+
* Privilege dropping is strategy-probed at runtime: Node honors the child
|
|
12
|
+
* process `uid`/`gid` spawn options, but Bun currently ignores them, so we
|
|
13
|
+
* verify the drop actually happens (`id -u` must report the owner) and fall
|
|
14
|
+
* back to `setpriv`, `runuser`, or `su` wrappers when it does not.
|
|
15
|
+
*
|
|
16
|
+
* The embedded binaries themselves may also live under an untraversable
|
|
17
|
+
* prefix (for example a root-owned `~/.nvm` global install), so the caller can
|
|
18
|
+
* probe them as the unprivileged owner and fall back to a one-time copy into
|
|
19
|
+
* the cluster base directory.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { cpSync, existsSync } from "node:fs";
|
|
23
|
+
import { homedir } from "node:os";
|
|
24
|
+
import { dirname, join } from "node:path";
|
|
25
|
+
import { runLocalCommand, type LocalCommandResult } from "./local-command.js";
|
|
26
|
+
|
|
27
|
+
export interface EmbeddedPostgresOwner {
|
|
28
|
+
readonly uid: number;
|
|
29
|
+
readonly gid: number;
|
|
30
|
+
readonly name: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface EmbeddedPostgresRunContext {
|
|
34
|
+
/** Directory that holds the cluster, log file, and setup locks. */
|
|
35
|
+
readonly baseDir: string;
|
|
36
|
+
/** Present only when commands must drop privileges (Linux root). */
|
|
37
|
+
readonly owner?: EmbeddedPostgresOwner;
|
|
38
|
+
/** Runs a command as the owner; identity pass-through when no owner. */
|
|
39
|
+
readonly runAsOwner: LocalCommandRunner;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface EmbeddedPostgresBinaryPaths {
|
|
43
|
+
readonly pg_ctl: string;
|
|
44
|
+
readonly initdb: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type LocalCommandRunner = (
|
|
48
|
+
command: string,
|
|
49
|
+
args: readonly string[],
|
|
50
|
+
options?: { readonly uid?: number; readonly gid?: number },
|
|
51
|
+
) => Promise<LocalCommandResult>;
|
|
52
|
+
|
|
53
|
+
/** Root-safe cluster location: system path, traversable by system accounts. */
|
|
54
|
+
export const ROOT_EMBEDDED_BASE_DIR = "/var/lib/atomic-postgres";
|
|
55
|
+
|
|
56
|
+
/** Unprivileged accounts tried in order; `postgres` wins when present. */
|
|
57
|
+
const OWNER_CANDIDATES = ["postgres", "nobody", "daemon"] as const;
|
|
58
|
+
|
|
59
|
+
export function defaultEmbeddedBaseDir(): string {
|
|
60
|
+
return join(homedir(), ".atomic", "postgres");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Resolve where and as whom the embedded cluster should run. Non-root (and
|
|
65
|
+
* every non-Linux platform) keeps the historical home-directory layout. Linux
|
|
66
|
+
* root without a resolvable unprivileged account, or without any working
|
|
67
|
+
* privilege-drop mechanism, also falls through to the default context so
|
|
68
|
+
* PostgreSQL's own root refusal surfaces with full detail.
|
|
69
|
+
*/
|
|
70
|
+
export async function resolveEmbeddedRunContext(
|
|
71
|
+
runner: LocalCommandRunner = runLocalCommand,
|
|
72
|
+
euid: number | undefined = process.getuid?.(),
|
|
73
|
+
platform: NodeJS.Platform = process.platform,
|
|
74
|
+
): Promise<EmbeddedPostgresRunContext> {
|
|
75
|
+
if (platform !== "linux" || euid !== 0) {
|
|
76
|
+
return { baseDir: defaultEmbeddedBaseDir(), runAsOwner: runner };
|
|
77
|
+
}
|
|
78
|
+
for (const name of OWNER_CANDIDATES) {
|
|
79
|
+
const owner = await lookupOwner(runner, name);
|
|
80
|
+
if (owner === undefined) continue;
|
|
81
|
+
const runAsOwner = await resolvePrivilegeDrop(runner, owner);
|
|
82
|
+
if (runAsOwner === undefined) continue;
|
|
83
|
+
return { baseDir: ROOT_EMBEDDED_BASE_DIR, owner, runAsOwner };
|
|
84
|
+
}
|
|
85
|
+
return { baseDir: defaultEmbeddedBaseDir(), runAsOwner: runner };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Return a runner that verifiably executes commands as the owner, or
|
|
90
|
+
* `undefined` when no drop mechanism works. Each strategy is validated by
|
|
91
|
+
* running `id -u` through it and requiring the owner's uid on stdout — the
|
|
92
|
+
* spawn `uid`/`gid` options in particular are silently ignored by Bun.
|
|
93
|
+
*/
|
|
94
|
+
export async function resolvePrivilegeDrop(
|
|
95
|
+
runner: LocalCommandRunner,
|
|
96
|
+
owner: EmbeddedPostgresOwner,
|
|
97
|
+
): Promise<LocalCommandRunner | undefined> {
|
|
98
|
+
const strategies: readonly LocalCommandRunner[] = [
|
|
99
|
+
(command, args) => runner(command, args, { uid: owner.uid, gid: owner.gid }),
|
|
100
|
+
(command, args) => runner("setpriv", [
|
|
101
|
+
`--reuid=${owner.uid}`, `--regid=${owner.gid}`, "--clear-groups", "--", command, ...args,
|
|
102
|
+
]),
|
|
103
|
+
(command, args) => runner("runuser", ["-u", owner.name, "--", command, ...args]),
|
|
104
|
+
(command, args) => runner("su", ["-s", "/bin/sh", "-c", shellCommand(command, args), owner.name]),
|
|
105
|
+
];
|
|
106
|
+
for (const strategy of strategies) {
|
|
107
|
+
const probe = await strategy("id", ["-u"]).catch(() => undefined);
|
|
108
|
+
if (probe !== undefined && probe.exitCode === 0 && probe.stdout.trim() === String(owner.uid)) {
|
|
109
|
+
return strategy;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Ensure the embedded binaries are executable by the drop-privilege owner.
|
|
117
|
+
* Probes `initdb --version` as the owner; on failure (typically an
|
|
118
|
+
* untraversable ancestor such as `/root`) copies the package's `native` tree
|
|
119
|
+
* into the cluster base directory once and reuses it afterwards.
|
|
120
|
+
*/
|
|
121
|
+
export async function prepareBinariesForOwner(
|
|
122
|
+
binaries: EmbeddedPostgresBinaryPaths,
|
|
123
|
+
context: EmbeddedPostgresRunContext,
|
|
124
|
+
runner: LocalCommandRunner = runLocalCommand,
|
|
125
|
+
): Promise<EmbeddedPostgresBinaryPaths> {
|
|
126
|
+
const owner = context.owner;
|
|
127
|
+
if (owner === undefined) return binaries;
|
|
128
|
+
|
|
129
|
+
const probe = await context.runAsOwner(binaries.initdb, ["--version"]).catch(() => undefined);
|
|
130
|
+
if (probe !== undefined && probe.exitCode === 0) return binaries;
|
|
131
|
+
|
|
132
|
+
// `<packageRoot>/native/bin/initdb` → copy the whole `native` tree so the
|
|
133
|
+
// binaries keep their relative `../lib` runtime library references.
|
|
134
|
+
const nativeDir = dirname(dirname(binaries.initdb));
|
|
135
|
+
const copiedNativeDir = join(context.baseDir, "pg-runtime", "native");
|
|
136
|
+
const copied: EmbeddedPostgresBinaryPaths = {
|
|
137
|
+
pg_ctl: join(copiedNativeDir, "bin", "pg_ctl"),
|
|
138
|
+
initdb: join(copiedNativeDir, "bin", "initdb"),
|
|
139
|
+
};
|
|
140
|
+
if (!existsSync(copied.initdb)) {
|
|
141
|
+
cpSync(nativeDir, copiedNativeDir, { recursive: true });
|
|
142
|
+
}
|
|
143
|
+
const chown = await runner("chown", ["-R", `${owner.uid}:${owner.gid}`, join(context.baseDir, "pg-runtime")]);
|
|
144
|
+
if (chown.exitCode !== 0) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`Could not hand the copied embedded Postgres runtime to ${owner.name}: ${chown.stderr.trim() || chown.stdout.trim() || `exit ${chown.exitCode}`}`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
return copied;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function lookupOwner(runner: LocalCommandRunner, name: string): Promise<EmbeddedPostgresOwner | undefined> {
|
|
153
|
+
const uid = await lookupId(runner, ["-u", name]);
|
|
154
|
+
const gid = await lookupId(runner, ["-g", name]);
|
|
155
|
+
if (uid === undefined || gid === undefined || uid === 0) return undefined;
|
|
156
|
+
return { uid, gid, name };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function lookupId(runner: LocalCommandRunner, args: readonly string[]): Promise<number | undefined> {
|
|
160
|
+
try {
|
|
161
|
+
const result = await runner("id", args);
|
|
162
|
+
if (result.exitCode !== 0) return undefined;
|
|
163
|
+
const value = Number.parseInt(result.stdout.trim(), 10);
|
|
164
|
+
return Number.isInteger(value) && value > 0 ? value : undefined;
|
|
165
|
+
} catch {
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Single-quote a command line for `su -c`; arguments never embed user input. */
|
|
171
|
+
function shellCommand(command: string, args: readonly string[]): string {
|
|
172
|
+
return [command, ...args].map((part) => `'${part.replaceAll("'", "'\\''")}'`).join(" ");
|
|
173
|
+
}
|
|
@@ -10,12 +10,23 @@
|
|
|
10
10
|
* is started with `pg_ctl`, which daemonizes the server into its own session:
|
|
11
11
|
* it survives Atomic exiting and is shared by every concurrent Atomic session.
|
|
12
12
|
* Atomic never stops it.
|
|
13
|
+
*
|
|
14
|
+
* PostgreSQL refuses to run as UID 0, so a root Atomic process (containers,
|
|
15
|
+
* CI sandboxes, eval harnesses) resolves an unprivileged system account, keeps
|
|
16
|
+
* the cluster under `/var/lib/atomic-postgres` instead (a root home directory
|
|
17
|
+
* is untraversable for that account), and runs every Postgres command with
|
|
18
|
+
* dropped privileges. See dbos-embedded-postgres-root.ts.
|
|
13
19
|
*/
|
|
14
20
|
|
|
15
|
-
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
|
|
16
|
-
import {
|
|
21
|
+
import { chmodSync, chownSync, copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
|
|
22
|
+
import { tmpdir } from "node:os";
|
|
17
23
|
import { dirname, join, relative } from "node:path";
|
|
18
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
prepareBinariesForOwner,
|
|
26
|
+
resolveEmbeddedRunContext,
|
|
27
|
+
type EmbeddedPostgresRunContext,
|
|
28
|
+
} from "./dbos-embedded-postgres-root.js";
|
|
29
|
+
import { commandFailureDetail, delay, tcpReachable } from "./local-command.js";
|
|
19
30
|
|
|
20
31
|
const EMBEDDED_HOST = "127.0.0.1";
|
|
21
32
|
const EMBEDDED_PORT = 5439;
|
|
@@ -47,17 +58,20 @@ export function ensureEmbeddedDbosPostgres(): Promise<void> {
|
|
|
47
58
|
|
|
48
59
|
async function ensure(): Promise<void> {
|
|
49
60
|
if (await tcpReachable(EMBEDDED_HOST, EMBEDDED_PORT)) return;
|
|
50
|
-
const
|
|
51
|
-
hydrateBinaryLibraryLinks(
|
|
52
|
-
const
|
|
61
|
+
const loaded = await loadEmbeddedPostgresBinaries();
|
|
62
|
+
hydrateBinaryLibraryLinks(loaded.pg_ctl);
|
|
63
|
+
const context = await resolveEmbeddedRunContext();
|
|
64
|
+
const root = context.baseDir;
|
|
53
65
|
const dataDir = join(root, `v${EMBEDDED_PG_MAJOR}`);
|
|
54
66
|
const logFile = join(root, `v${EMBEDDED_PG_MAJOR}.log`);
|
|
55
67
|
mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
68
|
+
if (context.owner !== undefined) chownSync(root, context.owner.uid, context.owner.gid);
|
|
69
|
+
const binaries = await prepareBinariesForOwner(loaded, context);
|
|
56
70
|
|
|
57
71
|
await withSetupLock(join(root, `v${EMBEDDED_PG_MAJOR}.setup-lock`), async () => {
|
|
58
72
|
if (await tcpReachable(EMBEDDED_HOST, EMBEDDED_PORT)) return;
|
|
59
|
-
if (!existsSync(join(dataDir, "PG_VERSION"))) await initializeCluster(binaries.initdb, dataDir);
|
|
60
|
-
await startCluster(binaries.pg_ctl, dataDir, logFile);
|
|
73
|
+
if (!existsSync(join(dataDir, "PG_VERSION"))) await initializeCluster(binaries.initdb, dataDir, context);
|
|
74
|
+
await startCluster(binaries.pg_ctl, dataDir, logFile, context);
|
|
61
75
|
});
|
|
62
76
|
|
|
63
77
|
for (let attempt = 0; attempt < READY_ATTEMPTS; attempt += 1) {
|
|
@@ -67,11 +81,16 @@ async function ensure(): Promise<void> {
|
|
|
67
81
|
throw new Error(`Embedded Postgres started but never accepted connections on ${EMBEDDED_HOST}:${EMBEDDED_PORT}; see ${logFile}.`);
|
|
68
82
|
}
|
|
69
83
|
|
|
70
|
-
async function initializeCluster(
|
|
84
|
+
async function initializeCluster(
|
|
85
|
+
initdb: string,
|
|
86
|
+
dataDir: string,
|
|
87
|
+
context: EmbeddedPostgresRunContext,
|
|
88
|
+
): Promise<void> {
|
|
71
89
|
const passwordFile = join(tmpdir(), `atomic-pg-pw-${process.pid}-${crypto.randomUUID().slice(0, 8)}`);
|
|
72
90
|
writeFileSync(passwordFile, `${EMBEDDED_PASSWORD}\n`, { mode: 0o600 });
|
|
73
91
|
try {
|
|
74
|
-
|
|
92
|
+
if (context.owner !== undefined) chownSync(passwordFile, context.owner.uid, context.owner.gid);
|
|
93
|
+
const result = await context.runAsOwner(initdb, [
|
|
75
94
|
"-D", dataDir,
|
|
76
95
|
"-U", EMBEDDED_USER,
|
|
77
96
|
"-A", "password",
|
|
@@ -87,8 +106,13 @@ async function initializeCluster(initdb: string, dataDir: string): Promise<void>
|
|
|
87
106
|
}
|
|
88
107
|
}
|
|
89
108
|
|
|
90
|
-
async function startCluster(
|
|
91
|
-
|
|
109
|
+
async function startCluster(
|
|
110
|
+
pgCtl: string,
|
|
111
|
+
dataDir: string,
|
|
112
|
+
logFile: string,
|
|
113
|
+
context: EmbeddedPostgresRunContext,
|
|
114
|
+
): Promise<void> {
|
|
115
|
+
const result = await context.runAsOwner(pgCtl, [
|
|
92
116
|
"-D", dataDir,
|
|
93
117
|
"-l", logFile,
|
|
94
118
|
"-o", `-p ${EMBEDDED_PORT} -c listen_addresses=${EMBEDDED_HOST}`,
|
|
@@ -119,10 +119,16 @@ export function getReadyDbosBackendSync(): DbosDurableBackend | undefined {
|
|
|
119
119
|
|
|
120
120
|
export async function shutdownDbos(): Promise<void> {
|
|
121
121
|
if (shutdownPromise !== undefined) return await shutdownPromise;
|
|
122
|
-
|
|
122
|
+
const configuredPromise = configured;
|
|
123
|
+
if (configuredPromise === undefined) return;
|
|
123
124
|
shutdownPromise = (async () => {
|
|
124
|
-
|
|
125
|
-
|
|
125
|
+
// A backend that never reached "ready" has nothing to flush or stop.
|
|
126
|
+
// `configured`/`launchPromise` memoize rejections, so re-awaiting them
|
|
127
|
+
// unguarded would rethrow the original provisioning failure out of every
|
|
128
|
+
// session dispose — crashing otherwise-successful runs at process exit.
|
|
129
|
+
const durability = await configuredPromise.catch(() => undefined);
|
|
130
|
+
if (durability === undefined) return;
|
|
131
|
+
if (launchPromise !== undefined) await launchPromise.catch(() => undefined);
|
|
126
132
|
if (state !== "ready") return;
|
|
127
133
|
state = "shutting_down";
|
|
128
134
|
await durability.backend.flush();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** DBOS-
|
|
1
|
+
/** DBOS-first durable backend factory with a non-durable last-resort fallback. */
|
|
2
2
|
|
|
3
3
|
import { InMemoryDurableBackend, type DurableWorkflowBackend } from "./backend.js";
|
|
4
4
|
import {
|
|
@@ -9,8 +9,9 @@ import {
|
|
|
9
9
|
|
|
10
10
|
let injectedBackend: DurableWorkflowBackend | undefined;
|
|
11
11
|
let initializedBackend: DurableWorkflowBackend | undefined;
|
|
12
|
+
let initializing: Promise<DurableWorkflowBackend> | undefined;
|
|
12
13
|
|
|
13
|
-
/** Return the injected test backend or the process-wide
|
|
14
|
+
/** Return the injected test backend or the process-wide initialized backend. */
|
|
14
15
|
export function getDurableBackend(): DurableWorkflowBackend {
|
|
15
16
|
const backend = injectedBackend ?? initializedBackend ?? getReadyDbosBackendSync();
|
|
16
17
|
if (backend === undefined) throw new DbosNotReadyError();
|
|
@@ -20,7 +21,10 @@ export function getDurableBackend(): DurableWorkflowBackend {
|
|
|
20
21
|
/** Internal injection seam. Production initialization uses DBOS. */
|
|
21
22
|
export function setDurableBackend(backend: DurableWorkflowBackend | undefined): void {
|
|
22
23
|
injectedBackend = backend;
|
|
23
|
-
if (backend === undefined)
|
|
24
|
+
if (backend === undefined) {
|
|
25
|
+
initializedBackend = undefined;
|
|
26
|
+
initializing = undefined;
|
|
27
|
+
}
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
/** Create an isolated current-interface backend for tests only. */
|
|
@@ -28,9 +32,34 @@ export function createInMemoryTestBackend(): InMemoryDurableBackend {
|
|
|
28
32
|
return new InMemoryDurableBackend();
|
|
29
33
|
}
|
|
30
34
|
|
|
31
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Configure, register, launch, and install the DBOS backend.
|
|
37
|
+
*
|
|
38
|
+
* When no durable backend can be provisioned (no `DBOS_SYSTEM_DATABASE_URL`,
|
|
39
|
+
* embedded Postgres unavailable — e.g. running as root without an
|
|
40
|
+
* unprivileged account — and no Docker), workflows degrade to a process-local
|
|
41
|
+
* in-memory backend with a loud warning instead of refusing to run at all.
|
|
42
|
+
* Non-durable runs execute normally but do not survive the process:
|
|
43
|
+
* `/workflow resume` after exit has nothing to restore.
|
|
44
|
+
*/
|
|
32
45
|
export async function initializeDurableBackend(): Promise<DurableWorkflowBackend> {
|
|
33
46
|
if (injectedBackend !== undefined) return injectedBackend;
|
|
34
|
-
initializedBackend
|
|
35
|
-
|
|
47
|
+
if (initializedBackend !== undefined) return initializedBackend;
|
|
48
|
+
initializing ??= getReadyDbosBackend()
|
|
49
|
+
.catch((error: unknown) => degradeToNonDurableBackend(error))
|
|
50
|
+
.then((backend) => {
|
|
51
|
+
initializedBackend = backend;
|
|
52
|
+
return backend;
|
|
53
|
+
});
|
|
54
|
+
return await initializing;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function degradeToNonDurableBackend(error: unknown): DurableWorkflowBackend {
|
|
58
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
59
|
+
console.error(
|
|
60
|
+
"atomic-workflows: durable backend unavailable — continuing NON-DURABLY with an in-memory backend. "
|
|
61
|
+
+ "Workflow runs will execute, but their state will not survive this process and `/workflow resume` "
|
|
62
|
+
+ `after exit will not work. Restore durability by fixing Postgres provisioning: ${detail}`,
|
|
63
|
+
);
|
|
64
|
+
return new InMemoryDurableBackend();
|
|
36
65
|
}
|