@bastani/atomic 0.9.41 → 0.9.42-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 +49 -0
- package/dist/builtin/intercom/CHANGELOG.md +13 -0
- package/dist/builtin/intercom/README.md +7 -3
- package/dist/builtin/intercom/broker/broker-status.ts +48 -0
- package/dist/builtin/intercom/broker/broker.ts +90 -9
- package/dist/builtin/intercom/broker/client.ts +112 -12
- package/dist/builtin/intercom/broker/control.ts +225 -0
- package/dist/builtin/intercom/broker/multinode-router.ts +32 -39
- package/dist/builtin/intercom/broker/paths.ts +4 -0
- package/dist/builtin/intercom/broker/pending-send-registry.ts +27 -5
- package/dist/builtin/intercom/broker/shared-storage-setting.ts +60 -0
- package/dist/builtin/intercom/broker/spawn.ts +13 -4
- package/dist/builtin/intercom/broker/sql-lease-store.ts +23 -0
- package/dist/builtin/intercom/broker/watchdog.ts +105 -0
- package/dist/builtin/intercom/index.bundle.mjs +200 -22
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/intercom/types.ts +34 -2
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/subagents/src/extension/index.bundle.mjs +1 -0
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +12 -0
- package/dist/builtin/workflows/README.md +10 -0
- package/dist/builtin/workflows/builtin/{chunk-txbc93t6.js → chunk-3s95f7ry.js} +1 -1
- package/dist/builtin/workflows/builtin/{chunk-n2gkzkty.js → chunk-edhf30ef.js} +1 -1
- package/dist/builtin/workflows/builtin/{chunk-8x61e0s7.js → chunk-frqxd4d1.js} +18 -9
- package/dist/builtin/workflows/builtin/goal.js +2 -2
- package/dist/builtin/workflows/builtin/index.js +3 -3
- package/dist/builtin/workflows/builtin/ralph.js +2 -2
- package/dist/builtin/workflows/package.json +6 -1
- package/dist/builtin/workflows/src/authoring.d.ts +3 -1
- package/dist/builtin/workflows/src/durable/journal/internal.d.ts +13 -0
- package/dist/builtin/workflows/src/durable/journal/resolution.d.ts +2 -0
- package/dist/builtin/workflows/src/durable/journal/sql-run-journal.d.ts +1 -0
- package/dist/builtin/workflows/src/extension/index.bundle.mjs +88554 -82820
- package/dist/builtin/workflows/src/extension/local-worker-entry.mjs +72635 -0
- package/dist/builtin/workflows/src/index.js +53824 -5907
- package/dist/builtin/workflows/src/local-worker-entry.mjs +72635 -0
- package/dist/builtin/workflows/src/shared/authoring-contract-stage.d.ts +7 -0
- package/dist/builtin/workflows/src/shared/authoring-contract-ui.d.ts +2 -0
- package/dist/builtin/workflows/src/workers/public-options.d.ts +31 -0
- package/dist/bun/app-host-modules.d.ts +9 -0
- package/dist/bun/app-host-modules.d.ts.map +1 -0
- package/dist/bun/app-host-modules.js +10 -0
- package/dist/bun/app-host-modules.js.map +1 -0
- package/dist/bun/cli.d.ts +3 -1
- package/dist/bun/cli.d.ts.map +1 -1
- package/dist/bun/cli.js +15 -1
- package/dist/bun/cli.js.map +1 -1
- package/dist/bun/internal-intercom-broker.d.ts +17 -0
- package/dist/bun/internal-intercom-broker.d.ts.map +1 -1
- package/dist/bun/internal-intercom-broker.js +29 -0
- package/dist/bun/internal-intercom-broker.js.map +1 -1
- package/dist/bun/internal-workflow-worker.d.ts +4 -0
- package/dist/bun/internal-workflow-worker.d.ts.map +1 -0
- package/dist/bun/internal-workflow-worker.js +37 -0
- package/dist/bun/internal-workflow-worker.js.map +1 -0
- package/dist/bun/split-loader.js +10 -2
- package/dist/bun/split-loader.js.map +1 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +3 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/intercom-command.d.ts +15 -0
- package/dist/cli/intercom-command.d.ts.map +1 -0
- package/dist/cli/intercom-command.js +87 -0
- package/dist/cli/intercom-command.js.map +1 -0
- package/dist/cli/storage-command.d.ts +39 -0
- package/dist/cli/storage-command.d.ts.map +1 -0
- package/dist/cli/storage-command.js +367 -0
- package/dist/cli/storage-command.js.map +1 -0
- package/dist/core/agent-session-events.d.ts.map +1 -1
- package/dist/core/agent-session-events.js +5 -1
- package/dist/core/agent-session-events.js.map +1 -1
- package/dist/core/agent-session-methods.d.ts +1 -0
- package/dist/core/agent-session-methods.d.ts.map +1 -1
- package/dist/core/agent-session-methods.js.map +1 -1
- package/dist/core/agent-session-tasks.d.ts.map +1 -1
- package/dist/core/agent-session-tasks.js +1 -0
- package/dist/core/agent-session-tasks.js.map +1 -1
- package/dist/core/agent-session-types.d.ts +1 -0
- package/dist/core/agent-session-types.d.ts.map +1 -1
- package/dist/core/agent-session-types.js.map +1 -1
- package/dist/core/agent-session.d.ts +1 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +2 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/builtin-install-layout.d.ts +6 -0
- package/dist/core/builtin-install-layout.d.ts.map +1 -1
- package/dist/core/builtin-install-layout.js +9 -0
- package/dist/core/builtin-install-layout.js.map +1 -1
- package/dist/core/child-session-options.d.ts.map +1 -1
- package/dist/core/child-session-options.js +3 -0
- package/dist/core/child-session-options.js.map +1 -1
- package/dist/core/exec.js +2 -2
- package/dist/core/exec.js.map +1 -1
- package/dist/core/export-html/template.css +9 -0
- package/dist/core/export-html/template.js +165 -45
- package/dist/core/extensions/loader-host-modules.d.ts +6 -0
- package/dist/core/extensions/loader-host-modules.d.ts.map +1 -1
- package/dist/core/extensions/loader-host-modules.js +12 -1
- package/dist/core/extensions/loader-host-modules.js.map +1 -1
- package/dist/core/extensions/loader-virtual-modules.d.ts +6 -0
- package/dist/core/extensions/loader-virtual-modules.d.ts.map +1 -1
- package/dist/core/extensions/loader-virtual-modules.js +18 -0
- package/dist/core/extensions/loader-virtual-modules.js.map +1 -1
- package/dist/core/extensions/message-types.d.ts +4 -0
- package/dist/core/extensions/message-types.d.ts.map +1 -1
- package/dist/core/extensions/message-types.js.map +1 -1
- package/dist/core/fast-model-routing-transport.d.ts.map +1 -1
- package/dist/core/fast-model-routing-transport.js +5 -1
- package/dist/core/fast-model-routing-transport.js.map +1 -1
- package/dist/core/intercom-broker-control.d.ts +67 -0
- package/dist/core/intercom-broker-control.d.ts.map +1 -0
- package/dist/core/intercom-broker-control.js +17 -0
- package/dist/core/intercom-broker-control.js.map +1 -0
- package/dist/core/model-runtime-streaming.d.ts.map +1 -1
- package/dist/core/model-runtime-streaming.js +38 -1
- package/dist/core/model-runtime-streaming.js.map +1 -1
- package/dist/core/package-manager-git.d.ts +2 -2
- package/dist/core/package-manager-git.d.ts.map +1 -1
- package/dist/core/package-manager-git.js.map +1 -1
- package/dist/core/package-manager-manifest.d.ts.map +1 -1
- package/dist/core/package-manager-manifest.js +9 -1
- package/dist/core/package-manager-manifest.js.map +1 -1
- package/dist/core/package-manager-npm.d.ts +2 -2
- package/dist/core/package-manager-npm.d.ts.map +1 -1
- package/dist/core/package-manager-npm.js.map +1 -1
- package/dist/core/package-manager-paths.d.ts +7 -7
- package/dist/core/package-manager-paths.d.ts.map +1 -1
- package/dist/core/package-manager-paths.js.map +1 -1
- package/dist/core/package-manager-storage-adapters.d.ts +7 -0
- package/dist/core/package-manager-storage-adapters.d.ts.map +1 -0
- package/dist/core/package-manager-storage-adapters.js +76 -0
- package/dist/core/package-manager-storage-adapters.js.map +1 -0
- package/dist/core/package-manager-types.d.ts +4 -0
- package/dist/core/package-manager-types.d.ts.map +1 -1
- package/dist/core/package-manager-types.js.map +1 -1
- package/dist/core/provider-rate-limits.d.ts +23 -0
- package/dist/core/provider-rate-limits.d.ts.map +1 -0
- package/dist/core/provider-rate-limits.js +113 -0
- package/dist/core/provider-rate-limits.js.map +1 -0
- package/dist/core/provider-request-admission.d.ts +7 -0
- package/dist/core/provider-request-admission.d.ts.map +1 -0
- package/dist/core/provider-request-admission.js +91 -0
- package/dist/core/provider-request-admission.js.map +1 -0
- package/dist/core/resource-admission-scope.d.ts +4 -0
- package/dist/core/resource-admission-scope.d.ts.map +1 -0
- package/dist/core/resource-admission-scope.js +9 -0
- package/dist/core/resource-admission-scope.js.map +1 -0
- package/dist/core/sdk-types.d.ts +16 -0
- package/dist/core/sdk-types.d.ts.map +1 -1
- package/dist/core/sdk-types.js.map +1 -1
- package/dist/core/sdk.d.ts +2 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +18 -2
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-lifecycle-work.d.ts +1 -0
- package/dist/core/session-lifecycle-work.d.ts.map +1 -1
- package/dist/core/session-lifecycle-work.js +1 -0
- package/dist/core/session-lifecycle-work.js.map +1 -1
- package/dist/core/settings-manager-basic-accessors.d.ts +3 -0
- package/dist/core/settings-manager-basic-accessors.d.ts.map +1 -1
- package/dist/core/settings-manager-basic-accessors.js +6 -0
- package/dist/core/settings-manager-basic-accessors.js.map +1 -1
- package/dist/core/settings-schema.d.ts +22 -0
- package/dist/core/settings-schema.d.ts.map +1 -1
- package/dist/core/settings-schema.js +18 -1
- package/dist/core/settings-schema.js.map +1 -1
- package/dist/core/storage/index.d.ts +1 -0
- package/dist/core/storage/index.d.ts.map +1 -1
- package/dist/core/storage/index.js +1 -0
- package/dist/core/storage/index.js.map +1 -1
- package/dist/core/storage/node-sqlite.d.ts +2 -1
- package/dist/core/storage/node-sqlite.d.ts.map +1 -1
- package/dist/core/storage/node-sqlite.js +5 -4
- package/dist/core/storage/node-sqlite.js.map +1 -1
- package/dist/core/storage/provider.d.ts +6 -0
- package/dist/core/storage/provider.d.ts.map +1 -1
- package/dist/core/storage/provider.js +27 -7
- package/dist/core/storage/provider.js.map +1 -1
- package/dist/core/storage/sqlite-facade.d.ts +49 -0
- package/dist/core/storage/sqlite-facade.d.ts.map +1 -0
- package/dist/core/storage/sqlite-facade.js +180 -0
- package/dist/core/storage/sqlite-facade.js.map +1 -0
- package/dist/core/storage/storage-status.d.ts +81 -0
- package/dist/core/storage/storage-status.d.ts.map +1 -0
- package/dist/core/storage/storage-status.js +409 -0
- package/dist/core/storage/storage-status.js.map +1 -0
- package/dist/core/tasks/supervisor.d.ts +2 -1
- package/dist/core/tasks/supervisor.d.ts.map +1 -1
- package/dist/core/tasks/supervisor.js +105 -4
- package/dist/core/tasks/supervisor.js.map +1 -1
- package/dist/core/tools/bash-pty-native.d.ts.map +1 -1
- package/dist/core/tools/bash-pty-native.js +1 -1
- package/dist/core/tools/bash-pty-native.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +28 -2
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts +3 -1
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +7 -3
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/chat-message-renderer.d.ts +9 -1
- package/dist/modes/interactive/components/chat-message-renderer.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-message-renderer.js +16 -8
- package/dist/modes/interactive/components/chat-message-renderer.js.map +1 -1
- package/dist/modes/interactive/components/login-dialog.d.ts +5 -0
- package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
- package/dist/modes/interactive/components/login-dialog.js +69 -2
- package/dist/modes/interactive/components/login-dialog.js.map +1 -1
- package/dist/modes/interactive/components/markdown-transform.d.ts +1 -1
- package/dist/modes/interactive/components/markdown-transform.d.ts.map +1 -1
- package/dist/modes/interactive/components/markdown-transform.js +2 -2
- package/dist/modes/interactive/components/markdown-transform.js.map +1 -1
- package/dist/modes/interactive/components/session-selector-search.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector-search.js +80 -22
- package/dist/modes/interactive/components/session-selector-search.js.map +1 -1
- package/dist/modes/interactive/components/user-message.d.ts +3 -1
- package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/user-message.js +6 -2
- package/dist/modes/interactive/components/user-message.js.map +1 -1
- package/dist/modes/interactive/interactive-mode-deps.d.ts +1 -1
- package/dist/modes/interactive/interactive-mode-deps.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode-deps.js.map +1 -1
- package/dist/modes/interactive/interactive-render-chat.js +6 -2
- package/dist/modes/interactive/interactive-render-chat.js.map +1 -1
- package/dist/modes/interactive-engine/engine-health.d.ts +8 -4
- package/dist/modes/interactive-engine/engine-health.d.ts.map +1 -1
- package/dist/modes/interactive-engine/engine-health.js +41 -6
- package/dist/modes/interactive-engine/engine-health.js.map +1 -1
- package/dist/modes/interactive-engine/isolated-runtime.d.ts.map +1 -1
- package/dist/modes/interactive-engine/isolated-runtime.js +19 -17
- package/dist/modes/interactive-engine/isolated-runtime.js.map +1 -1
- package/dist/modes/interactive-engine/remote-queue-pause.js +1 -1
- package/dist/modes/interactive-engine/remote-queue-pause.js.map +1 -1
- package/dist/modes/rpc/rpc-input-scheduler.d.ts +1 -0
- package/dist/modes/rpc/rpc-input-scheduler.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-input-scheduler.js +4 -1
- package/dist/modes/rpc/rpc-input-scheduler.js.map +1 -1
- package/dist/modes/rpc/rpc-input.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-input.js +29 -7
- package/dist/modes/rpc/rpc-input.js.map +1 -1
- package/dist/utils/shell.d.ts.map +1 -1
- package/dist/utils/shell.js +14 -0
- package/dist/utils/shell.js.map +1 -1
- package/docs/custom-provider/oauth.md +8 -1
- package/docs/extensions/api-reference.md +2 -0
- package/docs/intercom/operations.md +17 -8
- package/docs/intercom/reference.md +1 -1
- package/docs/keybindings.md +1 -1
- package/docs/packages/authoring.md +15 -0
- package/docs/packages.md +3 -2
- package/docs/providers.md +2 -0
- package/docs/reference/cli.md +17 -0
- package/docs/sdk.md +37 -0
- package/docs/settings.md +33 -2
- package/docs/storage.md +191 -219
- package/docs/usage.md +1 -1
- package/docs/workflows/api-reference.md +50 -0
- package/docs/workflows/operations.md +44 -1
- package/docs/workflows.md +25 -0
- package/examples/extensions/gondolin/package-lock.json +14 -24
- package/examples/extensions/gondolin/package.json +1 -1
- package/npm-shrinkwrap.json +35 -35
- package/package.json +4 -4
- package/schemas/settings.schema.json +87 -1
package/docs/storage.md
CHANGED
|
@@ -21,7 +21,7 @@ try {
|
|
|
21
21
|
|
|
22
22
|
`exec`, `run`, `get`, and `all` return promises. Bind values through positional `?` parameters; `run` returns `{ changes }`, `get` returns a row or `undefined`, and `all` returns an array. Values may be null, number, bigint, string, or Uint8Array. Binary values are copied when the call is made, so reusing a buffer afterward does not change queued work. INTEGER values inside the safe integer range are returned as numbers; larger values are returned as bigints.
|
|
23
23
|
|
|
24
|
-
The node adapter creates parent directories and enables WAL, full synchronization, and foreign keys. Its busy timeout defaults to 5000 ms; override it with `openNodeSqliteDatabase(path, { busyTimeoutMs: 1000 })`. Use `:memory:` for an ephemeral database.
|
|
24
|
+
The node adapter creates parent directories and enables WAL, full synchronization, and foreign keys. Its busy timeout defaults to 5000 ms; override it with `openNodeSqliteDatabase(path, { busyTimeoutMs: 1000 })`. Use `:memory:` for an ephemeral database. Pass `{ readOnly: true }` to inspect an existing file without creating it or changing its journal mode.
|
|
25
25
|
|
|
26
26
|
Transactions from separate `openNodeSqliteDatabase` connections to the same file queue asynchronously within one process; symlink paths refer to the same queue. Separate in-memory databases remain independent. SQLite locking and the busy timeout still govern contention from other processes. Wrap a caller-owned connection consistently: its transaction queue is shared by wrappers of that connection, not by other connections to its file.
|
|
27
27
|
|
|
@@ -33,55 +33,102 @@ Errors are exported classes with typed fields. `StorageUnavailableError` provide
|
|
|
33
33
|
|
|
34
34
|
String bindings containing U+0000 are rejected before SQL executes with `StorageInvalidTextError`. Its `parameterIndex` is zero-based and its `reason` is `"nul-character"`. This rule applies to database calls and transaction handles. Store JSON with `JSON.stringify`, which escapes NUL characters, or use `Uint8Array` for binary data containing zero bytes. This validation does not inspect SQL source or SQL-generated values.
|
|
35
35
|
|
|
36
|
-
##
|
|
36
|
+
## Turso and libSQL
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
`@bastani/atomic-storage-libsql` is the maintained adapter for Turso and other libSQL databases. It stores sessions, workflow runs, and Intercom's multi-node routing in the database. Install it once per machine, in user scope so Atomic can find it:
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
```bash
|
|
41
|
+
atomic install npm:@bastani/atomic-storage-libsql
|
|
42
|
+
```
|
|
41
43
|
|
|
42
|
-
|
|
44
|
+
Then select it in `~/.atomic/agent/settings.json`:
|
|
43
45
|
|
|
44
|
-
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"storage": {
|
|
49
|
+
"adapter": "@bastani/atomic-storage-libsql",
|
|
50
|
+
"options": { "url": "libsql://<db>.turso.io" },
|
|
51
|
+
"secrets": { "authToken": { "file": "turso-token" } }
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
45
55
|
|
|
46
|
-
|
|
47
|
-
import type { SqliteDatabase } from "@bastani/atomic/storage";
|
|
48
|
-
import { openLibsqlDatabase } from "./storage-libsql-wrapper.js";
|
|
56
|
+
Save the token with `turso db tokens create <db> > turso-token` and keep it readable only by you (`chmod 600`); a trailing newline is ignored. A relative `file` resolves against Atomic home, normally `~/.atomic`. Use `{ "env": "TURSO_TOKEN" }` instead to read it from an environment variable. For a local database file, use a `file:` URL such as `file:/home/me/shared.sqlite` and omit `secrets`. Every machine that uses the same `storage` block shares the same sessions, workflow runs, and Intercom routing; add a `prefix` to keep projects or teams apart. See [Sharing storage across hosts](#sharing-storage-across-hosts).
|
|
49
57
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
Like any [adapter](#selecting-an-adapter), it runs with Atomic's privileges. To use a different driver, adapt it with [Bring your own driver](#bring-your-own-driver).
|
|
59
|
+
|
|
60
|
+
SDK callers can import `openLibsqlDatabase` from `@bastani/atomic-storage-libsql` directly. The package ships plain ESM JavaScript and TypeScript declarations, so supported Node versions need no TypeScript loader.
|
|
61
|
+
|
|
62
|
+
### Joining a machine to a shared database
|
|
63
|
+
|
|
64
|
+
Instead of editing settings by hand, install the adapter and join the database:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
atomic install npm:@bastani/atomic-storage-libsql
|
|
68
|
+
turso db tokens create <db> > ~/turso-token
|
|
69
|
+
atomic storage join libsql://<db>.turso.io --token-file ~/turso-token
|
|
70
|
+
atomic intercom restart
|
|
71
|
+
atomic storage status
|
|
61
72
|
```
|
|
62
73
|
|
|
63
|
-
|
|
74
|
+
`atomic storage join <url> --token-file <path> [--prefix <prefix>]`:
|
|
75
|
+
|
|
76
|
+
1. reads the token and removes surrounding whitespace; an empty token is an error;
|
|
77
|
+
2. connects with `@bastani/atomic-storage-libsql` and runs `SELECT 1`. It creates no tables and changes no data; sessions, workflow runs, and Intercom brokers create their own tables when they first use the database;
|
|
78
|
+
3. stores the token in a new `~/.atomic/agent/storage-token.<id>` file (in the agent directory under `ATOMIC_CODING_AGENT_DIR`), readable only by you (mode 600). Your input token file and tokens used by previous selections are left as they are;
|
|
79
|
+
4. writes only the top-level `storage` key of the global settings, `~/.atomic/agent/settings.json`, with the adapter, the URL, the token file, and the prefix. Every other setting keeps its value;
|
|
80
|
+
5. lists the environment variables and settings that take precedence over the new selection, and says whether the running Intercom broker still uses its previous one.
|
|
81
|
+
|
|
82
|
+
Nothing is written when the token is empty or the connection fails. If saving settings fails, the new token file is removed and the previous token remains untouched. Previous token files are kept after a successful join too, so already-running sessions can continue using them; remove them only after those sessions stop and no settings reference them. When the adapter is not installed, the command prints the `atomic install` command to run first. An absolute `file:` URL, such as `file:///home/me/shared.sqlite`, needs no `--token-file`; relative file URLs are refused so another working directory cannot select a different database. The `libsql://`, `https://`, and `wss://` schemes are accepted. A URL that contains the token itself, as a user name, a password, or an `authToken` parameter, is refused: pass the token with `--token-file` so it stays out of settings and output.
|
|
83
|
+
|
|
84
|
+
After storage is saved, a problem reading project settings only affects the final override report: the command warns you and still exits successfully. Fix the reported settings error, then run `atomic storage status` to inspect the effective selection.
|
|
64
85
|
|
|
65
|
-
|
|
66
|
-
|
|
86
|
+
The running Intercom broker keeps the selection it started with. `atomic intercom restart` replaces it: connected sessions reconnect within a few seconds and start a broker that reads the new selection. See [Running Intercom across machines](/intercom/operations#running-intercom-across-machines).
|
|
87
|
+
|
|
88
|
+
### Checking the selected storage
|
|
89
|
+
|
|
90
|
+
`atomic storage status` shows, for sessions, workflow runs, and Intercom:
|
|
91
|
+
|
|
92
|
+
- the selected storage and where the selection comes from: an environment variable, project settings, global settings (`storage`, `sessions.storage`, or `workflows.journal`), or the default. Selections that are set but outranked are listed as `overrides`. See [Selecting storage per process](#selecting-storage-per-process) for the order;
|
|
93
|
+
- whether the adapter loads, and from which installed package;
|
|
94
|
+
- whether the database answers a `SELECT 1`. Local SQLite files, including adapter `file:` URLs, are checked through a read-only SQLite connection instead of an adapter hook that could create the file. Missing files are reported without creating them; no journal mode or table is changed. Relative adapter `file:` URLs are not probed: use an absolute URL to identify the file unambiguously;
|
|
95
|
+
- for Intercom, whether a broker started now would route across machines, and how many brokers are live in the shared database;
|
|
96
|
+
- whether an Intercom broker is running, how many sessions it serves, whether it uses the current selection, and its multi-node routing state.
|
|
97
|
+
|
|
98
|
+
It also notes project settings that are not applied because the project is not trusted, and a project `storage` setting, which Intercom never uses. Run it in the project directory to see that project's selection. `--json` prints the same report as JSON. The command reports settings load errors instead of silently checking defaults, and exits 1 when settings cannot load, a selection is invalid, an adapter cannot load, or a database does not answer. Adapter loading, database operations, and connection cleanup each have a 15-second wait limit, so a stalled close cannot leave the command waiting indefinitely.
|
|
99
|
+
|
|
100
|
+
When the shared `storage` selection (global settings, `--storage`, or `ATOMIC_STORAGE`) names an adapter that the running Atomic cannot load, interactive and print sessions warn at startup, naming the adapter and the cause. The Intercom broker would otherwise leave multi-node routing off with only a line in its log.
|
|
101
|
+
|
|
102
|
+
## Bring your own driver
|
|
103
|
+
|
|
104
|
+
Atomic does not bundle remote database drivers. Install your chosen SQLite-compatible driver and turn it into a `SqliteDatabase` with `createSqliteDatabase` from `@bastani/atomic/storage`. You supply the driver calls:
|
|
105
|
+
|
|
106
|
+
- `descriptor`: the diagnostic `{ adapter, location }`. The facade passes `location` through `redact`.
|
|
107
|
+
- `execute(sql, params)`: runs one statement with positional parameters and returns `{ columns, rows, changes }`, where each row lists its values in column order. Return integers as `bigint` to keep large values exact.
|
|
108
|
+
- `executeMultiple(sql)`: runs zero or more statements that take no parameters.
|
|
109
|
+
- `beginTransaction()`: begins a write transaction and returns its own `execute` and `executeMultiple`, plus `commit()`, `rollback()`, and an optional `close()` that releases it afterward.
|
|
110
|
+
- `close()`: releases the connection.
|
|
111
|
+
- `redact(text)`, optional: removes credentials from diagnostic text. `redactSecrets(secrets)` builds one that also removes each secret's URI-encoded and decoded forms, longest first.
|
|
112
|
+
|
|
113
|
+
Driver calls may return values or promises. The returned facade follows the rules described above: it queues operations in call order and keeps them outside active transactions, snapshots binary parameters when a call is made, rejects NUL text with `StorageInvalidTextError` before SQL runs, returns safe-range integers as numbers and blobs as `Uint8Array` copies, expires transaction handles when their callback settles, and closes the driver once after draining accepted work. A failed driver call rejects with `StorageUnavailableError`, whose `cause` is the driver error with credentials redacted. A failed rollback rejects with `StorageRollbackError`, and callback failures are preserved. Do not fall back to memory after an opening failure.
|
|
114
|
+
|
|
115
|
+
You can also implement `SqliteDatabase` yourself by following the same rules. Either way, run the [conformance suite](#adapter-conformance) against it.
|
|
116
|
+
|
|
117
|
+
### libSQL / Turso example
|
|
118
|
+
|
|
119
|
+
This module is a complete adapter. Install `@libsql/client` where the module can import it, for example as a dependency of an [Atomic package](#adapters-from-atomic-packages). It accepts caller-provided credentials only. For a local file, create its parent directory first; no network or credentials are needed.
|
|
67
120
|
|
|
68
121
|
{/* custom-driver-example */}
|
|
69
122
|
```ts
|
|
70
|
-
import type { Client,
|
|
123
|
+
import type { Client, Transaction } from "@libsql/client";
|
|
71
124
|
import {
|
|
125
|
+
createSqliteDatabase,
|
|
126
|
+
redactSecrets,
|
|
72
127
|
type SqliteDatabase,
|
|
73
|
-
type
|
|
74
|
-
type
|
|
75
|
-
type SqliteValue,
|
|
76
|
-
type StorageDescriptor,
|
|
128
|
+
type SqliteDriverExecutor,
|
|
129
|
+
type StorageAdapterModule,
|
|
77
130
|
StorageAdapterLoadError,
|
|
78
|
-
StorageError,
|
|
79
131
|
type StorageFailureCause,
|
|
80
|
-
StorageInvalidTextError,
|
|
81
|
-
StorageRollbackError,
|
|
82
|
-
StorageTransactionClosedError,
|
|
83
|
-
StorageUnavailableError,
|
|
84
|
-
StoreClosedError,
|
|
85
132
|
} from "@bastani/atomic/storage";
|
|
86
133
|
|
|
87
134
|
export interface LibsqlDatabaseOptions {
|
|
@@ -89,224 +136,122 @@ export interface LibsqlDatabaseOptions {
|
|
|
89
136
|
authToken?: string;
|
|
90
137
|
}
|
|
91
138
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
139
|
+
/** Credentials a libSQL URL can carry: its userinfo and every `authToken` query value. */
|
|
140
|
+
function urlCredentials(url: string): string[] {
|
|
141
|
+
const secrets: string[] = [];
|
|
142
|
+
// Scan each key/value once, including malformed URLs, without retrying a growing suffix at every '?'.
|
|
143
|
+
for (let index = 0; index < url.length; index++) {
|
|
144
|
+
if (url[index] !== "?" && url[index] !== "&") continue;
|
|
145
|
+
const start = ++index;
|
|
146
|
+
while (index < url.length && !"?&=#".includes(url[index]!)) index++;
|
|
147
|
+
if (url[index] === "=") {
|
|
148
|
+
const valueStart = ++index;
|
|
149
|
+
while (index < url.length && url[index] !== "&" && url[index] !== "#") index++;
|
|
150
|
+
const token = new URLSearchParams(url.slice(start, index)).get("authToken");
|
|
151
|
+
if (token !== null) secrets.push(url.slice(valueStart, index), token);
|
|
152
|
+
}
|
|
153
|
+
index--;
|
|
97
154
|
}
|
|
98
|
-
const userinfo =
|
|
155
|
+
const userinfo = url.match(/\/\/([^/?#]*)@/)?.[1];
|
|
99
156
|
if (userinfo !== undefined) secrets.push(...userinfo.split(":"));
|
|
100
|
-
|
|
101
|
-
const url = new URL(options.url);
|
|
102
|
-
secrets.push(url.username, url.password, ...url.searchParams.getAll("authToken"));
|
|
103
|
-
} catch {}
|
|
104
|
-
for (const secret of [...secrets]) {
|
|
105
|
-
try {
|
|
106
|
-
secrets.push(encodeURIComponent(secret));
|
|
107
|
-
secrets.push(decodeURIComponent(secret));
|
|
108
|
-
} catch {}
|
|
109
|
-
}
|
|
110
|
-
return (text) =>
|
|
111
|
-
secrets
|
|
112
|
-
.filter(Boolean)
|
|
113
|
-
.sort((first, second) => second.length - first.length)
|
|
114
|
-
.reduce((value, secret) => value.replaceAll(secret, "[redacted]"), text);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function redactCause(cause: StorageFailureCause, redact: (text: string) => string): StorageFailureCause {
|
|
118
|
-
if (typeof cause === "string") return redact(cause);
|
|
119
|
-
if (!(cause instanceof Error)) return cause;
|
|
120
|
-
const nested = cause.cause as StorageFailureCause;
|
|
121
|
-
const safeCause = redactCause(nested, redact);
|
|
122
|
-
const message = redact(cause.message);
|
|
123
|
-
const stack = cause.stack === undefined ? undefined : redact(cause.stack);
|
|
124
|
-
if (message === cause.message && stack === cause.stack && safeCause === nested) return cause;
|
|
125
|
-
const safe = new StorageError(message, { cause: safeCause });
|
|
126
|
-
safe.stack = stack;
|
|
127
|
-
return safe;
|
|
157
|
+
return secrets;
|
|
128
158
|
}
|
|
129
159
|
|
|
130
|
-
function
|
|
131
|
-
return
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
return
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
value <= BigInt(Number.MAX_SAFE_INTEGER)
|
|
139
|
-
? Number(value)
|
|
140
|
-
: value instanceof ArrayBuffer
|
|
141
|
-
? new Uint8Array(value.slice(0))
|
|
142
|
-
: value,
|
|
143
|
-
];
|
|
144
|
-
}),
|
|
145
|
-
);
|
|
160
|
+
function statements(connection: () => Client | Transaction): SqliteDriverExecutor {
|
|
161
|
+
return {
|
|
162
|
+
async execute(sql, params) {
|
|
163
|
+
const result = await connection().execute({ sql, args: [...params] });
|
|
164
|
+
return { columns: result.columns, rows: result.rows, changes: result.rowsAffected };
|
|
165
|
+
},
|
|
166
|
+
executeMultiple: (sql) => connection().executeMultiple(sql),
|
|
167
|
+
};
|
|
146
168
|
}
|
|
147
169
|
|
|
148
|
-
|
|
149
|
-
readonly descriptor: StorageDescriptor;
|
|
150
|
-
private readonly client: Client;
|
|
151
|
-
private readonly redact: (text: string) => string;
|
|
152
|
-
private tail: Promise<void> = Promise.resolve();
|
|
153
|
-
private closing = false;
|
|
154
|
-
private closePromise?: Promise<void>;
|
|
155
|
-
private readonly executor: SqliteExecutor;
|
|
156
|
-
|
|
157
|
-
constructor(client: Client, descriptor: StorageDescriptor, redact: (text: string) => string) {
|
|
158
|
-
this.client = client;
|
|
159
|
-
this.descriptor = descriptor;
|
|
160
|
-
this.redact = redact;
|
|
161
|
-
this.executor = this.createExecutor(client, () => true);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
private async driver<T>(operation: () => T | Promise<T>): Promise<T> {
|
|
165
|
-
try {
|
|
166
|
-
return await operation();
|
|
167
|
-
} catch (cause) {
|
|
168
|
-
throw new StorageUnavailableError(this.descriptor, redactCause(cause as StorageFailureCause, this.redact));
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
private createExecutor(connection: Client | Transaction, active: () => boolean): SqliteExecutor {
|
|
173
|
-
const execute = async <T>(operation: () => Promise<T>, params: readonly SqliteValue[] = []): Promise<T> => {
|
|
174
|
-
if (!active()) throw new StorageTransactionClosedError();
|
|
175
|
-
const invalidIndex = params.findIndex((value) => typeof value === "string" && value.includes("\0"));
|
|
176
|
-
if (invalidIndex !== -1) throw new StorageInvalidTextError(invalidIndex);
|
|
177
|
-
return this.driver(operation);
|
|
178
|
-
};
|
|
179
|
-
const statement = (sql: string, params: readonly SqliteValue[]): Promise<ResultSet> =>
|
|
180
|
-
connection.execute({
|
|
181
|
-
sql,
|
|
182
|
-
args: params.map((value) => (value instanceof Uint8Array ? new Uint8Array(value) : value)),
|
|
183
|
-
});
|
|
184
|
-
return {
|
|
185
|
-
exec: (sql) => execute(() => connection.executeMultiple(sql)),
|
|
186
|
-
run: (sql, ...params) =>
|
|
187
|
-
execute(async () => ({ changes: (await statement(sql, params)).rowsAffected }), params),
|
|
188
|
-
get: <T extends SqliteRow>(sql: string, ...params: readonly SqliteValue[]) =>
|
|
189
|
-
execute(async () => {
|
|
190
|
-
const result = await statement(sql, params);
|
|
191
|
-
const row = result.rows[0];
|
|
192
|
-
return row === undefined ? undefined : (normalizeRow(row, result.columns) as T);
|
|
193
|
-
}, params),
|
|
194
|
-
all: <T extends SqliteRow>(sql: string, ...params: readonly SqliteValue[]) =>
|
|
195
|
-
execute(async () => {
|
|
196
|
-
const result = await statement(sql, params);
|
|
197
|
-
return result.rows.map((row) => normalizeRow(row, result.columns) as T);
|
|
198
|
-
}, params),
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
private enqueue<T>(operation: () => Promise<T>): Promise<T> {
|
|
203
|
-
if (this.closing) return Promise.reject(new StoreClosedError(this.descriptor));
|
|
204
|
-
const result = this.tail.then(operation);
|
|
205
|
-
this.tail = result.then(
|
|
206
|
-
() => {},
|
|
207
|
-
() => {},
|
|
208
|
-
);
|
|
209
|
-
return result;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
exec(sql: string): Promise<void> {
|
|
213
|
-
return this.enqueue(() => this.executor.exec(sql));
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
run(sql: string, ...params: readonly SqliteValue[]) {
|
|
217
|
-
const bound = params.map((value) => (value instanceof Uint8Array ? new Uint8Array(value) : value));
|
|
218
|
-
return this.enqueue(() => this.executor.run(sql, ...bound));
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
get<T extends SqliteRow>(sql: string, ...params: readonly SqliteValue[]): Promise<T | undefined> {
|
|
222
|
-
const bound = params.map((value) => (value instanceof Uint8Array ? new Uint8Array(value) : value));
|
|
223
|
-
return this.enqueue(() => this.executor.get<T>(sql, ...bound));
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
all<T extends SqliteRow>(sql: string, ...params: readonly SqliteValue[]): Promise<T[]> {
|
|
227
|
-
const bound = params.map((value) => (value instanceof Uint8Array ? new Uint8Array(value) : value));
|
|
228
|
-
return this.enqueue(() => this.executor.all<T>(sql, ...bound));
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
transaction<T>(callback: (transaction: SqliteExecutor) => Promise<T>): Promise<T> {
|
|
232
|
-
return this.enqueue(async () => {
|
|
233
|
-
const connection = await this.driver(() => this.client.transaction("write"));
|
|
234
|
-
let active = true;
|
|
235
|
-
try {
|
|
236
|
-
const result = await callback(this.createExecutor(connection, () => active));
|
|
237
|
-
active = false;
|
|
238
|
-
await this.driver(() => connection.commit());
|
|
239
|
-
return result;
|
|
240
|
-
} catch (cause) {
|
|
241
|
-
active = false;
|
|
242
|
-
try {
|
|
243
|
-
await this.driver(() => connection.rollback());
|
|
244
|
-
} catch (rollbackCause) {
|
|
245
|
-
throw new StorageRollbackError(cause as StorageFailureCause, rollbackCause as StorageFailureCause);
|
|
246
|
-
}
|
|
247
|
-
throw cause;
|
|
248
|
-
} finally {
|
|
249
|
-
await this.driver(() => connection.close());
|
|
250
|
-
}
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
close(): Promise<void> {
|
|
255
|
-
if (!this.closePromise) {
|
|
256
|
-
this.closing = true;
|
|
257
|
-
this.closePromise = this.tail.then(() => this.driver(() => this.client.close()));
|
|
258
|
-
}
|
|
259
|
-
return this.closePromise;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
export async function openLibsqlDatabase(options: LibsqlDatabaseOptions): Promise<SqliteDatabase> {
|
|
264
|
-
const config = { url: options.url, authToken: options.authToken };
|
|
265
|
-
const redact = credentialRedactor(config);
|
|
266
|
-
const descriptor = { adapter: "libsql", location: redact(config.url) };
|
|
170
|
+
export async function openLibsqlDatabase({ url, authToken }: LibsqlDatabaseOptions): Promise<SqliteDatabase> {
|
|
267
171
|
let createClient: typeof import("@libsql/client").createClient;
|
|
268
172
|
try {
|
|
269
173
|
({ createClient } = await import("@libsql/client"));
|
|
270
174
|
} catch (cause) {
|
|
271
|
-
throw new StorageAdapterLoadError("@libsql/client",
|
|
175
|
+
throw new StorageAdapterLoadError("@libsql/client", cause as StorageFailureCause);
|
|
272
176
|
}
|
|
273
177
|
let client: Client | undefined;
|
|
178
|
+
// Create the client inside a driver call, so the facade redacts credentials from its failures.
|
|
179
|
+
const connection = () => (client ??= createClient({ url, authToken, intMode: "bigint" }));
|
|
180
|
+
const database = createSqliteDatabase({
|
|
181
|
+
descriptor: { adapter: "libsql", location: url },
|
|
182
|
+
redact: redactSecrets([authToken ?? "", ...urlCredentials(url)]),
|
|
183
|
+
...statements(connection),
|
|
184
|
+
async beginTransaction() {
|
|
185
|
+
const transaction = await connection().transaction("write");
|
|
186
|
+
return {
|
|
187
|
+
...statements(() => transaction),
|
|
188
|
+
commit: () => transaction.commit(),
|
|
189
|
+
rollback: () => transaction.rollback(),
|
|
190
|
+
close: () => transaction.close(),
|
|
191
|
+
};
|
|
192
|
+
},
|
|
193
|
+
close: () => client?.close(),
|
|
194
|
+
});
|
|
274
195
|
try {
|
|
275
|
-
client = createClient({ ...config, intMode: "bigint" });
|
|
276
|
-
const database = new LibsqlDatabase(client, descriptor, redact);
|
|
277
196
|
await database.exec("PRAGMA foreign_keys = ON");
|
|
278
197
|
return database;
|
|
279
|
-
} catch (
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
} catch {}
|
|
283
|
-
throw new StorageUnavailableError(descriptor, redactCause(cause as StorageFailureCause, redact));
|
|
198
|
+
} catch (error) {
|
|
199
|
+
await database.close().catch(() => {});
|
|
200
|
+
throw error;
|
|
284
201
|
}
|
|
285
202
|
}
|
|
203
|
+
|
|
204
|
+
export default {
|
|
205
|
+
atomicStorageAdapter: 1,
|
|
206
|
+
openDatabase: (options) =>
|
|
207
|
+
openLibsqlDatabase({
|
|
208
|
+
url: String(options.url),
|
|
209
|
+
authToken: typeof options.authToken === "string" ? options.authToken : undefined,
|
|
210
|
+
}),
|
|
211
|
+
} satisfies StorageAdapterModule;
|
|
286
212
|
```
|
|
287
213
|
{/* /custom-driver-example */}
|
|
288
214
|
|
|
289
|
-
|
|
215
|
+
Its default export lets settings select it as an [adapter](#selecting-an-adapter). From the SDK, call `openLibsqlDatabase` directly:
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
import { openLibsqlDatabase } from "./libsql-adapter.js";
|
|
219
|
+
|
|
220
|
+
const database = await openLibsqlDatabase({ url: "file:./data.sqlite" });
|
|
221
|
+
// For Turso instead: { url: "libsql://your-database.turso.io", authToken: token }
|
|
222
|
+
try {
|
|
223
|
+
await database.exec("CREATE TABLE IF NOT EXISTS notes (text TEXT)");
|
|
224
|
+
await database.transaction(async (tx) => {
|
|
225
|
+
await tx.run("INSERT INTO notes VALUES (?)", "hello");
|
|
226
|
+
});
|
|
227
|
+
const notes = await database.all("SELECT text FROM notes");
|
|
228
|
+
} finally {
|
|
229
|
+
await database.close();
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Pass the returned facade as SDK `sessionStore: { kind: "database", database, prefix: "project_" }` or workflow `journal: { database, prefix: "project_" }`. Both can use the same facade without closing your database.
|
|
290
234
|
|
|
291
235
|
## Selecting an adapter
|
|
292
236
|
|
|
293
|
-
|
|
237
|
+
An adapter is a module whose default export is a `StorageAdapterModule`, like the libSQL example above:
|
|
294
238
|
|
|
295
239
|
```ts
|
|
296
240
|
import type { StorageAdapterModule } from "@bastani/atomic/storage";
|
|
297
|
-
import { openLibsqlDatabase } from "./storage-libsql-wrapper.js";
|
|
298
241
|
|
|
299
242
|
export default {
|
|
300
243
|
atomicStorageAdapter: 1,
|
|
301
244
|
async openDatabase(options, context) {
|
|
302
|
-
return
|
|
245
|
+
return openMyDatabase({ url: String(options.url), authToken: String(options.authToken) });
|
|
303
246
|
},
|
|
304
247
|
} satisfies StorageAdapterModule;
|
|
305
248
|
```
|
|
306
249
|
|
|
307
|
-
Select it with `sessions.storage: { "adapter": "
|
|
250
|
+
Select it with `sessions.storage: { "adapter": "./storage/libsql-adapter.ts" }`, `workflows.journal`, or shared top-level `storage` in [settings](/settings#workflow-storage). SDK sessions accept `sessionStore: { kind: "adapter", module, options, secrets, prefix }`; workflow runs accept `journal: { adapter, options, secrets, prefix }`. Relative module paths resolve from the agent directory, normally `~/.atomic/agent`; absolute module paths also work. A package name selects an adapter from an [installed Atomic package](#adapters-from-atomic-packages), or otherwise resolves from the agent directory. Project settings follow the same trust boundary as project extensions. A module executes with Atomic's privileges.
|
|
251
|
+
|
|
252
|
+
Adapter modules import `@bastani/atomic/storage` and `@bastani/atomic` from the running Atomic, in the npm package and the standalone binary alike, exactly as extensions do. They need no installed or linked copy of Atomic, and Atomic recognizes the storage errors they throw. List `@bastani/atomic` in an adapter package's `peerDependencies`, not its `dependencies`.
|
|
308
253
|
|
|
309
|
-
`options` is an object of SQLite scalar values or booleans. In JSON settings use strings, numbers, booleans, or null. `secrets` maps option names to `{ env }` or `{ file }
|
|
254
|
+
`options` is an object of SQLite scalar values or booleans. In JSON settings use strings, numbers, booleans, or null. `secrets` maps option names to `{ env }` or `{ file }`, and resolved values override same-named options. A `{ file }` secret is read as written, except that one trailing line ending (`\n` or `\r\n`) is removed, so a token saved with `turso db tokens create <db> > token` works as is. Other whitespace is kept. Keep credentials out of `options` and database descriptors. `context` contains `agentDir`, `domain: "sessions" | "journal" | "intercom"`, and an optional `signal`. Intercom brokers open the top-level `storage` selection with `domain: "intercom"` through `openDatabase`.
|
|
310
255
|
|
|
311
256
|
A SQLite adapter implements `openDatabase(options, context): Promise<SqliteDatabase>`. Atomic applies its domain schema and optional table prefix. A full adapter can instead implement `openSessionStore(options, context, storage)` or `openRunJournal(options, context, storage)`; the domain hook takes priority over `openDatabase`. The typed third argument is `{ readonly prefix?: string }`. Domain adapters must honor this prefix to isolate selected stores. A missing domain/database hook raises `StorageConfigError` with reason `"adapter-domain-unsupported"`. Load failures and invalid module contracts raise `StorageAdapterLoadError`; driver opening failures raise `StorageUnavailableError`. None selects memory as a fallback.
|
|
312
257
|
|
|
@@ -318,6 +263,33 @@ If opening storage fails and closing its owned database also fails, the original
|
|
|
318
263
|
|
|
319
264
|
Factory-created journal headers record a credential-free `storage` descriptor for diagnostics. Full domain adapters are responsible for recording their own safe descriptor. Directly supplied journals and their caller-provided headers are not rewritten.
|
|
320
265
|
|
|
266
|
+
### Adapters from Atomic packages
|
|
267
|
+
|
|
268
|
+
An [Atomic package](/packages) can provide storage adapters, so `atomic install` brings an adapter together with its driver and the driver's dependencies. List the adapter modules, relative to the package root, under `storageAdapters` in the package manifest:
|
|
269
|
+
|
|
270
|
+
```json
|
|
271
|
+
{
|
|
272
|
+
"name": "@example/atomic-libsql",
|
|
273
|
+
"dependencies": { "@libsql/client": "0.18.0" },
|
|
274
|
+
"peerDependencies": { "@bastani/atomic": "*" },
|
|
275
|
+
"atomic": { "storageAdapters": ["./libsql-adapter.ts"] }
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Install it with `atomic install npm:@example/atomic-libsql` and select it by package name:
|
|
280
|
+
|
|
281
|
+
```json
|
|
282
|
+
{
|
|
283
|
+
"storage": {
|
|
284
|
+
"adapter": "@example/atomic-libsql",
|
|
285
|
+
"options": { "url": "libsql://team.example" },
|
|
286
|
+
"secrets": { "authToken": { "file": "turso-token" } }
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
The package name selects the package's first listed adapter. When a package lists several, select one with `<package>/<name>`, where `<name>` is the adapter file's name without its extension, for example `@example/atomic-libsql/libsql-adapter`. Atomic searches the packages in your effective global settings, including legacy `~/.pi/agent/settings.json` settings where applicable; an explicit agent directory does not inherit packages from another home. `atomic install` writes user settings without `-l`; project packages are not searched. When no installed package lists a matching adapter, the name resolves from the agent directory as before.
|
|
292
|
+
|
|
321
293
|
## Selecting storage per process
|
|
322
294
|
|
|
323
295
|
A host that starts Atomic processes, for example a supervisor that launches `atomic --mode rpc` children, can select storage for those processes alone. Each option holds the same JSON selection the settings accept, including `adapter`, `options`, `secrets`, and `prefix`:
|
|
@@ -339,7 +311,7 @@ atomic --mode rpc --storage-file /etc/atomic/storage.json
|
|
|
339
311
|
|
|
340
312
|
Nothing is written to settings. A process started without these options keeps using its settings, so an interactive `atomic` and an RPC child started with `ATOMIC_STORAGE` in the same account each keep their own sessions and workflow runs, and `/resume` in each lists only its own store. `--storage` and `--storage-file` cannot be combined. The interactive CLI passes its `--storage` selection to its background engine process. Other processes that Atomic starts inherit the environment variables but not the flag.
|
|
341
313
|
|
|
342
|
-
The Intercom broker an Atomic process starts uses the same selection for multi-node routing: `--storage`, `--storage-file`, or `ATOMIC_STORAGE` from that process outranks the top-level `storage` setting, and a broker started by a process without them reads settings. One broker serves every session in an agent directory, so it keeps the selection of the process that started it until it exits.
|
|
314
|
+
The Intercom broker an Atomic process starts uses the same selection for multi-node routing: `--storage`, `--storage-file`, or `ATOMIC_STORAGE` from that process outranks the top-level `storage` setting, and a broker started by a process without them reads settings. One broker serves every session in an agent directory, so it keeps the selection of the process that started it until it exits or `atomic intercom restart` replaces it. Project settings never select the broker's storage.
|
|
343
315
|
|
|
344
316
|
Precedence for each domain, highest first:
|
|
345
317
|
|
|
@@ -369,7 +341,7 @@ These options come from the process that starts Atomic, so they are not subject
|
|
|
369
341
|
|
|
370
342
|
## Sharing storage across hosts
|
|
371
343
|
|
|
372
|
-
By default, sessions and workflow runs live in a local file that serves one machine. To share them across machines, for example several service replicas or a CI fleet, select the same remote SQLite-compatible database on every host through an [adapter](#selecting-an-adapter), such as the
|
|
344
|
+
By default, sessions and workflow runs live in a local file that serves one machine. To share them across machines, for example several service replicas or a CI fleet, select the same remote SQLite-compatible database on every host through an [adapter](#selecting-an-adapter), such as the [Turso / libSQL package](#turso-and-libsql). Installing the adapter as an [Atomic package](#adapters-from-atomic-packages) lets every host use the same `storage` block. Give each project or tenant its own `prefix`. Do not put the local SQLite file on a network filesystem; use a database driver instead.
|
|
373
345
|
|
|
374
346
|
With a shared journal:
|
|
375
347
|
|
package/docs/usage.md
CHANGED
|
@@ -110,7 +110,7 @@ Both interrupts hold a queued message only while it is still waiting in the queu
|
|
|
110
110
|
|
|
111
111
|
Both abort routes ask the agent to stop cooperatively. Escape waits as long as the agent needs, and neither route terminates the engine during normal cancellation.
|
|
112
112
|
|
|
113
|
-
Ctrl+C also provides a recovery path. It reaches Atomic even when an extension's custom UI owns the screen, closing that UI if it does not handle the key. If the engine stops answering
|
|
113
|
+
Ctrl+C also provides a recovery path. It reaches Atomic even when an extension's custom UI owns the screen, closing that UI if it does not handle the key. If the engine stops answering, a queue clear or cancellation remains unanswered for over one second, or either operation fails, Ctrl+C replaces the engine. This includes cancelling chat, shell commands, compaction, and retries, even while engine heartbeats continue. It also covers a replacement that hangs during startup or fails to start. Recovery is explicit: a stalled control does not restart the engine on its own. Replacing the engine interrupts its running workflows and tools; inspect workflow status before resuming work.
|
|
114
114
|
|
|
115
115
|
If a message could not be sent, Atomic restores it to the editor exactly as typed, including pasted content. It appears above anything typed while the send was pending, separated by a blank line, together with later queued messages in their original order. Atomic does not also show a red error. See [Keybindings](/keybindings#application).
|
|
116
116
|
|
|
@@ -1213,6 +1213,50 @@ export default workflow({
|
|
|
1213
1213
|
});
|
|
1214
1214
|
```
|
|
1215
1215
|
|
|
1216
|
+
### `loadWorkerWorkflow(path, exportName?)`
|
|
1217
|
+
|
|
1218
|
+
```typescript
|
|
1219
|
+
function loadWorkerWorkflow(path: string, exportName?: string): Promise<AnyWorkflowDefinition>;
|
|
1220
|
+
```
|
|
1221
|
+
|
|
1222
|
+
Loads and records a workflow definition from an absolute module path for use as a local worker child. The export name defaults to `"default"`. Pass the returned definition to `ctx.workflow(child, ...)` and list the same path/export pair in the root's `localWorkers.definitions`. The root itself may remain an inline closure. Worker definitions must be importable; a closure cannot cross the process boundary. The saved compatibility digest covers the entry module only, not its imported modules.
|
|
1223
|
+
|
|
1224
|
+
```ts
|
|
1225
|
+
import { loadWorkerWorkflow, run, workflow } from "@bastani/atomic/workflows";
|
|
1226
|
+
|
|
1227
|
+
const research = await loadWorkerWorkflow(`${process.cwd()}/workflows/research.ts`, "research");
|
|
1228
|
+
const root = workflow({
|
|
1229
|
+
name: "research-pool",
|
|
1230
|
+
description: "Run independent research tasks on local workers.",
|
|
1231
|
+
inputs: {},
|
|
1232
|
+
outputs: {},
|
|
1233
|
+
async run(ctx) {
|
|
1234
|
+
await Promise.all(["one", "two"].map((topic) => ctx.workflow(research, { inputs: { topic } })));
|
|
1235
|
+
return {};
|
|
1236
|
+
},
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1239
|
+
await run(root, {}, {
|
|
1240
|
+
cwd: process.cwd(),
|
|
1241
|
+
localWorkers: { count: 2, definitions: [{ path: `${process.cwd()}/workflows/research.ts`, exportName: "research" }] },
|
|
1242
|
+
admission: { limits: { session: 2, process: 4 } },
|
|
1243
|
+
});
|
|
1244
|
+
```
|
|
1245
|
+
|
|
1246
|
+
Only first-level child workflow subtrees are placed on workers. Nested children stay on their assigned worker. Stage placement is not distributed independently. If a child is absent from the inventory or cannot be imported, the run fails before executing it; there is no local fallback.
|
|
1247
|
+
|
|
1248
|
+
#### Local worker options
|
|
1249
|
+
|
|
1250
|
+
`RunOpts.localWorkers` enables a local process pool for this root. `count` must be a positive integer. `definitions` lists `{ path, exportName }` entries. The definitions are loaded in each worker from the local filesystem. `RunOpts.admission` accepts `limits` for one root, `userLimits` across runs, a `userDatabase` canonical local SQLite path, and optional host thresholds `maxLoadPerCpu` and `minFreeMemoryBytes`.
|
|
1251
|
+
|
|
1252
|
+
The option is off by default. Without it, Atomic creates no pool tables, worker processes, or polling loops. CLI users can set the same options under `workflows.localWorkers` and `workflows.admission` in settings. Settings paths resolve against the invocation working directory; SDK paths resolve against `cwd` (or the current directory). Effective settings and definition entry-module digests are persisted with the root and checked on resume. Changing them fails compatibility checks instead of silently changing placement.
|
|
1253
|
+
|
|
1254
|
+
Workers are supported only on Linux and macOS hosts with native process identity and boot identity. Windows and hosts without those capabilities refuse activation, including when no process cap is configured. Remote hosts are not supported. Pooled SDK runs reject process-bound adapters, UI, persistence, and similar runtime overrides before worker launch. The CLI keeps its host-specific bindings at the root and gives worker sessions their own adapters.
|
|
1255
|
+
|
|
1256
|
+
Admission limits count actual resources: a session from creation through disposal, including fallback and delegated sessions, and a managed tool process until confirmed cleanup. Yielding an observation wait does not free a process permit. Subprocesses started outside Atomic's managed APIs are not counted. Host load and free-memory thresholds are launch gates, not hard OS limits. `userLimits` are reliable only when every participating run uses the configured canonical database path; they are local-user coordination, not a tenant security boundary.
|
|
1257
|
+
|
|
1258
|
+
Worker recovery replays unfinished child work from committed checkpoints, so external effects before the latest checkpoint may run again. Use stable logical idempotency keys where duplicate effects matter.
|
|
1259
|
+
|
|
1216
1260
|
Programmatic callers import `run` and call `run(definition, inputs)` with an exported definition and validated inputs. Use `createRegistry()` when an integration needs to register, merge, or look up several definitions before selecting one to run. The extension also registers the `/workflow` commands and the `workflow` tool for named execution, discovery, inspection, messaging, run control, and reload.
|
|
1217
1261
|
|
|
1218
1262
|
### `workflow(spec)`
|
|
@@ -1292,6 +1336,8 @@ function run<
|
|
|
1292
1336
|
|
|
1293
1337
|
Executes a compiled definition programmatically with validated inputs. Empty-input workflows accept an empty readonly record.
|
|
1294
1338
|
|
|
1339
|
+
`run()` uses Intercom for stages that have access to the `intercom` tool. This also works in standalone scripts, including runs with `executionMode: "non_interactive"`. Atomic opens the route only when an eligible stage starts and releases it when the run settles. Nested stages use the same workflow group. To send from another local session, join `workflow:<rootRunId>` before targeting a stage or `workflow:<rootRunId>/**`. Tool restrictions such as `tools`, `excludedTools`, or `noTools: "all"` continue to disable Intercom for a stage.
|
|
1340
|
+
|
|
1295
1341
|
#### Stage sessions and adapters
|
|
1296
1342
|
|
|
1297
1343
|
When `opts.adapters` supplies neither `agentSession` nor `prompt`, `run()` uses `createAgentSessionAdapter({ cwd: opts.cwd })`. Each stage then runs its own in-process Atomic `createAgentSession` session with persistent session storage by default. Use the stage's `sessionDir` to choose its session directory, or supply an explicitly selected in-memory session manager through a custom adapter for ephemeral stages. A persistent storage failure fails stage creation; it never falls back to memory. Stage sessions resolve models, credentials, settings, and resources the same way `createAgentSession` does. A `prompt` adapter on its own answers stage prompts itself, so `run()` adds no default session adapter beside it.
|
|
@@ -1362,6 +1408,8 @@ interface RunOpts {
|
|
|
1362
1408
|
readonly deferWorkflowStart?: boolean;
|
|
1363
1409
|
readonly config?: WorkflowRuntimeConfig;
|
|
1364
1410
|
readonly journal?: RunJournalOption;
|
|
1411
|
+
readonly localWorkers?: LocalWorkerOptions;
|
|
1412
|
+
readonly admission?: WorkflowAdmissionOptions;
|
|
1365
1413
|
readonly models?: WorkflowModelCatalogPort;
|
|
1366
1414
|
readonly registry?: WorkflowRegistry;
|
|
1367
1415
|
readonly depth?: number;
|
|
@@ -1385,6 +1433,8 @@ interface RunOpts {
|
|
|
1385
1433
|
|
|
1386
1434
|
Supplies runtime adapters, execution policy, persistence, MCP, cancellation, graph/store integration, continuation metadata, and lifecycle callbacks to `run(...)`. Every field is optional.
|
|
1387
1435
|
|
|
1436
|
+
`LocalWorkerOptions` is `{ count: number, definitions: { path: string, exportName: string }[] }`, where `count` is a positive integer. `WorkflowAdmissionOptions` contains optional root `limits`, user-wide `userLimits`, `userDatabase`, and host thresholds. `limits` and `userLimits` accept `session` and `process` counts.
|
|
1437
|
+
|
|
1388
1438
|
`onStageSessionEvent` streams every agent-session event from each stage (for example `message_update`, `tool_execution_start`, and `tool_execution_end`), tagged with the stage's `runId` and `stageId`. `AgentSessionEvent` is exported by `@bastani/atomic`. The stream continues when a fallback model replaces a stage's session. Stages in nested `ctx.workflow(...)` runs report the child run's `runId`. Exceptions thrown by the listener, and rejections from an async listener, are ignored and do not affect the stage.
|
|
1389
1439
|
|
|
1390
1440
|
The public authoring declaration intentionally excludes runtime-only executor fields such as `defaultSessionDir`, `gitWorktreeSetupCache`, `durableBackend`, `durableScope`, and `onStageSession`.
|