@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.
Files changed (94) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/builtin/cursor/CHANGELOG.md +6 -0
  3. package/dist/builtin/cursor/package.json +2 -2
  4. package/dist/builtin/intercom/CHANGELOG.md +6 -0
  5. package/dist/builtin/intercom/package.json +2 -2
  6. package/dist/builtin/mcp/CHANGELOG.md +6 -0
  7. package/dist/builtin/mcp/package.json +1 -1
  8. package/dist/builtin/subagents/CHANGELOG.md +6 -0
  9. package/dist/builtin/subagents/package.json +1 -1
  10. package/dist/builtin/web-access/CHANGELOG.md +6 -0
  11. package/dist/builtin/web-access/package.json +1 -1
  12. package/dist/builtin/workflows/CHANGELOG.md +17 -0
  13. package/dist/builtin/workflows/README.md +8 -3
  14. package/dist/builtin/workflows/builtin/deep-research-codebase.ts +1 -1
  15. package/dist/builtin/workflows/package.json +1 -1
  16. package/dist/builtin/workflows/src/durable/backend.ts +2 -2
  17. package/dist/builtin/workflows/src/durable/dbos-embedded-postgres-root.ts +173 -0
  18. package/dist/builtin/workflows/src/durable/dbos-embedded-postgres.ts +36 -12
  19. package/dist/builtin/workflows/src/durable/dbos-lifecycle.ts +9 -3
  20. package/dist/builtin/workflows/src/durable/factory.ts +35 -6
  21. package/dist/builtin/workflows/src/durable/local-command.ts +11 -2
  22. package/dist/builtin/workflows/src/extension/extension-lifecycle.ts +14 -2
  23. package/dist/builtin/workflows/src/extension/public-types.ts +3 -4
  24. package/dist/builtin/workflows/src/extension/render-call.ts +4 -0
  25. package/dist/builtin/workflows/src/extension/render-result.ts +31 -1
  26. package/dist/builtin/workflows/src/extension/workflow-prompts.ts +5 -2
  27. package/dist/builtin/workflows/src/extension/workflow-schema.ts +3 -2
  28. package/dist/builtin/workflows/src/extension/workflow-tool-content.ts +1 -0
  29. package/dist/builtin/workflows/src/extension/workflow-tool-control.ts +88 -2
  30. package/dist/builtin/workflows/src/extension/workflow-tool.ts +13 -0
  31. package/dist/core/agent-session-events.d.ts.map +1 -1
  32. package/dist/core/agent-session-events.js +2 -1
  33. package/dist/core/agent-session-events.js.map +1 -1
  34. package/dist/core/agent-session-retry.d.ts.map +1 -1
  35. package/dist/core/agent-session-retry.js +7 -0
  36. package/dist/core/agent-session-retry.js.map +1 -1
  37. package/dist/core/agent-session-runtime.d.ts +13 -0
  38. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  39. package/dist/core/agent-session-runtime.js +12 -0
  40. package/dist/core/agent-session-runtime.js.map +1 -1
  41. package/dist/core/atomic-guide-command.d.ts.map +1 -1
  42. package/dist/core/atomic-guide-command.js +2 -0
  43. package/dist/core/atomic-guide-command.js.map +1 -1
  44. package/dist/core/auth-storage-backends.d.ts +4 -0
  45. package/dist/core/auth-storage-backends.d.ts.map +1 -1
  46. package/dist/core/auth-storage-backends.js +48 -3
  47. package/dist/core/auth-storage-backends.js.map +1 -1
  48. package/dist/core/auth-storage.d.ts.map +1 -1
  49. package/dist/core/auth-storage.js +27 -11
  50. package/dist/core/auth-storage.js.map +1 -1
  51. package/dist/core/codex-errors.d.ts +3 -0
  52. package/dist/core/codex-errors.d.ts.map +1 -0
  53. package/dist/core/codex-errors.js +12 -0
  54. package/dist/core/codex-errors.js.map +1 -0
  55. package/dist/core/slash-commands.js +1 -1
  56. package/dist/core/slash-commands.js.map +1 -1
  57. package/dist/modes/interactive/interactive-auth-routing.d.ts +2 -1
  58. package/dist/modes/interactive/interactive-auth-routing.d.ts.map +1 -1
  59. package/dist/modes/interactive/interactive-auth-routing.js +10 -5
  60. package/dist/modes/interactive/interactive-auth-routing.js.map +1 -1
  61. package/dist/modes/interactive/interactive-autocomplete.js +12 -2
  62. package/dist/modes/interactive/interactive-autocomplete.js.map +1 -1
  63. package/dist/modes/interactive-engine/extension-ui-bridge.d.ts +3 -1
  64. package/dist/modes/interactive-engine/extension-ui-bridge.d.ts.map +1 -1
  65. package/dist/modes/interactive-engine/extension-ui-bridge.js +6 -0
  66. package/dist/modes/interactive-engine/extension-ui-bridge.js.map +1 -1
  67. package/dist/modes/interactive-engine/isolated-runtime.d.ts +4 -5
  68. package/dist/modes/interactive-engine/isolated-runtime.d.ts.map +1 -1
  69. package/dist/modes/interactive-engine/isolated-runtime.js +14 -24
  70. package/dist/modes/interactive-engine/isolated-runtime.js.map +1 -1
  71. package/dist/modes/interactive-engine/remote-model-catalog.d.ts +15 -0
  72. package/dist/modes/interactive-engine/remote-model-catalog.d.ts.map +1 -0
  73. package/dist/modes/interactive-engine/remote-model-catalog.js +67 -0
  74. package/dist/modes/interactive-engine/remote-model-catalog.js.map +1 -0
  75. package/dist/modes/rpc/rpc-client-api.d.ts +8 -1
  76. package/dist/modes/rpc/rpc-client-api.d.ts.map +1 -1
  77. package/dist/modes/rpc/rpc-client-api.js +9 -0
  78. package/dist/modes/rpc/rpc-client-api.js.map +1 -1
  79. package/dist/modes/rpc/rpc-command-handler.d.ts.map +1 -1
  80. package/dist/modes/rpc/rpc-command-handler.js +30 -0
  81. package/dist/modes/rpc/rpc-command-handler.js.map +1 -1
  82. package/dist/modes/rpc/rpc-types.d.ts +67 -7
  83. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  84. package/dist/modes/rpc/rpc-types.js.map +1 -1
  85. package/docs/docs.json +9 -1
  86. package/docs/models/artificial-analysis-index.md +82 -0
  87. package/docs/models/model-selection.md +63 -0
  88. package/docs/models/pareto-efficiency.md +64 -0
  89. package/docs/providers.md +5 -1
  90. package/docs/quickstart.md +1 -1
  91. package/docs/rpc.md +27 -0
  92. package/docs/workflows.md +20 -7
  93. package/npm-shrinkwrap.json +24 -24
  94. 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.10",
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.10",
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.10",
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.20.6",
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.10",
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.10",
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.10",
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
- Scout + research-history chain → two parallel specialist waves → aggregator. Ideal for deep investigation of a codebase topic across locator, pattern, analyzer, and ecosystem angles.
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: "Scout + research-history chain parallel specialist waves → aggregator for deep codebase research.",
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({
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/workflows",
3
- "version": "0.9.10",
3
+ "version": "0.9.11-alpha.1",
4
4
  "private": true,
5
5
  "description": "Atomic extension for multi-stage workflow authoring and execution.",
6
6
  "contributors": [
@@ -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 an explicit test seam. */
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 current-interface backend for explicit test injection. */
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 { homedir, tmpdir } from "node:os";
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 { commandFailureDetail, delay, runLocalCommand, tcpReachable } from "./local-command.js";
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 binaries = await loadEmbeddedPostgresBinaries();
51
- hydrateBinaryLibraryLinks(binaries.pg_ctl);
52
- const root = join(homedir(), ".atomic", "postgres");
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(initdb: string, dataDir: string): Promise<void> {
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
- const result = await runLocalCommand(initdb, [
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(pgCtl: string, dataDir: string, logFile: string): Promise<void> {
91
- const result = await runLocalCommand(pgCtl, [
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
- if (configured === undefined) return;
122
+ const configuredPromise = configured;
123
+ if (configuredPromise === undefined) return;
123
124
  shutdownPromise = (async () => {
124
- const durability = await configured;
125
- if (launchPromise !== undefined) await launchPromise;
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-only durable backend factory and internal test injection seam. */
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 ready DBOS backend. */
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) initializedBackend = 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
- /** Configure, register, launch, and install the mandatory DBOS backend. */
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 ??= await getReadyDbosBackend();
35
- return initializedBackend;
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
  }