@bastani/atomic 0.9.41 → 0.9.42
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 +99 -0
- package/dist/builtin/intercom/CHANGELOG.md +26 -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 +24 -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/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,105 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.9.42] - 2026-10-23
|
|
6
|
+
|
|
7
|
+
Sessions, workflow runs, and Intercom can now share one libSQL / Turso database across machines, and provider rate limits are respected across every request in a process.
|
|
8
|
+
|
|
9
|
+
- **Shared storage across machines:** install `@bastani/atomic-storage-libsql`, join a machine with `atomic storage join <url> --token-file <path>`, and check the result with `atomic storage status`. Atomic packages can ship their own storage adapters, and `createSqliteDatabase()` applies the facade rules when you write one ([#3856](https://github.com/bastani-inc/atomic/issues/3856), [#3857](https://github.com/bastani-inc/atomic/issues/3857))
|
|
10
|
+
- **Intercom you can inspect and restart:** `atomic intercom status` and `atomic intercom restart` show and refresh the broker's storage selection and multi-node routing, and a broker that stops answering is replaced after about a minute instead of hanging every session ([#3857](https://github.com/bastani-inc/atomic/issues/3857), [#3862](https://github.com/bastani-inc/atomic/issues/3862))
|
|
11
|
+
- **Rate limits handled across requests:** `configureProviderRequestBudget()` sets process-wide request budgets, 429 cooldowns are shared between model requests, and workflow model chains wait for the earliest eligible account instead of exhausting it ([#3864](https://github.com/bastani-inc/atomic/issues/3864))
|
|
12
|
+
- **Admission limits and local workflow pools:** SDK hosts can share session and command limits with `resourceAdmission`, and workflows can run child subtrees in local worker processes ([#3869](https://github.com/bastani-inc/atomic/issues/3869))
|
|
13
|
+
- **More responsive sessions and workflows:** Ctrl+C and queue clearing act without waiting for a busy engine, workflow stages reload resources four at a time, and standalone `run()` gives stages Intercom without an interactive host ([#3861](https://github.com/bastani-inc/atomic/issues/3861), [#3863](https://github.com/bastani-inc/atomic/issues/3863), [#3865](https://github.com/bastani-inc/atomic/issues/3865))
|
|
14
|
+
- **Login, export, and search fixes:** device-code `/login` waits for Enter before opening the browser, HTML exports show billed costs and the session's own system prompt, and `/resume` search ranks name matches first ([pi #9282](https://github.com/earendil-works/pi/issues/9282), [pi #9980](https://github.com/earendil-works/pi/issues/9980), [pi #11261](https://github.com/earendil-works/pi/issues/11261), [pi #11220](https://github.com/earendil-works/pi/issues/11220))
|
|
15
|
+
- **Security updates:** the standalone binaries embed Bun 1.4.4 with its upstream security fixes, and the bundled `ip-address` dependency no longer stalls on a long crafted IPv6 string ([GHSA-rxp7-6hxx-x8q5](https://github.com/advisories/GHSA-rxp7-6hxx-x8q5))
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- Markdown transformers registered with `pi.registerMarkdownTransformer()` now receive `messageId` and `timestamp` in their context when a message is rendered from the saved session, so an extension can tie its output to a session entry. Both are `undefined` for live messages. See [registerMarkdownTransformer](docs/extensions/api-reference.md#pi-registermarkdowntransformer-transformer)
|
|
20
|
+
- Added `configureProviderRequestBudget(provider, { maxRequests, intervalMs })` for process-wide rolling request budgets shared across SDK model runtimes. Model requests also share provider cooldowns after 429 responses. Separate configured provider IDs remain independent ([#3864](https://github.com/bastani-inc/atomic/issues/3864)).
|
|
21
|
+
- Storage adapter modules can import `@bastani/atomic/storage` and `@bastani/atomic` without an installed or linked copy of Atomic. They get the running Atomic's own modules, as extensions do, so Atomic recognizes the storage errors they throw ([#3856](https://github.com/bastani-inc/atomic/issues/3856))
|
|
22
|
+
- `createSqliteDatabase()` in `@bastani/atomic/storage` turns a SQLite-compatible driver's calls into a `SqliteDatabase` that applies the facade rules for you: call-order queueing, transaction handle lifetime, NUL rejection, integer and blob normalization, and typed errors with credentials redacted. `redactSecrets()` builds the credential redactor. The libSQL/Turso example in [Storage](docs/storage.md#bring-your-own-driver) now uses it and is a complete adapter ([#3856](https://github.com/bastani-inc/atomic/issues/3856))
|
|
23
|
+
- Atomic packages can provide storage adapters with a `storageAdapters` manifest entry, and `storage.adapter` can name one by package (`<package>` or `<package>/<name>`), so `atomic install npm:<adapter-package>` installs the adapter together with its driver. See [Adapters from Atomic packages](docs/storage.md#adapters-from-atomic-packages) ([#3856](https://github.com/bastani-inc/atomic/issues/3856))
|
|
24
|
+
- `@bastani/atomic-storage-libsql` is a first-party libSQL / Turso storage adapter package. Install it with `atomic install npm:@bastani/atomic-storage-libsql` and select it with `"storage": { "adapter": "@bastani/atomic-storage-libsql" }` to share sessions, workflow runs, and Intercom routing across machines. See [Turso and libSQL](docs/storage.md#turso-and-libsql) ([#3857](https://github.com/bastani-inc/atomic/issues/3857))
|
|
25
|
+
- `atomic storage status` shows, for sessions, workflow runs, and Intercom, the selected storage and where the selection comes from (environment variable, project or global settings, or default), the outranked selections, whether the adapter loads, and whether the database answers a `SELECT 1`, without creating anything. It also shows whether the running Intercom broker uses the current selection, its multi-node routing state, and the brokers live in the shared database. `--json` prints the same report. See [Checking the selected storage](docs/storage.md#checking-the-selected-storage) ([#3856](https://github.com/bastani-inc/atomic/issues/3856), [#3857](https://github.com/bastani-inc/atomic/issues/3857))
|
|
26
|
+
- `atomic storage join <url> --token-file <path>` joins this machine to a shared libSQL / Turso database in one step: it checks the token and the connection, stores the trimmed token in the agent directory with mode 600, writes only the global `storage` setting, creates no tables, and lists the environment variables and settings that override the new selection. It refuses a URL that carries the token itself, so a credential never reaches settings or output. When `@bastani/atomic-storage-libsql` is not installed, it prints the install command. See [Joining a machine to a shared database](docs/storage.md#joining-a-machine-to-a-shared-database) ([#3857](https://github.com/bastani-inc/atomic/issues/3857))
|
|
27
|
+
- `atomic intercom restart` replaces the running Intercom broker so it reads a changed storage selection; connected sessions reconnect to the new broker. `atomic intercom status` shows the broker, whether it uses the current selection, and its multi-node routing state ([#3857](https://github.com/bastani-inc/atomic/issues/3857))
|
|
28
|
+
- Interactive and print sessions warn at startup when the shared `storage` selection names an adapter that the running Atomic cannot load, naming the adapter and the cause, instead of Intercom multi-node routing staying off silently ([#3857](https://github.com/bastani-inc/atomic/issues/3857))
|
|
29
|
+
- SDK hosts can share admission limits through `resourceAdmission` and `withResourceAdmission()`. Session and managed-command permits remain held until cleanup, including background commands. Delegated sessions inherit the same service and request immediate admission or rejection rather than waiting on a permit held by their parent. See [Shared resource admission](docs/sdk.md#shared-resource-admission) ([#3869](https://github.com/bastani-inc/atomic/issues/3869)).
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- The standalone `atomic` binaries are now built with Bun 1.4.4, which includes upstream security fixes in the embedded runtime
|
|
34
|
+
- Device-code `/login` flows (Kimi Code, GitHub Copilot, Meta, and extension providers) no longer open the browser on their own. Press Enter to open the verification URL and copy the code, or log in manually. Providers can turn off either action with `openBrowserOnConfirm: false` or `copyCodeOnConfirm: false`. See [Provider OAuth](docs/custom-provider/oauth.md#oauth-support) ([pi #9282](https://github.com/earendil-works/pi/issues/9282))
|
|
35
|
+
- A `{ "file": ... }` storage secret now drops one trailing line ending (`\n` or `\r\n`), so a token saved with `turso db tokens create <db> > token` works without trimming it first. Other whitespace is still kept ([#3856](https://github.com/bastani-inc/atomic/issues/3856))
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
- HTML session exports now show the recorded total cost of each response, such as OpenRouter's billed cost, instead of the sum of its catalog-estimated components. Sessions recorded before totals were stored still use the component sum ([pi #9980](https://github.com/earendil-works/pi/issues/9980))
|
|
40
|
+
- On macOS, cancelling a running bash command no longer lets the next command in a `a; b` sequence start while the shell is being killed: the process group is stopped before it is killed ([pi #11252](https://github.com/earendil-works/pi/issues/11252))
|
|
41
|
+
- HTML session exports, including `atomic --export`, now show the system prompt and available tools from the session's first system message, and later system messages as named prompt-section and tool updates. Sessions without system entries still show the prompt and tools from the agent state ([pi #11261](https://github.com/earendil-works/pi/issues/11261))
|
|
42
|
+
- HTML session exports of very long sessions no longer fail to render the session tree with a call-stack overflow ([pi #11225](https://github.com/earendil-works/pi/issues/11225))
|
|
43
|
+
- `pi.exec()` now reports why a command could not be started (for example `ENOENT` for a missing executable) in `stderr` instead of returning an empty `stderr` with exit code 1 ([pi #11085](https://github.com/earendil-works/pi/issues/11085))
|
|
44
|
+
- `/resume` search matches the session name, summary, id, messages and working directory separately. A name match ranks first, then text found in the id, summary, messages or directory, then characters scattered through the name, summary or directory. Matches deep in a long transcript are no longer ranked last, and scattered characters in session ids or message text no longer produce noise matches ([pi #11220](https://github.com/earendil-works/pi/issues/11220))
|
|
45
|
+
- Updated the bundled `ip-address` dependency to 10.7.4, which fixes a quadratic-time zone-identifier match in IPv6 address parsing that a single long string could use to stall the process ([GHSA-rxp7-6hxx-x8q5](https://github.com/advisories/GHSA-rxp7-6hxx-x8q5))
|
|
46
|
+
- Provider request-budget waits no longer consume OpenAI, Anthropic, Mistral, or explicit Codex SSE HTTP timeouts, and successive Anthropic federation-authenticated requests keep reusing their cached token. Cancelling a queued model request reports an aborted result without dispatching it ([#3864](https://github.com/bastani-inc/atomic/issues/3864)).
|
|
47
|
+
- Cancelling or clearing queued messages no longer waits for a busy interactive engine to finish draining its output before acting. Cancelled prompts and shell commands waiting to start do not run later when output resumes. If queue clearing or cancellation of chat, shell commands, compaction, or retries stalls or fails, Ctrl+C can still replace the engine even while it continues sending heartbeats, including controls sent while a replacement initializes; an earlier failed queue pause no longer prevents an abort from being sent ([#3865](https://github.com/bastani-inc/atomic/issues/3865))
|
|
48
|
+
- The standalone binary's Intercom broker now runs multi-node routing with a shared `storage` selection and loads storage adapters, as the npm installation does ([#3856](https://github.com/bastani-inc/atomic/issues/3856))
|
|
49
|
+
- `atomic storage join` keeps previous credentials intact when saving settings fails, rejects relative `file:` URLs that would select different databases from different directories, and rejects credentials embedded in `file:` URLs before printing or saving them. New joins use separate private token files, preserving existing sessions' credentials. A successful join no longer reports failure when malformed project settings prevent the final override report: it warns that storage was saved and asks you to fix the settings before running status ([#3857](https://github.com/bastani-inc/atomic/issues/3857))
|
|
50
|
+
- `atomic storage status` reports settings load failures, opens local SQLite files (including adapter `file:` URLs) read-only without creating missing databases or changing journal mode, and bounds connection cleanup even when a query stalls ([#3856](https://github.com/bastani-inc/atomic/issues/3856))
|
|
51
|
+
- `createSqliteDatabase()` preserves callback and rollback failures when transaction cleanup also fails ([#3856](https://github.com/bastani-inc/atomic/issues/3856))
|
|
52
|
+
- Package-name storage adapters now honor packages listed in legacy global settings, while explicit agent directories remain isolated ([#3857](https://github.com/bastani-inc/atomic/issues/3857))
|
|
53
|
+
- On macOS, pausing a managed command no longer causes it to be reported as completed and terminated ([#3869](https://github.com/bastani-inc/atomic/issues/3869)).
|
|
54
|
+
|
|
55
|
+
## [0.9.42-alpha.1] - 2026-10-23
|
|
56
|
+
|
|
57
|
+
Sessions, workflow runs, and Intercom can now share one libSQL / Turso database across machines, and provider rate limits are respected across every request in a process.
|
|
58
|
+
|
|
59
|
+
- **Shared storage across machines:** install `@bastani/atomic-storage-libsql`, join a machine with `atomic storage join <url> --token-file <path>`, and check the result with `atomic storage status`. Atomic packages can ship storage adapters, and `createSqliteDatabase()` handles the facade rules when you write your own ([#3856](https://github.com/bastani-inc/atomic/issues/3856), [#3857](https://github.com/bastani-inc/atomic/issues/3857))
|
|
60
|
+
- **Intercom you can inspect and restart:** `atomic intercom status` and `atomic intercom restart` show and refresh the broker's storage selection and multi-node routing, and a broker that stops answering is replaced after about a minute instead of hanging every session ([#3857](https://github.com/bastani-inc/atomic/issues/3857), [#3862](https://github.com/bastani-inc/atomic/issues/3862))
|
|
61
|
+
- **Rate limits handled across requests:** `configureProviderRequestBudget()` sets process-wide request budgets, 429 cooldowns are shared between model requests, and workflow model chains wait for the earliest eligible account instead of exhausting it ([#3864](https://github.com/bastani-inc/atomic/issues/3864))
|
|
62
|
+
- **Admission limits and local workflow pools:** SDK hosts can share session and command limits with `resourceAdmission`, and workflows can run child subtrees in local worker processes ([#3869](https://github.com/bastani-inc/atomic/issues/3869))
|
|
63
|
+
- **More responsive sessions and workflows:** Ctrl+C and queue clearing act without waiting for a busy engine, workflow stages reload resources four at a time, and standalone `run()` gives stages Intercom without an interactive host ([#3861](https://github.com/bastani-inc/atomic/issues/3861), [#3863](https://github.com/bastani-inc/atomic/issues/3863), [#3865](https://github.com/bastani-inc/atomic/issues/3865))
|
|
64
|
+
- **Login, export, and search fixes:** device-code `/login` waits for Enter before opening the browser, HTML exports show billed costs and the session's own system prompt, and `/resume` search ranks name matches first ([pi #9282](https://github.com/earendil-works/pi/issues/9282), [pi #9980](https://github.com/earendil-works/pi/issues/9980), [pi #11261](https://github.com/earendil-works/pi/issues/11261), [pi #11220](https://github.com/earendil-works/pi/issues/11220))
|
|
65
|
+
|
|
66
|
+
### Added
|
|
67
|
+
|
|
68
|
+
- Markdown transformers registered with `pi.registerMarkdownTransformer()` now receive `messageId` and `timestamp` in their context when a message is rendered from the saved session, so an extension can tie its output to a session entry. Both are `undefined` for live messages. See [registerMarkdownTransformer](docs/extensions/api-reference.md#pi-registermarkdowntransformer-transformer)
|
|
69
|
+
- Added `configureProviderRequestBudget(provider, { maxRequests, intervalMs })` for process-wide rolling request budgets shared across SDK model runtimes. Model requests also share provider cooldowns after 429 responses. Separate configured provider IDs remain independent ([#3864](https://github.com/bastani-inc/atomic/issues/3864)).
|
|
70
|
+
- Storage adapter modules can import `@bastani/atomic/storage` and `@bastani/atomic` without an installed or linked copy of Atomic. They get the running Atomic's own modules, as extensions do, so Atomic recognizes the storage errors they throw ([#3856](https://github.com/bastani-inc/atomic/issues/3856))
|
|
71
|
+
- `createSqliteDatabase()` in `@bastani/atomic/storage` turns a SQLite-compatible driver's calls into a `SqliteDatabase` that applies the facade rules for you: call-order queueing, transaction handle lifetime, NUL rejection, integer and blob normalization, and typed errors with credentials redacted. `redactSecrets()` builds the credential redactor. The libSQL/Turso example in [Storage](docs/storage.md#bring-your-own-driver) now uses it and is a complete adapter ([#3856](https://github.com/bastani-inc/atomic/issues/3856))
|
|
72
|
+
- Atomic packages can provide storage adapters with a `storageAdapters` manifest entry, and `storage.adapter` can name one by package (`<package>` or `<package>/<name>`), so `atomic install npm:<adapter-package>` installs the adapter together with its driver. See [Adapters from Atomic packages](docs/storage.md#adapters-from-atomic-packages) ([#3856](https://github.com/bastani-inc/atomic/issues/3856))
|
|
73
|
+
- `@bastani/atomic-storage-libsql` is a first-party libSQL / Turso storage adapter package. Install it with `atomic install npm:@bastani/atomic-storage-libsql` and select it with `"storage": { "adapter": "@bastani/atomic-storage-libsql" }` to share sessions, workflow runs, and Intercom routing across machines. See [Turso and libSQL](docs/storage.md#turso-and-libsql) ([#3857](https://github.com/bastani-inc/atomic/issues/3857))
|
|
74
|
+
- `atomic storage status` shows, for sessions, workflow runs, and Intercom, the selected storage and where the selection comes from (environment variable, project or global settings, or default), the outranked selections, whether the adapter loads, and whether the database answers a `SELECT 1`, without creating anything. It also shows whether the running Intercom broker uses the current selection, its multi-node routing state, and the brokers live in the shared database. `--json` prints the same report. See [Checking the selected storage](docs/storage.md#checking-the-selected-storage) ([#3856](https://github.com/bastani-inc/atomic/issues/3856), [#3857](https://github.com/bastani-inc/atomic/issues/3857))
|
|
75
|
+
- `atomic storage join <url> --token-file <path>` joins this machine to a shared libSQL / Turso database in one step: it checks the token and the connection, stores the trimmed token in the agent directory with mode 600, writes only the global `storage` setting, creates no tables, and lists the environment variables and settings that override the new selection. It refuses a URL that carries the token itself, so a credential never reaches settings or output. When `@bastani/atomic-storage-libsql` is not installed, it prints the install command. See [Joining a machine to a shared database](docs/storage.md#joining-a-machine-to-a-shared-database) ([#3857](https://github.com/bastani-inc/atomic/issues/3857))
|
|
76
|
+
- `atomic intercom restart` replaces the running Intercom broker so it reads a changed storage selection; connected sessions reconnect to the new broker. `atomic intercom status` shows the broker, whether it uses the current selection, and its multi-node routing state ([#3857](https://github.com/bastani-inc/atomic/issues/3857))
|
|
77
|
+
- Interactive and print sessions warn at startup when the shared `storage` selection names an adapter that the running Atomic cannot load, naming the adapter and the cause, instead of Intercom multi-node routing staying off silently ([#3857](https://github.com/bastani-inc/atomic/issues/3857))
|
|
78
|
+
- SDK hosts can share admission limits through `resourceAdmission` and `withResourceAdmission()`. Session and managed-command permits remain held until cleanup, including background commands. Delegated sessions inherit the same service and request immediate admission or rejection rather than waiting on a permit held by their parent. See [Shared resource admission](docs/sdk.md#shared-resource-admission) ([#3869](https://github.com/bastani-inc/atomic/issues/3869)).
|
|
79
|
+
|
|
80
|
+
### Changed
|
|
81
|
+
|
|
82
|
+
- The standalone `atomic` binaries are now built with Bun 1.4.4, which includes upstream security fixes in the embedded runtime
|
|
83
|
+
- Device-code `/login` flows (Kimi Code, GitHub Copilot, Meta, and extension providers) no longer open the browser on their own. Press Enter to open the verification URL and copy the code, or log in manually. Providers can turn off either action with `openBrowserOnConfirm: false` or `copyCodeOnConfirm: false`. See [Provider OAuth](docs/custom-provider/oauth.md#oauth-support) ([pi #9282](https://github.com/earendil-works/pi/issues/9282))
|
|
84
|
+
- A `{ "file": ... }` storage secret now drops one trailing line ending (`\n` or `\r\n`), so a token saved with `turso db tokens create <db> > token` works without trimming it first. Other whitespace is still kept ([#3856](https://github.com/bastani-inc/atomic/issues/3856))
|
|
85
|
+
|
|
86
|
+
### Fixed
|
|
87
|
+
|
|
88
|
+
- HTML session exports now show the recorded total cost of each response, such as OpenRouter's billed cost, instead of the sum of its catalog-estimated components. Sessions recorded before totals were stored still use the component sum ([pi #9980](https://github.com/earendil-works/pi/issues/9980))
|
|
89
|
+
- On macOS, cancelling a running bash command no longer lets the next command in a `a; b` sequence start while the shell is being killed: the process group is stopped before it is killed ([pi #11252](https://github.com/earendil-works/pi/issues/11252))
|
|
90
|
+
- HTML session exports, including `atomic --export`, now show the system prompt and available tools from the session's first system message, and later system messages as named prompt-section and tool updates. Sessions without system entries still show the prompt and tools from the agent state ([pi #11261](https://github.com/earendil-works/pi/issues/11261))
|
|
91
|
+
- HTML session exports of very long sessions no longer fail to render the session tree with a call-stack overflow ([pi #11225](https://github.com/earendil-works/pi/issues/11225))
|
|
92
|
+
- `pi.exec()` now reports why a command could not be started (for example `ENOENT` for a missing executable) in `stderr` instead of returning an empty `stderr` with exit code 1 ([pi #11085](https://github.com/earendil-works/pi/issues/11085))
|
|
93
|
+
- `/resume` search matches the session name, summary, id, messages and working directory separately. A name match ranks first, then text found in the id, summary, messages or directory, then characters scattered through the name, summary or directory. Matches deep in a long transcript are no longer ranked last, and scattered characters in session ids or message text no longer produce noise matches ([pi #11220](https://github.com/earendil-works/pi/issues/11220))
|
|
94
|
+
- Updated the bundled `ip-address` dependency to 10.7.4, which fixes a quadratic-time zone-identifier match in IPv6 address parsing that a single long string could use to stall the process ([GHSA-rxp7-6hxx-x8q5](https://github.com/advisories/GHSA-rxp7-6hxx-x8q5))
|
|
95
|
+
- Provider request-budget waits no longer consume OpenAI, Anthropic, Mistral, or explicit Codex SSE HTTP timeouts, and successive Anthropic federation-authenticated requests keep reusing their cached token. Cancelling a queued model request reports an aborted result without dispatching it ([#3864](https://github.com/bastani-inc/atomic/issues/3864)).
|
|
96
|
+
- Cancelling or clearing queued messages no longer waits for a busy interactive engine to finish draining its output before acting. Cancelled prompts and shell commands waiting to start do not run later when output resumes. If queue clearing or cancellation of chat, shell commands, compaction, or retries stalls or fails, Ctrl+C can still replace the engine even while it continues sending heartbeats, including controls sent while a replacement initializes; an earlier failed queue pause no longer prevents an abort from being sent ([#3865](https://github.com/bastani-inc/atomic/issues/3865))
|
|
97
|
+
- The standalone binary's Intercom broker now runs multi-node routing with a shared `storage` selection and loads storage adapters, as the npm installation does ([#3856](https://github.com/bastani-inc/atomic/issues/3856))
|
|
98
|
+
- `atomic storage join` keeps previous credentials intact when saving settings fails, rejects relative `file:` URLs that would select different databases from different directories, and rejects credentials embedded in `file:` URLs before printing or saving them. New joins use separate private token files, preserving existing sessions' credentials. A successful join no longer reports failure when malformed project settings prevent the final override report: it warns that storage was saved and asks you to fix the settings before running status ([#3857](https://github.com/bastani-inc/atomic/issues/3857))
|
|
99
|
+
- `atomic storage status` reports settings load failures, opens local SQLite files (including adapter `file:` URLs) read-only without creating missing databases or changing journal mode, and bounds connection cleanup even when a query stalls ([#3856](https://github.com/bastani-inc/atomic/issues/3856))
|
|
100
|
+
- `createSqliteDatabase()` preserves callback and rollback failures when transaction cleanup also fails ([#3856](https://github.com/bastani-inc/atomic/issues/3856))
|
|
101
|
+
- Package-name storage adapters now honor packages listed in legacy global settings, while explicit agent directories remain isolated ([#3857](https://github.com/bastani-inc/atomic/issues/3857))
|
|
102
|
+
- On macOS, pausing a managed command no longer causes it to be reported as completed and terminated ([#3869](https://github.com/bastani-inc/atomic/issues/3869)).
|
|
103
|
+
|
|
5
104
|
## [0.9.41] - 2026-10-22
|
|
6
105
|
|
|
7
106
|
Workflows stay contained when they fail, classifiers read images, and usage totals say where their cost came from.
|
|
@@ -4,6 +4,32 @@ All notable changes to the `pi-intercom` extension will be documented in this fi
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.9.42] - 2026-10-23
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `intercom({ action: "status" })` reports the broker's multi-node routing state, for example `Multi-node routing: on (adapter @bastani/atomic-storage-libsql, prefix none), 1 peer broker live (build-host)`, or why it is off or unavailable ([#3857](https://github.com/bastani-inc/atomic/issues/3857))
|
|
12
|
+
- The broker writes a positive line to `broker.log` when it starts: `Intercom multi-node routing is on (adapter <adapter>, prefix <prefix>)`, or `Intercom multi-node routing is off: <reason>`, also for an absent, in-memory, or domain-only selection, so a log without errors is no longer ambiguous ([#3856](https://github.com/bastani-inc/atomic/issues/3856), [#3857](https://github.com/bastani-inc/atomic/issues/3857))
|
|
13
|
+
- The broker answers status and shutdown requests from `atomic storage status`, `atomic intercom status`, and `atomic intercom restart`: its storage selection, sessions, multi-node state, and the other brokers live in the shared database. A restart lets connected sessions reconnect to a broker that reads the current storage selection, without closing them ([#3857](https://github.com/bastani-inc/atomic/issues/3857))
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Fixed Intercom remaining stuck on a broker that accepts connections but stops answering. New brokers monitor their own request handling and terminate after about a minute of failed health checks so clients can start one replacement. Timed-out client requests also check connection health before entering reconnect recovery, without restarting a responsive broker. Identical in-flight sends keep sharing one result through the health check. A bounded previous broker log and the latest watchdog diagnosis survive replacement ([#3862](https://github.com/bastani-inc/atomic/issues/3862)).
|
|
18
|
+
- Restarting an older broker on Windows no longer mistakes a lingering named pipe for a restarted broker after its process has exited ([#3857](https://github.com/bastani-inc/atomic/issues/3857))
|
|
19
|
+
|
|
20
|
+
## [0.9.42-alpha.1] - 2026-10-23
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- `intercom({ action: "status" })` reports the broker's multi-node routing state, for example `Multi-node routing: on (adapter @bastani/atomic-storage-libsql, prefix none), 1 peer broker live (build-host)`, or why it is off or unavailable ([#3857](https://github.com/bastani-inc/atomic/issues/3857))
|
|
25
|
+
- The broker writes a positive line to `broker.log` when it starts: `Intercom multi-node routing is on (adapter <adapter>, prefix <prefix>)`, or `Intercom multi-node routing is off: <reason>`, also for an absent, in-memory, or domain-only selection, so a log without errors is no longer ambiguous ([#3856](https://github.com/bastani-inc/atomic/issues/3856), [#3857](https://github.com/bastani-inc/atomic/issues/3857))
|
|
26
|
+
- The broker answers status and shutdown requests from `atomic storage status`, `atomic intercom status`, and `atomic intercom restart`: its storage selection, sessions, multi-node state, and the other brokers live in the shared database. A restart lets connected sessions reconnect to a broker that reads the current storage selection, without closing them ([#3857](https://github.com/bastani-inc/atomic/issues/3857))
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- Fixed Intercom remaining stuck on a broker that accepts connections but stops answering. New brokers monitor their own request handling and terminate after about a minute of failed health checks so clients can start one replacement. Timed-out client requests also check connection health before entering reconnect recovery, without restarting a responsive broker. Identical in-flight sends keep sharing one result through the health check. A bounded previous broker log and the latest watchdog diagnosis survive replacement ([#3862](https://github.com/bastani-inc/atomic/issues/3862)).
|
|
31
|
+
- Restarting an older broker on Windows no longer mistakes a lingering named pipe for a restarted broker after its process has exited ([#3857](https://github.com/bastani-inc/atomic/issues/3857))
|
|
32
|
+
|
|
7
33
|
## [0.9.41] - 2026-10-22
|
|
8
34
|
|
|
9
35
|
### Changed
|
|
@@ -389,7 +389,7 @@ When multiple live agent stages share a name, `ask` refuses the ambiguous target
|
|
|
389
389
|
|
|
390
390
|
`contact_supervisor` decisions and interviews are deliberately exclusive per child: one supervisor wait may coexist with ordinary peer asks, but a second concurrent supervisor wait receives `Already waiting for a supervisor reply`. Claimed foreground parent handoffs remain first-claim-wins and do not allocate a waiter. Multiple children can contact the same parent independently because inbound asks and handoffs are keyed by child/message identity. Mutual peer asks are supported, but each side must process inbound work to reply; the per-waiter timeout remains the deadlock backstop.
|
|
391
391
|
|
|
392
|
-
**`status`** — Shows connection status, session ID, every current membership, and the total visible-session count. A `group` filter remains a read-only peek.
|
|
392
|
+
**`status`** — Shows connection status, session ID, every current membership, and the total visible-session count. It also reports the broker's multi-node routing state on a `Multi-node routing:` line: `on (adapter <adapter>, prefix <prefix>)` with the number of other brokers live in the shared database, or why routing is off or unavailable; `details.multiNode` carries the same fields. A broker that predates status reporting shows `unknown`, and `atomic intercom restart` replaces it. A `group` filter remains a read-only peek.
|
|
393
393
|
|
|
394
394
|
## Keyboard Shortcuts
|
|
395
395
|
|
|
@@ -463,6 +463,8 @@ graph TB
|
|
|
463
463
|
|
|
464
464
|
The broker is a standalone TypeScript process that manages session registration and message routing. It auto-spawns when the first session that invokes Intercom needs it and exits after 5 seconds when it last has no registered sessions, including brokers that never received a connection and sockets that close before register. Clients reconnect automatically if the broker disappears and later comes back. A failed reconnect schedules the next attempt on a bounded backoff (1s, 2s, 5s, 10s, then 30s) once it releases reconnect ownership, so a transient failure never leaves a live session with nothing owning recovery; a failed explicit tool or overlay connection still returns its error to the caller and leaves the retry behind. A reconnect that fails after the broker already accepted it closes that connection first, so a session never appears twice in `intercom list`.
|
|
465
465
|
|
|
466
|
+
A broker-owned watchdog checks the local request path independently of the broker's main thread. After about a minute without a valid response from its own broker instance, it terminates its own process; clients reconnect through the existing spawn lock. It never signals a PID read from `broker.pid`. A timed-out client request shares one connection-health check with other simultaneous timeouts on that connection, and invalidates a silent connection. A responsive broker is left running. Brokers already running from an older version gain the watchdog only after a restart; an OS-suspended process cannot run it.
|
|
467
|
+
|
|
466
468
|
Each `send`, `ask`, or `reply` invocation retries typed recoverable disconnects internally up to three times, after delays of 1, 2, and 5 seconds. Retries keep the same message ID, exact arguments, attachment order and presence, and reply route. The model does not supply or receive a retry token; existing integrations must remove `retryToken`, which is now refused without sending. Every new invocation remains a fresh operation, even with identical text. Confirmation happens once, and neither retries nor renewed reply waits extend the original 11-minute operation deadline.
|
|
467
469
|
|
|
468
470
|
Lazy initialization before delivery has a separate limit of three reconnect retries on the same schedule. If that phase stops, the tool reports `outcome: "not_sent"`. The initialization wrapper never re-executes a delivery that has already started.
|
|
@@ -494,7 +496,9 @@ Runtime files live under the active agent directory. Atomic defaults to `~/.atom
|
|
|
494
496
|
- `delivered-messages.key` — random owner-only HMAC key paired with the authority database
|
|
495
497
|
- `broker.spawn.lock` — Lock used to avoid duplicate auto-spawns. It records the spawner and its broker, and is held until the broker answers or the processes it names are gone, with a 60 second backstop. On Windows, where the hidden launcher does not expose the broker process, it ends with the spawner's 5 second readiness wait, and a later spawn that finds `broker.log` still held by a starting broker waits for that broker instead of starting another
|
|
496
498
|
- `broker.sock.replace.lock` — Short-lived lock a starting broker holds while it re-checks and replaces a stale socket file, so two brokers never both replace it
|
|
497
|
-
- `broker.log` — Broker stderr,
|
|
499
|
+
- `broker.log` — Broker stderr, capped at 8 KiB by the broker itself. It records whether multi-node routing is on (`Intercom multi-node routing is on (adapter <adapter>, prefix <prefix>)`) or off and why
|
|
500
|
+
- `broker.log.previous` — The bounded tail of the previous nonempty broker log, preserved on spawn
|
|
501
|
+
- `broker-watchdog.log` — The latest watchdog termination's time, PID, instance, and reason; kept across spawns
|
|
498
502
|
- `config.json` — User configuration
|
|
499
503
|
|
|
500
504
|
The broker is a detached subprocess and never inherits the host's extension-loader module aliases, so every module reachable from `broker/broker.ts` resolves to Node built-ins or Intercom's own files. Its stderr is captured to `broker.log` through an already-open file descriptor rather than a pipe, because a pipe to a process that outlives its parent breaks once the parent exits. Startup failures — both an early exit and a readiness timeout — quote the log path and a bounded tail of that output.
|
|
@@ -505,7 +509,7 @@ The broker caps the file from the inside, since nothing on the parent side can b
|
|
|
505
509
|
|
|
506
510
|
**Local IPC instead of TCP.** Same-machine only by design. `pi-intercom` uses Unix sockets on macOS/Linux and a named pipe on Windows, which keeps setup simple and avoids port management.
|
|
507
511
|
|
|
508
|
-
**Auto-spawn with file lock.** The broker starts on first connection and exits after 5 seconds idle. There is no daemon to manage. A spawn lock file, keyed by PID and timestamp, prevents duplicate brokers when multiple sessions start at once. A broker that starts while another broker already answers on the socket exits instead of taking the socket over; it replaces only a stale socket file. A broker whose socket path comes to point at another broker shuts down, and its sessions reconnect to the broker that owns the path.
|
|
512
|
+
**Auto-spawn with file lock.** The broker starts on first connection and exits after 5 seconds idle. There is no daemon to manage. A spawn lock file, keyed by PID and timestamp, prevents duplicate brokers when multiple sessions start at once. A broker that starts while another broker already answers on the socket exits instead of taking the socket over; it replaces only a stale socket file. A broker whose socket path comes to point at another broker shuts down, and its sessions reconnect to the broker that owns the path. `atomic intercom restart` asks the broker to shut down over its socket (a `shutdown` request, accepted before registration like `broker_status`), so its sessions reconnect to a broker that reads the current storage selection.
|
|
509
513
|
|
|
510
514
|
**`ask` stays client-side.** The broker still routes plain messages; it does not have a special request/response mode for `ask`. The client waits for a matching reply before it triggers a new turn, then returns that reply as the tool result. Reply hints make that flow practical by showing the recipient the exact `send` call to use. Separately, `list` / `sessions` now carry a `requestId` so a delayed session-list reply cannot be mistaken for a newer one.
|
|
511
515
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { BrokerMultiNodeStatus, BrokerStatusReport } from "../types.js";
|
|
2
|
+
import type { JsonWireValue } from "./framing.js";
|
|
3
|
+
|
|
4
|
+
type WireObject = { [key: string]: JsonWireValue };
|
|
5
|
+
|
|
6
|
+
function isObject(value: JsonWireValue | undefined): value is WireObject {
|
|
7
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function isOptionalString(value: JsonWireValue | undefined): boolean {
|
|
11
|
+
return value === undefined || typeof value === "string";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Whether a `broker_status` frame's `report` has the shape this version reads. */
|
|
15
|
+
export function isBrokerStatusReport(value: JsonWireValue | undefined): value is WireObject & BrokerStatusReport {
|
|
16
|
+
if (!isObject(value) || typeof value.pid !== "number" || typeof value.sessions !== "number") return false;
|
|
17
|
+
const { storage, multiNode } = value;
|
|
18
|
+
if (
|
|
19
|
+
storage !== undefined &&
|
|
20
|
+
(!isObject(storage) ||
|
|
21
|
+
typeof storage.digest !== "string" ||
|
|
22
|
+
(storage.source !== "env" && storage.source !== "settings"))
|
|
23
|
+
)
|
|
24
|
+
return false;
|
|
25
|
+
return (
|
|
26
|
+
isObject(multiNode) &&
|
|
27
|
+
(multiNode.status === "off" ||
|
|
28
|
+
multiNode.status === "starting" ||
|
|
29
|
+
multiNode.status === "on" ||
|
|
30
|
+
multiNode.status === "unavailable") &&
|
|
31
|
+
isOptionalString(multiNode.reason) &&
|
|
32
|
+
isOptionalString(multiNode.adapter) &&
|
|
33
|
+
isOptionalString(multiNode.prefix) &&
|
|
34
|
+
(multiNode.peers === undefined ||
|
|
35
|
+
(Array.isArray(multiNode.peers) && multiNode.peers.every((peer) => typeof peer === "string")))
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** One line for a broker's multi-node routing state, e.g. `on (adapter x, prefix none), 1 peer broker live (host-b)`. */
|
|
40
|
+
export function describeMultiNodeStatus(multiNode: BrokerMultiNodeStatus): string {
|
|
41
|
+
const storage = multiNode.adapter === undefined ? "" : ` (adapter ${multiNode.adapter}, prefix ${multiNode.prefix ?? "none"})`;
|
|
42
|
+
if (multiNode.status === "off" || multiNode.status === "unavailable") {
|
|
43
|
+
return `${multiNode.status}${storage}${multiNode.reason === undefined ? "" : `: ${multiNode.reason}`}`;
|
|
44
|
+
}
|
|
45
|
+
if (multiNode.status === "starting" || multiNode.peers === undefined) return `${multiNode.status}${storage}`;
|
|
46
|
+
const count = `${multiNode.peers.length} peer broker${multiNode.peers.length === 1 ? "" : "s"} live`;
|
|
47
|
+
return `on${storage}, ${count}${multiNode.peers.length === 0 ? "" : ` (${[...new Set(multiNode.peers)].join(", ")})`}`;
|
|
48
|
+
}
|
|
@@ -17,17 +17,21 @@ import {
|
|
|
17
17
|
} from "./socket-writes.js";
|
|
18
18
|
import { isSocketAnswering } from "./socket-liveness.js";
|
|
19
19
|
import { acquireSocketReplacementLock } from "./socket-replacement-lock.js";
|
|
20
|
+
import { startBrokerWatchdog } from "./watchdog.js";
|
|
20
21
|
import {
|
|
21
22
|
getBrokerDeliveredMessagesPath,
|
|
22
23
|
getBrokerPidPath,
|
|
24
|
+
getBrokerWatchdogLogPath,
|
|
23
25
|
getBrokerSocketPath,
|
|
24
26
|
getBrokerSocketReplacementLockPath,
|
|
25
27
|
getIntercomDirPath,
|
|
26
28
|
} from "./paths.js";
|
|
29
|
+
import { describeSharedStorage, sharedStorageDigest, sharedStorageSettingSource } from "./shared-storage-setting.js";
|
|
27
30
|
import type {
|
|
28
31
|
SessionInfo,
|
|
29
32
|
Message,
|
|
30
33
|
BrokerMessage,
|
|
34
|
+
BrokerStatusReport,
|
|
31
35
|
SupervisorRegistration,
|
|
32
36
|
WorkflowStageRosterAnnouncement,
|
|
33
37
|
WorkflowStageRosterEntry,
|
|
@@ -86,6 +90,8 @@ import {
|
|
|
86
90
|
|
|
87
91
|
const INTERCOM_DIR = getIntercomDirPath();
|
|
88
92
|
const SOCKET_PATH = getBrokerSocketPath();
|
|
93
|
+
const BROKER_IDLE_TIMEOUT_MS = 5_000;
|
|
94
|
+
const UNCLASSIFIED_CONNECTION_RECHECK_MS = 100;
|
|
89
95
|
const PID_PATH = getBrokerPidPath();
|
|
90
96
|
const SOCKET_REPLACEMENT_LOCK_PATH = getBrokerSocketReplacementLockPath();
|
|
91
97
|
|
|
@@ -94,6 +100,13 @@ const LISTEN_ATTEMPTS = 5;
|
|
|
94
100
|
const LISTEN_RETRY_DELAY_MS = 100;
|
|
95
101
|
const SOCKET_OWNERSHIP_CHECK_MS = 1_000;
|
|
96
102
|
const MULTI_NODE_CLOSE_BOUND_MS = 2_000;
|
|
103
|
+
const NO_SHARED_STORAGE_REASON = "no shared storage selection (top-level `storage` in global settings, or ATOMIC_STORAGE)";
|
|
104
|
+
/** The off reasons `resolveBrokerStateSource` reports without an error, as the log and `broker_status` state them. */
|
|
105
|
+
const MULTI_NODE_OFF_REASONS: Readonly<Record<string, string>> = {
|
|
106
|
+
"no-storage": "the shared storage selection names no database",
|
|
107
|
+
memory: "the shared storage selection is in memory",
|
|
108
|
+
"domain-only-adapter": "the storage adapter has no openDatabase hook",
|
|
109
|
+
};
|
|
97
110
|
|
|
98
111
|
function exitBecauseLiveBrokerOwnsSocket(): never {
|
|
99
112
|
console.error(`Intercom broker not started: a live broker already answers on ${SOCKET_PATH}`);
|
|
@@ -302,7 +315,9 @@ function isWorkflowPossibleStageAnnouncements(
|
|
|
302
315
|
|
|
303
316
|
class IntercomBroker {
|
|
304
317
|
private sessions = new Map<string, ConnectedSession>();
|
|
318
|
+
private unclassifiedConnections = new Map<net.Socket, number>();
|
|
305
319
|
private server: net.Server;
|
|
320
|
+
private readonly instanceId = randomUUID();
|
|
306
321
|
private shutdownTimer: NodeJS.Timeout | null = null;
|
|
307
322
|
private boundSocketInode: number | undefined;
|
|
308
323
|
private lastConnectionAt = performance.now();
|
|
@@ -328,6 +343,8 @@ class IntercomBroker {
|
|
|
328
343
|
this.sharedStorageSetting === undefined ? undefined : (request) => this.routeRemoteSend(request);
|
|
329
344
|
private multiNode: MultiNodeRouter | undefined;
|
|
330
345
|
private multiNodeStatus: "starting" | "on" | "off" | "unavailable" = "off";
|
|
346
|
+
/** Why multi-node routing is off or unavailable, for `broker_status` and the log. */
|
|
347
|
+
private multiNodeReason: string | undefined = NO_SHARED_STORAGE_REASON;
|
|
331
348
|
private multiNodeFirstAttempt: Promise<void> = Promise.resolve();
|
|
332
349
|
private shuttingDown = false;
|
|
333
350
|
|
|
@@ -340,6 +357,7 @@ class IntercomBroker {
|
|
|
340
357
|
start(): void {
|
|
341
358
|
this.listen(1);
|
|
342
359
|
if (this.sharedStorageSetting !== undefined) this.startMultiNode(this.sharedStorageSetting);
|
|
360
|
+
else console.error(`Intercom multi-node routing is off: ${NO_SHARED_STORAGE_REASON}`);
|
|
343
361
|
process.on("SIGTERM", () => this.shutdown());
|
|
344
362
|
process.on("SIGINT", () => this.shutdown());
|
|
345
363
|
}
|
|
@@ -356,6 +374,7 @@ class IntercomBroker {
|
|
|
356
374
|
releaseReplacementLock();
|
|
357
375
|
writeFileSync(PID_PATH, String(process.pid));
|
|
358
376
|
console.log(`Intercom broker started (pid: ${process.pid})`);
|
|
377
|
+
startBrokerWatchdog(SOCKET_PATH, this.instanceId, getBrokerWatchdogLogPath());
|
|
359
378
|
this.lastConnectionAt = performance.now();
|
|
360
379
|
this.scheduleShutdownCheck();
|
|
361
380
|
if (this.boundSocketInode !== undefined) this.watchSocketOwnership();
|
|
@@ -415,11 +434,18 @@ class IntercomBroker {
|
|
|
415
434
|
|
|
416
435
|
private handleConnection(socket: net.Socket): void {
|
|
417
436
|
let sessionId: string | null = null;
|
|
418
|
-
this.
|
|
419
|
-
|
|
437
|
+
this.unclassifiedConnections.set(socket, performance.now());
|
|
438
|
+
const recordActivity = () => {
|
|
439
|
+
this.lastConnectionAt = performance.now();
|
|
440
|
+
this.restartShutdownCheck();
|
|
441
|
+
};
|
|
420
442
|
|
|
421
443
|
const reader = createMessageReader((msg) => {
|
|
422
444
|
if (socket.destroyed || socket.writableEnded) return;
|
|
445
|
+
if (this.unclassifiedConnections.delete(socket)) {
|
|
446
|
+
const health = typeof msg === "object" && msg !== null && !Array.isArray(msg) && msg.type === "health";
|
|
447
|
+
if (!health) recordActivity();
|
|
448
|
+
}
|
|
423
449
|
this.handleMessage(socket, msg, sessionId, (id) => {
|
|
424
450
|
sessionId = id;
|
|
425
451
|
});
|
|
@@ -441,6 +467,7 @@ class IntercomBroker {
|
|
|
441
467
|
// is lost. disconnectSession is idempotent, so 'end', 'error' and 'close'
|
|
442
468
|
// may each fire without duplicating the departure broadcast.
|
|
443
469
|
const retire = () => {
|
|
470
|
+
if (this.unclassifiedConnections.delete(socket)) recordActivity();
|
|
444
471
|
const departing = sessionId;
|
|
445
472
|
if (departing === null) return;
|
|
446
473
|
sessionId = null;
|
|
@@ -464,17 +491,24 @@ class IntercomBroker {
|
|
|
464
491
|
});
|
|
465
492
|
}
|
|
466
493
|
|
|
467
|
-
private scheduleShutdownCheck(): void {
|
|
494
|
+
private scheduleShutdownCheck(delayMs = BROKER_IDLE_TIMEOUT_MS): void {
|
|
468
495
|
if (this.shutdownTimer) return;
|
|
469
496
|
|
|
470
497
|
this.shutdownTimer = setTimeout(() => {
|
|
471
498
|
this.shutdownTimer = null;
|
|
472
499
|
if (this.sessions.size === 0) {
|
|
500
|
+
const now = performance.now();
|
|
501
|
+
const awaitingFirstFrame = [...this.unclassifiedConnections.values()]
|
|
502
|
+
.some((acceptedAt) => now - acceptedAt < BROKER_IDLE_TIMEOUT_MS);
|
|
503
|
+
if (awaitingFirstFrame) {
|
|
504
|
+
this.scheduleShutdownCheck(UNCLASSIFIED_CONNECTION_RECHECK_MS);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
473
507
|
const idleMs = Math.round(performance.now() - this.lastConnectionAt);
|
|
474
508
|
console.log(`No sessions connected for ${idleMs}ms since the last connection, shutting down`);
|
|
475
509
|
this.shutdown();
|
|
476
510
|
}
|
|
477
|
-
},
|
|
511
|
+
}, delayMs);
|
|
478
512
|
}
|
|
479
513
|
|
|
480
514
|
private restartShutdownCheck(): void {
|
|
@@ -1473,6 +1507,27 @@ class IntercomBroker {
|
|
|
1473
1507
|
|
|
1474
1508
|
const clientMessage = msg;
|
|
1475
1509
|
|
|
1510
|
+
if (clientMessage.type === "health") {
|
|
1511
|
+
if (typeof clientMessage.requestId !== "string" || clientMessage.requestId.length > 128) {
|
|
1512
|
+
throw new Error("Invalid health request");
|
|
1513
|
+
}
|
|
1514
|
+
writeMessageIfOpen(socket, { type: "health", requestId: clientMessage.requestId, instanceId: this.instanceId });
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
// Control requests need no registration: `atomic intercom restart` and `atomic storage status` send them.
|
|
1519
|
+
if (clientMessage.type === "broker_status" || clientMessage.type === "shutdown") {
|
|
1520
|
+
if (typeof clientMessage.requestId !== "string") throw new Error(`Invalid ${clientMessage.type} message`);
|
|
1521
|
+
const requestId = clientMessage.requestId;
|
|
1522
|
+
if (clientMessage.type === "broker_status") {
|
|
1523
|
+
void this.statusReport().then((report) => writeMessageIfOpen(socket, { type: "broker_status", requestId, report }));
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
console.error("Intercom broker shutting down on request, so its sessions reconnect to a new broker");
|
|
1527
|
+
writeMessageWithOutcome(socket, { type: "shutdown_ack", requestId }, () => this.shutdown());
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1476
1531
|
if (currentId === null && clientMessage.type !== "register") {
|
|
1477
1532
|
throw new Error(`Received ${clientMessage.type} before register`);
|
|
1478
1533
|
}
|
|
@@ -1567,6 +1622,7 @@ class IntercomBroker {
|
|
|
1567
1622
|
...(supervisorId ? { supervisorSessionId: supervisorId } : {}),
|
|
1568
1623
|
...(clientMessage.acknowledgesReceipt === true ? { acknowledgesReceipt: true as const } : {}),
|
|
1569
1624
|
acknowledgesPendingStageRoutes: true as const,
|
|
1625
|
+
brokerStatus: true as const,
|
|
1570
1626
|
});
|
|
1571
1627
|
if (isAgentRecipient(info)) this.broadcastToMemberships({ type: "session_joined", session: info }, sessionGroups(info), id);
|
|
1572
1628
|
// #3761: messages written to this recipient's previous connection that it never confirmed saving go out again.
|
|
@@ -2097,12 +2153,32 @@ class IntercomBroker {
|
|
|
2097
2153
|
});
|
|
2098
2154
|
}
|
|
2099
2155
|
|
|
2156
|
+
/** This broker's `broker_status` report: its storage selection's digest, multi-node state and live peers. */
|
|
2157
|
+
private async statusReport(): Promise<BrokerStatusReport> {
|
|
2158
|
+
const setting = this.sharedStorageSetting;
|
|
2159
|
+
const peers = this.multiNodeStatus === "on" ? await this.multiNode?.peerBrokers() : undefined;
|
|
2160
|
+
return {
|
|
2161
|
+
pid: process.pid,
|
|
2162
|
+
sessions: this.sessions.size,
|
|
2163
|
+
...(setting === undefined
|
|
2164
|
+
? {}
|
|
2165
|
+
: { storage: { digest: sharedStorageDigest(setting), source: sharedStorageSettingSource() } }),
|
|
2166
|
+
multiNode: {
|
|
2167
|
+
status: this.multiNodeStatus,
|
|
2168
|
+
...(this.multiNodeReason === undefined ? {} : { reason: this.multiNodeReason }),
|
|
2169
|
+
...(setting === undefined ? {} : describeSharedStorage(setting)),
|
|
2170
|
+
...(peers === undefined ? {} : { peers }),
|
|
2171
|
+
},
|
|
2172
|
+
};
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2100
2175
|
/**
|
|
2101
2176
|
* The shared state is loaded lazily, so a single-node broker never loads the host storage layer. A database that
|
|
2102
2177
|
* cannot be opened is retried with exponential backoff; meanwhile local delivery continues.
|
|
2103
2178
|
*/
|
|
2104
|
-
private startMultiNode(setting: Record<string,
|
|
2179
|
+
private startMultiNode(setting: Record<string, JsonWireValue>): void {
|
|
2105
2180
|
this.multiNodeStatus = "starting";
|
|
2181
|
+
this.multiNodeReason = undefined;
|
|
2106
2182
|
let settleFirstAttempt: () => void = () => {};
|
|
2107
2183
|
this.multiNodeFirstAttempt = new Promise<void>((resolve) => {
|
|
2108
2184
|
settleFirstAttempt = resolve;
|
|
@@ -2112,7 +2188,8 @@ class IntercomBroker {
|
|
|
2112
2188
|
settleFirstAttempt();
|
|
2113
2189
|
};
|
|
2114
2190
|
const report = (error: unknown): void => {
|
|
2115
|
-
|
|
2191
|
+
this.multiNodeReason = error instanceof Error ? error.message : String(error);
|
|
2192
|
+
console.error(`Intercom multi-node routing is unavailable: ${this.multiNodeReason}`);
|
|
2116
2193
|
};
|
|
2117
2194
|
void (async () => {
|
|
2118
2195
|
const startup = await resolveMultiNodeStartup(setting, dirname(INTERCOM_DIR), async () => {
|
|
@@ -2126,9 +2203,10 @@ class IntercomBroker {
|
|
|
2126
2203
|
return opener;
|
|
2127
2204
|
});
|
|
2128
2205
|
if (startup.status === "off") {
|
|
2129
|
-
// A
|
|
2130
|
-
//
|
|
2131
|
-
|
|
2206
|
+
// A selection that does not resolve to a database, or an adapter that cannot load, leaves this broker
|
|
2207
|
+
// single-node, exactly as without a shared `storage`.
|
|
2208
|
+
this.multiNodeReason = startup.error === undefined ? (MULTI_NODE_OFF_REASONS[startup.reason] ?? startup.reason) : startup.reason;
|
|
2209
|
+
console.error(`Intercom multi-node routing is off: ${this.multiNodeReason}`);
|
|
2132
2210
|
settle("off");
|
|
2133
2211
|
return;
|
|
2134
2212
|
}
|
|
@@ -2171,6 +2249,9 @@ class IntercomBroker {
|
|
|
2171
2249
|
}
|
|
2172
2250
|
}
|
|
2173
2251
|
if (this.shuttingDown) await this.multiNode.close().catch(() => {});
|
|
2252
|
+
this.multiNodeReason = undefined;
|
|
2253
|
+
const { adapter, prefix } = describeSharedStorage(setting);
|
|
2254
|
+
console.error(`Intercom multi-node routing is on (adapter ${adapter}, prefix ${prefix ?? "none"})`);
|
|
2174
2255
|
settle("on");
|
|
2175
2256
|
return;
|
|
2176
2257
|
} catch (error) {
|