@bastani/atomic 0.9.28-alpha.4 → 0.9.28
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 +13 -0
- package/dist/builtin/intercom/README.md +3 -2
- package/dist/builtin/intercom/broker/broker.ts +90 -14
- package/dist/builtin/intercom/broker/paths.ts +4 -0
- package/dist/builtin/intercom/broker/socket-liveness.ts +32 -0
- package/dist/builtin/intercom/broker/socket-replacement-lock.ts +104 -0
- package/dist/builtin/intercom/broker/spawn.ts +108 -63
- package/dist/builtin/intercom/index.bundle.mjs +114 -61
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/CHANGELOG.md +6 -0
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/CHANGELOG.md +6 -0
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +41 -0
- package/dist/builtin/workflows/builtin/{chunk-aqv8ny38.js → chunk-9ywkdsa9.js} +1 -1
- package/dist/builtin/workflows/builtin/{chunk-p6jagvyf.js → chunk-bnz4knhy.js} +1 -1
- package/dist/builtin/workflows/builtin/{chunk-br4ac9a3.js → chunk-dx5sjaxe.js} +1 -1
- package/dist/builtin/workflows/builtin/{chunk-p3xmajmj.js → chunk-v4scsq3y.js} +41 -13
- 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 +1 -1
- package/dist/builtin/workflows/src/extension/index.bundle.mjs +38 -14
- package/dist/builtin/workflows/src/index.js +38 -14
- package/dist/cli/startup-ui.d.ts +3 -2
- package/dist/cli/startup-ui.d.ts.map +1 -1
- package/dist/cli/startup-ui.js +19 -4
- package/dist/cli/startup-ui.js.map +1 -1
- package/dist/core/agent-session-runtime.d.ts +5 -1
- package/dist/core/agent-session-runtime.d.ts.map +1 -1
- package/dist/core/agent-session-runtime.js +13 -9
- package/dist/core/agent-session-runtime.js.map +1 -1
- package/dist/core/extensions/session-events.d.ts +2 -0
- package/dist/core/extensions/session-events.d.ts.map +1 -1
- package/dist/core/extensions/session-events.js.map +1 -1
- package/dist/core/provider-alias.d.ts.map +1 -1
- package/dist/core/provider-alias.js +8 -0
- package/dist/core/provider-alias.js.map +1 -1
- package/dist/core/provider-composer-internal.d.ts.map +1 -1
- package/dist/core/provider-composer-internal.js +6 -3
- package/dist/core/provider-composer-internal.js.map +1 -1
- package/dist/core/resource-loader-reload.d.ts.map +1 -1
- package/dist/core/resource-loader-reload.js +1 -0
- package/dist/core/resource-loader-reload.js.map +1 -1
- package/dist/core/resource-loader-types.d.ts +1 -0
- package/dist/core/resource-loader-types.d.ts.map +1 -1
- package/dist/core/resource-loader-types.js.map +1 -1
- package/dist/extensions/herdr/index.d.ts.map +1 -1
- package/dist/extensions/herdr/index.js +20 -2
- package/dist/extensions/herdr/index.js.map +1 -1
- package/dist/extensions/herdr/pane-owner.d.ts +7 -0
- package/dist/extensions/herdr/pane-owner.d.ts.map +1 -1
- package/dist/extensions/herdr/pane-owner.js +53 -11
- package/dist/extensions/herdr/pane-owner.js.map +1 -1
- package/dist/main-deferred-startup.d.ts +8 -0
- package/dist/main-deferred-startup.d.ts.map +1 -1
- package/dist/main-deferred-startup.js +6 -0
- package/dist/main-deferred-startup.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +11 -3
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/interactive-auth-routing.js +2 -2
- package/dist/modes/interactive/interactive-auth-routing.js.map +1 -1
- package/dist/modes/interactive/interactive-extension-dialogs.js +8 -0
- package/dist/modes/interactive/interactive-extension-dialogs.js.map +1 -1
- package/dist/modes/interactive/interactive-mode-base.d.ts +1 -0
- package/dist/modes/interactive/interactive-mode-base.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode-base.js +1 -0
- package/dist/modes/interactive/interactive-mode-base.js.map +1 -1
- package/dist/modes/interactive/interactive-mode-helpers.js +1 -1
- package/dist/modes/interactive/interactive-mode-helpers.js.map +1 -1
- package/dist/modes/interactive/interactive-mode-types.d.ts +1 -0
- package/dist/modes/interactive/interactive-mode-types.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode-types.js.map +1 -1
- package/dist/modes/interactive/interactive-process-lifecycle.js +1 -1
- package/dist/modes/interactive/interactive-process-lifecycle.js.map +1 -1
- package/dist/modes/interactive/interactive-startup-trust.d.ts +6 -0
- package/dist/modes/interactive/interactive-startup-trust.d.ts.map +1 -0
- package/dist/modes/interactive/interactive-startup-trust.js +15 -0
- package/dist/modes/interactive/interactive-startup-trust.js.map +1 -0
- package/dist/modes/interactive/interactive-startup.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-startup.js +3 -0
- package/dist/modes/interactive/interactive-startup.js.map +1 -1
- package/dist/modes/interactive-engine/engine-child-liveness.d.ts +1 -0
- package/dist/modes/interactive-engine/engine-child-liveness.d.ts.map +1 -1
- package/dist/modes/interactive-engine/engine-child-liveness.js +15 -1
- package/dist/modes/interactive-engine/engine-child-liveness.js.map +1 -1
- package/dist/modes/interactive-engine/engine-monitor.d.ts +4 -0
- package/dist/modes/interactive-engine/engine-monitor.d.ts.map +1 -1
- package/dist/modes/interactive-engine/engine-monitor.js +17 -0
- package/dist/modes/interactive-engine/engine-monitor.js.map +1 -1
- package/dist/modes/interactive-engine/engine-quit-intent.d.ts +16 -0
- package/dist/modes/interactive-engine/engine-quit-intent.d.ts.map +1 -0
- package/dist/modes/interactive-engine/engine-quit-intent.js +27 -0
- package/dist/modes/interactive-engine/engine-quit-intent.js.map +1 -0
- package/dist/modes/interactive-engine/extension-ui-bridge.d.ts +1 -0
- package/dist/modes/interactive-engine/extension-ui-bridge.d.ts.map +1 -1
- package/dist/modes/interactive-engine/extension-ui-bridge.js +5 -0
- package/dist/modes/interactive-engine/extension-ui-bridge.js.map +1 -1
- package/dist/modes/interactive-engine/isolated-runtime.d.ts +4 -1
- package/dist/modes/interactive-engine/isolated-runtime.d.ts.map +1 -1
- package/dist/modes/interactive-engine/isolated-runtime.js +19 -2
- package/dist/modes/interactive-engine/isolated-runtime.js.map +1 -1
- package/dist/modes/interactive-engine/protocol.d.ts +18 -1
- package/dist/modes/interactive-engine/protocol.d.ts.map +1 -1
- package/dist/modes/interactive-engine/protocol.js +6 -0
- package/dist/modes/interactive-engine/protocol.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts +11 -0
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +42 -0
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-input-scheduler.js +2 -2
- package/dist/modes/rpc/rpc-input-scheduler.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +23 -6
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-provider-auth.js +1 -1
- package/dist/modes/rpc/rpc-provider-auth.js.map +1 -1
- package/docs/extensions/events.md +3 -0
- package/docs/herdr.md +11 -2
- package/docs/models.md +1 -1
- package/docs/providers.md +2 -0
- package/docs/sdk.md +1 -1
- package/docs/settings.md +1 -1
- package/npm-shrinkwrap.json +32 -32
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,105 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.9.28] - 2026-10-08
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added `fromSignal` to the `session_shutdown` event. It is `true` when a `quit` comes from a host `SIGTERM` or `SIGHUP` instead of Ctrl+D or `/quit`, and `runtimeHost.dispose({ fromSignal: true })` sets it for SDK hosts ([#3492](https://github.com/bastani-inc/atomic/issues/3492)).
|
|
10
|
+
- Added the API-key-only providers `openai-api` and `anthropic-api`, so you can keep a ChatGPT or Claude subscription login on `openai`/`anthropic` and still use an API key for the same models. See [Subscription and API key at the same time](docs/providers.md#subscription-and-api-key-at-the-same-time).
|
|
11
|
+
- `session.compact()` and extension `ctx.compact()` accept `compactionModel` to pick the compaction model for one run without changing the `compactionModel` setting. An unknown ID rejects before the active run is interrupted. See [Compaction and tree navigation](docs/sdk.md#compaction-and-tree-navigation).
|
|
12
|
+
- Added terminal program-status reporting through OSC 7501 for runs, compaction, extension prompts, and sign-in. Compatible terminals can show when Atomic is working, waiting for input, done, or has failed.
|
|
13
|
+
- Added recorded response and tool execution durations. Bash tool results show their execution time, including after a session is resumed.
|
|
14
|
+
- Added OpenAI's GPT-6 Luna classifier through the Decisions API with `OPENAI_API_KEY`, and image input for codemode's `models.classify()` context. See [Classifier models](docs/models.md#use-classifier-models).
|
|
15
|
+
- Added native classification for llama.cpp decision models through `/v1/systemone`. Decision-only models stay out of the chat selector, while chat models keep the next-token classifier fallback. Cached native classifiers remain available after restart.
|
|
16
|
+
- Added exact-name `+name` and `-name` modifiers to CLI `--tools` and SDK `tools`, for example `atomic --tools +codemode,-write`. They change the resolved defaults instead of replacing them and persist across `/reload`.
|
|
17
|
+
- Added `compactionModel` and a **Compaction model** selector in `/settings` to choose a compactor independently of the chat model ([#3470](https://github.com/bastani-inc/atomic/issues/3470)).
|
|
18
|
+
- Added verbatim classifier compaction with registered models such as `typesafe/jev-latest`, retaining protected context and recent messages ([#3470](https://github.com/bastani-inc/atomic/issues/3470)).
|
|
19
|
+
- Added Morph verbatim compaction with `morph/morph-compactor`, authenticated through `/login morph` or `MORPH_API_KEY` ([#3470](https://github.com/bastani-inc/atomic/issues/3470)).
|
|
20
|
+
- Added `session.workflows.observe(observer)`, so SDK hosts can follow the workflow runs the agent launches without writing an extension. The observer receives a snapshot first, then changes in order. Root activity now includes an optional `graph` that lists each stage and `ctx.tool` node with its status, parents, and pending prompt, plus any run-level prompts ([#3476](https://github.com/bastani-inc/atomic/issues/3476)).
|
|
21
|
+
- Added `RunOpts.onStageSessionEvent` so workflow `run()` callers receive each stage session's events, such as message updates and tool executions, tagged with the run and stage ids. Events continue across fallback-model sessions and include nested workflow stages ([#3474](https://github.com/bastani-inc/atomic/issues/3474)).
|
|
22
|
+
- Added `createTranscript()`, which turns session events into the assistant's output as ordered, JSON-serializable text, thinking, and tool call parts, with each tool call joined to its result ([#3475](https://github.com/bastani-inc/atomic/issues/3475)).
|
|
23
|
+
- Added `createAgentSessionAdapter(baseOptions?)` to `@bastani/atomic/workflows`, which builds the default workflow stage-session adapter with `createAgentSession` options shared by every stage ([#3472](https://github.com/bastani-inc/atomic/issues/3472)).
|
|
24
|
+
- Added `ctx.isPresentationOnly` so extension lifecycle hooks can leave execution to the isolated interactive engine while retaining host-side UI ([#3468](https://github.com/bastani-inc/atomic/issues/3468)).
|
|
25
|
+
- Exported `publishExtensionContextEffect(ctx, effect, phase?)` with a synchronous commit phase for generation identity handoff, and `registerExtensionContextRetirementEffect(ctx, dispose)` for observer cleanup before authority revocation ([#3468](https://github.com/bastani-inc/atomic/issues/3468)).
|
|
26
|
+
- Added Azure Foundry Chat Completions with `azure/deepseek-v4-pro`, using the existing Azure endpoint and deployment-name settings.
|
|
27
|
+
- Added `*` patterns to `--tools` and `--exclude-tools`, including MCP tool names.
|
|
28
|
+
- Added `--no-mcp` to disable built-in MCP support for one run, including interactive sessions.
|
|
29
|
+
- Added `ExtensionAPI.getWorkflowHostModules()` to supply shared host module instances for the exact workflow imports `@bastani/atomic`, `@bastani/pi-ai`, and `@bastani/pi-ai/providers/all`, avoiding repeated package loading ([#3454](https://github.com/bastani-inc/atomic/issues/3454)).
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- `openai-decisions/gpt-6-luna` no longer has its own `/login` entry, so you do not need to sign in again. The classifier uses the API key saved for `openai-api`, then the one saved for `openai`, then `OPENAI_API_KEY`. A ChatGPT sign-in on `openai` cannot call the Decisions API and is not used, so the classifier stays unavailable until one of those keys exists. See [Classifier models](docs/models.md#use-classifier-models).
|
|
34
|
+
- Moved the GPT-6 Luna classifier from `openai/gpt-6-luna` to its own API-key-only provider, `openai-decisions/gpt-6-luna`, named "GPT-6 Luna Decisions". `openai/gpt-6-luna` now always means the chat model, and the classifier stays available while `openai` uses ChatGPT sign-in. Update `routerModel`, `compactionModel`, `structured_output` and codemode references that meant the classifier. See [Classifier models](docs/models.md#use-classifier-models).
|
|
35
|
+
- Codemode labels returned output items with numbered headers and places console output in a separate trailing block.
|
|
36
|
+
- Codemode now says that `ALL_TOOLS`, `searchTools()`, and `describeTool()` list only script-callable tools, and `searchTools()`/`describeTool()` answer an exact model-only tool name such as `subagent` with a hint to call it directly instead of returning nothing ([#3510](https://github.com/bastani-inc/atomic/issues/3510))
|
|
37
|
+
- Changed `atomic mcp login --timeout` to limit the whole sign-in, including requests to the authorization server, instead of only the wait for the browser.
|
|
38
|
+
- Compaction planners now receive structured per-message lines instead of a numbered transcript ([#3470](https://github.com/bastani-inc/atomic/issues/3470)).
|
|
39
|
+
- Compaction cards show the backend and model used, including `summary (pi fallback)` for policy-refusal summaries ([#3470](https://github.com/bastani-inc/atomic/issues/3470)).
|
|
40
|
+
- Home and End move the editor cursor to the start and end of the line. Ctrl+Home and Ctrl+End jump to the top and bottom of the fullscreen transcript.
|
|
41
|
+
- Renamed the Azure provider to `azure`. Existing provider references remain supported, and startup migrates credentials and configuration without replacing existing `azure` entries. Project files are migrated only when trusted.
|
|
42
|
+
- Codemode built-ins are frozen in pi 1.0.4. Patches to built-ins are ignored instead of crashing the host.
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
- Fixed the startup project trust prompt opening inside the fully painted interface. Atomic now shows the prompt on its own and paints the interface after you choose.
|
|
47
|
+
- Fixed the "This project is not trusted" warning staying in the transcript after you trusted the project at the startup prompt. The warning still appears when you decline trust or leave the project untrusted.
|
|
48
|
+
- Fixed Herdr restoring panes without their Atomic conversation after a Herdr restart. With Herdr 0.9.2 or newer, Atomic now reports `atomic --session <id>` (plus `--session-dir` for a custom session directory) so Herdr can reopen the same session in the same pane, and keeps it registered when Herdr or the system ends Atomic with `SIGTERM` or `SIGHUP`. Quitting with Ctrl+D or `/quit` still clears it, and older Herdr versions keep status reporting without restore ([#3492](https://github.com/bastani-inc/atomic/issues/3492)).
|
|
49
|
+
- Updated the sandbox example extension's lockfile to `shell-quote` 1.12.0, resolving the `quote()` command-injection advisory [GHSA-pqg4-j6r4-53mv](https://github.com/advisories/GHSA-pqg4-j6r4-53mv) in its `@anthropic-ai/sandbox-runtime` dependency.
|
|
50
|
+
- Fixed hyperlinks being disabled in Herdr panes, while keeping image protocols off even when outer-terminal environment variables are inherited ([#10573](https://github.com/earendil-works/pi/issues/10573)).
|
|
51
|
+
- Fixed `outputPad` applying inconsistently to tool results, shell output, summaries, skill invocations, and custom transcript entries. Changing it in `/settings` now updates existing blocks without rebuilding the transcript.
|
|
52
|
+
- Fixed fullscreen text selections surviving a transcript rebuild and selecting unrelated replacement text.
|
|
53
|
+
- Fixed extension-registered MCP servers silently remaining unused when an SDK host has no MCP contribution consumer. Atomic now reports each unhandled server once.
|
|
54
|
+
- Fixed a false `Timeout waiting for response to resume_queued_messages` error when you send a message while a paused queue is still waiting for the agent's current turn to finish, and the follow-up `Agent is already processing` error caused by the stale paused state. The resume now waits for the turn instead of giving up after 30 seconds ([#3493](https://github.com/bastani-inc/atomic/issues/3493)).
|
|
55
|
+
- Fixed extension `session_shutdown` handlers being cut off when you quit the interactive CLI. The engine now gets up to 5 seconds to finish extension shutdown, instead of about 750 ms, before it is force-stopped.
|
|
56
|
+
- Fixed slow extension loading and `/reload` on Windows. Imports such as `./helper.js` that point at TypeScript sources no longer make the loader try hundreds of missing files before finding `helper.ts`.
|
|
57
|
+
- Fixed `/mcp` waiting for startup connections before opening. The manager now updates live and stays usable while enabling, disabling, or reconnecting servers.
|
|
58
|
+
- Fixed images being dropped as unresizable under `node --watch` when Node sends its own messages on the image resize worker channel.
|
|
59
|
+
- Fixed codemode's tool description omitting `await` for `searchTools()`, `describeTool()`, and `describeNamespace()`, which could make scripts return unresolved promises instead of tool information.
|
|
60
|
+
- Fixed slash commands and shell-command drafts typed during interactive startup running before Enter, including when launching with `--model` or `--provider`. Enter-terminated input stays ordered and unfinished text remains in the editor.
|
|
61
|
+
- Fixed a stuck MCP server startup blocking re-enabling the server or shutting down after it was disabled.
|
|
62
|
+
- Fixed clipboard paste doing nothing in Termux, and failed copies there omitting the Termux:API install hint.
|
|
63
|
+
- Fixed `!` and RPC `bash` output keeping fragments of color codes, such as a stray `m`, when a code was split across output chunks.
|
|
64
|
+
- Fixed MCP OAuth sign-ins that could not be cancelled while waiting on the authorization server and kept running after the session ended. The sign-in screen now cancels with Esc at every step, session shutdown aborts a running sign-in, and each request to the authorization server times out after 15 seconds.
|
|
65
|
+
- Fixed shutdown waiting up to 15 seconds to refresh an MCP OAuth token that was about to expire, only to close the server's session.
|
|
66
|
+
- Policy refusals from any chat compaction model, including `auto`, explicit chat-model IDs, and borrowed fallback entries, now try pi's summary compaction on the same model before continuing through fallback models. Summary requests retry recoverable errors; policy refusals are never retried. Classifier and Morph failures advance directly to fallback models ([#3470](https://github.com/bastani-inc/atomic/issues/3470)).
|
|
67
|
+
- Morph compaction failures now include the HTTP status and a bounded, credential-redacted response excerpt in error diagnostics ([#3470](https://github.com/bastani-inc/atomic/issues/3470)).
|
|
68
|
+
- Fixed Herdr publishing duplicate pane activity when startup workflow recovery refreshes an unchanged activity snapshot ([#3468](https://github.com/bastani-inc/atomic/issues/3468)).
|
|
69
|
+
- Fixed Herdr skipping a return to an earlier pane activity after an intervening report timed out or failed ([#3468](https://github.com/bastani-inc/atomic/issues/3468)).
|
|
70
|
+
- Fixed project `.atomic/settings.json`, extensions, skills, and other trust-gated resources being ignored without notice in untrusted projects. Interactive sessions now show a warning that points to `/trust`, matching upstream pi.
|
|
71
|
+
- Fixed `@bastani/atomic/workflows` type declarations disagreeing with the runtime: `StageSnapshot` now declares `parentIds`, `executionOrder`, `model`, `startedAt`, `endedAt`, and `durationMs`; `Store` now declares `snapshot()`, `graphSnapshot()`, and `subscribe()`; and an `AgentSessionAdapter.create` can return `createAgentSession(...)` directly without a type cast ([#3473](https://github.com/bastani-inc/atomic/issues/3473)).
|
|
72
|
+
- Fixed `run()` from `@bastani/atomic/workflows` failing at the first `ctx.task` or `ctx.stage` with "prompt adapter not configured" when `opts.adapters` was omitted. Stages now default to in-process `createAgentSession` sessions with in-memory session managers ([#3472](https://github.com/bastani-inc/atomic/issues/3472)).
|
|
73
|
+
- Fixed startup extension confirmations, including workflow recovery prompts, losing keyboard focus during terminal initialization ([#3468](https://github.com/bastani-inc/atomic/issues/3468)).
|
|
74
|
+
- Fixed queued protected workflow notices producing `SessionClosed` replies during reload. Successful reload now preserves those notices for the replacement extensions without releasing an explicitly paused queue ([#3468](https://github.com/bastani-inc/atomic/issues/3468)).
|
|
75
|
+
- Fixed idle workflow status and control commands not displaying their output in isolated interactive sessions, including after `/reload` ([#3468](https://github.com/bastani-inc/atomic/issues/3468)).
|
|
76
|
+
- Fixed extension reload skipping remaining commit effects and predecessor shutdown when a commit effect and an SDK error observer both throw. Reload now retains both failures while keeping the replacement extensions active ([#3468](https://github.com/bastani-inc/atomic/issues/3468)).
|
|
77
|
+
- Fixed syntax colors leaking across lines in multiline tokens and interpolation substitutions inheriting string colors.
|
|
78
|
+
- Invalid router and provider-list settings retain their configuration validation errors during Azure provider migration.
|
|
79
|
+
- Fixed codemode `tools.read()` returning text instead of an image block for image files. Pass the result to `image()` to display it ([#10251](https://github.com/earendil-works/pi/issues/10251)).
|
|
80
|
+
- Fixed MCP OAuth dynamic registration omitting `application_type`, which caused redirect URI rejections on OpenID Connect servers ([#10493](https://github.com/earendil-works/pi/issues/10493)).
|
|
81
|
+
- Fixed `--tools` removing MCP tools when selecting codemode. MCP tools remain callable unless the allowlist contains an entry starting with `mcp__`; unmatched tools are not declared directly.
|
|
82
|
+
- Fixed shutdown leaving MCP connections open while a server was still connecting ([#10249](https://github.com/earendil-works/pi/issues/10249)).
|
|
83
|
+
- Fixed hidden tools appearing in prompt rules and skill-reading hints. Codemode tool declarations now include each tool's prompt guidelines ([#10343](https://github.com/earendil-works/pi/issues/10343)).
|
|
84
|
+
- Fixed interactive quit waiting on pending extension commands, which could leave connecting MCP servers running.
|
|
85
|
+
- Fixed HTTP/2 pending stream cancellations being treated as user cancellations instead of retryable transport failures.
|
|
86
|
+
- Updated the bundled `proxy-addr` dependency to 2.0.8, fixing IP spoofing through IPv4-mapped IPv6 trusted subnets.
|
|
87
|
+
|
|
88
|
+
## [0.9.28-alpha.5] - 2026-10-08
|
|
89
|
+
|
|
90
|
+
### Added
|
|
91
|
+
|
|
92
|
+
- Added `fromSignal` to the `session_shutdown` event. It is `true` when a `quit` comes from a host `SIGTERM` or `SIGHUP` instead of Ctrl+D or `/quit`, and `runtimeHost.dispose({ fromSignal: true })` sets it for SDK hosts ([#3492](https://github.com/bastani-inc/atomic/issues/3492)).
|
|
93
|
+
|
|
94
|
+
### Changed
|
|
95
|
+
|
|
96
|
+
- `openai-decisions/gpt-6-luna` no longer has its own `/login` entry, so you do not need to sign in again. The classifier uses the API key saved for `openai-api`, then the one saved for `openai`, then `OPENAI_API_KEY`. A ChatGPT sign-in on `openai` cannot call the Decisions API and is not used, so the classifier stays unavailable until one of those keys exists. See [Classifier models](docs/models.md#use-classifier-models).
|
|
97
|
+
|
|
98
|
+
### Fixed
|
|
99
|
+
|
|
100
|
+
- Fixed the startup project trust prompt opening inside the fully painted interface. Atomic now shows the prompt on its own and paints the interface after you choose.
|
|
101
|
+
- Fixed the "This project is not trusted" warning staying in the transcript after you trusted the project at the startup prompt. The warning still appears when you decline trust or leave the project untrusted.
|
|
102
|
+
- Fixed Herdr restoring panes without their Atomic conversation after a Herdr restart. With Herdr 0.9.2 or newer, Atomic now reports `atomic --session <id>` (plus `--session-dir` for a custom session directory) so Herdr can reopen the same session in the same pane, and keeps it registered when Herdr or the system ends Atomic with `SIGTERM` or `SIGHUP`. Quitting with Ctrl+D or `/quit` still clears it, and older Herdr versions keep status reporting without restore ([#3492](https://github.com/bastani-inc/atomic/issues/3492)).
|
|
103
|
+
|
|
5
104
|
## [0.9.28-alpha.4] - 2026-10-07
|
|
6
105
|
|
|
7
106
|
### Added
|
|
@@ -4,6 +4,19 @@ All notable changes to the `pi-intercom` extension will be documented in this fi
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.9.28] - 2026-10-08
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Fixed a slow-starting intercom broker taking over a live broker's socket, which left a connected session unreachable by `intercom list` and `send`, so messages to it were silently not delivered ([#3505](https://github.com/bastani-inc/atomic/issues/3505)).
|
|
12
|
+
- Fixed intercom occasionally reporting `Intercom not connected: connect ENOENT …/broker.sock` when a session connected just as an idle broker was shutting down.
|
|
13
|
+
|
|
14
|
+
## [0.9.28-alpha.5] - 2026-10-08
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Fixed a slow-starting intercom broker taking over a live broker's socket, which left a connected session unreachable by `intercom list` and `send`, so messages to it were silently not delivered ([#3505](https://github.com/bastani-inc/atomic/issues/3505)).
|
|
19
|
+
|
|
7
20
|
## [0.9.28-alpha.1] - 2026-10-05
|
|
8
21
|
|
|
9
22
|
### Fixed
|
|
@@ -492,7 +492,8 @@ Runtime files live under the active agent directory. Atomic defaults to `~/.atom
|
|
|
492
492
|
- `broker.pid` — Broker process ID
|
|
493
493
|
- `delivered-messages.sqlite` — bounded durable authority containing keyed digests, never plaintext payloads
|
|
494
494
|
- `delivered-messages.key` — random owner-only HMAC key paired with the authority database
|
|
495
|
-
- `broker.spawn.lock` —
|
|
495
|
+
- `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
|
+
- `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
|
|
496
497
|
- `broker.log` — Broker stderr, truncated on every spawn and capped at 8 KiB by the broker itself
|
|
497
498
|
- `config.json` — User configuration
|
|
498
499
|
|
|
@@ -504,7 +505,7 @@ The broker caps the file from the inside, since nothing on the parent side can b
|
|
|
504
505
|
|
|
505
506
|
**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.
|
|
506
507
|
|
|
507
|
-
**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.
|
|
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.
|
|
508
509
|
|
|
509
510
|
**`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.
|
|
510
511
|
|
|
@@ -2,14 +2,17 @@
|
|
|
2
2
|
// only position from which the stderr cap covers the other modules' own initialization.
|
|
3
3
|
import "./bounded-stderr-install.js";
|
|
4
4
|
import net from "net";
|
|
5
|
-
import { chmodSync, writeFileSync, unlinkSync, mkdirSync, readFileSync } from "fs";
|
|
5
|
+
import { chmodSync, writeFileSync, unlinkSync, mkdirSync, readFileSync, statSync } from "fs";
|
|
6
6
|
import { randomUUID } from "crypto";
|
|
7
7
|
import { createMessageReader, type JsonWireValue } from "./framing.js";
|
|
8
8
|
import { writeMessageIfOpen, writeMessageWithOutcome } from "./socket-writes.js";
|
|
9
|
+
import { isSocketAnswering } from "./socket-liveness.js";
|
|
10
|
+
import { acquireSocketReplacementLock } from "./socket-replacement-lock.js";
|
|
9
11
|
import {
|
|
10
12
|
getBrokerDeliveredMessagesPath,
|
|
11
13
|
getBrokerPidPath,
|
|
12
14
|
getBrokerSocketPath,
|
|
15
|
+
getBrokerSocketReplacementLockPath,
|
|
13
16
|
getIntercomDirPath,
|
|
14
17
|
} from "./paths.js";
|
|
15
18
|
import type {
|
|
@@ -60,8 +63,17 @@ import {
|
|
|
60
63
|
const INTERCOM_DIR = getIntercomDirPath();
|
|
61
64
|
const SOCKET_PATH = getBrokerSocketPath();
|
|
62
65
|
const PID_PATH = getBrokerPidPath();
|
|
66
|
+
const SOCKET_REPLACEMENT_LOCK_PATH = getBrokerSocketReplacementLockPath();
|
|
63
67
|
|
|
64
68
|
const PENDING_STAGE_MESSAGE_TIMEOUT_MS = 10_000;
|
|
69
|
+
const LISTEN_ATTEMPTS = 5;
|
|
70
|
+
const LISTEN_RETRY_DELAY_MS = 100;
|
|
71
|
+
const SOCKET_OWNERSHIP_CHECK_MS = 1_000;
|
|
72
|
+
|
|
73
|
+
function exitBecauseLiveBrokerOwnsSocket(): never {
|
|
74
|
+
console.error(`Intercom broker not started: a live broker already answers on ${SOCKET_PATH}`);
|
|
75
|
+
process.exit(0);
|
|
76
|
+
}
|
|
65
77
|
|
|
66
78
|
interface PendingStageRouteRegistration {
|
|
67
79
|
readonly sessionId: string;
|
|
@@ -249,6 +261,7 @@ class IntercomBroker {
|
|
|
249
261
|
private sessions = new Map<string, ConnectedSession>();
|
|
250
262
|
private server: net.Server;
|
|
251
263
|
private shutdownTimer: NodeJS.Timeout | null = null;
|
|
264
|
+
private boundSocketInode: number | undefined;
|
|
252
265
|
private lastConnectionAt = performance.now();
|
|
253
266
|
private deliveredMessages = new DeliveredMessageCache(
|
|
254
267
|
DELIVERED_MESSAGE_TTL_MS,
|
|
@@ -267,25 +280,82 @@ class IntercomBroker {
|
|
|
267
280
|
constructor() {
|
|
268
281
|
mkdirSync(INTERCOM_DIR, { recursive: true, mode: 0o700 });
|
|
269
282
|
if (process.platform !== "win32") chmodSync(INTERCOM_DIR, 0o700);
|
|
270
|
-
if (process.platform !== "win32") {
|
|
271
|
-
try {
|
|
272
|
-
unlinkSync(SOCKET_PATH);
|
|
273
|
-
} catch {
|
|
274
|
-
// A clean startup has no stale socket to remove.
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
283
|
this.server = net.createServer(this.handleConnection.bind(this));
|
|
278
284
|
}
|
|
279
285
|
|
|
280
286
|
start(): void {
|
|
287
|
+
this.listen(1);
|
|
288
|
+
process.on("SIGTERM", () => this.shutdown());
|
|
289
|
+
process.on("SIGINT", () => this.shutdown());
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private listen(attempt: number, releaseReplacementLock: () => void = () => {}): void {
|
|
293
|
+
const onError = (error: NodeJS.ErrnoException) => {
|
|
294
|
+
releaseReplacementLock();
|
|
295
|
+
void this.recoverFromListenError(error, attempt);
|
|
296
|
+
};
|
|
297
|
+
this.server.once("error", onError);
|
|
281
298
|
this.server.listen(SOCKET_PATH, () => {
|
|
299
|
+
this.server.off("error", onError);
|
|
300
|
+
if (process.platform !== "win32") this.boundSocketInode = statSync(SOCKET_PATH).ino;
|
|
301
|
+
releaseReplacementLock();
|
|
282
302
|
writeFileSync(PID_PATH, String(process.pid));
|
|
283
303
|
console.log(`Intercom broker started (pid: ${process.pid})`);
|
|
284
304
|
this.lastConnectionAt = performance.now();
|
|
285
305
|
this.scheduleShutdownCheck();
|
|
306
|
+
if (this.boundSocketInode !== undefined) this.watchSocketOwnership();
|
|
286
307
|
});
|
|
287
|
-
|
|
288
|
-
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private ownsSocketPath(): boolean {
|
|
311
|
+
if (this.boundSocketInode === undefined) return process.platform === "win32";
|
|
312
|
+
try {
|
|
313
|
+
return statSync(SOCKET_PATH).ino === this.boundSocketInode;
|
|
314
|
+
} catch {
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* A broker whose socket path now leads to another broker can no longer be reached by new clients, so its sessions
|
|
321
|
+
* would be stranded. It shuts down instead, and its clients reconnect through broker.sock to the broker that owns it.
|
|
322
|
+
*/
|
|
323
|
+
private watchSocketOwnership(): void {
|
|
324
|
+
const timer = setInterval(() => {
|
|
325
|
+
if (this.ownsSocketPath()) return;
|
|
326
|
+
clearInterval(timer);
|
|
327
|
+
console.error(`Intercom broker lost ${SOCKET_PATH} to another broker; shutting down so its sessions reconnect`);
|
|
328
|
+
this.shutdown();
|
|
329
|
+
}, SOCKET_OWNERSHIP_CHECK_MS);
|
|
330
|
+
timer.unref();
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Take over only a stale socket: a path a live broker answers on is never unlinked. */
|
|
334
|
+
private async recoverFromListenError(error: NodeJS.ErrnoException, attempt: number): Promise<void> {
|
|
335
|
+
if (error.code === "EADDRINUSE" && (await isSocketAnswering(SOCKET_PATH))) exitBecauseLiveBrokerOwnsSocket();
|
|
336
|
+
if (error.code !== "EADDRINUSE" || attempt >= LISTEN_ATTEMPTS) {
|
|
337
|
+
console.error(`Intercom broker could not listen on ${SOCKET_PATH}: ${error.message}`);
|
|
338
|
+
process.exit(1);
|
|
339
|
+
}
|
|
340
|
+
if (process.platform === "win32") {
|
|
341
|
+
await new Promise<void>((resolve) => setTimeout(resolve, LISTEN_RETRY_DELAY_MS));
|
|
342
|
+
this.listen(attempt + 1);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
const releaseReplacementLock = await acquireSocketReplacementLock(SOCKET_REPLACEMENT_LOCK_PATH, {
|
|
346
|
+
onWait: () => console.error(`Intercom broker waiting for another broker replacing ${SOCKET_PATH}`),
|
|
347
|
+
});
|
|
348
|
+
// Another broker may have replaced the stale socket while this one waited for the lock.
|
|
349
|
+
if (await isSocketAnswering(SOCKET_PATH)) {
|
|
350
|
+
releaseReplacementLock();
|
|
351
|
+
exitBecauseLiveBrokerOwnsSocket();
|
|
352
|
+
}
|
|
353
|
+
try {
|
|
354
|
+
unlinkSync(SOCKET_PATH);
|
|
355
|
+
} catch {
|
|
356
|
+
// A clean path has no stale socket to remove.
|
|
357
|
+
}
|
|
358
|
+
this.listen(attempt + 1, releaseReplacementLock);
|
|
289
359
|
}
|
|
290
360
|
|
|
291
361
|
private handleConnection(socket: net.Socket): void {
|
|
@@ -1807,9 +1877,10 @@ class IntercomBroker {
|
|
|
1807
1877
|
const ownsRuntime = this.readPidFile() === process.pid;
|
|
1808
1878
|
this.unlinkRuntimeFilesIfOwned();
|
|
1809
1879
|
// Node unlinks a Unix socket path on server.close() even after a successor
|
|
1810
|
-
// rebound that path. Skip close when we no longer own the pid
|
|
1811
|
-
// still closes our fd. Windows named pipes are not
|
|
1812
|
-
|
|
1880
|
+
// rebound that path. Skip close when we no longer own the pid or the socket
|
|
1881
|
+
// path; process.exit still closes our fd. Windows named pipes are not
|
|
1882
|
+
// path-unlinked this way.
|
|
1883
|
+
if (process.platform === "win32" || (ownsRuntime && this.ownsSocketPath())) {
|
|
1813
1884
|
this.server.close();
|
|
1814
1885
|
}
|
|
1815
1886
|
process.exit(0);
|
|
@@ -1817,4 +1888,9 @@ class IntercomBroker {
|
|
|
1817
1888
|
}
|
|
1818
1889
|
|
|
1819
1890
|
// The stderr cap is already in place: `./bounded-stderr-install.js` is this file's first import.
|
|
1820
|
-
|
|
1891
|
+
async function startUnlessLiveBrokerAnswers(): Promise<void> {
|
|
1892
|
+
if (await isSocketAnswering(SOCKET_PATH)) exitBecauseLiveBrokerOwnsSocket();
|
|
1893
|
+
new IntercomBroker().start();
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
void startUnlessLiveBrokerAnswers();
|
|
@@ -46,6 +46,10 @@ export function getBrokerSpawnLockPath(agentDir: string = getAgentDir()): string
|
|
|
46
46
|
return join(getIntercomDirPath(agentDir), "broker.spawn.lock");
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
export function getBrokerSocketReplacementLockPath(agentDir: string = getAgentDir()): string {
|
|
50
|
+
return join(getIntercomDirPath(agentDir), "broker.sock.replace.lock");
|
|
51
|
+
}
|
|
52
|
+
|
|
49
53
|
/** Durable accepted-operation authority shared by replacement broker processes. */
|
|
50
54
|
export function getBrokerDeliveredMessagesPath(agentDir: string = getAgentDir()): string {
|
|
51
55
|
return join(getIntercomDirPath(agentDir), "delivered-messages.sqlite");
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import net from "net";
|
|
2
|
+
|
|
3
|
+
export const SOCKET_PROBE_TIMEOUT_MS = 1000;
|
|
4
|
+
|
|
5
|
+
/** True when something accepts a connection on the Unix socket or Windows named pipe at `socketPath`. */
|
|
6
|
+
export function isSocketAnswering(socketPath: string, timeoutMs: number = SOCKET_PROBE_TIMEOUT_MS): Promise<boolean> {
|
|
7
|
+
return new Promise((resolve) => {
|
|
8
|
+
const socket = net.connect(socketPath);
|
|
9
|
+
const finish = (isConnected: boolean) => {
|
|
10
|
+
clearTimeout(timeout);
|
|
11
|
+
socket.off("connect", onConnect);
|
|
12
|
+
socket.off("error", onError);
|
|
13
|
+
resolve(isConnected);
|
|
14
|
+
};
|
|
15
|
+
const onConnect = () => {
|
|
16
|
+
// end() starts a half-close; a reset can still arrive before close. Keep a handler until then.
|
|
17
|
+
socket.on("error", () => {});
|
|
18
|
+
socket.end();
|
|
19
|
+
finish(true);
|
|
20
|
+
};
|
|
21
|
+
const onError = () => {
|
|
22
|
+
socket.destroy();
|
|
23
|
+
finish(false);
|
|
24
|
+
};
|
|
25
|
+
socket.on("connect", onConnect);
|
|
26
|
+
socket.on("error", onError);
|
|
27
|
+
const timeout = setTimeout(() => {
|
|
28
|
+
socket.destroy();
|
|
29
|
+
finish(false);
|
|
30
|
+
}, timeoutMs);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { readFileSync, statSync, unlinkSync, writeFileSync } from "fs";
|
|
2
|
+
|
|
3
|
+
const SOCKET_REPLACEMENT_LOCK_POLL_MS = 20;
|
|
4
|
+
/** Locks and tokens get their pid as they are created, so one still unreadable after this was left by a crash. */
|
|
5
|
+
const UNREADABLE_FILE_GRACE_MS = 1_000;
|
|
6
|
+
|
|
7
|
+
export interface SocketReplacementLockOptions {
|
|
8
|
+
readonly onWait?: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function isProcessAlive(pid: number): boolean {
|
|
12
|
+
try {
|
|
13
|
+
process.kill(pid, 0);
|
|
14
|
+
return true;
|
|
15
|
+
} catch (error) {
|
|
16
|
+
return (error as NodeJS.ErrnoException).code === "EPERM";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function holderPid(path: string): number | undefined {
|
|
21
|
+
try {
|
|
22
|
+
const pid = Number.parseInt(readFileSync(path, "utf-8").trim(), 10);
|
|
23
|
+
return Number.isFinite(pid) ? pid : undefined;
|
|
24
|
+
} catch {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type Holder = { readonly kind: "live" } | { readonly kind: "abandoned"; readonly pid: number | undefined };
|
|
30
|
+
|
|
31
|
+
/** A file whose pid is dead, or that stayed unreadable past the grace period, was left by a crashed broker. */
|
|
32
|
+
function holderOf(path: string): Holder | undefined {
|
|
33
|
+
const pid = holderPid(path);
|
|
34
|
+
if (pid !== undefined) return isProcessAlive(pid) ? { kind: "live" } : { kind: "abandoned", pid };
|
|
35
|
+
try {
|
|
36
|
+
return Date.now() - statSync(path).mtimeMs >= UNREADABLE_FILE_GRACE_MS
|
|
37
|
+
? { kind: "abandoned", pid: undefined }
|
|
38
|
+
: { kind: "live" };
|
|
39
|
+
} catch {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function removeFile(path: string): void {
|
|
45
|
+
try {
|
|
46
|
+
unlinkSync(path);
|
|
47
|
+
} catch {
|
|
48
|
+
// Already removed.
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function createExclusively(path: string): boolean {
|
|
53
|
+
try {
|
|
54
|
+
writeFileSync(path, `${process.pid}\n`, { flag: "wx" });
|
|
55
|
+
return true;
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Removes an abandoned lock without ever touching a live one. Only the waiter that creates the takeover token for
|
|
64
|
+
* that holder may remove its lock, and while that lock exists no newer lock can be created, so the lock it removes is
|
|
65
|
+
* still the abandoned one. An abandoned token is removed so a later waiter can take over.
|
|
66
|
+
*/
|
|
67
|
+
function takeOverAbandonedLock(lockPath: string, abandonedPid: number | undefined): void {
|
|
68
|
+
const token = `${lockPath}.takeover-${abandonedPid ?? "unreadable"}`;
|
|
69
|
+
if (!createExclusively(token)) {
|
|
70
|
+
if (holderOf(token)?.kind === "abandoned") removeFile(token);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
if (holderPid(lockPath) === abandonedPid) removeFile(lockPath);
|
|
75
|
+
} finally {
|
|
76
|
+
removeFile(token);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Serializes stale-socket replacement across brokers, so a broker that probed the socket as stale cannot unlink a
|
|
82
|
+
* socket another broker bound in the meantime: the holder re-probes before it unlinks. A live holder's lock is never
|
|
83
|
+
* taken; an abandoned one is. Resolves with the release function, which removes the lock only while this process
|
|
84
|
+
* holds it.
|
|
85
|
+
*/
|
|
86
|
+
export async function acquireSocketReplacementLock(
|
|
87
|
+
lockPath: string,
|
|
88
|
+
{ onWait }: SocketReplacementLockOptions = {},
|
|
89
|
+
): Promise<() => void> {
|
|
90
|
+
let announcedWait = false;
|
|
91
|
+
while (!createExclusively(lockPath)) {
|
|
92
|
+
const holder = holderOf(lockPath);
|
|
93
|
+
if (holder?.kind === "abandoned") {
|
|
94
|
+
takeOverAbandonedLock(lockPath, holder.pid);
|
|
95
|
+
} else if (holder?.kind === "live" && !announcedWait) {
|
|
96
|
+
announcedWait = true;
|
|
97
|
+
onWait?.();
|
|
98
|
+
}
|
|
99
|
+
await new Promise<void>((resolve) => setTimeout(resolve, SOCKET_REPLACEMENT_LOCK_POLL_MS));
|
|
100
|
+
}
|
|
101
|
+
return () => {
|
|
102
|
+
if (holderPid(lockPath) === process.pid) removeFile(lockPath);
|
|
103
|
+
};
|
|
104
|
+
}
|