@oh-my-pi/pi-coding-agent 17.2.4 → 17.2.6
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 +69 -0
- package/dist/{CHANGELOG-bpmhv26t.md → CHANGELOG-gs76k6wc.md} +69 -0
- package/dist/cli.js +4438 -3962
- package/dist/{template-8vdv6xb4.js → template-c2hyaytt.js} +8 -26
- package/dist/types/cli/args.d.ts +4 -1
- package/dist/types/cli/browser-relay-cli.d.ts +15 -0
- package/dist/types/cli/command-help.d.ts +99 -0
- package/dist/types/cli/gc-cli.d.ts +1 -0
- package/dist/types/cli/help-extra.d.ts +2 -0
- package/dist/types/cli/thinking-levels.d.ts +5 -0
- package/dist/types/cli/update-cli.d.ts +13 -0
- package/dist/types/commands/browser-relay.d.ts +38 -0
- package/dist/types/commands/commit.d.ts +3 -0
- package/dist/types/commands/complete.d.ts +1 -1
- package/dist/types/commands/completions.d.ts +6 -0
- package/dist/types/commands/grep.d.ts +3 -0
- package/dist/types/commands/join.d.ts +4 -0
- package/dist/types/commands/launch-help.d.ts +171 -0
- package/dist/types/commands/launch.d.ts +5 -1
- package/dist/types/commands/models.d.ts +3 -0
- package/dist/types/commands/say.d.ts +10 -0
- package/dist/types/commands/worktree.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +5 -0
- package/dist/types/config/model-discovery.d.ts +2 -2
- package/dist/types/config/models-config-schema-bundle.d.ts +3 -0
- package/dist/types/config/models-config-schema.d.ts +2 -0
- package/dist/types/config/models-config.d.ts +1 -0
- package/dist/types/config/service-tier.d.ts +2 -0
- package/dist/types/config/settings-schema.d.ts +69 -21
- package/dist/types/cursor-bridge-tools.d.ts +1 -1
- package/dist/types/cursor.d.ts +7 -4
- package/dist/types/edit/index.d.ts +3 -3
- package/dist/types/edit/modes/replace.d.ts +22 -15
- package/dist/types/edit/renderer.d.ts +1 -1
- package/dist/types/eval/py/kernel.d.ts +3 -1
- package/dist/types/eval/py/spawn-options.d.ts +12 -16
- package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +8 -1
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -1
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +2 -0
- package/dist/types/launch/broker.d.ts +1 -1
- package/dist/types/launch/client.d.ts +15 -4
- package/dist/types/launch/ensure.d.ts +8 -0
- package/dist/types/launch/paths.d.ts +1 -1
- package/dist/types/launch/protocol.d.ts +19 -2
- package/dist/types/lsp/client.d.ts +2 -0
- package/dist/types/lsp/mux/daemon.d.ts +15 -0
- package/dist/types/lsp/mux/protocol.d.ts +57 -0
- package/dist/types/lsp/mux/server.d.ts +18 -0
- package/dist/types/lsp/types.d.ts +22 -2
- package/dist/types/main.d.ts +2 -0
- package/dist/types/mnemopi/embed-client.d.ts +1 -1
- package/dist/types/mnemopi/state.d.ts +0 -23
- package/dist/types/modes/components/assistant-message.d.ts +2 -0
- package/dist/types/modes/components/btw-panel.d.ts +3 -0
- package/dist/types/modes/components/custom-editor.d.ts +2 -1
- package/dist/types/modes/components/read-tool-group.d.ts +2 -0
- package/dist/types/modes/components/stripped-tool-calls-placeholder.d.ts +12 -0
- package/dist/types/modes/components/tool-execution.d.ts +2 -0
- package/dist/types/modes/controllers/btw-controller.d.ts +2 -0
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/controllers/input-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +5 -1
- package/dist/types/modes/types.d.ts +4 -1
- package/dist/types/modes/workflow.d.ts +2 -1
- package/dist/types/sdk.d.ts +3 -1
- package/dist/types/secrets/index.d.ts +1 -1
- package/dist/types/security/contracts/schemas.d.ts +405 -403
- package/dist/types/session/agent-session-types.d.ts +8 -1
- package/dist/types/session/agent-session.d.ts +26 -2
- package/dist/types/session/indexed-session-storage.d.ts +1 -0
- package/dist/types/session/launch-completion.d.ts +10 -0
- package/dist/types/session/session-advisors.d.ts +1 -1
- package/dist/types/session/session-context.d.ts +2 -1
- package/dist/types/session/session-entries.d.ts +15 -1
- package/dist/types/session/session-maintenance.d.ts +1 -0
- package/dist/types/session/session-manager.d.ts +23 -11
- package/dist/types/session/session-stats.d.ts +10 -0
- package/dist/types/session/session-storage.d.ts +15 -3
- package/dist/types/session/yield-queue.d.ts +6 -1
- package/dist/types/system-prompt.d.ts +4 -1
- package/dist/types/task/index.d.ts +2 -1
- package/dist/types/task/spawn-policy.d.ts +5 -0
- package/dist/types/thinking.d.ts +1 -7
- package/dist/types/tools/ast-grep.d.ts +1 -1
- package/dist/types/tools/browser/registry.d.ts +2 -1
- package/dist/types/tools/browser/relay/bridge.d.ts +35 -0
- package/dist/types/tools/browser/relay/daemon.d.ts +16 -0
- package/dist/types/tools/browser/relay/kind.d.ts +19 -0
- package/dist/types/tools/browser/relay/protocol.d.ts +97 -0
- package/dist/types/tools/browser/relay/server.d.ts +35 -0
- package/dist/types/tools/browser/render.d.ts +1 -0
- package/dist/types/tools/browser.d.ts +3 -0
- package/dist/types/tools/computer/exposure.d.ts +9 -7
- package/dist/types/tools/computer/protocol.d.ts +70 -17
- package/dist/types/tools/computer/supervisor.d.ts +23 -10
- package/dist/types/tools/computer/worker-entry.d.ts +1 -0
- package/dist/types/tools/computer/worker.d.ts +42 -7
- package/dist/types/tools/computer-renderer.d.ts +7 -5
- package/dist/types/tools/computer.d.ts +25 -44
- package/dist/types/tools/context.d.ts +3 -0
- package/dist/types/tools/glob.d.ts +1 -1
- package/dist/types/tools/grep.d.ts +1 -1
- package/dist/types/tools/index.d.ts +9 -0
- package/dist/types/tools/render-utils.d.ts +2 -0
- package/dist/types/tools/{browser/run-cancellation.d.ts → run-scope.d.ts} +8 -8
- package/dist/types/tools/tool-timeouts.d.ts +5 -0
- package/dist/types/tools/xdev.d.ts +8 -0
- package/dist/types/utils/clipboard.d.ts +2 -2
- package/dist/types/web/kagi.d.ts +1 -0
- package/dist/types/web/search/index.d.ts +1 -1
- package/dist/types/web/search/providers/anthropic.d.ts +1 -0
- package/dist/types/web/search/providers/base.d.ts +2 -0
- package/dist/types/web/search/providers/brave.d.ts +1 -0
- package/dist/types/web/search/providers/browser-page.d.ts +1 -0
- package/dist/types/web/search/providers/codex.d.ts +1 -0
- package/dist/types/web/search/providers/exa.d.ts +1 -0
- package/dist/types/web/search/providers/firecrawl.d.ts +1 -0
- package/dist/types/web/search/providers/gemini.d.ts +1 -0
- package/dist/types/web/search/providers/jina.d.ts +1 -0
- package/dist/types/web/search/providers/kagi.d.ts +1 -0
- package/dist/types/web/search/providers/kimi.d.ts +1 -0
- package/dist/types/web/search/providers/parallel.d.ts +1 -0
- package/dist/types/web/search/providers/perplexity.d.ts +1 -0
- package/dist/types/web/search/providers/searxng.d.ts +1 -0
- package/dist/types/web/search/providers/tavily.d.ts +1 -0
- package/dist/types/web/search/providers/tinyfish.d.ts +1 -0
- package/dist/types/web/search/providers/utils.d.ts +6 -9
- package/dist/types/web/search/providers/zai.d.ts +1 -0
- package/dist/types/web/search/types.d.ts +4 -0
- package/package.json +12 -12
- package/src/cli/args.ts +7 -87
- package/src/cli/browser-relay-cli.ts +119 -0
- package/src/cli/command-help.ts +103 -0
- package/src/cli/completion-gen.ts +5 -5
- package/src/cli/flag-tables.ts +9 -0
- package/src/cli/gc-cli.ts +641 -21
- package/src/cli/help-extra.ts +89 -0
- package/src/cli/setup-cli.ts +13 -21
- package/src/cli/thinking-levels.ts +7 -0
- package/src/cli/update-cli.ts +26 -7
- package/src/cli-commands.ts +170 -33
- package/src/cli.ts +17 -5
- package/src/commands/acp.ts +3 -1
- package/src/commands/agents.ts +3 -2
- package/src/commands/auth-broker.ts +3 -2
- package/src/commands/auth-gateway.ts +3 -2
- package/src/commands/bench.ts +2 -3
- package/src/commands/browser-relay.ts +53 -0
- package/src/commands/cleanse.ts +2 -2
- package/src/commands/commit.ts +3 -2
- package/src/commands/complete.ts +2 -1
- package/src/commands/completions.ts +3 -2
- package/src/commands/config.ts +3 -2
- package/src/commands/dry-balance.ts +2 -2
- package/src/commands/gallery.ts +3 -2
- package/src/commands/gc.ts +3 -2
- package/src/commands/grep.ts +3 -2
- package/src/commands/grievances.ts +3 -2
- package/src/commands/install.ts +2 -2
- package/src/commands/join.ts +3 -2
- package/src/commands/launch-help.ts +116 -0
- package/src/commands/launch.ts +7 -191
- package/src/commands/models.ts +3 -2
- package/src/commands/plugin.ts +3 -2
- package/src/commands/read.ts +3 -2
- package/src/commands/say.ts +3 -2
- package/src/commands/setup.ts +3 -2
- package/src/commands/shell.ts +3 -2
- package/src/commands/ssh.ts +3 -2
- package/src/commands/stats.ts +3 -2
- package/src/commands/tiny-models.ts +2 -2
- package/src/commands/token.ts +2 -2
- package/src/commands/ttsr.ts +2 -2
- package/src/commands/update.ts +3 -2
- package/src/commands/usage.ts +3 -2
- package/src/commands/web-search.ts +3 -2
- package/src/commands/worktree.ts +3 -2
- package/src/config/keybindings.ts +5 -0
- package/src/config/model-discovery.ts +44 -27
- package/src/config/model-registry.ts +19 -13
- package/src/config/models-config-schema-bundle.ts +9 -0
- package/src/config/resolve-config-value.ts +2 -1
- package/src/config/service-tier.ts +5 -0
- package/src/config/settings-schema.ts +72 -20
- package/src/config/settings.ts +1 -1
- package/src/cursor-bridge-tools.ts +1 -1
- package/src/cursor.ts +10 -14
- package/src/edit/diff.ts +17 -8
- package/src/edit/index.ts +20 -7
- package/src/edit/modes/replace.ts +93 -40
- package/src/edit/renderer.ts +1 -1
- package/src/edit/streaming.ts +14 -17
- package/src/eval/py/kernel.ts +2 -1
- package/src/eval/py/spawn-options.ts +24 -29
- package/src/export/html/index.ts +10 -3
- package/src/export/html/template.js +8 -26
- package/src/export/share.ts +4 -0
- package/src/extensibility/extensions/wrapper.ts +1 -0
- package/src/extensibility/legacy-pi-ai-shim.ts +22 -1
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +7 -6
- package/src/extensibility/plugins/legacy-pi-compat.ts +124 -57
- package/src/extensibility/skills.ts +11 -0
- package/src/extensibility/utils.ts +10 -2
- package/src/launch/broker.ts +223 -7
- package/src/launch/client.ts +192 -21
- package/src/launch/ensure.ts +73 -0
- package/src/launch/paths.ts +1 -1
- package/src/launch/protocol.ts +54 -2
- package/src/lsp/client.ts +23 -2
- package/src/lsp/index.ts +8 -0
- package/src/lsp/mux/daemon.ts +348 -0
- package/src/lsp/mux/protocol.ts +96 -0
- package/src/lsp/mux/server.ts +797 -0
- package/src/lsp/types.ts +28 -2
- package/src/main.ts +20 -8
- package/src/mcp/config-writer.ts +1 -1
- package/src/mcp/oauth-flow.ts +11 -1
- package/src/mnemopi/embed-client.ts +49 -2
- package/src/mnemopi/state.ts +57 -23
- package/src/modes/components/assistant-message.ts +11 -0
- package/src/modes/components/btw-panel.ts +41 -4
- package/src/modes/components/chat-transcript-builder.ts +4 -0
- package/src/modes/components/custom-editor.ts +9 -0
- package/src/modes/components/read-tool-group.ts +10 -0
- package/src/modes/components/status-line/component.ts +1 -1
- package/src/modes/components/stripped-tool-calls-placeholder.ts +35 -0
- package/src/modes/components/tool-execution.ts +8 -0
- package/src/modes/components/tree-selector.ts +46 -48
- package/src/modes/controllers/btw-controller.ts +55 -7
- package/src/modes/controllers/command-controller.ts +31 -0
- package/src/modes/controllers/event-controller.ts +17 -0
- package/src/modes/controllers/input-controller.ts +63 -8
- package/src/modes/controllers/mcp-command-controller.ts +11 -5
- package/src/modes/controllers/selector-controller.ts +20 -0
- package/src/modes/interactive-mode.ts +18 -2
- package/src/modes/types.ts +9 -1
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/modes/utils/interactive-context-helpers.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +6 -10
- package/src/modes/workflow.ts +8 -2
- package/src/prompts/agents/init.md +1 -1
- package/src/prompts/session/launch-completion.md +1 -0
- package/src/prompts/system/plan-mode-active.md +2 -2
- package/src/prompts/system/system-prompt.md +26 -52
- package/src/prompts/system/workflow-notice.md +3 -3
- package/src/prompts/tools/ast-grep.md +1 -1
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/browser.md +1 -0
- package/src/prompts/tools/computer.md +21 -21
- package/src/prompts/tools/glob.md +1 -1
- package/src/prompts/tools/grep.md +1 -1
- package/src/prompts/tools/replace.md +5 -4
- package/src/prompts/tools/task.md +4 -4
- package/src/sdk.ts +49 -6
- package/src/secrets/index.ts +1 -1
- package/src/security/contracts/schemas.ts +205 -183
- package/src/security/contracts/validation.ts +5 -1
- package/src/security/store.ts +2 -2
- package/src/session/agent-session-types.ts +9 -1
- package/src/session/agent-session.ts +204 -15
- package/src/session/indexed-session-storage.ts +9 -0
- package/src/session/launch-completion.ts +37 -0
- package/src/session/prewalk.ts +24 -2
- package/src/session/session-advisors.ts +2 -2
- package/src/session/session-context.ts +42 -12
- package/src/session/session-dump-format.ts +1 -1
- package/src/session/session-entries.ts +17 -1
- package/src/session/session-maintenance.ts +38 -4
- package/src/session/session-manager.ts +123 -67
- package/src/session/session-paths.ts +118 -55
- package/src/session/session-stats.ts +48 -3
- package/src/session/session-storage.ts +30 -22
- package/src/session/stream-guards.ts +1 -1
- package/src/session/yield-queue.ts +121 -16
- package/src/slash-commands/builtin-registry.ts +37 -24
- package/src/system-prompt.ts +8 -3
- package/src/task/index.ts +23 -8
- package/src/task/spawn-policy.ts +14 -0
- package/src/thinking.ts +2 -8
- package/src/tools/ast-grep.ts +10 -4
- package/src/tools/browser/cmux/cmux-tab.ts +97 -14
- package/src/tools/browser/cmux/rpc.ts +2 -8
- package/src/tools/browser/registry.ts +53 -2
- package/src/tools/browser/relay/bridge.ts +945 -0
- package/src/tools/browser/relay/daemon.ts +117 -0
- package/src/tools/browser/relay/extension-assets/background.js.txt +242 -0
- package/src/tools/browser/relay/extension-assets/manifest.json.txt +14 -0
- package/src/tools/browser/relay/extension-assets/options.html.txt +53 -0
- package/src/tools/browser/relay/extension-assets/options.js.txt +23 -0
- package/src/tools/browser/relay/kind.ts +41 -0
- package/src/tools/browser/relay/protocol.ts +54 -0
- package/src/tools/browser/relay/server.ts +141 -0
- package/src/tools/browser/render.ts +4 -1
- package/src/tools/browser/shared-daemon.ts +7 -58
- package/src/tools/browser/tab-supervisor.ts +5 -4
- package/src/tools/browser/tab-worker.ts +33 -12
- package/src/tools/browser.ts +26 -1
- package/src/tools/computer/exposure.ts +11 -35
- package/src/tools/computer/protocol.ts +51 -10
- package/src/tools/computer/supervisor.ts +227 -124
- package/src/tools/computer/worker-entry.ts +11 -6
- package/src/tools/computer/worker.ts +695 -81
- package/src/tools/computer-renderer.ts +104 -65
- package/src/tools/computer.ts +134 -428
- package/src/tools/context.ts +3 -0
- package/src/tools/glob.ts +9 -2
- package/src/tools/grep.ts +12 -6
- package/src/tools/hub/launch.ts +122 -6
- package/src/tools/index.ts +9 -0
- package/src/tools/inspect-image.ts +32 -6
- package/src/tools/render-utils.ts +2 -0
- package/src/tools/{browser/run-cancellation.ts → run-scope.ts} +11 -11
- package/src/tools/tool-timeouts.ts +1 -0
- package/src/tools/xdev.ts +29 -2
- package/src/utils/clipboard.ts +40 -14
- package/src/utils/command-args.ts +26 -28
- package/src/utils/tool-choice.ts +0 -13
- package/src/web/kagi.ts +2 -1
- package/src/web/search/index.ts +18 -2
- package/src/web/search/providers/anthropic.ts +4 -1
- package/src/web/search/providers/base.ts +2 -0
- package/src/web/search/providers/brave.ts +3 -1
- package/src/web/search/providers/browser-page.ts +6 -4
- package/src/web/search/providers/codex.ts +4 -1
- package/src/web/search/providers/duckduckgo.ts +2 -1
- package/src/web/search/providers/ecosia.ts +2 -1
- package/src/web/search/providers/exa.ts +4 -2
- package/src/web/search/providers/firecrawl.ts +3 -1
- package/src/web/search/providers/gemini.ts +8 -2
- package/src/web/search/providers/google.ts +2 -1
- package/src/web/search/providers/jina.ts +12 -2
- package/src/web/search/providers/kagi.ts +3 -0
- package/src/web/search/providers/kimi.ts +5 -1
- package/src/web/search/providers/mojeek.ts +2 -1
- package/src/web/search/providers/parallel.ts +5 -1
- package/src/web/search/providers/perplexity.ts +6 -3
- package/src/web/search/providers/public.ts +1 -1
- package/src/web/search/providers/searxng.ts +11 -5
- package/src/web/search/providers/startpage.ts +10 -4
- package/src/web/search/providers/synthetic.ts +11 -5
- package/src/web/search/providers/tavily.ts +3 -1
- package/src/web/search/providers/tinyfish.ts +3 -1
- package/src/web/search/providers/utils.ts +12 -10
- package/src/web/search/providers/xai.ts +2 -2
- package/src/web/search/providers/zai.ts +18 -3
- package/src/web/search/types.ts +6 -0
- package/dist/types/config/file-lock.d.ts +0 -29
- package/src/config/file-lock.ts +0 -164
package/src/launch/broker.ts
CHANGED
|
@@ -15,14 +15,17 @@ import {
|
|
|
15
15
|
DAEMON_PTY_COLUMNS,
|
|
16
16
|
DAEMON_PTY_ROWS,
|
|
17
17
|
DAEMON_RUNTIME_DIR_ENV,
|
|
18
|
+
type DaemonCompletionNotification,
|
|
18
19
|
type DaemonOperation,
|
|
19
20
|
type DaemonReadySpec,
|
|
20
21
|
type DaemonRpcResult,
|
|
21
22
|
type DaemonSignal,
|
|
22
23
|
type DaemonSnapshot,
|
|
23
24
|
type DaemonSpec,
|
|
25
|
+
type DaemonWireRequest,
|
|
24
26
|
parseDaemonSnapshot,
|
|
25
27
|
parseDaemonSpec,
|
|
28
|
+
parseDaemonWireMessage,
|
|
26
29
|
parseDaemonWireRequest,
|
|
27
30
|
} from "./protocol";
|
|
28
31
|
import { resolveDaemonSpawnOptions } from "./spawn-options";
|
|
@@ -81,6 +84,9 @@ interface ManagedDaemon {
|
|
|
81
84
|
readyPattern?: RegExp;
|
|
82
85
|
restartTimer?: NodeJS.Timeout;
|
|
83
86
|
consecutiveFailures: number;
|
|
87
|
+
completionCapable: boolean;
|
|
88
|
+
pendingCompletions: DaemonCompletionNotification[];
|
|
89
|
+
completionSubscriptionId?: string;
|
|
84
90
|
persistQueue: Promise<void>;
|
|
85
91
|
}
|
|
86
92
|
|
|
@@ -107,6 +113,10 @@ function settledState(state: DaemonSnapshot["state"]): boolean {
|
|
|
107
113
|
return terminalState(state) || state === "restarting";
|
|
108
114
|
}
|
|
109
115
|
|
|
116
|
+
function publishesCompletionOwners(request: DaemonWireRequest): boolean {
|
|
117
|
+
return request.completionEvents === true && (request.completionAcks?.length ?? 0) === 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
110
120
|
/**
|
|
111
121
|
* Order daemons for the `list` response: non-terminal (active) daemons first,
|
|
112
122
|
* oldest to newest, so the process the user is acting on is immediately visible
|
|
@@ -352,6 +362,9 @@ class DaemonBroker {
|
|
|
352
362
|
*/
|
|
353
363
|
readonly #startingNames = new Set<string>();
|
|
354
364
|
readonly #clients = new Set<net.Socket>();
|
|
365
|
+
readonly #ownerSockets = new Map<string, { socket: net.Socket; subscriptionId: string | undefined }>();
|
|
366
|
+
readonly #completionSubscriptions = new Map<string, string | undefined>();
|
|
367
|
+
readonly #pendingCompletions = new Map<string, Map<string, DaemonCompletionNotification>>();
|
|
355
368
|
readonly #finished = Promise.withResolvers<void>();
|
|
356
369
|
readonly #sockets = new Set<net.Socket>();
|
|
357
370
|
#server: net.Server | undefined;
|
|
@@ -393,6 +406,7 @@ class DaemonBroker {
|
|
|
393
406
|
await record.log?.close();
|
|
394
407
|
await record.persistQueue;
|
|
395
408
|
}
|
|
409
|
+
this.#ownerSockets.clear();
|
|
396
410
|
for (const socket of this.#sockets) socket.destroy();
|
|
397
411
|
this.#sockets.clear();
|
|
398
412
|
this.#clients.clear();
|
|
@@ -439,6 +453,9 @@ class DaemonBroker {
|
|
|
439
453
|
if (!authenticated) return;
|
|
440
454
|
this.#clients.delete(socket);
|
|
441
455
|
this.#scheduleIdleShutdown();
|
|
456
|
+
for (const [owner, registration] of this.#ownerSockets) {
|
|
457
|
+
if (registration.socket === socket) this.#ownerSockets.delete(owner);
|
|
458
|
+
}
|
|
442
459
|
});
|
|
443
460
|
}
|
|
444
461
|
|
|
@@ -450,6 +467,78 @@ class DaemonBroker {
|
|
|
450
467
|
id = request.id;
|
|
451
468
|
if (request.token !== this.#token) throw new Error("Daemon broker authentication failed");
|
|
452
469
|
onAuthenticated();
|
|
470
|
+
for (const owner of request.completionUnsubscribes ?? []) {
|
|
471
|
+
const subscriptionId = this.#completionSubscriptions.get(owner);
|
|
472
|
+
if (
|
|
473
|
+
!this.#completionSubscriptions.has(owner) ||
|
|
474
|
+
(subscriptionId !== undefined && subscriptionId !== request.completionSubscriptionId)
|
|
475
|
+
) {
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
this.#ownerSockets.delete(owner);
|
|
479
|
+
this.#completionSubscriptions.delete(owner);
|
|
480
|
+
await this.#setRecordCompletionCapability(owner, false);
|
|
481
|
+
this.#pendingCompletions.delete(owner);
|
|
482
|
+
}
|
|
483
|
+
for (const completionId of request.completionAcks ?? []) {
|
|
484
|
+
for (const [owner, pending] of this.#pendingCompletions) {
|
|
485
|
+
const registration = this.#ownerSockets.get(owner);
|
|
486
|
+
if (
|
|
487
|
+
!registration ||
|
|
488
|
+
registration.socket !== socket ||
|
|
489
|
+
registration.subscriptionId !== request.completionSubscriptionId
|
|
490
|
+
) {
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
const completion = pending.get(completionId);
|
|
494
|
+
if (!completion) continue;
|
|
495
|
+
pending.delete(completionId);
|
|
496
|
+
if (pending.size === 0) this.#pendingCompletions.delete(owner);
|
|
497
|
+
const record = this.#records.get(completion.daemon.name);
|
|
498
|
+
const index = record?.pendingCompletions.findIndex(item => item.completionId === completionId) ?? -1;
|
|
499
|
+
if (record && index >= 0) {
|
|
500
|
+
record.pendingCompletions.splice(index, 1);
|
|
501
|
+
this.#persist(record);
|
|
502
|
+
await record.persistQueue;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
if (publishesCompletionOwners(request)) {
|
|
507
|
+
const replayOwners = new Set(request.completionReplays ?? []);
|
|
508
|
+
const activeOwners = new Set(request.owners ?? []);
|
|
509
|
+
const detachedOwners = new Set(request.detachedOwners ?? []);
|
|
510
|
+
const advertisedOwners = new Set([...activeOwners, ...detachedOwners]);
|
|
511
|
+
for (const [owner, subscriptionId] of this.#completionSubscriptions) {
|
|
512
|
+
if (subscriptionId !== request.completionSubscriptionId || advertisedOwners.has(owner)) continue;
|
|
513
|
+
this.#ownerSockets.delete(owner);
|
|
514
|
+
this.#completionSubscriptions.delete(owner);
|
|
515
|
+
await this.#setRecordCompletionCapability(owner, false);
|
|
516
|
+
this.#pendingCompletions.delete(owner);
|
|
517
|
+
}
|
|
518
|
+
for (const owner of activeOwners) {
|
|
519
|
+
this.#completionSubscriptions.set(owner, request.completionSubscriptionId);
|
|
520
|
+
await this.#setRecordCompletionCapability(owner, true);
|
|
521
|
+
const previous = this.#ownerSockets.get(owner);
|
|
522
|
+
this.#ownerSockets.set(owner, {
|
|
523
|
+
socket,
|
|
524
|
+
subscriptionId: request.completionSubscriptionId,
|
|
525
|
+
});
|
|
526
|
+
if (previous?.socket === socket && !replayOwners.has(owner)) continue;
|
|
527
|
+
for (const completion of this.#pendingCompletions.get(owner)?.values() ?? []) {
|
|
528
|
+
socket.write(`${JSON.stringify(completion)}\n`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
for (const owner of detachedOwners) {
|
|
532
|
+
const subscriptionId = this.#completionSubscriptions.get(owner);
|
|
533
|
+
if (this.#completionSubscriptions.has(owner) && subscriptionId !== request.completionSubscriptionId) {
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
this.#completionSubscriptions.set(owner, request.completionSubscriptionId);
|
|
537
|
+
await this.#setRecordCompletionCapability(owner, true);
|
|
538
|
+
const registration = this.#ownerSockets.get(owner);
|
|
539
|
+
if (registration?.subscriptionId === request.completionSubscriptionId) this.#ownerSockets.delete(owner);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
453
542
|
const result = await this.#dispatch(request.operation);
|
|
454
543
|
socket.write(`${JSON.stringify({ id, ok: true, result })}\n`);
|
|
455
544
|
if (request.operation.op === "shutdown") setTimeout(() => void this.shutdown(), 10);
|
|
@@ -520,6 +609,9 @@ class DaemonBroker {
|
|
|
520
609
|
if (existing && !terminalState(existing.snapshot.state)) {
|
|
521
610
|
throw new Error(`Daemon ${spec.name} is already ${existing.snapshot.state}`);
|
|
522
611
|
}
|
|
612
|
+
if (existing && existing.pendingCompletions.length > 0) {
|
|
613
|
+
throw new Error(`Daemon ${spec.name} has unacknowledged completion notifications`);
|
|
614
|
+
}
|
|
523
615
|
if (spec.ready?.log) {
|
|
524
616
|
try {
|
|
525
617
|
new RegExp(spec.ready.log, "u");
|
|
@@ -556,6 +648,9 @@ class DaemonBroker {
|
|
|
556
648
|
readyPattern: spec.ready?.log ? new RegExp(spec.ready.log, "u") : undefined,
|
|
557
649
|
consecutiveFailures: 0,
|
|
558
650
|
persistQueue: Promise.resolve(),
|
|
651
|
+
completionCapable: owner !== undefined && this.#completionSubscriptions.has(owner),
|
|
652
|
+
completionSubscriptionId: owner === undefined ? undefined : this.#completionSubscriptions.get(owner),
|
|
653
|
+
pendingCompletions: [],
|
|
559
654
|
};
|
|
560
655
|
syncReadyPending(record);
|
|
561
656
|
this.#records.set(spec.name, record);
|
|
@@ -785,6 +880,14 @@ class DaemonBroker {
|
|
|
785
880
|
await this.#settle(record, generation);
|
|
786
881
|
}
|
|
787
882
|
|
|
883
|
+
async #monitorRecoveredDetached(record: ManagedDaemon, generation: number): Promise<void> {
|
|
884
|
+
while (!this.#shuttingDown && generation === record.generation && !settledState(record.snapshot.state)) {
|
|
885
|
+
await Bun.sleep(100);
|
|
886
|
+
if (this.#shuttingDown || generation !== record.generation) return;
|
|
887
|
+
await this.#refreshDetached(record);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
788
891
|
async #pollPort(record: ManagedDaemon, generation: number, ready: DaemonReadySpec): Promise<void> {
|
|
789
892
|
const host = ready.host ?? "127.0.0.1";
|
|
790
893
|
const port = ready.port;
|
|
@@ -812,6 +915,15 @@ class DaemonBroker {
|
|
|
812
915
|
return this.#settle(record, generation, result.exitCode, result.timedOut ? "timed out" : undefined);
|
|
813
916
|
}
|
|
814
917
|
|
|
918
|
+
#notifyCompletion(completion: DaemonCompletionNotification): void {
|
|
919
|
+
const pending = this.#pendingCompletions.get(completion.owner) ?? new Map<string, DaemonCompletionNotification>();
|
|
920
|
+
pending.set(completion.completionId, completion);
|
|
921
|
+
this.#pendingCompletions.set(completion.owner, pending);
|
|
922
|
+
const registration = this.#ownerSockets.get(completion.owner);
|
|
923
|
+
if (!registration || registration.socket.destroyed) return;
|
|
924
|
+
registration.socket.write(`${JSON.stringify(completion)}\n`);
|
|
925
|
+
}
|
|
926
|
+
|
|
815
927
|
async #settle(record: ManagedDaemon, generation: number, exitCode?: number, error?: string): Promise<void> {
|
|
816
928
|
// `restarting` is a settled state (child exited, relaunch timer armed). Any op that
|
|
817
929
|
// runs #refreshDetached on such a record must not re-settle it: re-entry double-counts
|
|
@@ -855,9 +967,29 @@ class DaemonBroker {
|
|
|
855
967
|
return;
|
|
856
968
|
}
|
|
857
969
|
record.snapshot.state = failed && !record.stopRequested ? "failed" : "exited";
|
|
970
|
+
const completion =
|
|
971
|
+
record.snapshot.owner !== undefined &&
|
|
972
|
+
!record.stopRequested &&
|
|
973
|
+
this.#completionSubscriptions.has(record.snapshot.owner)
|
|
974
|
+
? ({
|
|
975
|
+
event: "daemon-completed",
|
|
976
|
+
completionId: crypto.randomUUID(),
|
|
977
|
+
owner: record.snapshot.owner,
|
|
978
|
+
daemon: { ...record.snapshot },
|
|
979
|
+
} satisfies DaemonCompletionNotification)
|
|
980
|
+
: undefined;
|
|
981
|
+
if (completion) record.pendingCompletions.push(completion);
|
|
858
982
|
this.#persist(record);
|
|
859
983
|
await record.log?.close();
|
|
860
984
|
record.log = undefined;
|
|
985
|
+
await record.persistQueue;
|
|
986
|
+
if (
|
|
987
|
+
completion &&
|
|
988
|
+
this.#completionSubscriptions.has(completion.owner) &&
|
|
989
|
+
record.pendingCompletions.some(pending => pending.completionId === completion.completionId)
|
|
990
|
+
) {
|
|
991
|
+
this.#notifyCompletion(completion);
|
|
992
|
+
}
|
|
861
993
|
}
|
|
862
994
|
|
|
863
995
|
async #logs(operation: Extract<DaemonOperation, { op: "logs" }>): Promise<DaemonRpcResult> {
|
|
@@ -1027,9 +1159,21 @@ class DaemonBroker {
|
|
|
1027
1159
|
#persist(record: ManagedDaemon): void {
|
|
1028
1160
|
const metaPath = path.join(record.dir, META_FILE);
|
|
1029
1161
|
const tempPath = `${metaPath}.${process.pid}.tmp`;
|
|
1162
|
+
const metadata = {
|
|
1163
|
+
daemon: { ...record.snapshot },
|
|
1164
|
+
spec: record.spec,
|
|
1165
|
+
completionEvents: record.completionCapable,
|
|
1166
|
+
completionSubscriptionId: record.completionSubscriptionId,
|
|
1167
|
+
completionPending: record.pendingCompletions.length > 0,
|
|
1168
|
+
pendingCompletion: record.pendingCompletions.at(-1)?.daemon,
|
|
1169
|
+
pendingCompletions: record.pendingCompletions.map(completion => ({
|
|
1170
|
+
...completion,
|
|
1171
|
+
daemon: { ...completion.daemon },
|
|
1172
|
+
})),
|
|
1173
|
+
};
|
|
1030
1174
|
record.persistQueue = record.persistQueue
|
|
1031
1175
|
.then(async () => {
|
|
1032
|
-
await Bun.write(tempPath, JSON.stringify(
|
|
1176
|
+
await Bun.write(tempPath, JSON.stringify(metadata));
|
|
1033
1177
|
await fs.rename(tempPath, metaPath);
|
|
1034
1178
|
})
|
|
1035
1179
|
.catch(error => {
|
|
@@ -1040,6 +1184,28 @@ class DaemonBroker {
|
|
|
1040
1184
|
});
|
|
1041
1185
|
}
|
|
1042
1186
|
|
|
1187
|
+
async #setRecordCompletionCapability(owner: string, capable: boolean): Promise<void> {
|
|
1188
|
+
const subscriptionId = capable ? this.#completionSubscriptions.get(owner) : undefined;
|
|
1189
|
+
const persistence: Promise<void>[] = [];
|
|
1190
|
+
for (const record of this.#records.values()) {
|
|
1191
|
+
const clearPendingCompletions = !capable && record.pendingCompletions.length > 0;
|
|
1192
|
+
if (
|
|
1193
|
+
record.snapshot.owner !== owner ||
|
|
1194
|
+
(record.completionCapable === capable &&
|
|
1195
|
+
record.completionSubscriptionId === subscriptionId &&
|
|
1196
|
+
!clearPendingCompletions)
|
|
1197
|
+
) {
|
|
1198
|
+
continue;
|
|
1199
|
+
}
|
|
1200
|
+
record.completionCapable = capable;
|
|
1201
|
+
record.completionSubscriptionId = subscriptionId;
|
|
1202
|
+
if (!capable) record.pendingCompletions = [];
|
|
1203
|
+
this.#persist(record);
|
|
1204
|
+
persistence.push(record.persistQueue);
|
|
1205
|
+
}
|
|
1206
|
+
await Promise.all(persistence);
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1043
1209
|
async #recoverRecords(): Promise<void> {
|
|
1044
1210
|
const root = path.join(this.#runtimeDir, "daemons");
|
|
1045
1211
|
const entries = await fs.readdir(root, { withFileTypes: true }).catch(error => {
|
|
@@ -1057,11 +1223,9 @@ class DaemonBroker {
|
|
|
1057
1223
|
const snapshot = parseDaemonSnapshot(decoded.daemon);
|
|
1058
1224
|
const spec = parseDaemonSpec(decoded.spec);
|
|
1059
1225
|
const processRef = snapshot.pid === undefined ? null : Process.fromPid(snapshot.pid);
|
|
1060
|
-
const
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
snapshot.state !== "stopping" &&
|
|
1064
|
-
processRef?.status() === "running";
|
|
1226
|
+
const recoverableExit = !terminalState(snapshot.state) && snapshot.state !== "stopping";
|
|
1227
|
+
const detached = spec.detached && recoverableExit && processRef?.status() === "running";
|
|
1228
|
+
const recoveredDead = recoverableExit && !detached;
|
|
1065
1229
|
if (!detached) {
|
|
1066
1230
|
// Reap only records that were still alive when the previous broker
|
|
1067
1231
|
// exited; already-terminal records keep their real exit time so
|
|
@@ -1088,12 +1252,64 @@ class DaemonBroker {
|
|
|
1088
1252
|
readyPattern: spec.ready?.log ? new RegExp(spec.ready.log, "u") : undefined,
|
|
1089
1253
|
consecutiveFailures: 0,
|
|
1090
1254
|
persistQueue: Promise.resolve(),
|
|
1255
|
+
completionCapable: "completionEvents" in decoded && decoded.completionEvents === true,
|
|
1256
|
+
completionSubscriptionId:
|
|
1257
|
+
"completionSubscriptionId" in decoded && typeof decoded.completionSubscriptionId === "string"
|
|
1258
|
+
? decoded.completionSubscriptionId
|
|
1259
|
+
: undefined,
|
|
1260
|
+
pendingCompletions: (() => {
|
|
1261
|
+
if ("pendingCompletions" in decoded && Array.isArray(decoded.pendingCompletions)) {
|
|
1262
|
+
return decoded.pendingCompletions.map(value => {
|
|
1263
|
+
const message = parseDaemonWireMessage(value);
|
|
1264
|
+
if (!("event" in message)) throw new Error("Pending daemon completion is not an event");
|
|
1265
|
+
return message;
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
const pendingSnapshot =
|
|
1269
|
+
"pendingCompletion" in decoded
|
|
1270
|
+
? parseDaemonSnapshot(decoded.pendingCompletion)
|
|
1271
|
+
: "completionPending" in decoded && decoded.completionPending === true
|
|
1272
|
+
? { ...snapshot }
|
|
1273
|
+
: undefined;
|
|
1274
|
+
return pendingSnapshot
|
|
1275
|
+
? [
|
|
1276
|
+
{
|
|
1277
|
+
event: "daemon-completed",
|
|
1278
|
+
completionId: crypto.randomUUID(),
|
|
1279
|
+
owner: pendingSnapshot.owner ?? snapshot.owner ?? "",
|
|
1280
|
+
daemon: pendingSnapshot,
|
|
1281
|
+
},
|
|
1282
|
+
]
|
|
1283
|
+
: [];
|
|
1284
|
+
})(),
|
|
1091
1285
|
};
|
|
1286
|
+
if (recoveredDead && record.completionCapable && snapshot.owner && record.pendingCompletions.length === 0) {
|
|
1287
|
+
record.pendingCompletions.push({
|
|
1288
|
+
event: "daemon-completed",
|
|
1289
|
+
completionId: crypto.randomUUID(),
|
|
1290
|
+
owner: snapshot.owner,
|
|
1291
|
+
daemon: { ...snapshot },
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1092
1294
|
syncReadyPending(record);
|
|
1093
1295
|
this.#records.set(snapshot.name, record);
|
|
1296
|
+
if (snapshot.owner && record.completionCapable && (detached || record.pendingCompletions.length > 0)) {
|
|
1297
|
+
this.#completionSubscriptions.set(snapshot.owner, record.completionSubscriptionId);
|
|
1298
|
+
}
|
|
1299
|
+
if (record.completionCapable) {
|
|
1300
|
+
for (const completion of record.pendingCompletions) this.#notifyCompletion(completion);
|
|
1301
|
+
}
|
|
1094
1302
|
if (detached && spec.ready?.port !== undefined && snapshot.state !== "ready") {
|
|
1095
1303
|
void this.#pollPort(record, record.generation, spec.ready);
|
|
1096
1304
|
}
|
|
1305
|
+
if (detached) {
|
|
1306
|
+
void this.#monitorRecoveredDetached(record, record.generation).catch(error => {
|
|
1307
|
+
logger.warn("Failed to monitor recovered detached daemon", {
|
|
1308
|
+
name: record.snapshot.name,
|
|
1309
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1310
|
+
});
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1097
1313
|
this.#persist(record);
|
|
1098
1314
|
} catch (error) {
|
|
1099
1315
|
logger.warn("Failed to recover daemon record", {
|
|
@@ -1124,7 +1340,7 @@ class DaemonBroker {
|
|
|
1124
1340
|
}
|
|
1125
1341
|
}
|
|
1126
1342
|
|
|
1127
|
-
/** Start the detached
|
|
1343
|
+
/** Start the detached project or global daemon broker selected by the CLI worker host. */
|
|
1128
1344
|
export async function startDaemonBrokerFromEnvironment(): Promise<void> {
|
|
1129
1345
|
const projectDir = process.env[DAEMON_PROJECT_DIR_ENV];
|
|
1130
1346
|
const runtimeDir = process.env[DAEMON_RUNTIME_DIR_ENV];
|
package/src/launch/client.ts
CHANGED
|
@@ -2,7 +2,7 @@ import * as fs from "node:fs/promises";
|
|
|
2
2
|
import * as net from "node:net";
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
-
import { isEexist, isEisdir, isEnoent, postmortem } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import { getGlobalDaemonRuntimeDir, isEexist, isEisdir, isEnoent, logger, postmortem } from "@oh-my-pi/pi-utils";
|
|
6
6
|
import { hostHasInheritableConsole } from "../eval/py/spawn-options";
|
|
7
7
|
import { resolveWorkerSpawnCmd, workerEnvFromParent } from "../subprocess/worker-client";
|
|
8
8
|
import { daemonBrokerEndpoint, daemonRuntimeDir } from "./paths";
|
|
@@ -11,11 +11,12 @@ import {
|
|
|
11
11
|
DAEMON_IDLE_GRACE_ENV,
|
|
12
12
|
DAEMON_PROJECT_DIR_ENV,
|
|
13
13
|
DAEMON_RUNTIME_DIR_ENV,
|
|
14
|
+
type DaemonCompletionNotification,
|
|
14
15
|
type DaemonOperation,
|
|
15
16
|
type DaemonRpcResult,
|
|
16
|
-
type
|
|
17
|
+
type DaemonWireMessage,
|
|
17
18
|
parseDaemonRpcResult,
|
|
18
|
-
|
|
19
|
+
parseDaemonWireMessage,
|
|
19
20
|
} from "./protocol";
|
|
20
21
|
import { resolveDaemonSpawnOptions } from "./spawn-options";
|
|
21
22
|
|
|
@@ -43,13 +44,26 @@ export interface DaemonBrokerClientOptions {
|
|
|
43
44
|
idleGraceMs?: number;
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
|
|
47
|
+
export interface DaemonCompletionUnregisterOptions {
|
|
48
|
+
/** Detach this process without deleting broker-persisted pending notifications. */
|
|
49
|
+
preservePending?: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Persistent per-process connection to one project or global daemon broker. */
|
|
47
53
|
export interface DaemonBrokerClient {
|
|
54
|
+
onCompletion(
|
|
55
|
+
owner: string,
|
|
56
|
+
sink: (notification: DaemonCompletionNotification) => Promise<void> | void,
|
|
57
|
+
): (options?: DaemonCompletionUnregisterOptions) => void;
|
|
58
|
+
/** Canonical project directory or synthetic directory identifying a global scope. */
|
|
48
59
|
readonly projectDir: string;
|
|
49
60
|
request(operation: DaemonOperation, signal?: AbortSignal): Promise<DaemonRpcResult>;
|
|
50
61
|
close(): void;
|
|
51
62
|
}
|
|
52
63
|
|
|
64
|
+
/** A request reached the broker and the broker rejected the operation. */
|
|
65
|
+
export class DaemonBrokerRejectedError extends Error {}
|
|
66
|
+
|
|
53
67
|
async function canonicalProjectDir(projectDir: string): Promise<string> {
|
|
54
68
|
const resolved = path.resolve(projectDir);
|
|
55
69
|
try {
|
|
@@ -133,12 +147,20 @@ class SocketDaemonClient implements DaemonBrokerClient {
|
|
|
133
147
|
readonly #runtimeDir: string;
|
|
134
148
|
readonly #endpoint: string;
|
|
135
149
|
readonly #token: string;
|
|
150
|
+
readonly #seenCompletionIds = new Set<string>();
|
|
136
151
|
readonly #idleGraceMs: number | undefined;
|
|
137
152
|
readonly #pending = new Map<string, PendingRequest>();
|
|
153
|
+
readonly #completionSinks = new Map<string, (notification: DaemonCompletionNotification) => Promise<void> | void>();
|
|
154
|
+
readonly #completionUnsubscribes = new Set<string>();
|
|
155
|
+
readonly #preservedCompletionOwners = new Set<string>();
|
|
156
|
+
readonly #completionReplays = new Set<string>();
|
|
157
|
+
readonly #inFlightCompletionIds = new Set<string>();
|
|
158
|
+
readonly #completionSubscriptionId = crypto.randomUUID();
|
|
138
159
|
#socket: net.Socket | undefined;
|
|
139
160
|
#connectPromise: Promise<void> | undefined;
|
|
140
161
|
#buffer = "";
|
|
141
162
|
#closed = false;
|
|
163
|
+
#completionReconnectTimer: NodeJS.Timeout | undefined;
|
|
142
164
|
|
|
143
165
|
constructor(projectDir: string, runtimeDir: string, token: string, options: DaemonBrokerClientOptions) {
|
|
144
166
|
this.projectDir = projectDir;
|
|
@@ -155,6 +177,8 @@ class SocketDaemonClient implements DaemonBrokerClient {
|
|
|
155
177
|
const socket = this.#socket;
|
|
156
178
|
if (!socket || socket.destroyed) throw new Error("Daemon broker socket is unavailable");
|
|
157
179
|
|
|
180
|
+
const completionUnsubscribes = [...this.#completionUnsubscribes];
|
|
181
|
+
const completionReplays = [...this.#completionReplays];
|
|
158
182
|
const id = crypto.randomUUID();
|
|
159
183
|
const { promise, resolve, reject } = Promise.withResolvers<DaemonRpcResult>();
|
|
160
184
|
const timer = setTimeout(() => {
|
|
@@ -175,18 +199,88 @@ class SocketDaemonClient implements DaemonBrokerClient {
|
|
|
175
199
|
pending.removeAbort = () => signal.removeEventListener("abort", abort);
|
|
176
200
|
}
|
|
177
201
|
this.#pending.set(id, pending);
|
|
178
|
-
socket.write(
|
|
179
|
-
|
|
202
|
+
socket.write(
|
|
203
|
+
`${JSON.stringify({
|
|
204
|
+
id,
|
|
205
|
+
token: this.#token,
|
|
206
|
+
owners: [...this.#completionSinks.keys()],
|
|
207
|
+
detachedOwners: [...this.#preservedCompletionOwners],
|
|
208
|
+
completionEvents: true,
|
|
209
|
+
completionUnsubscribes,
|
|
210
|
+
completionReplays,
|
|
211
|
+
completionSubscriptionId: this.#completionSubscriptionId,
|
|
212
|
+
operation,
|
|
213
|
+
})}\n`,
|
|
214
|
+
);
|
|
215
|
+
const result = await promise;
|
|
216
|
+
for (const owner of completionUnsubscribes) {
|
|
217
|
+
if (!this.#completionSinks.has(owner)) this.#completionUnsubscribes.delete(owner);
|
|
218
|
+
}
|
|
219
|
+
for (const owner of completionReplays) {
|
|
220
|
+
if (this.#completionSinks.has(owner)) this.#completionReplays.delete(owner);
|
|
221
|
+
}
|
|
222
|
+
return result;
|
|
180
223
|
}
|
|
181
224
|
|
|
182
225
|
close(): void {
|
|
183
226
|
if (this.#closed) return;
|
|
184
227
|
this.#closed = true;
|
|
228
|
+
clearTimeout(this.#completionReconnectTimer);
|
|
229
|
+
this.#completionReconnectTimer = undefined;
|
|
185
230
|
this.#socket?.destroy();
|
|
231
|
+
this.#completionSinks.clear();
|
|
232
|
+
this.#preservedCompletionOwners.clear();
|
|
233
|
+
this.#completionReplays.clear();
|
|
186
234
|
this.#socket = undefined;
|
|
187
235
|
this.#rejectPending(new Error("Daemon broker client closed"));
|
|
188
236
|
}
|
|
189
237
|
|
|
238
|
+
onCompletion(
|
|
239
|
+
owner: string,
|
|
240
|
+
sink: (notification: DaemonCompletionNotification) => Promise<void> | void,
|
|
241
|
+
): (options?: DaemonCompletionUnregisterOptions) => void {
|
|
242
|
+
this.#completionUnsubscribes.delete(owner);
|
|
243
|
+
if (this.#preservedCompletionOwners.delete(owner)) this.#completionReplays.add(owner);
|
|
244
|
+
this.#completionSinks.set(owner, sink);
|
|
245
|
+
this.#publishCompletionOwners();
|
|
246
|
+
return options => {
|
|
247
|
+
if (this.#completionSinks.get(owner) !== sink) return;
|
|
248
|
+
this.#completionSinks.delete(owner);
|
|
249
|
+
if (options?.preservePending) {
|
|
250
|
+
this.#preservedCompletionOwners.add(owner);
|
|
251
|
+
} else {
|
|
252
|
+
this.#preservedCompletionOwners.delete(owner);
|
|
253
|
+
this.#completionUnsubscribes.add(owner);
|
|
254
|
+
}
|
|
255
|
+
if (this.#completionSinks.size === 0 && this.#completionReconnectTimer) {
|
|
256
|
+
clearTimeout(this.#completionReconnectTimer);
|
|
257
|
+
this.#completionReconnectTimer = undefined;
|
|
258
|
+
}
|
|
259
|
+
this.#publishCompletionOwners();
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
#publishCompletionOwners(): void {
|
|
264
|
+
if (this.#closed) return;
|
|
265
|
+
void this.request({ op: "ping" }).catch(() => this.#scheduleCompletionReconnect());
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
#scheduleCompletionReconnect(): void {
|
|
269
|
+
if (
|
|
270
|
+
this.#closed ||
|
|
271
|
+
this.#completionSinks.size === 0 ||
|
|
272
|
+
this.#completionReconnectTimer !== undefined ||
|
|
273
|
+
(this.#socket !== undefined && !this.#socket.destroyed)
|
|
274
|
+
) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
this.#completionReconnectTimer = setTimeout(() => {
|
|
278
|
+
this.#completionReconnectTimer = undefined;
|
|
279
|
+
this.#publishCompletionOwners();
|
|
280
|
+
}, CONNECT_RETRY_MS);
|
|
281
|
+
this.#completionReconnectTimer.unref();
|
|
282
|
+
}
|
|
283
|
+
|
|
190
284
|
async #connect(): Promise<void> {
|
|
191
285
|
if (this.#socket && !this.#socket.destroyed) return;
|
|
192
286
|
if (this.#connectPromise) return this.#connectPromise;
|
|
@@ -250,6 +344,7 @@ class SocketDaemonClient implements DaemonBrokerClient {
|
|
|
250
344
|
socket.on("close", () => {
|
|
251
345
|
if (this.#socket === socket) this.#socket = undefined;
|
|
252
346
|
this.#rejectPending(new Error("Daemon broker connection closed"));
|
|
347
|
+
this.#scheduleCompletionReconnect();
|
|
253
348
|
});
|
|
254
349
|
}
|
|
255
350
|
|
|
@@ -261,21 +356,32 @@ class SocketDaemonClient implements DaemonBrokerClient {
|
|
|
261
356
|
const line = this.#buffer.slice(0, newline);
|
|
262
357
|
this.#buffer = this.#buffer.slice(newline + 1);
|
|
263
358
|
if (line.length === 0) continue;
|
|
264
|
-
let
|
|
359
|
+
let decoded: unknown;
|
|
265
360
|
try {
|
|
266
|
-
|
|
267
|
-
response = parseDaemonWireResponse(decoded);
|
|
361
|
+
decoded = JSON.parse(line);
|
|
268
362
|
} catch (error) {
|
|
269
363
|
this.#rejectPending(error instanceof Error ? error : new Error(String(error)));
|
|
270
364
|
continue;
|
|
271
365
|
}
|
|
366
|
+
let message: DaemonWireMessage;
|
|
367
|
+
try {
|
|
368
|
+
message = parseDaemonWireMessage(decoded);
|
|
369
|
+
} catch (error) {
|
|
370
|
+
this.#rejectPending(error instanceof Error ? error : new Error(String(error)));
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
if ("event" in message) {
|
|
374
|
+
void this.#deliverCompletion(message);
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
const response = message;
|
|
272
378
|
const pending = this.#pending.get(response.id);
|
|
273
379
|
if (!pending) continue;
|
|
274
380
|
this.#pending.delete(response.id);
|
|
275
381
|
clearTimeout(pending.timer);
|
|
276
382
|
pending.removeAbort?.();
|
|
277
383
|
if (!response.ok) {
|
|
278
|
-
pending.reject(new
|
|
384
|
+
pending.reject(new DaemonBrokerRejectedError(response.error));
|
|
279
385
|
continue;
|
|
280
386
|
}
|
|
281
387
|
try {
|
|
@@ -286,6 +392,53 @@ class SocketDaemonClient implements DaemonBrokerClient {
|
|
|
286
392
|
}
|
|
287
393
|
}
|
|
288
394
|
|
|
395
|
+
async #deliverCompletion(message: DaemonCompletionNotification): Promise<void> {
|
|
396
|
+
if (this.#seenCompletionIds.has(message.completionId)) {
|
|
397
|
+
this.#ackCompletion(message.completionId);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (this.#inFlightCompletionIds.has(message.completionId)) return;
|
|
401
|
+
const sink = this.#completionSinks.get(message.owner);
|
|
402
|
+
if (!sink) return;
|
|
403
|
+
this.#inFlightCompletionIds.add(message.completionId);
|
|
404
|
+
try {
|
|
405
|
+
await sink(message);
|
|
406
|
+
if (this.#seenCompletionIds.size >= 512) {
|
|
407
|
+
const oldest = this.#seenCompletionIds.values().next().value;
|
|
408
|
+
if (oldest !== undefined) this.#seenCompletionIds.delete(oldest);
|
|
409
|
+
}
|
|
410
|
+
this.#seenCompletionIds.add(message.completionId);
|
|
411
|
+
this.#ackCompletion(message.completionId);
|
|
412
|
+
} catch (error) {
|
|
413
|
+
logger.warn("Daemon completion sink failed", {
|
|
414
|
+
owner: message.owner,
|
|
415
|
+
completionId: message.completionId,
|
|
416
|
+
error: error instanceof Error ? error.message : String(error),
|
|
417
|
+
});
|
|
418
|
+
this.#socket?.destroy();
|
|
419
|
+
} finally {
|
|
420
|
+
this.#inFlightCompletionIds.delete(message.completionId);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
#ackCompletion(completionId: string): void {
|
|
425
|
+
const socket = this.#socket;
|
|
426
|
+
if (!socket || socket.destroyed) return;
|
|
427
|
+
socket.write(
|
|
428
|
+
`${JSON.stringify({
|
|
429
|
+
id: crypto.randomUUID(),
|
|
430
|
+
token: this.#token,
|
|
431
|
+
owners: [...this.#completionSinks.keys()],
|
|
432
|
+
detachedOwners: [...this.#preservedCompletionOwners],
|
|
433
|
+
completionEvents: true,
|
|
434
|
+
completionAcks: [completionId],
|
|
435
|
+
completionUnsubscribes: [...this.#completionUnsubscribes],
|
|
436
|
+
completionSubscriptionId: this.#completionSubscriptionId,
|
|
437
|
+
operation: { op: "ping" },
|
|
438
|
+
})}\n`,
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
289
442
|
#rejectPending(error: Error): void {
|
|
290
443
|
for (const pending of this.#pending.values()) {
|
|
291
444
|
clearTimeout(pending.timer);
|
|
@@ -299,7 +452,19 @@ class SocketDaemonClient implements DaemonBrokerClient {
|
|
|
299
452
|
const sharedClients = new Map<string, Promise<DaemonBrokerClient>>();
|
|
300
453
|
let cancelExitCleanup: (() => void) | undefined;
|
|
301
454
|
|
|
302
|
-
|
|
455
|
+
function sharedDaemonClient(key: string, create: () => Promise<DaemonBrokerClient>): Promise<DaemonBrokerClient> {
|
|
456
|
+
let pending = sharedClients.get(key);
|
|
457
|
+
if (!pending) {
|
|
458
|
+
pending = create();
|
|
459
|
+
sharedClients.set(key, pending);
|
|
460
|
+
if (!cancelExitCleanup) {
|
|
461
|
+
cancelExitCleanup = postmortem.register("daemon-broker-clients", () => closeDaemonClients());
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return pending;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** Create an independent socket connection to one daemon broker scope. */
|
|
303
468
|
export async function createDaemonBrokerClient(
|
|
304
469
|
projectDir: string,
|
|
305
470
|
options: DaemonBrokerClientOptions = {},
|
|
@@ -313,18 +478,24 @@ export async function createDaemonBrokerClient(
|
|
|
313
478
|
/** Get the process-shared daemon broker client for one canonical project directory. */
|
|
314
479
|
export async function daemonClientForProject(projectDir: string): Promise<DaemonBrokerClient> {
|
|
315
480
|
const canonical = await canonicalProjectDir(projectDir);
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
481
|
+
return sharedDaemonClient(`project:${canonical}`, () => createDaemonBrokerClient(canonical));
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/** Get the process-shared client that leases one profile-independent, machine-global daemon broker. */
|
|
485
|
+
export async function daemonClientForGlobal(service: string): Promise<DaemonBrokerClient> {
|
|
486
|
+
const runtimeDir = getGlobalDaemonRuntimeDir(service);
|
|
487
|
+
// Canonicalize only after creation so the first caller and later callers
|
|
488
|
+
// derive the same Windows pipe key even when an ancestor is a symlink.
|
|
489
|
+
await fs.mkdir(runtimeDir, { recursive: true, mode: 0o700 });
|
|
490
|
+
const canonical = await fs.realpath(runtimeDir);
|
|
491
|
+
return sharedDaemonClient(`global:${canonical}`, () =>
|
|
492
|
+
createDaemonBrokerClient(canonical, {
|
|
493
|
+
runtimeDir: canonical,
|
|
494
|
+
}),
|
|
495
|
+
);
|
|
325
496
|
}
|
|
326
497
|
|
|
327
|
-
/** Close every project broker connection held by this omp process. */
|
|
498
|
+
/** Close every project and machine-global broker connection held by this omp process. */
|
|
328
499
|
export async function closeDaemonClients(): Promise<void> {
|
|
329
500
|
const pending = [...sharedClients.values()];
|
|
330
501
|
sharedClients.clear();
|