@aria-cli/cli 1.0.57 → 1.0.58
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/bin/aria.mjs +2 -2
- package/package.json +17 -76
- package/dist/.tsbuildinfo +0 -1
- package/dist/attached-local-control-client.js +0 -826
- package/dist/bootstrap-local-control-client.js +0 -2
- package/dist/capability-aware-method-proxy.js +0 -42
- package/dist/cli-context.js +0 -160
- package/dist/commands/arions.js +0 -174
- package/dist/commands/auth.js +0 -123
- package/dist/commands/daemon.js +0 -367
- package/dist/commands/definitions.js +0 -176
- package/dist/commands/index.js +0 -80
- package/dist/commands/login-handler.js +0 -1108
- package/dist/commands/logout-handler.js +0 -92
- package/dist/commands/memory-handlers.js +0 -89
- package/dist/commands/pairing.js +0 -60
- package/dist/commands/runtime-cutover-reset-command.js +0 -12
- package/dist/commands/runtime-cutover-reset.js +0 -265
- package/dist/commands/terminal-setup.js +0 -84
- package/dist/config/aria-config.js +0 -238
- package/dist/config/index.js +0 -3
- package/dist/config/loader.js +0 -97
- package/dist/config.js +0 -142
- package/dist/daemon-info.js +0 -10
- package/dist/ensure-daemon.js +0 -128
- package/dist/entrypoints/command-mode.js +0 -5
- package/dist/entrypoints/daemon.js +0 -50
- package/dist/entrypoints/headless-stdio.js +0 -25
- package/dist/entrypoints/interactive.js +0 -80
- package/dist/event-loop-watchdog.js +0 -73
- package/dist/headless/auth-orchestrator.js +0 -508
- package/dist/headless/auth-service.js +0 -43
- package/dist/headless/bootstrap-fast-path.js +0 -112
- package/dist/headless/call-command.js +0 -143
- package/dist/headless/daemon-service.js +0 -318
- package/dist/headless/hook-actions.js +0 -235
- package/dist/headless/hook-service.js +0 -42
- package/dist/headless/kernel-services.js +0 -216
- package/dist/headless/kernel.js +0 -785
- package/dist/headless/operations/arion.js +0 -119
- package/dist/headless/operations/auth.js +0 -45
- package/dist/headless/operations/client.js +0 -31
- package/dist/headless/operations/config.js +0 -69
- package/dist/headless/operations/daemon.js +0 -47
- package/dist/headless/operations/hook.js +0 -56
- package/dist/headless/operations/index.js +0 -11
- package/dist/headless/operations/memory.js +0 -102
- package/dist/headless/operations/message.js +0 -279
- package/dist/headless/operations/model.js +0 -100
- package/dist/headless/operations/peer.js +0 -56
- package/dist/headless/operations/run.js +0 -24
- package/dist/headless/operations/session.js +0 -90
- package/dist/headless/operations/system.js +0 -19
- package/dist/headless/operations/utils.js +0 -35
- package/dist/headless/run-orchestrator.js +0 -703
- package/dist/headless/stdio-server.js +0 -439
- package/dist/history/SessionHistory.js +0 -8
- package/dist/history/SessionHistoryClient.js +0 -186
- package/dist/history/conversation-message.js +0 -112
- package/dist/history/index.js +0 -8
- package/dist/history/jsonl-replay.js +0 -154
- package/dist/history/repair-tool-pairing.js +0 -84
- package/dist/history/stall-phase-bridge.js +0 -11
- package/dist/history/turn-accumulator.js +0 -427
- package/dist/index.js +0 -7
- package/dist/ink-repl.js +0 -4183
- package/dist/local-control-bootstrap.js +0 -26
- package/dist/local-control-client.js +0 -2
- package/dist/local-control-error-reporting.js +0 -34
- package/dist/local-control-http-client.js +0 -362
- package/dist/local-control-lazy-wrapper.js +0 -363
- package/dist/local-control-manager.js +0 -146
- package/dist/main.js +0 -62
- package/dist/network-security.js +0 -62
- package/dist/networking-server.js +0 -38
- package/dist/peer-identity.js +0 -23
- package/dist/polling-subscription.js +0 -34
- package/dist/relaunch.js +0 -617
- package/dist/release-notes.js +0 -35
- package/dist/repl-cleanup.js +0 -47
- package/dist/runtime/configure-bun-sqlite.js +0 -3
- package/dist/runtime/crash-handlers.js +0 -111
- package/dist/runtime/interactive-invocation.js +0 -39
- package/dist/runtime/internal-mode.js +0 -14
- package/dist/runtime/launch-spec.js +0 -64
- package/dist/runtime/owner-lease.js +0 -44
- package/dist/runtime/public-mode.js +0 -20
- package/dist/runtime/run-internal-mode.js +0 -18
- package/dist/runtime/runtime-kind.js +0 -32
- package/dist/runtime/spawn-aria.js +0 -38
- package/dist/selectable-client.js +0 -2
- package/dist/selectable-peer.js +0 -2
- package/dist/session.js +0 -203
- package/dist/slash-commands.js +0 -80
- package/dist/sounds.js +0 -210
- package/dist/ui/App.js +0 -526
- package/dist/ui/components/AnthropicMethodPicker.js +0 -6
- package/dist/ui/components/ArionPrompt.js +0 -15
- package/dist/ui/components/AutocompleteDropdown.js +0 -23
- package/dist/ui/components/AutonomySelector.js +0 -55
- package/dist/ui/components/Banner.js +0 -98
- package/dist/ui/components/ConversationHistory.js +0 -175
- package/dist/ui/components/CopilotDeviceLoginFlow.js +0 -88
- package/dist/ui/components/CopilotSourcePicker.js +0 -50
- package/dist/ui/components/Cost.js +0 -10
- package/dist/ui/components/CustomSelect/option-map.js +0 -30
- package/dist/ui/components/CustomSelect/select-option.js +0 -13
- package/dist/ui/components/CustomSelect/select.js +0 -42
- package/dist/ui/components/CustomSelect/use-select-state.js +0 -179
- package/dist/ui/components/CustomSelect/use-select.js +0 -15
- package/dist/ui/components/ErrorDisplay.js +0 -35
- package/dist/ui/components/FallbackToolUseRejectedMessage.js +0 -7
- package/dist/ui/components/FileEditToolUpdatedMessage.js +0 -57
- package/dist/ui/components/HandoffMarker.js +0 -18
- package/dist/ui/components/HighlightedCode.js +0 -21
- package/dist/ui/components/InputArea.js +0 -187
- package/dist/ui/components/Message.js +0 -25
- package/dist/ui/components/OAuthLoginFlow.js +0 -113
- package/dist/ui/components/OutputTruncation.js +0 -35
- package/dist/ui/components/PermissionPrompt.js +0 -79
- package/dist/ui/components/PipelineTimingPanel.js +0 -15
- package/dist/ui/components/ProviderMethodPicker.js +0 -61
- package/dist/ui/components/ProviderPicker.js +0 -63
- package/dist/ui/components/RenderItemView.js +0 -71
- package/dist/ui/components/Spinner.js +0 -46
- package/dist/ui/components/StatusBar.js +0 -95
- package/dist/ui/components/StreamingIndicator.js +0 -55
- package/dist/ui/components/StructuredDiff.js +0 -168
- package/dist/ui/components/TextInputOverlay.js +0 -43
- package/dist/ui/components/ThinkingBlock.js +0 -82
- package/dist/ui/components/ToolCost.js +0 -17
- package/dist/ui/components/ToolExecution.js +0 -61
- package/dist/ui/components/ToolHeader.js +0 -51
- package/dist/ui/components/ToolRenderLayoutContext.js +0 -14
- package/dist/ui/components/ToolResultWrapper.js +0 -6
- package/dist/ui/components/ToolUseLoader.js +0 -35
- package/dist/ui/components/TraceWaterfall.js +0 -91
- package/dist/ui/components/index.js +0 -33
- package/dist/ui/components/messages/AssistantTextMessage.js +0 -25
- package/dist/ui/components/messages/UserImageMessage.js +0 -12
- package/dist/ui/components/messages/UserTextMessage.js +0 -12
- package/dist/ui/components/overlays/ArionSelector.js +0 -68
- package/dist/ui/components/overlays/ClientSelector.js +0 -62
- package/dist/ui/components/overlays/CommandPalette.js +0 -67
- package/dist/ui/components/overlays/DaemonControl.js +0 -87
- package/dist/ui/components/overlays/InviteShareOverlay.js +0 -15
- package/dist/ui/components/overlays/JoinInviteOverlay.js +0 -32
- package/dist/ui/components/overlays/MemoryBrowser.js +0 -100
- package/dist/ui/components/overlays/MessageSelector.js +0 -123
- package/dist/ui/components/overlays/ModelSelector.js +0 -211
- package/dist/ui/components/overlays/PairRequestOverlay.js +0 -42
- package/dist/ui/components/overlays/PeerSelector.js +0 -84
- package/dist/ui/components/overlays/SessionSelector.js +0 -102
- package/dist/ui/components/overlays/SoundSelector.js +0 -86
- package/dist/ui/components/overlays/ThemeSelector.js +0 -139
- package/dist/ui/components/overlays/index.js +0 -15
- package/dist/ui/components/permissions/BashPermissionRequest/BashPermissionRequest.js +0 -53
- package/dist/ui/components/permissions/FallbackPermissionRequest.js +0 -56
- package/dist/ui/components/permissions/FileEditPermissionRequest/FileEditPermissionRequest.js +0 -76
- package/dist/ui/components/permissions/FileEditPermissionRequest/FileEditToolDiff.js +0 -18
- package/dist/ui/components/permissions/FileWritePermissionRequest/FileWritePermissionRequest.js +0 -64
- package/dist/ui/components/permissions/FileWritePermissionRequest/FileWriteToolDiff.js +0 -26
- package/dist/ui/components/permissions/FilesystemPermissionRequest/FilesystemPermissionRequest.js +0 -141
- package/dist/ui/components/permissions/PermissionRequest.js +0 -70
- package/dist/ui/components/permissions/PermissionRequestTitle.js +0 -41
- package/dist/ui/components/permissions/hooks.js +0 -10
- package/dist/ui/components/permissions/toolUseOptions.js +0 -68
- package/dist/ui/components/permissions/utils.js +0 -10
- package/dist/ui/components/text-input/Cursor.js +0 -326
- package/dist/ui/components/text-input/TextInput.js +0 -231
- package/dist/ui/components/text-input/imagePaste.js +0 -28
- package/dist/ui/components/text-input/index.js +0 -6
- package/dist/ui/components/text-input/useDoublePress.js +0 -30
- package/dist/ui/components/text-input/useTextInput.js +0 -245
- package/dist/ui/components/tool-types.js +0 -9
- package/dist/ui/constants/figures.js +0 -4
- package/dist/ui/constants/index.js +0 -3
- package/dist/ui/display-mode.js +0 -93
- package/dist/ui/display-policy.js +0 -19
- package/dist/ui/hooks/index.js +0 -6
- package/dist/ui/hooks/useCommandAutocomplete.js +0 -93
- package/dist/ui/hooks/useDoublePress.js +0 -37
- package/dist/ui/hooks/useIndicatorState.js +0 -55
- package/dist/ui/hooks/useInterval.js +0 -23
- package/dist/ui/hooks/useKeyboardShortcuts.js +0 -127
- package/dist/ui/hooks/useTerminalSize.js +0 -55
- package/dist/ui/hooks/useUnifiedMessages.js +0 -117
- package/dist/ui/indicator-state.js +0 -44
- package/dist/ui/markdown/highlight.js +0 -44
- package/dist/ui/markdown/index.js +0 -1460
- package/dist/ui/markdown/tokenizer.js +0 -24
- package/dist/ui/render-item.js +0 -5
- package/dist/ui/screens/REPL.js +0 -119
- package/dist/ui/screens/approval-lifecycle.js +0 -38
- package/dist/ui/status-line.js +0 -72
- package/dist/ui/theme/index.js +0 -51
- package/dist/ui/theme/themes/claude-dark-daltonized.js +0 -51
- package/dist/ui/theme/themes/claude-dark.js +0 -50
- package/dist/ui/theme/themes/claude-light-daltonized.js +0 -51
- package/dist/ui/theme/themes/claude-light.js +0 -50
- package/dist/ui/theme/themes/dark-accessible.js +0 -18
- package/dist/ui/theme/themes/dark.js +0 -49
- package/dist/ui/theme/themes/light-accessible.js +0 -18
- package/dist/ui/theme/themes/light.js +0 -49
- package/dist/ui/theme/types.js +0 -3
- package/dist/ui/theme.js +0 -142
- package/dist/ui/to-render-items.js +0 -145
- package/dist/ui/tools/AgentTool/index.js +0 -30
- package/dist/ui/tools/ArchitectTool/index.js +0 -31
- package/dist/ui/tools/AskUserTool/index.js +0 -46
- package/dist/ui/tools/BashTool/BashToolResultMessage.js +0 -11
- package/dist/ui/tools/BashTool/OutputLine.js +0 -21
- package/dist/ui/tools/BashTool/index.js +0 -91
- package/dist/ui/tools/BrowseTool/index.js +0 -43
- package/dist/ui/tools/BrowserTool/index.js +0 -47
- package/dist/ui/tools/CbmTool/index.js +0 -188
- package/dist/ui/tools/CheckDelegationTool/index.js +0 -46
- package/dist/ui/tools/CheckMessagesTool/index.js +0 -85
- package/dist/ui/tools/CreateQuipTool/index.js +0 -30
- package/dist/ui/tools/CreateSkillTool/index.js +0 -22
- package/dist/ui/tools/CreateToolTool/index.js +0 -31
- package/dist/ui/tools/DelegateRemoteTool/index.js +0 -42
- package/dist/ui/tools/DeployTool/index.js +0 -47
- package/dist/ui/tools/FffTool/index.js +0 -103
- package/dist/ui/tools/FileEditTool/index.js +0 -67
- package/dist/ui/tools/FileReadTool/index.js +0 -68
- package/dist/ui/tools/FileWriteTool/index.js +0 -61
- package/dist/ui/tools/ForkTool/index.js +0 -47
- package/dist/ui/tools/FrgTool/index.js +0 -96
- package/dist/ui/tools/GetThreadTool/index.js +0 -39
- package/dist/ui/tools/GlobTool/index.js +0 -50
- package/dist/ui/tools/GrepTool/index.js +0 -84
- package/dist/ui/tools/HatchArionTool/index.js +0 -36
- package/dist/ui/tools/LearnSkillTool/index.js +0 -22
- package/dist/ui/tools/LearnTool/index.js +0 -43
- package/dist/ui/tools/LearnToolTool/index.js +0 -22
- package/dist/ui/tools/ListClientsTool/index.js +0 -39
- package/dist/ui/tools/LspTool/index.js +0 -261
- package/dist/ui/tools/MCPTool/index.js +0 -33
- package/dist/ui/tools/ManageNetworkTool/index.js +0 -53
- package/dist/ui/tools/MemoryReadTool/index.js +0 -64
- package/dist/ui/tools/MemoryWriteTool/index.js +0 -20
- package/dist/ui/tools/NotebookEditTool/index.js +0 -33
- package/dist/ui/tools/NotebookReadTool/index.js +0 -25
- package/dist/ui/tools/OutlookReadTool/index.js +0 -66
- package/dist/ui/tools/OutlookReplyTool/index.js +0 -49
- package/dist/ui/tools/OutlookSendTool/index.js +0 -49
- package/dist/ui/tools/PauseDelegationTool/index.js +0 -35
- package/dist/ui/tools/ProbeTool/index.js +0 -121
- package/dist/ui/tools/ProcessTool/index.js +0 -66
- package/dist/ui/tools/QuestListTool/index.js +0 -46
- package/dist/ui/tools/QuestReportTool/index.js +0 -49
- package/dist/ui/tools/QuestUpdateTool/index.js +0 -87
- package/dist/ui/tools/QuipCommentTool/index.js +0 -69
- package/dist/ui/tools/QuipReadTool/index.js +0 -71
- package/dist/ui/tools/RestArionTool/index.js +0 -32
- package/dist/ui/tools/RestartTool/index.js +0 -35
- package/dist/ui/tools/ResumeDelegationTool/index.js +0 -35
- package/dist/ui/tools/RetireArionTool/index.js +0 -32
- package/dist/ui/tools/RgTool/index.js +0 -73
- package/dist/ui/tools/SearchKnowledgeTool/index.js +0 -43
- package/dist/ui/tools/SearchMessagesTool/index.js +0 -43
- package/dist/ui/tools/SelfDiagnoseTool/index.js +0 -61
- package/dist/ui/tools/SendMessageTool/index.js +0 -45
- package/dist/ui/tools/SerenaTool/index.js +0 -124
- package/dist/ui/tools/SessionHistoryTool/index.js +0 -52
- package/dist/ui/tools/SgTool/index.js +0 -80
- package/dist/ui/tools/SlackReactTool/index.js +0 -41
- package/dist/ui/tools/SlackReadTool/index.js +0 -48
- package/dist/ui/tools/SlackSendTool/index.js +0 -45
- package/dist/ui/tools/SpawnWorkerTool/index.js +0 -33
- package/dist/ui/tools/StickerRequestTool/index.js +0 -19
- package/dist/ui/tools/ThinkTool/index.js +0 -17
- package/dist/ui/tools/UgTool/index.js +0 -108
- package/dist/ui/tools/UseSkillTool/index.js +0 -22
- package/dist/ui/tools/WakeArionTool/index.js +0 -32
- package/dist/ui/tools/WebFetchTool/index.js +0 -56
- package/dist/ui/tools/WebSearchTool/index.js +0 -44
- package/dist/ui/tools/lsTool/index.js +0 -58
- package/dist/ui/tools/registry.js +0 -197
- package/dist/ui/tools/tool-renderer.js +0 -11
- package/dist/ui/tools/truncation.js +0 -35
- package/dist/ui/types/anthropic.js +0 -4
- package/dist/ui/types/index.js +0 -2
- package/dist/ui/types/message.js +0 -3
- package/dist/ui/types/tool.js +0 -4
- package/dist/ui/utils/array.js +0 -4
- package/dist/ui/utils/cursor.js +0 -131
- package/dist/ui/utils/diff.js +0 -120
- package/dist/ui/utils/format.js +0 -42
- package/dist/ui/utils/fuzzy.js +0 -59
- package/dist/ui/utils/index.js +0 -11
- package/dist/ui/utils/keys.js +0 -8
- package/dist/ui/utils/patch.js +0 -17
- package/dist/ui/utils/risk.js +0 -114
- package/dist/ui/utils/terminal-image.js +0 -70
- package/dist/ui/utils/validation.js +0 -48
- package/dist/ui/verb-pairs.js +0 -248
- package/dist/ui.js +0 -131
- package/src/entrypoints/command-mode.ts +0 -5
- package/src/entrypoints/daemon.ts +0 -54
- package/src/entrypoints/headless-stdio.ts +0 -27
- package/src/entrypoints/interactive.ts +0 -112
- package/src/main.ts +0 -72
- package/src/runtime/configure-bun-sqlite.ts +0 -3
- package/src/runtime/crash-handlers.ts +0 -128
- package/src/runtime/interactive-invocation.test.ts +0 -42
- package/src/runtime/interactive-invocation.ts +0 -51
- package/src/runtime/internal-mode.test.ts +0 -19
- package/src/runtime/internal-mode.ts +0 -24
- package/src/runtime/launch-spec.test.ts +0 -26
- package/src/runtime/launch-spec.ts +0 -84
- package/src/runtime/owner-lease.ts +0 -52
- package/src/runtime/public-mode.test.ts +0 -18
- package/src/runtime/public-mode.ts +0 -19
- package/src/runtime/run-internal-mode.ts +0 -19
- package/src/runtime/runtime-kind.test.ts +0 -23
- package/src/runtime/runtime-kind.ts +0 -41
- package/src/runtime/spawn-aria.ts +0 -62
|
@@ -1,826 +0,0 @@
|
|
|
1
|
-
import * as net from "node:net";
|
|
2
|
-
import { appendFileSync, mkdirSync } from "node:fs";
|
|
3
|
-
import { join as pathJoin } from "node:path";
|
|
4
|
-
import { randomUUID } from "node:crypto";
|
|
5
|
-
import { createRuntimeSocketAttachedLocalControlClient, createRuntimeSocketLocalControlClient, createTrustedRuntimeError, AttachedClientLeaseGrantSchema, LocalControlSocketAttachClientRequestSchema, LocalControlSocketAttachClientResponseSchema, LocalControlSocketRequestSchema, LocalControlSocketResponseSchema, } from "@aria-cli/tools";
|
|
6
|
-
import { assertAttachableRuntimeBootstrap } from "./local-control-bootstrap.js";
|
|
7
|
-
import { reportTrustedLocalControlFailure } from "./local-control-error-reporting.js";
|
|
8
|
-
async function sleep(ms) {
|
|
9
|
-
await new Promise((resolve) => setTimeout(resolve, Math.max(ms, 0)));
|
|
10
|
-
}
|
|
11
|
-
const EXISTING_RUNTIME_ATTACH_TIMEOUT_MS = 10_000;
|
|
12
|
-
const EXISTING_RUNTIME_ATTACH_RETRY_MS = 250;
|
|
13
|
-
let hostSupervisorModulePromise;
|
|
14
|
-
let nodeMetadataModulePromise;
|
|
15
|
-
let nodeStoreModulePromise;
|
|
16
|
-
function loadHostSupervisorModule() {
|
|
17
|
-
hostSupervisorModulePromise ??= import("@aria-cli/server/runtime/host-supervisor");
|
|
18
|
-
return hostSupervisorModulePromise;
|
|
19
|
-
}
|
|
20
|
-
function loadNodeMetadataModule() {
|
|
21
|
-
nodeMetadataModulePromise ??= import("@aria-cli/server/runtime/node-metadata");
|
|
22
|
-
return nodeMetadataModulePromise;
|
|
23
|
-
}
|
|
24
|
-
function loadNodeStoreModule() {
|
|
25
|
-
nodeStoreModulePromise ??= import("@aria-cli/server/runtime/node-store");
|
|
26
|
-
return nodeStoreModulePromise;
|
|
27
|
-
}
|
|
28
|
-
async function resolveOrCreateLocalNode(ariaHome) {
|
|
29
|
-
const { resolveOrCreateNode } = await loadNodeMetadataModule();
|
|
30
|
-
return resolveOrCreateNode({ ariaHome });
|
|
31
|
-
}
|
|
32
|
-
async function resolveLocalRuntimeOwnerRecord(ariaHome, nodeId) {
|
|
33
|
-
const { NodeStore } = await loadNodeStoreModule();
|
|
34
|
-
const nodeStore = new NodeStore({ ariaHome });
|
|
35
|
-
try {
|
|
36
|
-
return nodeStore.readRuntimeOwnerRecord(nodeId);
|
|
37
|
-
}
|
|
38
|
-
finally {
|
|
39
|
-
nodeStore.close();
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
function isPublishedRuntimeUnavailableError(error) {
|
|
43
|
-
if (!(error instanceof Error)) {
|
|
44
|
-
return false;
|
|
45
|
-
}
|
|
46
|
-
const message = error.message;
|
|
47
|
-
return (message.includes("connect ENOENT") ||
|
|
48
|
-
message.includes("ECONNREFUSED") ||
|
|
49
|
-
message.includes("Missing runtime owner record") ||
|
|
50
|
-
message.includes("not attachable") ||
|
|
51
|
-
message.includes("not control-ready"));
|
|
52
|
-
}
|
|
53
|
-
async function connectToPublishedRuntimeSocket(options) {
|
|
54
|
-
const ownerRecord = await resolveLocalRuntimeOwnerRecord(options.ariaHome, options.nodeId);
|
|
55
|
-
if (!ownerRecord) {
|
|
56
|
-
throw new Error(`[local-control-client] Missing runtime owner record for node ${options.nodeId}`);
|
|
57
|
-
}
|
|
58
|
-
const control = createSocketLocalControlClient({
|
|
59
|
-
runtimeSocket: ownerRecord.runtimeSocket,
|
|
60
|
-
pollIntervalMs: options.pollIntervalMs,
|
|
61
|
-
});
|
|
62
|
-
const bootstrap = assertAttachableRuntimeBootstrap(options.nodeId, await control.getRuntimeBootstrap());
|
|
63
|
-
let externalClientId;
|
|
64
|
-
let externalClientAuthToken;
|
|
65
|
-
let resilientClient;
|
|
66
|
-
if (options.clientKind) {
|
|
67
|
-
resilientClient = await createResilientAttachedClient({
|
|
68
|
-
runtimeSocket: ownerRecord.runtimeSocket,
|
|
69
|
-
clientKind: options.clientKind,
|
|
70
|
-
logDir: pathJoin(options.ariaHome, "logs"),
|
|
71
|
-
});
|
|
72
|
-
externalClientId = resilientClient.getClientId();
|
|
73
|
-
externalClientAuthToken = resilientClient.getClientAuthToken();
|
|
74
|
-
}
|
|
75
|
-
let releasePromise = null;
|
|
76
|
-
return {
|
|
77
|
-
nodeId: options.nodeId,
|
|
78
|
-
runtimeId: bootstrap.runtimeId,
|
|
79
|
-
port: bootstrap.controlEndpoint.port,
|
|
80
|
-
ownership: options.ownership,
|
|
81
|
-
...(externalClientId ? { attachedClientId: externalClientId } : {}),
|
|
82
|
-
...(externalClientAuthToken ? { attachedClientAuthToken: externalClientAuthToken } : {}),
|
|
83
|
-
control: resilientClient ? resilientClient.api : control,
|
|
84
|
-
getConnectionState: resilientClient
|
|
85
|
-
? (() => {
|
|
86
|
-
const rc = resilientClient;
|
|
87
|
-
return () => rc.getState();
|
|
88
|
-
})()
|
|
89
|
-
: undefined,
|
|
90
|
-
onConnectionStateChange: resilientClient
|
|
91
|
-
? (() => {
|
|
92
|
-
const rc = resilientClient;
|
|
93
|
-
return (listener) => rc.onStateChange(listener);
|
|
94
|
-
})()
|
|
95
|
-
: undefined,
|
|
96
|
-
release: async (releaseOptions) => {
|
|
97
|
-
if (releasePromise) {
|
|
98
|
-
return releasePromise;
|
|
99
|
-
}
|
|
100
|
-
releasePromise = (async () => {
|
|
101
|
-
const rc = resilientClient;
|
|
102
|
-
resilientClient = undefined;
|
|
103
|
-
externalClientId = undefined;
|
|
104
|
-
externalClientAuthToken = undefined;
|
|
105
|
-
await rc?.release().catch(() => { });
|
|
106
|
-
await options.release(releaseOptions);
|
|
107
|
-
})();
|
|
108
|
-
return releasePromise;
|
|
109
|
-
},
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
export async function requestRuntimeSocketLease(runtimeSocket, payload) {
|
|
113
|
-
const request = LocalControlSocketRequestSchema.parse({
|
|
114
|
-
id: randomUUID(),
|
|
115
|
-
method: "attachClient",
|
|
116
|
-
payload,
|
|
117
|
-
});
|
|
118
|
-
return new Promise((resolve, reject) => {
|
|
119
|
-
const socket = net.createConnection(runtimeSocket);
|
|
120
|
-
let buffer = "";
|
|
121
|
-
let settled = false;
|
|
122
|
-
let closed = false;
|
|
123
|
-
let closeResolve;
|
|
124
|
-
const closePromise = new Promise((resolveClose) => {
|
|
125
|
-
closeResolve = resolveClose;
|
|
126
|
-
});
|
|
127
|
-
const markClosed = () => {
|
|
128
|
-
if (closed) {
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
closed = true;
|
|
132
|
-
closeResolve?.();
|
|
133
|
-
if (!settled) {
|
|
134
|
-
settled = true;
|
|
135
|
-
reject(new Error("Local control socket closed before establishing an attached-client lease"));
|
|
136
|
-
}
|
|
137
|
-
};
|
|
138
|
-
socket.setEncoding("utf8");
|
|
139
|
-
socket.once("connect", () => {
|
|
140
|
-
socket.write(`${JSON.stringify(request)}\n`);
|
|
141
|
-
});
|
|
142
|
-
socket.once("error", (error) => {
|
|
143
|
-
if (!settled) {
|
|
144
|
-
settled = true;
|
|
145
|
-
reject(error);
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
markClosed();
|
|
149
|
-
});
|
|
150
|
-
socket.once("end", markClosed);
|
|
151
|
-
socket.once("close", markClosed);
|
|
152
|
-
socket.on("data", async (chunk) => {
|
|
153
|
-
buffer += chunk;
|
|
154
|
-
const newlineIndex = buffer.indexOf("\n");
|
|
155
|
-
if (newlineIndex === -1 || settled) {
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
try {
|
|
159
|
-
const response = LocalControlSocketResponseSchema.parse(JSON.parse(buffer.slice(0, newlineIndex)));
|
|
160
|
-
if (response.id !== request.id) {
|
|
161
|
-
settled = true;
|
|
162
|
-
socket.destroy();
|
|
163
|
-
reject(new Error("Local control socket response ID mismatch"));
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
if (!response.ok) {
|
|
167
|
-
const trustedError = createTrustedRuntimeError(response.error, response.diagnostic);
|
|
168
|
-
await reportTrustedLocalControlFailure(trustedError, {
|
|
169
|
-
endpoint: "attachClient",
|
|
170
|
-
responseStatus: null,
|
|
171
|
-
payload: response,
|
|
172
|
-
transport: "runtime_socket_attach",
|
|
173
|
-
});
|
|
174
|
-
settled = true;
|
|
175
|
-
socket.destroy();
|
|
176
|
-
reject(trustedError);
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
const attached = AttachedClientLeaseGrantSchema.parse(LocalControlSocketAttachClientResponseSchema.parse(response.payload));
|
|
180
|
-
settled = true;
|
|
181
|
-
resolve({
|
|
182
|
-
clientId: attached.clientId,
|
|
183
|
-
clientAuthToken: attached.clientAuthToken,
|
|
184
|
-
release: async () => {
|
|
185
|
-
if (!closed) {
|
|
186
|
-
socket.destroy();
|
|
187
|
-
}
|
|
188
|
-
await closePromise;
|
|
189
|
-
},
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
catch (error) {
|
|
193
|
-
settled = true;
|
|
194
|
-
socket.destroy();
|
|
195
|
-
reject(error);
|
|
196
|
-
}
|
|
197
|
-
});
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
export async function attachExistingLocalControlClient(options) {
|
|
201
|
-
const resolvedNode = await resolveOrCreateLocalNode(options.ariaHome);
|
|
202
|
-
if (!(await resolveLocalRuntimeOwnerRecord(options.ariaHome, resolvedNode.nodeId))) {
|
|
203
|
-
return null;
|
|
204
|
-
}
|
|
205
|
-
const deadline = Date.now() + EXISTING_RUNTIME_ATTACH_TIMEOUT_MS;
|
|
206
|
-
while (Date.now() < deadline) {
|
|
207
|
-
try {
|
|
208
|
-
return await connectToPublishedRuntimeSocket({
|
|
209
|
-
ariaHome: options.ariaHome,
|
|
210
|
-
nodeId: resolvedNode.nodeId,
|
|
211
|
-
ownership: "reattached",
|
|
212
|
-
release: async () => { },
|
|
213
|
-
pollIntervalMs: options.pollIntervalMs,
|
|
214
|
-
clientKind: options.clientKind,
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
catch (error) {
|
|
218
|
-
if (!isPublishedRuntimeUnavailableError(error)) {
|
|
219
|
-
throw error;
|
|
220
|
-
}
|
|
221
|
-
if (!(await resolveLocalRuntimeOwnerRecord(options.ariaHome, resolvedNode.nodeId))) {
|
|
222
|
-
return null;
|
|
223
|
-
}
|
|
224
|
-
await sleep(EXISTING_RUNTIME_ATTACH_RETRY_MS);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
return null;
|
|
228
|
-
}
|
|
229
|
-
function createSocketLocalControlClient(options) {
|
|
230
|
-
if (options.auth) {
|
|
231
|
-
return createRuntimeSocketAttachedLocalControlClient({
|
|
232
|
-
runtimeSocket: options.runtimeSocket,
|
|
233
|
-
pollIntervalMs: options.pollIntervalMs,
|
|
234
|
-
auth: options.auth,
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
return createRuntimeSocketLocalControlClient(options);
|
|
238
|
-
}
|
|
239
|
-
export async function attachLocalControlClient(options) {
|
|
240
|
-
const attachToPublishedRuntime = async (nodeId, clientKind) => {
|
|
241
|
-
const retryIntervalMs = options.pollIntervalMs ?? 100;
|
|
242
|
-
const deadline = Date.now() + Math.max(retryIntervalMs * 20, 2_000);
|
|
243
|
-
let lastError = null;
|
|
244
|
-
while (Date.now() < deadline) {
|
|
245
|
-
if (options.signal?.aborted) {
|
|
246
|
-
throw new Error(`[local-control-client] Attach-only client ${clientKind} aborted while waiting for a live runtime owner on node ${nodeId}`);
|
|
247
|
-
}
|
|
248
|
-
try {
|
|
249
|
-
const ownerRecord = await resolveLocalRuntimeOwnerRecord(options.ariaHome, nodeId);
|
|
250
|
-
if (!ownerRecord) {
|
|
251
|
-
lastError = new Error(`[local-control-client] No live runtime owner available for attach-only client ${clientKind} on node ${nodeId}`);
|
|
252
|
-
}
|
|
253
|
-
else {
|
|
254
|
-
return await connectToPublishedRuntimeSocket({
|
|
255
|
-
ariaHome: options.ariaHome,
|
|
256
|
-
nodeId,
|
|
257
|
-
ownership: "reattached",
|
|
258
|
-
release: async () => { },
|
|
259
|
-
pollIntervalMs: options.pollIntervalMs,
|
|
260
|
-
clientKind,
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
catch (error) {
|
|
265
|
-
if (!isPublishedRuntimeUnavailableError(error)) {
|
|
266
|
-
throw error;
|
|
267
|
-
}
|
|
268
|
-
lastError = error;
|
|
269
|
-
}
|
|
270
|
-
await sleep(retryIntervalMs);
|
|
271
|
-
}
|
|
272
|
-
throw (lastError ??
|
|
273
|
-
new Error(`[local-control-client] No live runtime owner available for attach-only client ${clientKind} on node ${nodeId}`));
|
|
274
|
-
};
|
|
275
|
-
const { getHostSupervisor, HostSupervisorSplitBrainError } = await loadHostSupervisorModule();
|
|
276
|
-
const supervisor = getHostSupervisor();
|
|
277
|
-
let attachment;
|
|
278
|
-
try {
|
|
279
|
-
attachment = await supervisor.attach({
|
|
280
|
-
ariaHome: options.ariaHome,
|
|
281
|
-
arionName: options.arionName,
|
|
282
|
-
clientKind: options.clientKind,
|
|
283
|
-
memoriaFactory: options.memoriaFactory,
|
|
284
|
-
router: options.router,
|
|
285
|
-
authResolver: options.authResolver,
|
|
286
|
-
runSessionConfig: options.runSessionConfig,
|
|
287
|
-
mcpServers: options.mcpServers,
|
|
288
|
-
daemonSafetyPolicy: options.daemonSafetyPolicy,
|
|
289
|
-
autonomousIntervalMs: options.autonomousIntervalMs,
|
|
290
|
-
signal: options.signal,
|
|
291
|
-
silent: options.silent,
|
|
292
|
-
runtimeLifecycle: options.runtimeLifecycle,
|
|
293
|
-
port: options.port,
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
catch (error) {
|
|
297
|
-
if (error instanceof HostSupervisorSplitBrainError) {
|
|
298
|
-
return connectToPublishedRuntimeSocket({
|
|
299
|
-
ariaHome: options.ariaHome,
|
|
300
|
-
nodeId: error.nodeId,
|
|
301
|
-
ownership: "reattached",
|
|
302
|
-
release: async () => { },
|
|
303
|
-
pollIntervalMs: options.pollIntervalMs,
|
|
304
|
-
clientKind: options.clientKind,
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
throw error;
|
|
308
|
-
}
|
|
309
|
-
try {
|
|
310
|
-
await attachment.runtime.waitForInitialDiscovery();
|
|
311
|
-
return await connectToPublishedRuntimeSocket({
|
|
312
|
-
ariaHome: options.ariaHome,
|
|
313
|
-
nodeId: attachment.nodeId,
|
|
314
|
-
ownership: attachment.ownership,
|
|
315
|
-
pollIntervalMs: options.pollIntervalMs,
|
|
316
|
-
clientKind: options.clientKind,
|
|
317
|
-
release: (releaseOptions) => attachment.release(releaseOptions),
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
catch (error) {
|
|
321
|
-
await attachment.release();
|
|
322
|
-
throw error;
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
export async function resolveLocalControlClient(options) {
|
|
326
|
-
const resolvedNode = await resolveOrCreateLocalNode(options.ariaHome);
|
|
327
|
-
try {
|
|
328
|
-
const attached = await connectToPublishedRuntimeSocket({
|
|
329
|
-
ariaHome: options.ariaHome,
|
|
330
|
-
nodeId: resolvedNode.nodeId,
|
|
331
|
-
ownership: "reattached",
|
|
332
|
-
release: async () => { },
|
|
333
|
-
pollIntervalMs: options.pollIntervalMs,
|
|
334
|
-
});
|
|
335
|
-
return attached.control;
|
|
336
|
-
}
|
|
337
|
-
catch (error) {
|
|
338
|
-
if (isPublishedRuntimeUnavailableError(error)) {
|
|
339
|
-
return null;
|
|
340
|
-
}
|
|
341
|
-
throw error;
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
export function resolveLocalControlClientSync(options) {
|
|
345
|
-
void options;
|
|
346
|
-
// Published runtime liveness is an async property established by a bootstrap round-trip.
|
|
347
|
-
// A synchronous raw-socket fallback would recreate the stale-owner bypass.
|
|
348
|
-
return null;
|
|
349
|
-
}
|
|
350
|
-
// ---------------------------------------------------------------------------
|
|
351
|
-
// Resilient attached client — THE canonical way to get a client that survives
|
|
352
|
-
// daemon restarts. Fuses lease acquisition, auth, reattach, background
|
|
353
|
-
// reconnection, and diagnostic logging at the lowest level so callers cannot
|
|
354
|
-
// get it wrong.
|
|
355
|
-
//
|
|
356
|
-
// Design (modeled after ioredis / node-redis / gRPC channel semantics):
|
|
357
|
-
//
|
|
358
|
-
// 1. **Background reconnect loop** — runs forever with capped exponential
|
|
359
|
-
// backoff + jitter. As soon as the daemon is reachable the loop
|
|
360
|
-
// acquires a fresh lease and flips the state to `connected`.
|
|
361
|
-
//
|
|
362
|
-
// 2. **Connection state** — `connected | disconnected | reconnecting`
|
|
363
|
-
// with an EventEmitter so the REPL / overlay can react in real time.
|
|
364
|
-
//
|
|
365
|
-
// 3. **Per-call waiting** — when the connection is down a call does NOT
|
|
366
|
-
// fail immediately. It waits for the background loop to reconnect
|
|
367
|
-
// (up to `callTimeoutMs`, default 5 minutes) then retries. This
|
|
368
|
-
// means a daemon restart that takes 1-2 minutes is fully transparent.
|
|
369
|
-
//
|
|
370
|
-
// 4. **Jitter** — prevents thundering-herd reconnect storms when many
|
|
371
|
-
// clients try to reconnect at the same time.
|
|
372
|
-
// ---------------------------------------------------------------------------
|
|
373
|
-
/** Backoff parameters (all in milliseconds). */
|
|
374
|
-
const INITIAL_BACKOFF_MS = 250;
|
|
375
|
-
const MAX_BACKOFF_MS = 30_000;
|
|
376
|
-
const DEFAULT_CALL_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
377
|
-
/**
|
|
378
|
-
* Methods whose streams are safe to re-issue after reconnect (subscriptions).
|
|
379
|
-
* Streams NOT in this set are treated as non-idempotent executions and will
|
|
380
|
-
* NOT be silently restarted — the error propagates so callers can handle it.
|
|
381
|
-
*/
|
|
382
|
-
const RESUBSCRIBABLE_STREAMS = new Set([
|
|
383
|
-
"subscribeRuntimeEvents",
|
|
384
|
-
"subscribeInbox",
|
|
385
|
-
"subscribePeers",
|
|
386
|
-
"subscribeDirectClientInbox",
|
|
387
|
-
]);
|
|
388
|
-
function isRecoverableError(error) {
|
|
389
|
-
if (!(error instanceof Error))
|
|
390
|
-
return false;
|
|
391
|
-
const msg = error.message;
|
|
392
|
-
return (msg.includes("attached-local-client-only") ||
|
|
393
|
-
msg.includes("ECONNRESET") ||
|
|
394
|
-
msg.includes("ECONNREFUSED") ||
|
|
395
|
-
msg.includes("EPIPE") ||
|
|
396
|
-
msg.includes("connect ENOENT") ||
|
|
397
|
-
msg.includes("socket hang up") ||
|
|
398
|
-
msg.includes("closed before"));
|
|
399
|
-
}
|
|
400
|
-
function jitter(ms) {
|
|
401
|
-
return ms + Math.floor(Math.random() * ms * 0.3);
|
|
402
|
-
}
|
|
403
|
-
function backoffSleep(ms) {
|
|
404
|
-
if (ms <= 0)
|
|
405
|
-
return Promise.resolve();
|
|
406
|
-
return new Promise((resolve) => {
|
|
407
|
-
const timer = setTimeout(resolve, ms);
|
|
408
|
-
timer.unref?.();
|
|
409
|
-
});
|
|
410
|
-
}
|
|
411
|
-
/**
|
|
412
|
-
* Acquire a one-shot lease from the daemon socket. Returns credentials
|
|
413
|
-
* without creating a background reconnect loop. Use this when you only
|
|
414
|
-
* need clientId/token (e.g. for resolveCredentials) but do NOT need a
|
|
415
|
-
* long-lived resilient client.
|
|
416
|
-
*/
|
|
417
|
-
export async function requestOneShotLease(runtimeSocket, clientKind = "local-api") {
|
|
418
|
-
const lease = await requestRuntimeSocketLease(runtimeSocket, LocalControlSocketAttachClientRequestSchema.parse({
|
|
419
|
-
clientKind,
|
|
420
|
-
lease: true,
|
|
421
|
-
pid: process.pid,
|
|
422
|
-
}));
|
|
423
|
-
return {
|
|
424
|
-
clientId: lease.clientId,
|
|
425
|
-
clientAuthToken: lease.clientAuthToken,
|
|
426
|
-
release: () => lease.release(),
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
|
-
/**
|
|
430
|
-
* Creates a self-healing AttachedLocalControlApi that survives daemon
|
|
431
|
-
* restarts — including long outages of minutes or hours.
|
|
432
|
-
*
|
|
433
|
-
* Architecture:
|
|
434
|
-
*
|
|
435
|
-
* ┌────────────────────────────────┐
|
|
436
|
-
* │ Background Reconnect Loop │ runs forever until release()
|
|
437
|
-
* │ exponential backoff + jitter │ capped at 30 s
|
|
438
|
-
* │ acquires fresh lease on │
|
|
439
|
-
* │ success → state = connected │
|
|
440
|
-
* └──────────────┬─────────────────┘
|
|
441
|
-
* │
|
|
442
|
-
* ▼
|
|
443
|
-
* ┌────────────────────────────────┐
|
|
444
|
-
* │ Per-Call Path │
|
|
445
|
-
* │ connected → execute │
|
|
446
|
-
* │ disconnected → wait for │
|
|
447
|
-
* │ reconnect (up to 5 min) │
|
|
448
|
-
* │ then retry │
|
|
449
|
-
* └────────────────────────────────┘
|
|
450
|
-
*/
|
|
451
|
-
export async function createResilientAttachedClient(options) {
|
|
452
|
-
const { runtimeSocket, clientKind = "local-api" } = options;
|
|
453
|
-
const callTimeoutMs = options.callTimeoutMs ?? DEFAULT_CALL_TIMEOUT_MS;
|
|
454
|
-
let released = false;
|
|
455
|
-
let state = "disconnected";
|
|
456
|
-
/** Internal flag: true when the reconnect loop should attempt reconnection
|
|
457
|
-
* even though the external `state` has not been changed to `disconnected`.
|
|
458
|
-
* This lets transient errors trigger reconnection without flashing the UI. */
|
|
459
|
-
let needsReconnect = false;
|
|
460
|
-
let currentClientId;
|
|
461
|
-
let currentAuthToken;
|
|
462
|
-
let inner = null;
|
|
463
|
-
let currentReleaseLease = null;
|
|
464
|
-
// --- State management + listeners ---
|
|
465
|
-
const listeners = new Set();
|
|
466
|
-
function setState(next) {
|
|
467
|
-
if (next === state)
|
|
468
|
-
return;
|
|
469
|
-
state = next;
|
|
470
|
-
for (const fn of listeners) {
|
|
471
|
-
try {
|
|
472
|
-
fn(next);
|
|
473
|
-
}
|
|
474
|
-
catch {
|
|
475
|
-
/* never break the client */
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
// --- "connected" promise that per-call waiters block on ---
|
|
480
|
-
// Resolves every time the background loop transitions to `connected`.
|
|
481
|
-
let connectedResolve = null;
|
|
482
|
-
let connectedPromise = new Promise((r) => {
|
|
483
|
-
connectedResolve = r;
|
|
484
|
-
});
|
|
485
|
-
function signalConnected() {
|
|
486
|
-
connectedResolve?.();
|
|
487
|
-
// Reset for next disconnect cycle
|
|
488
|
-
connectedPromise = new Promise((r) => {
|
|
489
|
-
connectedResolve = r;
|
|
490
|
-
});
|
|
491
|
-
}
|
|
492
|
-
// --- Logging ---
|
|
493
|
-
const log = (event) => {
|
|
494
|
-
if (!options.logDir)
|
|
495
|
-
return;
|
|
496
|
-
try {
|
|
497
|
-
mkdirSync(options.logDir, { recursive: true });
|
|
498
|
-
appendFileSync(pathJoin(options.logDir, "reattach.jsonl"), JSON.stringify({ ts: new Date().toISOString(), pid: process.pid, ...event }) + "\n");
|
|
499
|
-
}
|
|
500
|
-
catch {
|
|
501
|
-
/* never break the client */
|
|
502
|
-
}
|
|
503
|
-
};
|
|
504
|
-
// --- Lease acquisition ---
|
|
505
|
-
async function acquireLease() {
|
|
506
|
-
if (currentReleaseLease) {
|
|
507
|
-
await currentReleaseLease().catch(() => { });
|
|
508
|
-
currentReleaseLease = null;
|
|
509
|
-
}
|
|
510
|
-
const lease = await requestRuntimeSocketLease(runtimeSocket, LocalControlSocketAttachClientRequestSchema.parse({
|
|
511
|
-
clientKind,
|
|
512
|
-
lease: true,
|
|
513
|
-
pid: process.pid,
|
|
514
|
-
displayName: options.displayName,
|
|
515
|
-
}));
|
|
516
|
-
currentClientId = lease.clientId;
|
|
517
|
-
currentAuthToken = lease.clientAuthToken;
|
|
518
|
-
currentReleaseLease = () => lease.release();
|
|
519
|
-
inner = createRuntimeSocketAttachedLocalControlClient({
|
|
520
|
-
runtimeSocket,
|
|
521
|
-
auth: { clientId: lease.clientId, clientAuthToken: lease.clientAuthToken },
|
|
522
|
-
});
|
|
523
|
-
log({ event: "lease_acquired", clientId: lease.clientId });
|
|
524
|
-
}
|
|
525
|
-
// --- Disconnect trigger ---
|
|
526
|
-
// Event-driven: the reconnect loop sleeps on this promise instead of
|
|
527
|
-
// polling. Resolved by triggerReconnect() so the loop wakes immediately.
|
|
528
|
-
let disconnectResolve = null;
|
|
529
|
-
let disconnectPromise = new Promise((r) => {
|
|
530
|
-
disconnectResolve = r;
|
|
531
|
-
});
|
|
532
|
-
// Release signal: resolved by release() to unblock all pending waiters.
|
|
533
|
-
// The .catch() suppresses the unhandled-rejection when no waiter is active.
|
|
534
|
-
let releaseReject = null;
|
|
535
|
-
const releasePromise = new Promise((_, reject) => {
|
|
536
|
-
releaseReject = (err) => reject(err);
|
|
537
|
-
});
|
|
538
|
-
releasePromise.catch(() => { });
|
|
539
|
-
/**
|
|
540
|
-
* Internal: wake the background reconnect loop. Does NOT immediately
|
|
541
|
-
* change external state — the reconnect loop decides when to surface
|
|
542
|
-
* `disconnected` vs `reconnecting` based on whether fast-reattach
|
|
543
|
-
* succeeds or fails. This prevents transient blips from flashing
|
|
544
|
-
* "disconnected" in the UI.
|
|
545
|
-
*/
|
|
546
|
-
function triggerReconnect() {
|
|
547
|
-
disconnectResolve?.();
|
|
548
|
-
disconnectPromise = new Promise((r) => {
|
|
549
|
-
disconnectResolve = r;
|
|
550
|
-
});
|
|
551
|
-
}
|
|
552
|
-
/**
|
|
553
|
-
* Try to acquire a fresh lease immediately (fast-path recovery for
|
|
554
|
-
* transient errors). Returns true if successful. On failure it wakes
|
|
555
|
-
* the background reconnect loop but does NOT change external state —
|
|
556
|
-
* that is the reconnect loop's responsibility. This prevents brief
|
|
557
|
-
* transient errors from flashing "disconnected" in the UI when the
|
|
558
|
-
* daemon is actually fine.
|
|
559
|
-
*/
|
|
560
|
-
async function tryFastReattach() {
|
|
561
|
-
try {
|
|
562
|
-
await acquireLease();
|
|
563
|
-
needsReconnect = false;
|
|
564
|
-
log({ event: "fast_reattach_success" });
|
|
565
|
-
return true;
|
|
566
|
-
}
|
|
567
|
-
catch (error) {
|
|
568
|
-
log({ event: "fast_reattach_failed", error: error.message });
|
|
569
|
-
// Set the internal flag so the reconnect loop knows to attempt
|
|
570
|
-
// reconnection, then wake it. External state is NOT changed here —
|
|
571
|
-
// the reconnect loop decides when to surface "disconnected" to the UI.
|
|
572
|
-
needsReconnect = true;
|
|
573
|
-
triggerReconnect();
|
|
574
|
-
return false;
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
// --- Background reconnect loop ---
|
|
578
|
-
// Runs forever. Sleeps on disconnectPromise while connected (no polling).
|
|
579
|
-
// When disconnected it retries with capped exponential backoff + jitter
|
|
580
|
-
// until the daemon comes back or the client is released/aborted.
|
|
581
|
-
async function reconnectLoop() {
|
|
582
|
-
let backoff = INITIAL_BACKOFF_MS;
|
|
583
|
-
while (!released && !options.signal?.aborted) {
|
|
584
|
-
if (state === "connected" && !needsReconnect) {
|
|
585
|
-
// Event-driven wait — no polling. Wakes only when triggerReconnect()
|
|
586
|
-
// is called or the abort signal fires.
|
|
587
|
-
const loopAc = new AbortController();
|
|
588
|
-
try {
|
|
589
|
-
await Promise.race([
|
|
590
|
-
disconnectPromise,
|
|
591
|
-
...(options.signal
|
|
592
|
-
? [
|
|
593
|
-
new Promise((r) => {
|
|
594
|
-
options.signal.addEventListener("abort", () => r(), {
|
|
595
|
-
once: true,
|
|
596
|
-
signal: loopAc.signal,
|
|
597
|
-
});
|
|
598
|
-
}),
|
|
599
|
-
]
|
|
600
|
-
: []),
|
|
601
|
-
]);
|
|
602
|
-
}
|
|
603
|
-
finally {
|
|
604
|
-
loopAc.abort();
|
|
605
|
-
}
|
|
606
|
-
continue;
|
|
607
|
-
}
|
|
608
|
-
// Attempt reconnection. Don't change external state to
|
|
609
|
-
// "reconnecting" / "disconnected" on the first attempt — give the
|
|
610
|
-
// daemon a chance to respond before alarming the UI.
|
|
611
|
-
const firstAttempt = state === "connected" && needsReconnect;
|
|
612
|
-
if (!firstAttempt) {
|
|
613
|
-
setState("reconnecting");
|
|
614
|
-
}
|
|
615
|
-
try {
|
|
616
|
-
await acquireLease();
|
|
617
|
-
backoff = INITIAL_BACKOFF_MS;
|
|
618
|
-
needsReconnect = false;
|
|
619
|
-
setState("connected");
|
|
620
|
-
signalConnected();
|
|
621
|
-
log({ event: "reconnect_success" });
|
|
622
|
-
}
|
|
623
|
-
catch (error) {
|
|
624
|
-
const delay = jitter(backoff);
|
|
625
|
-
log({
|
|
626
|
-
event: "reconnect_failed",
|
|
627
|
-
error: error.message,
|
|
628
|
-
nextRetryMs: delay,
|
|
629
|
-
});
|
|
630
|
-
// NOW surface the disconnect — reconnect genuinely failed.
|
|
631
|
-
needsReconnect = true;
|
|
632
|
-
setState("disconnected");
|
|
633
|
-
backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
|
|
634
|
-
await backoffSleep(delay);
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
// --- Per-call waiting ---
|
|
639
|
-
// If disconnected, wait for the background loop to reconnect (up to
|
|
640
|
-
// callTimeoutMs). Supports AbortSignal for cancellation.
|
|
641
|
-
// All racing promises are cleaned up after the race resolves to prevent
|
|
642
|
-
// timer leaks and unhandled rejections.
|
|
643
|
-
async function waitForConnection(perCallSignal) {
|
|
644
|
-
if (state === "connected" && !needsReconnect)
|
|
645
|
-
return;
|
|
646
|
-
if (released)
|
|
647
|
-
throw new Error("ResilientAttachedClient has been released");
|
|
648
|
-
if (options.signal?.aborted)
|
|
649
|
-
throw new Error("ResilientAttachedClient aborted");
|
|
650
|
-
if (perCallSignal?.aborted)
|
|
651
|
-
throw new Error("Call cancelled");
|
|
652
|
-
// Cleanup handles so we can teardown all racing promises after the race.
|
|
653
|
-
let timeoutTimer = null;
|
|
654
|
-
const ac = new AbortController(); // internal signal to clean up listeners
|
|
655
|
-
try {
|
|
656
|
-
const racers = [connectedPromise, releasePromise];
|
|
657
|
-
// Per-call timeout
|
|
658
|
-
racers.push(new Promise((_, reject) => {
|
|
659
|
-
timeoutTimer = setTimeout(() => reject(new Error("Timed out waiting for daemon reconnection")), callTimeoutMs);
|
|
660
|
-
timeoutTimer.unref?.();
|
|
661
|
-
}));
|
|
662
|
-
// Client-level abort signal
|
|
663
|
-
if (options.signal) {
|
|
664
|
-
racers.push(new Promise((_, reject) => {
|
|
665
|
-
const onAbort = () => reject(new Error("ResilientAttachedClient aborted"));
|
|
666
|
-
options.signal.addEventListener("abort", onAbort, { once: true, signal: ac.signal });
|
|
667
|
-
}));
|
|
668
|
-
}
|
|
669
|
-
// Per-call abort signal
|
|
670
|
-
if (perCallSignal) {
|
|
671
|
-
racers.push(new Promise((_, reject) => {
|
|
672
|
-
const onAbort = () => reject(new Error("Call cancelled"));
|
|
673
|
-
perCallSignal.addEventListener("abort", onAbort, { once: true, signal: ac.signal });
|
|
674
|
-
}));
|
|
675
|
-
}
|
|
676
|
-
await Promise.race(racers);
|
|
677
|
-
}
|
|
678
|
-
finally {
|
|
679
|
-
// Cleanup: cancel timeout and remove abort listeners to prevent leaks.
|
|
680
|
-
if (timeoutTimer !== null)
|
|
681
|
-
clearTimeout(timeoutTimer);
|
|
682
|
-
ac.abort();
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
// --- Call / stream wrappers ---
|
|
686
|
-
async function callWithReattach(methodName, fn) {
|
|
687
|
-
if (released)
|
|
688
|
-
throw new Error("ResilientAttachedClient has been released");
|
|
689
|
-
// Wait for a live connection (may resolve instantly or wait minutes)
|
|
690
|
-
await waitForConnection();
|
|
691
|
-
try {
|
|
692
|
-
return await fn(inner);
|
|
693
|
-
}
|
|
694
|
-
catch (error) {
|
|
695
|
-
if (!isRecoverableError(error))
|
|
696
|
-
throw error;
|
|
697
|
-
log({
|
|
698
|
-
event: "call_failed_recoverable",
|
|
699
|
-
method: methodName,
|
|
700
|
-
error: error.message,
|
|
701
|
-
});
|
|
702
|
-
// Try fast reattach first — if it succeeds, no UI disruption.
|
|
703
|
-
if (await tryFastReattach()) {
|
|
704
|
-
// Retry with the freshly-reattached connection.
|
|
705
|
-
return await fn(inner);
|
|
706
|
-
}
|
|
707
|
-
// Fast reattach failed — state is now disconnected, background loop
|
|
708
|
-
// is running. Wait for it to reconnect then retry.
|
|
709
|
-
await waitForConnection();
|
|
710
|
-
return await fn(inner);
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
async function* streamWithReattach(methodName, fn, resubscribable) {
|
|
714
|
-
if (released)
|
|
715
|
-
throw new Error("ResilientAttachedClient has been released");
|
|
716
|
-
await waitForConnection();
|
|
717
|
-
try {
|
|
718
|
-
yield* fn(inner);
|
|
719
|
-
return;
|
|
720
|
-
}
|
|
721
|
-
catch (error) {
|
|
722
|
-
if (!isRecoverableError(error))
|
|
723
|
-
throw error;
|
|
724
|
-
log({
|
|
725
|
-
event: "stream_failed_recoverable",
|
|
726
|
-
method: methodName,
|
|
727
|
-
resubscribable,
|
|
728
|
-
error: error.message,
|
|
729
|
-
});
|
|
730
|
-
if (!resubscribable) {
|
|
731
|
-
// Non-idempotent stream (e.g. streamRun) — do NOT silently restart.
|
|
732
|
-
// Try fast reattach so the background loop is primed, then propagate
|
|
733
|
-
// the error so the caller (run orchestrator) can handle it properly
|
|
734
|
-
// via its own abort/cleanup/session-ledger mechanism.
|
|
735
|
-
await tryFastReattach();
|
|
736
|
-
throw error;
|
|
737
|
-
}
|
|
738
|
-
// Resubscribable stream (subscriptions) — try fast reattach first.
|
|
739
|
-
if (await tryFastReattach()) {
|
|
740
|
-
yield* fn(inner);
|
|
741
|
-
return;
|
|
742
|
-
}
|
|
743
|
-
// Fast reattach failed — wait for background reconnect.
|
|
744
|
-
await waitForConnection();
|
|
745
|
-
yield* fn(inner);
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
// Static set of methods that return AsyncIterable (streaming).
|
|
749
|
-
const STREAMING_METHODS = new Set([
|
|
750
|
-
"streamRun",
|
|
751
|
-
"subscribeRuntimeEvents",
|
|
752
|
-
"subscribeInbox",
|
|
753
|
-
"subscribePeers",
|
|
754
|
-
"subscribeDirectClientInbox",
|
|
755
|
-
]);
|
|
756
|
-
// Build the proxy — every method call goes through reattach logic.
|
|
757
|
-
// IMPORTANT: we must NOT call the inner method to probe its return type,
|
|
758
|
-
// because that fires a real network request that's never awaited.
|
|
759
|
-
const api = new Proxy({}, {
|
|
760
|
-
get(_target, prop) {
|
|
761
|
-
if (prop === "then" || prop === "catch" || prop === "finally")
|
|
762
|
-
return undefined;
|
|
763
|
-
// Local wrapper-only methods: NOT RPC, never forward into the daemon API.
|
|
764
|
-
if (prop === "getConnectionState") {
|
|
765
|
-
return () => state;
|
|
766
|
-
}
|
|
767
|
-
if (prop === "onConnectionStateChange") {
|
|
768
|
-
return (listener) => {
|
|
769
|
-
listeners.add(listener);
|
|
770
|
-
return () => {
|
|
771
|
-
listeners.delete(listener);
|
|
772
|
-
};
|
|
773
|
-
};
|
|
774
|
-
}
|
|
775
|
-
if (prop === "getClientId") {
|
|
776
|
-
return () => currentClientId;
|
|
777
|
-
}
|
|
778
|
-
if (prop === "getClientAuthToken") {
|
|
779
|
-
return () => currentAuthToken;
|
|
780
|
-
}
|
|
781
|
-
if (STREAMING_METHODS.has(prop)) {
|
|
782
|
-
return (...args) => streamWithReattach(prop,
|
|
783
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
784
|
-
(client) => client[prop](...args), RESUBSCRIBABLE_STREAMS.has(prop));
|
|
785
|
-
}
|
|
786
|
-
return (...args) =>
|
|
787
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
788
|
-
callWithReattach(prop, (client) => client[prop](...args));
|
|
789
|
-
},
|
|
790
|
-
});
|
|
791
|
-
// --- Bootstrap ---
|
|
792
|
-
// Acquire initial lease, then start the background loop.
|
|
793
|
-
await acquireLease();
|
|
794
|
-
setState("connected");
|
|
795
|
-
signalConnected();
|
|
796
|
-
// Fire-and-forget the background reconnect loop.
|
|
797
|
-
// It runs for the lifetime of this client.
|
|
798
|
-
void reconnectLoop();
|
|
799
|
-
return {
|
|
800
|
-
api,
|
|
801
|
-
getClientId: () => currentClientId,
|
|
802
|
-
setSessionCredentialOverlay: (input) => api.setSessionCredentialOverlay?.(input),
|
|
803
|
-
clearSessionCredentialOverlay: () => api.clearSessionCredentialOverlay?.(),
|
|
804
|
-
getClientAuthToken: () => currentAuthToken,
|
|
805
|
-
getState: () => state,
|
|
806
|
-
onStateChange: (listener) => {
|
|
807
|
-
listeners.add(listener);
|
|
808
|
-
return () => {
|
|
809
|
-
listeners.delete(listener);
|
|
810
|
-
};
|
|
811
|
-
},
|
|
812
|
-
release: async () => {
|
|
813
|
-
released = true;
|
|
814
|
-
const releaseStack = new Error("release() caller").stack;
|
|
815
|
-
if (currentReleaseLease) {
|
|
816
|
-
await currentReleaseLease().catch(() => { });
|
|
817
|
-
currentReleaseLease = null;
|
|
818
|
-
}
|
|
819
|
-
// Unblock any pending waitForConnection() calls.
|
|
820
|
-
releaseReject?.(new Error("ResilientAttachedClient has been released"));
|
|
821
|
-
setState("disconnected");
|
|
822
|
-
log({ event: "released", clientId: currentClientId, releaseStack });
|
|
823
|
-
},
|
|
824
|
-
};
|
|
825
|
-
}
|
|
826
|
-
//# sourceMappingURL=attached-local-control-client.js.map
|