handmux 0.23.0 → 0.25.0
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/README.md +31 -8
- package/README.zh-CN.md +31 -8
- package/bin/handmux.js +14 -688
- package/bin/node-version.js +20 -0
- package/dist/bin/handmux.js +1015 -0
- package/dist/connectors/bridgeClient.js +385 -0
- package/dist/connectors/claude/index.js +476 -0
- package/dist/connectors/pi/bridgeClient.js +8 -0
- package/dist/connectors/pi/index.js +851 -0
- package/dist/hooks/handmux-notify.sh +64 -0
- package/{hooks → dist/hooks}/handmux-statusline.cjs +28 -6
- package/dist/hooks/handmux-write.cjs +264 -0
- package/dist/package.json +127 -0
- package/dist/public/assets/index-CTrqTqtT.css +32 -0
- package/dist/public/assets/index-DmjeMSQA.js +346 -0
- package/dist/public/assets/pcm-worklet-CxcDy7PB.js +1 -0
- package/{public → dist/public}/index.html +5 -4
- package/dist/src/agent-runtime/adapter.js +204 -0
- package/dist/src/agent-runtime/bridge.js +1006 -0
- package/dist/src/agent-runtime/bridgeStore.js +25 -0
- package/dist/src/agent-runtime/bridgeTransport.js +619 -0
- package/dist/src/agent-runtime/bridgeTypes.js +13 -0
- package/dist/src/agent-runtime/builtinRuntime.js +193 -0
- package/dist/src/agent-runtime/conversation.js +2179 -0
- package/dist/src/agent-runtime/conversationActivation.js +75 -0
- package/dist/src/agent-runtime/conversationActivity.js +69 -0
- package/dist/src/agent-runtime/conversationControls.js +206 -0
- package/dist/src/agent-runtime/conversationLiveHub.js +276 -0
- package/dist/src/agent-runtime/conversationStore.js +352 -0
- package/dist/src/agent-runtime/conversationTypes.js +1 -0
- package/dist/src/agent-runtime/conversationValidation.js +339 -0
- package/dist/src/agent-runtime/inbox.js +973 -0
- package/dist/src/agent-runtime/inboxPushProjection.js +69 -0
- package/dist/src/agent-runtime/inboxStore.js +25 -0
- package/dist/src/agent-runtime/inboxTypes.js +1 -0
- package/dist/src/agent-runtime/interaction.js +829 -0
- package/dist/src/agent-runtime/interactionLiveHub.js +225 -0
- package/dist/src/agent-runtime/interactionStore.js +18 -0
- package/dist/src/agent-runtime/interactionTypes.js +1 -0
- package/dist/src/agent-runtime/legacyInboxProjection.js +70 -0
- package/dist/src/agent-runtime/migrateLegacyCodexOutbox.js +336 -0
- package/dist/src/agent-runtime/resourceTypes.js +1 -0
- package/dist/src/agent-runtime/resources.js +245 -0
- package/dist/src/agent-runtime/run.js +328 -0
- package/dist/src/agent-runtime/runtime.js +793 -0
- package/dist/src/agent-runtime/sessionControl.js +164 -0
- package/dist/src/agent-runtime/subscriptionUsage.js +190 -0
- package/dist/src/agent-runtime/subscriptionUsageLegacy.js +18 -0
- package/dist/src/agent-runtime/tmuxRuntime.js +156 -0
- package/dist/src/agents/claude.js +203 -0
- package/dist/src/agents/claudeConversation.js +362 -0
- package/dist/src/agents/claudeInteraction.js +134 -0
- package/dist/src/agents/claudeSubscriptionUsage.js +61 -0
- package/dist/src/agents/codex.js +262 -0
- package/dist/src/agents/codexConversation.js +1103 -0
- package/dist/src/agents/codexConversationActivation.js +121 -0
- package/dist/src/agents/codexConversationControls.js +256 -0
- package/dist/src/agents/codexInteraction.js +306 -0
- package/dist/src/agents/codexSessionControl.js +118 -0
- package/dist/src/agents/codexSubscriptionUsage.js +313 -0
- package/dist/src/agents/codexTerminalControl.js +23 -0
- package/dist/src/agents/conversationProjectionSafety.js +416 -0
- package/dist/src/agents/index.js +39 -0
- package/dist/src/agents/nativeInbox.js +264 -0
- package/dist/src/agents/nativeInboxSources.js +115 -0
- package/dist/src/agents/pi.js +28 -0
- package/dist/src/agents/piConversation.js +523 -0
- package/dist/src/agents/piConversationHistory.js +502 -0
- package/dist/src/agents/piInboxBridge.js +368 -0
- package/dist/src/agents/piSessionControl.js +32 -0
- package/dist/src/agents/processIdentity.js +94 -0
- package/dist/src/agents/scanUtils.js +262 -0
- package/dist/src/apiAccountProviders.js +194 -0
- package/dist/src/apiAccounts.js +592 -0
- package/dist/src/apiErrors.js +86 -0
- package/{src → dist/src}/appName.js +12 -13
- package/dist/src/asr/iflyConfig.js +7 -0
- package/dist/src/asr/iflySign.js +14 -0
- package/dist/src/auth.js +27 -0
- package/dist/src/browser/bootstrap.js +72 -0
- package/dist/src/browser/cookieProfiles.js +413 -0
- package/dist/src/browser/coordinator.js +51 -0
- package/dist/src/browser/hammerheadCompat.js +104 -0
- package/dist/src/browser/hammerheadRedirectCompat.js +37 -0
- package/dist/src/browser/manager.js +560 -0
- package/dist/src/browser/originLabel.js +15 -0
- package/dist/src/browser/profilePersistence.js +235 -0
- package/dist/src/browser/publicProxy.js +176 -0
- package/dist/src/browser/routes.js +159 -0
- package/dist/src/browser/sessionStore.js +72 -0
- package/dist/src/browser/siteVersion.js +121 -0
- package/dist/src/browser/targetPolicy.js +163 -0
- package/{src → dist/src}/browser/worker.js +14 -18
- package/dist/src/browser/workerClient.js +286 -0
- package/dist/src/browser/workerServer.js +91 -0
- package/dist/src/captureBackground.js +95 -0
- package/dist/src/claudeEvents.js +525 -0
- package/dist/src/cli/agentIntegration.js +131 -0
- package/dist/src/cli/agentLauncher.js +26 -0
- package/dist/src/cli/claudeHooks.js +290 -0
- package/{src → dist/src}/cli/cloudflareUrl.js +2 -3
- package/dist/src/cli/cloudflared.js +106 -0
- package/dist/src/cli/codexManaged.js +80 -0
- package/{src → dist/src}/cli/cpolarUrl.js +8 -9
- package/dist/src/cli/drivers.js +98 -0
- package/dist/src/cli/hookScaffold.js +39 -0
- package/dist/src/cli/i18n/en.js +392 -0
- package/{src → dist/src}/cli/i18n/index.js +27 -25
- package/dist/src/cli/i18n/zh.js +391 -0
- package/dist/src/cli/legacyCodexHooks.js +52 -0
- package/{src → dist/src}/cli/natappUrl.js +2 -3
- package/dist/src/cli/options.js +245 -0
- package/dist/src/cli/piExtension.js +155 -0
- package/dist/src/cli/probe.js +16 -0
- package/{src → dist/src}/cli/prompt.js +4 -6
- package/dist/src/cli/pushCmd.js +102 -0
- package/{src → dist/src}/cli/qr.js +19 -19
- package/dist/src/cli/service.js +154 -0
- package/dist/src/cli/setupModel.js +194 -0
- package/dist/src/cli/setupWizard.js +647 -0
- package/dist/src/cli/shortcutEditor.js +351 -0
- package/dist/src/cli/sshTunnel.js +18 -0
- package/dist/src/cli/state.js +117 -0
- package/dist/src/cli/statusLine.js +115 -0
- package/dist/src/cli/supervisor.js +278 -0
- package/dist/src/cli/supervisorHealth.js +21 -0
- package/dist/src/cli/supervisorLaunch.js +148 -0
- package/dist/src/cli/supervisorProcesses.js +62 -0
- package/dist/src/cli/supervisorState.js +62 -0
- package/dist/src/cli/tmuxVersion.js +53 -0
- package/{src → dist/src}/cli/tunlite.js +7 -10
- package/dist/src/cli/tunnelClients.js +72 -0
- package/{src → dist/src}/cli/updateCheck.js +98 -69
- package/{src → dist/src}/cli/urlHost.js +8 -4
- package/dist/src/cli/workspaceCmd.js +347 -0
- package/dist/src/codexAppServer.js +3436 -0
- package/dist/src/codexConversationProjection.js +199 -0
- package/dist/src/codexDiff.js +118 -0
- package/dist/src/codexMessageIdentity.js +18 -0
- package/dist/src/codexPlan.js +30 -0
- package/dist/src/codexQueueProtocol.js +137 -0
- package/dist/src/codexStreamProtocol.js +207 -0
- package/dist/src/codexToolProtocol.js +68 -0
- package/dist/src/codexTranscriptParse.js +835 -0
- package/dist/src/codexUsageSnapshot.js +228 -0
- package/dist/src/config.js +8 -0
- package/{src → dist/src}/docPath.js +22 -24
- package/dist/src/docs.js +337 -0
- package/dist/src/git.js +284 -0
- package/dist/src/healthProtocol.js +97 -0
- package/dist/src/httpApi.js +96 -0
- package/dist/src/jsonStore.js +15 -0
- package/{src → dist/src}/keyNames.js +3 -3
- package/dist/src/notifications.js +111 -0
- package/dist/src/orphans.js +167 -0
- package/dist/src/paneInput.js +42 -0
- package/dist/src/pendingPrompt.js +194 -0
- package/dist/src/previewServer.js +202 -0
- package/dist/src/previews.js +179 -0
- package/dist/src/privateStateStore.js +124 -0
- package/dist/src/projectTask/backup.js +224 -0
- package/dist/src/projectTask/lock.js +108 -0
- package/dist/src/projectTask/migrations.js +90 -0
- package/dist/src/projectTask/routes.js +155 -0
- package/dist/src/projectTask/runtime.js +182 -0
- package/dist/src/projectTask/schema.js +56 -0
- package/dist/src/projectTask/store.js +457 -0
- package/dist/src/push.js +224 -0
- package/dist/src/routes/agents.js +631 -0
- package/dist/src/routes/apiAccounts.js +82 -0
- package/dist/src/routes/files.js +230 -0
- package/dist/src/routes/git.js +89 -0
- package/dist/src/routes/health.js +16 -0
- package/dist/src/routes/notifications.js +16 -0
- package/dist/src/routes/previews.js +43 -0
- package/dist/src/routes/push.js +145 -0
- package/dist/src/routes/sessions.js +244 -0
- package/dist/src/routes/system.js +220 -0
- package/dist/src/routes/terminal.js +244 -0
- package/dist/src/routes/workspace.js +267 -0
- package/dist/src/server.js +315 -0
- package/dist/src/shortcutConfig.js +83 -0
- package/{src → dist/src}/staticCache.js +5 -3
- package/{src → dist/src}/staticCompression.js +5 -6
- package/dist/src/terminalStream.js +441 -0
- package/{src → dist/src}/tmux/commands.js +185 -172
- package/dist/src/tmux/format.js +47 -0
- package/dist/src/transcriptParse.js +261 -0
- package/dist/src/transcriptReader.js +209 -0
- package/{src → dist/src}/trimCapture.js +11 -11
- package/dist/src/uploadTypes.js +31 -0
- package/dist/src/urlPolicy.js +18 -0
- package/dist/src/usagePaths.js +15 -0
- package/dist/src/workspace/agentRunner.js +301 -0
- package/dist/src/workspace/atomicJson.js +31 -0
- package/dist/src/workspace/capture.js +139 -0
- package/dist/src/workspace/checkpointer.js +228 -0
- package/dist/src/workspace/environment.js +71 -0
- package/dist/src/workspace/lock.js +137 -0
- package/dist/src/workspace/mapping.js +128 -0
- package/dist/src/workspace/operations.js +271 -0
- package/dist/src/workspace/paths.js +15 -0
- package/dist/src/workspace/planner.js +292 -0
- package/dist/src/workspace/restore.js +363 -0
- package/dist/src/workspace/runtime.js +318 -0
- package/dist/src/workspace/schema.js +156 -0
- package/dist/src/workspace/store.js +464 -0
- package/dist/src/workspace/tmuxAdapter.js +704 -0
- package/dist/src/workspaceProtocol.js +158 -0
- package/package.json +30 -17
- package/hooks/handmux-notify.sh +0 -22
- package/hooks/handmux-write.cjs +0 -78
- package/public/assets/index-C3N7OkyP.css +0 -32
- package/public/assets/index-D1DWcC5u.js +0 -337
- package/src/.gitkeep +0 -0
- package/src/agents/claude.js +0 -159
- package/src/agents/codex.js +0 -168
- package/src/agents/index.js +0 -21
- package/src/agents/processIdentity.js +0 -72
- package/src/agents/scanUtils.js +0 -225
- package/src/asr/iflyConfig.js +0 -10
- package/src/asr/iflySign.js +0 -16
- package/src/auth.js +0 -30
- package/src/browser/bootstrap.js +0 -68
- package/src/browser/cookieProfiles.js +0 -403
- package/src/browser/coordinator.js +0 -47
- package/src/browser/hammerheadCompat.js +0 -86
- package/src/browser/hammerheadRedirectCompat.js +0 -38
- package/src/browser/manager.js +0 -556
- package/src/browser/originLabel.js +0 -17
- package/src/browser/profilePersistence.js +0 -241
- package/src/browser/publicProxy.js +0 -175
- package/src/browser/routes.js +0 -154
- package/src/browser/sessionStore.js +0 -78
- package/src/browser/siteVersion.js +0 -124
- package/src/browser/targetPolicy.js +0 -137
- package/src/browser/workerClient.js +0 -253
- package/src/browser/workerServer.js +0 -98
- package/src/captureBackground.js +0 -93
- package/src/claudeEvents.js +0 -399
- package/src/cli/claudeHooks.js +0 -206
- package/src/cli/cloudflared.js +0 -99
- package/src/cli/codexManaged.js +0 -77
- package/src/cli/drivers.js +0 -97
- package/src/cli/hookScaffold.js +0 -33
- package/src/cli/i18n/en.js +0 -385
- package/src/cli/i18n/zh.js +0 -384
- package/src/cli/legacyCodexHooks.js +0 -49
- package/src/cli/options.js +0 -211
- package/src/cli/probe.js +0 -16
- package/src/cli/pushCmd.js +0 -60
- package/src/cli/service.js +0 -126
- package/src/cli/setupModel.js +0 -154
- package/src/cli/setupWizard.js +0 -506
- package/src/cli/shortcutEditor.js +0 -288
- package/src/cli/sshTunnel.js +0 -12
- package/src/cli/state.js +0 -90
- package/src/cli/statusLine.js +0 -90
- package/src/cli/supervisor.js +0 -183
- package/src/cli/supervisorProcesses.js +0 -57
- package/src/cli/tmuxVersion.js +0 -49
- package/src/cli/tunnelClients.js +0 -62
- package/src/cli/workspaceCmd.js +0 -300
- package/src/codexAppServer.js +0 -1620
- package/src/codexDiff.js +0 -108
- package/src/codexTranscriptParse.js +0 -413
- package/src/codexUsageSnapshot.cjs +0 -129
- package/src/config.js +0 -9
- package/src/docs.js +0 -228
- package/src/git.js +0 -208
- package/src/httpApi.js +0 -72
- package/src/jsonStore.js +0 -24
- package/src/notifications.js +0 -68
- package/src/orphans.js +0 -152
- package/src/pendingPrompt.js +0 -163
- package/src/previewServer.js +0 -168
- package/src/previews.js +0 -116
- package/src/push.js +0 -183
- package/src/routes/codex.js +0 -473
- package/src/routes/files.js +0 -172
- package/src/routes/git.js +0 -57
- package/src/routes/notifications.js +0 -19
- package/src/routes/previews.js +0 -35
- package/src/routes/push.js +0 -119
- package/src/routes/sessions.js +0 -168
- package/src/routes/system.js +0 -132
- package/src/routes/terminal.js +0 -210
- package/src/routes/transcript.js +0 -171
- package/src/routes/workspace.js +0 -267
- package/src/server.js +0 -186
- package/src/shortcutConfig.js +0 -75
- package/src/terminalStream.js +0 -391
- package/src/tmux/format.js +0 -45
- package/src/transcriptParse.js +0 -180
- package/src/transcriptReader.js +0 -101
- package/src/uploadTypes.js +0 -30
- package/src/urlPolicy.js +0 -14
- package/src/usage.js +0 -260
- package/src/workspace/agentRunner.js +0 -188
- package/src/workspace/atomicJson.js +0 -32
- package/src/workspace/capture.js +0 -139
- package/src/workspace/checkpointer.js +0 -187
- package/src/workspace/environment.js +0 -75
- package/src/workspace/lock.js +0 -128
- package/src/workspace/mapping.js +0 -124
- package/src/workspace/operations.js +0 -252
- package/src/workspace/paths.js +0 -16
- package/src/workspace/planner.js +0 -247
- package/src/workspace/restore.js +0 -279
- package/src/workspace/runtime.js +0 -314
- package/src/workspace/schema.js +0 -136
- package/src/workspace/store.js +0 -382
- package/src/workspace/tmuxAdapter.js +0 -554
- /package/{public → dist/public}/fonts/JetBrainsMonoNerdFontMono-Regular.woff2 +0 -0
- /package/{public → dist/public}/fonts/TWUnifont.woff2 +0 -0
- /package/{public → dist/public}/icons/apple-touch-icon.png +0 -0
- /package/{public → dist/public}/icons/badge-96.png +0 -0
- /package/{public → dist/public}/icons/icon-192-maskable.png +0 -0
- /package/{public → dist/public}/icons/icon-192.png +0 -0
- /package/{public → dist/public}/icons/icon-512-maskable.png +0 -0
- /package/{public → dist/public}/icons/icon-512.png +0 -0
- /package/{public → dist/public}/icons/logo.svg +0 -0
- /package/{public → dist/public}/manifest.webmanifest +0 -0
- /package/{public → dist/public}/offline.html +0 -0
- /package/{public → dist/public}/sw.js +0 -0
- /package/{src → dist/src}/browser/protocol.js +0 -0
- /package/{src → dist/src}/cli/cfNamed.js +0 -0
|
@@ -0,0 +1,793 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { PrivateStateStore } from '../privateStateStore.js';
|
|
4
|
+
import { resolveAgentIdentity, validateAgentAdapters, } from './adapter.js';
|
|
5
|
+
import { BridgeContractError, LocalAgentBridge } from './bridge.js';
|
|
6
|
+
import { FileBridgeStateStore } from './bridgeStore.js';
|
|
7
|
+
import { LocalAgentBridgeTransportServer, } from './bridgeTransport.js';
|
|
8
|
+
import { ConversationService } from './conversation.js';
|
|
9
|
+
import { RuntimeConversationActivitySource, } from './conversationActivity.js';
|
|
10
|
+
import { AgentConversationControlService } from './conversationControls.js';
|
|
11
|
+
import { AgentConversationActivationService } from './conversationActivation.js';
|
|
12
|
+
import { FileConversationStateStore } from './conversationStore.js';
|
|
13
|
+
import { InboxContractError, InboxService } from './inbox.js';
|
|
14
|
+
import { FileInboxStateStore } from './inboxStore.js';
|
|
15
|
+
import { InteractionContractError, InteractionService } from './interaction.js';
|
|
16
|
+
import { FileInteractionStateStore } from './interactionStore.js';
|
|
17
|
+
import { AgentResourceService } from './resources.js';
|
|
18
|
+
import { AgentSessionControlService } from './sessionControl.js';
|
|
19
|
+
import { SubscriptionUsageService } from './subscriptionUsage.js';
|
|
20
|
+
import { DeprecatedSubscriptionUsageFacade } from './subscriptionUsageLegacy.js';
|
|
21
|
+
import { AgentRunRuntime } from './run.js';
|
|
22
|
+
const DEFAULT_ACTIVATION_TIMEOUT_MS = 5_000;
|
|
23
|
+
const TOKEN_RE = /^[A-Za-z0-9_-]{32,1024}$/;
|
|
24
|
+
const NOOP_LOGGER = Object.freeze({
|
|
25
|
+
debug: () => { },
|
|
26
|
+
info: () => { },
|
|
27
|
+
warn: () => { },
|
|
28
|
+
error: () => { },
|
|
29
|
+
});
|
|
30
|
+
function cloneCandidate(candidate) {
|
|
31
|
+
return structuredClone(candidate);
|
|
32
|
+
}
|
|
33
|
+
function validConversationAdapter(value) {
|
|
34
|
+
const adapter = value;
|
|
35
|
+
return adapter !== null && typeof adapter === 'object' && adapter.apiVersion === 1
|
|
36
|
+
&& typeof adapter.discoverNative === 'function' && typeof adapter.readNativePage === 'function'
|
|
37
|
+
&& (adapter.dispatchPrompt === undefined || typeof adapter.dispatchPrompt === 'function')
|
|
38
|
+
&& (adapter.dispatchSteer === undefined || typeof adapter.dispatchSteer === 'function');
|
|
39
|
+
}
|
|
40
|
+
function validController(value, methods) {
|
|
41
|
+
const controller = value;
|
|
42
|
+
return controller !== null && typeof controller === 'object' && controller.apiVersion === 1
|
|
43
|
+
&& methods.every((method) => typeof controller[method] === 'function');
|
|
44
|
+
}
|
|
45
|
+
function validInteractionAdapter(value) {
|
|
46
|
+
const adapter = value;
|
|
47
|
+
return adapter !== null && typeof adapter === 'object' && adapter.apiVersion === 1
|
|
48
|
+
&& typeof adapter.observeNative === 'function' && typeof adapter.dispatchResponse === 'function';
|
|
49
|
+
}
|
|
50
|
+
function validSessionControlAdapter(value) {
|
|
51
|
+
const adapter = value;
|
|
52
|
+
return adapter !== null && typeof adapter === 'object' && adapter.apiVersion === 1
|
|
53
|
+
&& typeof adapter.readModelControl === 'function'
|
|
54
|
+
&& (adapter.updateModelControl === undefined || typeof adapter.updateModelControl === 'function');
|
|
55
|
+
}
|
|
56
|
+
export function validSubscriptionUsageAdapter(value) {
|
|
57
|
+
const adapter = value;
|
|
58
|
+
return adapter !== null && typeof adapter === 'object' && adapter.apiVersion === 1
|
|
59
|
+
&& typeof adapter.snapshot === 'function';
|
|
60
|
+
}
|
|
61
|
+
function sameProcess(candidate, pane, foreground) {
|
|
62
|
+
if (foreground.pid !== candidate.process.pid)
|
|
63
|
+
return false;
|
|
64
|
+
if (pane.foregroundPid !== undefined && pane.foregroundPid !== candidate.process.pid)
|
|
65
|
+
return false;
|
|
66
|
+
if (candidate.process.startedAt !== undefined
|
|
67
|
+
&& foreground.startedAt !== candidate.process.startedAt)
|
|
68
|
+
return false;
|
|
69
|
+
const observedTty = foreground.tty ?? pane.tty;
|
|
70
|
+
return candidate.process.tty === undefined || observedTty === candidate.process.tty;
|
|
71
|
+
}
|
|
72
|
+
function recoverCorruptState({ file, kind, logger, create, isContractError, }) {
|
|
73
|
+
try {
|
|
74
|
+
return create();
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (!(error instanceof SyntaxError) && !isContractError(error))
|
|
78
|
+
throw error;
|
|
79
|
+
const quarantinedFile = new PrivateStateStore(file).quarantine();
|
|
80
|
+
try {
|
|
81
|
+
logger.warn('Corrupt Agent Runtime state was quarantined', {
|
|
82
|
+
kind,
|
|
83
|
+
file,
|
|
84
|
+
quarantinedFile,
|
|
85
|
+
error: error instanceof Error ? error.message : String(error),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
catch { /* diagnostics must not prevent recovery */ }
|
|
89
|
+
return create();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function credential(file, requested, create) {
|
|
93
|
+
const store = new PrivateStateStore(file);
|
|
94
|
+
const existing = store.readStrict();
|
|
95
|
+
if (existing !== null) {
|
|
96
|
+
if (existing.version !== 1 || !TOKEN_RE.test(existing.authToken)) {
|
|
97
|
+
throw new Error('Agent Runtime Bridge credential is corrupt');
|
|
98
|
+
}
|
|
99
|
+
if (requested !== undefined && requested !== existing.authToken) {
|
|
100
|
+
throw new Error('Agent Runtime Bridge credential does not match persisted state');
|
|
101
|
+
}
|
|
102
|
+
return existing.authToken;
|
|
103
|
+
}
|
|
104
|
+
const authToken = requested ?? create();
|
|
105
|
+
if (!TOKEN_RE.test(authToken))
|
|
106
|
+
throw new Error('Agent Runtime Bridge credential is invalid');
|
|
107
|
+
store.write({ version: 1, authToken });
|
|
108
|
+
return authToken;
|
|
109
|
+
}
|
|
110
|
+
async function lifecycleWithin(operation, timeoutMs, label) {
|
|
111
|
+
let timer;
|
|
112
|
+
try {
|
|
113
|
+
return await Promise.race([
|
|
114
|
+
operation,
|
|
115
|
+
new Promise((_resolve, reject) => {
|
|
116
|
+
timer = setTimeout(() => reject(new Error(`${label} timed out`)), timeoutMs);
|
|
117
|
+
timer.unref?.();
|
|
118
|
+
}),
|
|
119
|
+
]);
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
if (timer)
|
|
123
|
+
clearTimeout(timer);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
export class AgentRuntime {
|
|
127
|
+
adapters;
|
|
128
|
+
issues;
|
|
129
|
+
runs;
|
|
130
|
+
bridge;
|
|
131
|
+
inbox;
|
|
132
|
+
conversation;
|
|
133
|
+
conversationActivation;
|
|
134
|
+
conversationControls;
|
|
135
|
+
interaction;
|
|
136
|
+
sessionControl;
|
|
137
|
+
subscriptionUsage;
|
|
138
|
+
deprecatedSubscriptionUsage;
|
|
139
|
+
resources;
|
|
140
|
+
socketPath;
|
|
141
|
+
bridgeAuthToken;
|
|
142
|
+
#panes;
|
|
143
|
+
#process;
|
|
144
|
+
#logger;
|
|
145
|
+
#adapterOptions;
|
|
146
|
+
#activationTimeoutMs;
|
|
147
|
+
#verifyTimeoutMs;
|
|
148
|
+
#controllers = new Map();
|
|
149
|
+
#bindings = new Map();
|
|
150
|
+
#startedBindings = new Set();
|
|
151
|
+
#lifecycles = new Map();
|
|
152
|
+
#tracked = new Map();
|
|
153
|
+
#authorizationQueues = new Map();
|
|
154
|
+
#health = new Map();
|
|
155
|
+
#transport;
|
|
156
|
+
#unsubscribePanes;
|
|
157
|
+
#reconcileTail = Promise.resolve();
|
|
158
|
+
#started = false;
|
|
159
|
+
#closed = false;
|
|
160
|
+
#closePromise;
|
|
161
|
+
constructor({ adapters, panes, process, stateDirectory, adapterFactories = {}, adapterOptions = {}, allowedResourceRoots = {}, logger = NOOP_LOGGER, socketPath = path.join(stateDirectory, 'bridge', 'agent.sock'), authToken, activationTimeoutMs = DEFAULT_ACTIVATION_TIMEOUT_MS, verifyTimeoutMs, newRunId, newBridgeConnectionId, newBridgeAuthToken = () => crypto.randomBytes(32).toString('base64url'), conversationStartupBlockReason, }) {
|
|
162
|
+
if (!path.isAbsolute(stateDirectory) || !path.isAbsolute(socketPath) || !panes || !process
|
|
163
|
+
|| !Number.isSafeInteger(activationTimeoutMs) || activationTimeoutMs <= 0) {
|
|
164
|
+
throw new TypeError('AgentRuntime requires absolute private paths, panes, and process context');
|
|
165
|
+
}
|
|
166
|
+
const validation = validateAgentAdapters(adapters);
|
|
167
|
+
if (!validation.available.length)
|
|
168
|
+
throw new Error('AgentRuntime has no valid Agent adapters');
|
|
169
|
+
this.adapters = Object.freeze([...validation.available]);
|
|
170
|
+
this.issues = Object.freeze(structuredClone(validation.issues));
|
|
171
|
+
this.#panes = panes;
|
|
172
|
+
this.#process = process;
|
|
173
|
+
this.#logger = logger;
|
|
174
|
+
this.#adapterOptions = adapterOptions;
|
|
175
|
+
this.#activationTimeoutMs = activationTimeoutMs;
|
|
176
|
+
this.#verifyTimeoutMs = Math.max(1, verifyTimeoutMs ?? 1_000);
|
|
177
|
+
this.runs = new AgentRunRuntime({
|
|
178
|
+
verifyTimeoutMs: this.#verifyTimeoutMs,
|
|
179
|
+
...(newRunId === undefined ? {} : { newRunId }),
|
|
180
|
+
});
|
|
181
|
+
const adapterIds = this.adapters.map((adapter) => adapter.id);
|
|
182
|
+
this.resources = new AgentResourceService({ allowedFileRoots: allowedResourceRoots });
|
|
183
|
+
const bridgeStateFile = path.join(stateDirectory, 'bridge-state.json');
|
|
184
|
+
this.bridge = recoverCorruptState({
|
|
185
|
+
file: bridgeStateFile,
|
|
186
|
+
kind: 'bridge',
|
|
187
|
+
logger,
|
|
188
|
+
isContractError: (error) => error instanceof BridgeContractError,
|
|
189
|
+
create: () => new LocalAgentBridge({
|
|
190
|
+
runs: this.runs,
|
|
191
|
+
adapterIds,
|
|
192
|
+
store: new FileBridgeStateStore(bridgeStateFile),
|
|
193
|
+
...(newBridgeConnectionId === undefined ? {} : { newConnectionId: newBridgeConnectionId }),
|
|
194
|
+
}),
|
|
195
|
+
});
|
|
196
|
+
const inboxStateFile = path.join(stateDirectory, 'inbox-state.json');
|
|
197
|
+
this.inbox = recoverCorruptState({
|
|
198
|
+
file: inboxStateFile,
|
|
199
|
+
kind: 'inbox',
|
|
200
|
+
logger,
|
|
201
|
+
isContractError: (error) => error instanceof InboxContractError,
|
|
202
|
+
create: () => new InboxService({
|
|
203
|
+
runs: this.runs,
|
|
204
|
+
adapterIds,
|
|
205
|
+
store: new FileInboxStateStore(inboxStateFile),
|
|
206
|
+
}),
|
|
207
|
+
});
|
|
208
|
+
for (const adapter of this.adapters) {
|
|
209
|
+
const raw = this.runs.controller(adapter.id, (candidate) => this.#verifyAttachment(adapter, candidate));
|
|
210
|
+
this.#controllers.set(adapter.id, this.#trackedController(adapter, raw));
|
|
211
|
+
this.#lifecycles.set(adapter.id, { adapter, abort: new AbortController(), cleanup: [] });
|
|
212
|
+
this.#health.set(`${adapter.id}\0`, {
|
|
213
|
+
adapterId: adapter.id,
|
|
214
|
+
availability: 'starting',
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
for (const factoryId of Object.keys(adapterFactories)) {
|
|
218
|
+
if (!this.#controllers.has(factoryId)) {
|
|
219
|
+
throw new Error(`Agent Runtime factory has no static adapter: ${factoryId}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const conversations = {};
|
|
223
|
+
const conversationActivities = {};
|
|
224
|
+
const conversationActivations = {};
|
|
225
|
+
const conversationControlBindings = {};
|
|
226
|
+
const interactions = {};
|
|
227
|
+
const sessionControls = {};
|
|
228
|
+
const subscriptionUsageAdapters = {};
|
|
229
|
+
const subscriptionUsageLegacy = {};
|
|
230
|
+
for (const adapter of this.adapters) {
|
|
231
|
+
const factory = adapterFactories[adapter.id];
|
|
232
|
+
if (!factory)
|
|
233
|
+
continue;
|
|
234
|
+
try {
|
|
235
|
+
const binding = factory(this.#context(adapter, adapterOptions[adapter.id] ?? {}));
|
|
236
|
+
if (!binding || typeof binding !== 'object'
|
|
237
|
+
|| (binding.inbox !== undefined && binding.inbox !== true)
|
|
238
|
+
|| (binding.start !== undefined && typeof binding.start !== 'function')
|
|
239
|
+
|| (binding.onBridgeConnected !== undefined
|
|
240
|
+
&& typeof binding.onBridgeConnected !== 'function')) {
|
|
241
|
+
throw new Error('factory returned an invalid binding');
|
|
242
|
+
}
|
|
243
|
+
this.#bindings.set(adapter.id, binding);
|
|
244
|
+
if (binding.conversation) {
|
|
245
|
+
if (adapter.capabilities.conversation?.apiVersion === 1
|
|
246
|
+
&& validConversationAdapter(binding.conversation)) {
|
|
247
|
+
conversations[adapter.id] = binding.conversation;
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
this.#report(adapter.id, {
|
|
251
|
+
capability: 'conversation', availability: 'unavailable',
|
|
252
|
+
message: 'invalid Conversation adapter',
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
else if (adapter.capabilities.conversation?.apiVersion === 1) {
|
|
257
|
+
this.#report(adapter.id, {
|
|
258
|
+
capability: 'conversation', availability: 'unavailable',
|
|
259
|
+
message: 'Conversation binding is missing',
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
if (binding.conversationActivity)
|
|
263
|
+
conversationActivities[adapter.id] = binding.conversationActivity;
|
|
264
|
+
if (binding.conversationActivation) {
|
|
265
|
+
if (adapter.capabilities.conversationActivation?.apiVersion === 1
|
|
266
|
+
&& validController(binding.conversationActivation, ['describe', 'activate'])) {
|
|
267
|
+
conversationActivations[adapter.id] = binding.conversationActivation;
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
this.#report(adapter.id, {
|
|
271
|
+
capability: 'conversationActivation', availability: 'unavailable',
|
|
272
|
+
message: 'invalid Conversation activation controller',
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
else if (adapter.capabilities.conversationActivation?.apiVersion === 1) {
|
|
277
|
+
this.#report(adapter.id, {
|
|
278
|
+
capability: 'conversationActivation', availability: 'unavailable',
|
|
279
|
+
message: 'Conversation activation binding is missing',
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
const controlBinding = {};
|
|
283
|
+
const controlSpecs = [
|
|
284
|
+
['conversationGoal', 'goal', ['read']],
|
|
285
|
+
['conversationPlan', 'plan', ['read']],
|
|
286
|
+
['conversationContext', 'context', ['read']],
|
|
287
|
+
['conversationPermission', 'permission', ['read']],
|
|
288
|
+
['conversationCommands', 'commands', ['execute']],
|
|
289
|
+
];
|
|
290
|
+
for (const [capability, slot, methods] of controlSpecs) {
|
|
291
|
+
const declared = adapter.capabilities[capability]?.apiVersion === 1;
|
|
292
|
+
const controller = binding[capability];
|
|
293
|
+
const commandsValid = capability !== 'conversationCommands'
|
|
294
|
+
|| (Array.isArray(binding.conversationCommands?.commands)
|
|
295
|
+
&& binding.conversationCommands.commands.every((command) => (command === 'compact' || command === 'clear')));
|
|
296
|
+
if (controller && declared && validController(controller, methods) && commandsValid) {
|
|
297
|
+
Object.assign(controlBinding, { [slot]: controller });
|
|
298
|
+
}
|
|
299
|
+
else if (controller || declared) {
|
|
300
|
+
this.#report(adapter.id, {
|
|
301
|
+
capability, availability: 'unavailable',
|
|
302
|
+
message: controller ? `invalid ${capability} controller` : `${capability} binding is missing`,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (Object.keys(controlBinding).length)
|
|
307
|
+
conversationControlBindings[adapter.id] = controlBinding;
|
|
308
|
+
if (binding.interaction) {
|
|
309
|
+
if (adapter.capabilities.interaction?.apiVersion === 1
|
|
310
|
+
&& validInteractionAdapter(binding.interaction)) {
|
|
311
|
+
interactions[adapter.id] = binding.interaction;
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
this.#report(adapter.id, {
|
|
315
|
+
capability: 'interaction', availability: 'unavailable',
|
|
316
|
+
message: 'invalid Interaction adapter',
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
else if (adapter.capabilities.interaction?.apiVersion === 1) {
|
|
321
|
+
this.#report(adapter.id, {
|
|
322
|
+
capability: 'interaction', availability: 'unavailable',
|
|
323
|
+
message: 'Interaction binding is missing',
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
if (binding.sessionControl) {
|
|
327
|
+
if (adapter.capabilities.sessionControl?.apiVersion === 1
|
|
328
|
+
&& validSessionControlAdapter(binding.sessionControl)) {
|
|
329
|
+
sessionControls[adapter.id] = binding.sessionControl;
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
this.#report(adapter.id, {
|
|
333
|
+
capability: 'sessionControl', availability: 'unavailable',
|
|
334
|
+
message: 'invalid Session Control adapter',
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
else if (adapter.capabilities.sessionControl?.apiVersion === 1) {
|
|
339
|
+
this.#report(adapter.id, {
|
|
340
|
+
capability: 'sessionControl', availability: 'unavailable',
|
|
341
|
+
message: 'Session Control binding is missing',
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
if (binding.subscriptionUsage) {
|
|
345
|
+
if (adapter.capabilities.subscriptionUsage?.apiVersion === 1
|
|
346
|
+
&& validSubscriptionUsageAdapter(binding.subscriptionUsage)) {
|
|
347
|
+
subscriptionUsageAdapters[adapter.id] = binding.subscriptionUsage;
|
|
348
|
+
if (binding.subscriptionUsageLegacy
|
|
349
|
+
&& typeof binding.subscriptionUsageLegacy.legacySnapshot === 'function') {
|
|
350
|
+
subscriptionUsageLegacy[adapter.id] = binding.subscriptionUsageLegacy;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
this.#report(adapter.id, {
|
|
355
|
+
capability: 'subscriptionUsage', availability: 'unavailable',
|
|
356
|
+
message: 'invalid Subscription Usage adapter',
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
else if (adapter.capabilities.subscriptionUsage?.apiVersion === 1) {
|
|
361
|
+
this.#report(adapter.id, {
|
|
362
|
+
capability: 'subscriptionUsage', availability: 'unavailable',
|
|
363
|
+
message: 'Subscription Usage binding is missing',
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
this.#report(adapter.id, {
|
|
369
|
+
capability: 'factory',
|
|
370
|
+
availability: 'unavailable',
|
|
371
|
+
message: `capability factory failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
const conversationStateFile = path.join(stateDirectory, 'conversation-state.json');
|
|
376
|
+
const createConversation = () => new ConversationService({
|
|
377
|
+
runs: this.runs,
|
|
378
|
+
adapters: conversations,
|
|
379
|
+
activitySource: new RuntimeConversationActivitySource(conversationActivities),
|
|
380
|
+
store: new FileConversationStateStore(conversationStateFile),
|
|
381
|
+
...(conversationStartupBlockReason === undefined ? {} : {
|
|
382
|
+
dispatchFences: { codex: conversationStartupBlockReason },
|
|
383
|
+
}),
|
|
384
|
+
});
|
|
385
|
+
if (!Object.keys(conversations).length)
|
|
386
|
+
this.conversation = null;
|
|
387
|
+
else {
|
|
388
|
+
try {
|
|
389
|
+
this.conversation = createConversation();
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
393
|
+
try {
|
|
394
|
+
logger.error('Conversation state is invalid; dispatch is disabled', {
|
|
395
|
+
kind: 'conversation', file: conversationStateFile, error: message,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
catch { /* diagnostics cannot weaken fail-closed startup */ }
|
|
399
|
+
for (const adapterId of Object.keys(conversations))
|
|
400
|
+
this.#report(adapterId, {
|
|
401
|
+
capability: 'conversation', availability: 'unavailable', message,
|
|
402
|
+
});
|
|
403
|
+
this.conversation = null;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (this.conversation && conversationStartupBlockReason && conversations.codex) {
|
|
407
|
+
this.#report('codex', {
|
|
408
|
+
capability: 'conversationDispatch', availability: 'unavailable',
|
|
409
|
+
message: conversationStartupBlockReason,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
this.conversationActivation = Object.keys(conversationActivations).length
|
|
413
|
+
? new AgentConversationActivationService(conversationActivations) : null;
|
|
414
|
+
this.conversationControls = Object.keys(conversationControlBindings).length
|
|
415
|
+
? new AgentConversationControlService(conversationControlBindings) : null;
|
|
416
|
+
const interactionStateFile = path.join(stateDirectory, 'interaction-state.json');
|
|
417
|
+
this.interaction = Object.keys(interactions).length ? recoverCorruptState({
|
|
418
|
+
file: interactionStateFile,
|
|
419
|
+
kind: 'interaction',
|
|
420
|
+
logger,
|
|
421
|
+
isContractError: (error) => error instanceof InteractionContractError,
|
|
422
|
+
create: () => new InteractionService({
|
|
423
|
+
runs: this.runs,
|
|
424
|
+
adapters: interactions,
|
|
425
|
+
store: new FileInteractionStateStore(interactionStateFile),
|
|
426
|
+
}),
|
|
427
|
+
}) : null;
|
|
428
|
+
this.sessionControl = Object.keys(sessionControls).length
|
|
429
|
+
? new AgentSessionControlService(sessionControls) : null;
|
|
430
|
+
this.subscriptionUsage = Object.keys(subscriptionUsageAdapters).length
|
|
431
|
+
? new SubscriptionUsageService({
|
|
432
|
+
adapters: subscriptionUsageAdapters,
|
|
433
|
+
descriptors: Object.fromEntries(this.adapters.map((adapter) => [adapter.id, {
|
|
434
|
+
label: adapter.label,
|
|
435
|
+
...(adapter.presentation === undefined ? {} : { presentation: adapter.presentation }),
|
|
436
|
+
}])),
|
|
437
|
+
reportHealth: (adapterId, update) => this.#report(adapterId, {
|
|
438
|
+
capability: 'subscriptionUsage', ...update,
|
|
439
|
+
}),
|
|
440
|
+
}) : null;
|
|
441
|
+
this.deprecatedSubscriptionUsage = this.subscriptionUsage
|
|
442
|
+
&& Object.keys(subscriptionUsageLegacy).length
|
|
443
|
+
? new DeprecatedSubscriptionUsageFacade({
|
|
444
|
+
usage: this.subscriptionUsage, projectors: subscriptionUsageLegacy,
|
|
445
|
+
}) : null;
|
|
446
|
+
this.socketPath = socketPath;
|
|
447
|
+
this.bridgeAuthToken = credential(path.join(stateDirectory, 'bridge-credential.json'), authToken, newBridgeAuthToken);
|
|
448
|
+
this.#transport = new LocalAgentBridgeTransportServer({
|
|
449
|
+
socketPath,
|
|
450
|
+
authToken: this.bridgeAuthToken,
|
|
451
|
+
bridge: this.bridge,
|
|
452
|
+
authorize: (adapterId, candidate, generation) => this.#authorize(adapterId, candidate, generation),
|
|
453
|
+
connected: (lease, connection) => this.#bridgeConnected(lease, connection),
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
runControlFor(adapterId) {
|
|
457
|
+
const controller = this.#controllers.get(adapterId);
|
|
458
|
+
if (!controller)
|
|
459
|
+
throw new Error(`Unknown Agent adapter: ${adapterId}`);
|
|
460
|
+
return controller;
|
|
461
|
+
}
|
|
462
|
+
bridgeHostFor(adapterId) {
|
|
463
|
+
return this.bridge.hostFor(adapterId);
|
|
464
|
+
}
|
|
465
|
+
capabilities() {
|
|
466
|
+
return this.adapters.map((adapter) => {
|
|
467
|
+
const binding = this.#bindings.get(adapter.id);
|
|
468
|
+
return {
|
|
469
|
+
id: adapter.id,
|
|
470
|
+
label: adapter.label,
|
|
471
|
+
...(adapter.presentation?.iconId === undefined ? {} : { iconId: adapter.presentation.iconId }),
|
|
472
|
+
capabilities: {
|
|
473
|
+
inbox: adapter.capabilities.inbox?.apiVersion === 1 && binding?.inbox === true,
|
|
474
|
+
conversation: adapter.capabilities.conversation?.apiVersion === 1
|
|
475
|
+
&& validConversationAdapter(binding?.conversation),
|
|
476
|
+
conversationActivation: adapter.capabilities.conversationActivation?.apiVersion === 1
|
|
477
|
+
&& validController(binding?.conversationActivation, ['describe', 'activate']),
|
|
478
|
+
conversationGoal: adapter.capabilities.conversationGoal?.apiVersion === 1
|
|
479
|
+
&& validController(binding?.conversationGoal, ['read']),
|
|
480
|
+
conversationPlan: adapter.capabilities.conversationPlan?.apiVersion === 1
|
|
481
|
+
&& validController(binding?.conversationPlan, ['read']),
|
|
482
|
+
conversationContext: adapter.capabilities.conversationContext?.apiVersion === 1
|
|
483
|
+
&& validController(binding?.conversationContext, ['read']),
|
|
484
|
+
conversationPermission: adapter.capabilities.conversationPermission?.apiVersion === 1
|
|
485
|
+
&& validController(binding?.conversationPermission, ['read']),
|
|
486
|
+
conversationCommands: adapter.capabilities.conversationCommands?.apiVersion === 1
|
|
487
|
+
&& validController(binding?.conversationCommands, ['execute'])
|
|
488
|
+
&& Array.isArray(binding?.conversationCommands?.commands),
|
|
489
|
+
interaction: adapter.capabilities.interaction?.apiVersion === 1
|
|
490
|
+
&& validInteractionAdapter(binding?.interaction),
|
|
491
|
+
sessionControl: adapter.capabilities.sessionControl?.apiVersion === 1
|
|
492
|
+
&& validSessionControlAdapter(binding?.sessionControl),
|
|
493
|
+
subscriptionUsage: adapter.capabilities.subscriptionUsage?.apiVersion === 1
|
|
494
|
+
&& validSubscriptionUsageAdapter(binding?.subscriptionUsage),
|
|
495
|
+
},
|
|
496
|
+
...(adapter.capabilities.conversation?.apiVersion === 1
|
|
497
|
+
&& validConversationAdapter(binding?.conversation)
|
|
498
|
+
? { capabilityMetadata: {
|
|
499
|
+
conversation: { experimental: adapter.capabilities.conversation.experimental === true },
|
|
500
|
+
} } : {}),
|
|
501
|
+
};
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
health() {
|
|
505
|
+
return [...this.#health.values()].map((entry) => structuredClone(entry));
|
|
506
|
+
}
|
|
507
|
+
activeRuns() {
|
|
508
|
+
return [...this.#tracked.values()]
|
|
509
|
+
.filter((tracked) => !tracked.lease.signal.aborted
|
|
510
|
+
&& this.runs.resolve(tracked.lease.ref) === tracked.lease)
|
|
511
|
+
.map((tracked) => structuredClone(tracked.lease.ref))
|
|
512
|
+
.sort((first, second) => first.paneId.localeCompare(second.paneId)
|
|
513
|
+
|| first.agentId.localeCompare(second.agentId));
|
|
514
|
+
}
|
|
515
|
+
async identifyPanes(panes) {
|
|
516
|
+
const activeAdapters = this.adapters.filter((adapter) => (!this.#lifecycles.get(adapter.id)?.abort.signal.aborted));
|
|
517
|
+
const resolved = await Promise.all(panes.map(async (pane) => {
|
|
518
|
+
let inspected;
|
|
519
|
+
const context = {
|
|
520
|
+
inspectForeground: () => {
|
|
521
|
+
inspected ??= this.#process.inspectForeground(pane);
|
|
522
|
+
return inspected;
|
|
523
|
+
},
|
|
524
|
+
};
|
|
525
|
+
const identity = await resolveAgentIdentity(pane, activeAdapters, context, {
|
|
526
|
+
verifyTimeoutMs: this.#verifyTimeoutMs,
|
|
527
|
+
});
|
|
528
|
+
if (identity.kind === 'matched')
|
|
529
|
+
return [pane.paneId, identity.adapter.id];
|
|
530
|
+
if (identity.kind === 'conflict') {
|
|
531
|
+
this.#logger.warn('Agent pane identity conflict', {
|
|
532
|
+
paneId: pane.paneId,
|
|
533
|
+
candidateIds: identity.candidateIds,
|
|
534
|
+
});
|
|
535
|
+
return [pane.paneId, null];
|
|
536
|
+
}
|
|
537
|
+
// Unknown means the verifier failed or timed out. Omitting the pane preserves the last confirmed
|
|
538
|
+
// owner in consumers; only an explicit `none` is authoritative evidence that the Agent exited.
|
|
539
|
+
return identity.kind === 'none' ? [pane.paneId, null] : null;
|
|
540
|
+
}));
|
|
541
|
+
return Object.fromEntries(resolved.filter((entry) => entry !== null));
|
|
542
|
+
}
|
|
543
|
+
async start() {
|
|
544
|
+
if (this.#closed)
|
|
545
|
+
throw new Error('AgentRuntime is closed');
|
|
546
|
+
if (this.#started)
|
|
547
|
+
return;
|
|
548
|
+
this.#started = true;
|
|
549
|
+
for (const adapter of this.adapters)
|
|
550
|
+
await this.#startAdapter(adapter);
|
|
551
|
+
await this.#transport.start();
|
|
552
|
+
this.#unsubscribePanes = this.#panes.subscribe((snapshot) => {
|
|
553
|
+
const operation = this.#reconcileTail.then(() => this.#reconcile(snapshot));
|
|
554
|
+
this.#reconcileTail = operation.catch((error) => {
|
|
555
|
+
this.#logger.warn('Agent pane reconciliation failed', {
|
|
556
|
+
error: error instanceof Error ? error.message : String(error),
|
|
557
|
+
});
|
|
558
|
+
});
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
close() {
|
|
562
|
+
if (this.#closePromise)
|
|
563
|
+
return this.#closePromise;
|
|
564
|
+
this.#closePromise = this.#close();
|
|
565
|
+
return this.#closePromise;
|
|
566
|
+
}
|
|
567
|
+
async #close() {
|
|
568
|
+
if (this.#closed)
|
|
569
|
+
return;
|
|
570
|
+
this.#closed = true;
|
|
571
|
+
this.#unsubscribePanes?.();
|
|
572
|
+
this.#unsubscribePanes = undefined;
|
|
573
|
+
await this.#reconcileTail.catch(() => { });
|
|
574
|
+
await this.#transport.close();
|
|
575
|
+
await this.runs.shutdown();
|
|
576
|
+
await this.interaction?.shutdown();
|
|
577
|
+
for (const lifecycle of [...this.#lifecycles.values()].reverse()) {
|
|
578
|
+
lifecycle.abort.abort('runtime_shutdown');
|
|
579
|
+
await this.#cleanupLifecycle(lifecycle);
|
|
580
|
+
}
|
|
581
|
+
await this.bridge.close();
|
|
582
|
+
}
|
|
583
|
+
async #startAdapter(adapter) {
|
|
584
|
+
const lifecycle = this.#lifecycles.get(adapter.id);
|
|
585
|
+
try {
|
|
586
|
+
if (adapter.activate) {
|
|
587
|
+
const cleanup = await lifecycleWithin(Promise.resolve().then(() => adapter.activate(this.#context(adapter, this.#adapterOptions[adapter.id] ?? {}))), this.#activationTimeoutMs, `${adapter.id} activation`);
|
|
588
|
+
if (cleanup)
|
|
589
|
+
lifecycle.cleanup.push(cleanup);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
catch (error) {
|
|
593
|
+
lifecycle.abort.abort('adapter_start_failed');
|
|
594
|
+
await this.runs.revokeAdapter(adapter.id);
|
|
595
|
+
await this.#cleanupLifecycle(lifecycle);
|
|
596
|
+
this.#report(adapter.id, {
|
|
597
|
+
availability: 'unavailable',
|
|
598
|
+
message: error instanceof Error ? error.message : String(error),
|
|
599
|
+
});
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
const binding = this.#bindings.get(adapter.id);
|
|
603
|
+
try {
|
|
604
|
+
if (binding?.start) {
|
|
605
|
+
const cleanup = await lifecycleWithin(Promise.resolve().then(() => binding.start()), this.#activationTimeoutMs, `${adapter.id} capability start`);
|
|
606
|
+
if (cleanup)
|
|
607
|
+
lifecycle.cleanup.push(cleanup);
|
|
608
|
+
}
|
|
609
|
+
if (binding)
|
|
610
|
+
this.#startedBindings.add(adapter.id);
|
|
611
|
+
}
|
|
612
|
+
catch (error) {
|
|
613
|
+
this.#report(adapter.id, {
|
|
614
|
+
capability: binding?.inbox ? 'inbox' : 'binding',
|
|
615
|
+
availability: 'unavailable',
|
|
616
|
+
message: error instanceof Error ? error.message : String(error),
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
this.#report(adapter.id, { availability: 'ready', lastSuccessAt: Date.now() });
|
|
620
|
+
}
|
|
621
|
+
async #cleanupLifecycle(lifecycle) {
|
|
622
|
+
for (const cleanup of lifecycle.cleanup.splice(0).reverse()) {
|
|
623
|
+
try {
|
|
624
|
+
await lifecycleWithin(Promise.resolve().then(cleanup), this.#activationTimeoutMs, 'Agent cleanup');
|
|
625
|
+
}
|
|
626
|
+
catch (error) {
|
|
627
|
+
this.#logger.warn('Agent cleanup failed', {
|
|
628
|
+
adapterId: lifecycle.adapter.id,
|
|
629
|
+
error: error instanceof Error ? error.message : String(error),
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
#context(adapter, options) {
|
|
635
|
+
const lifecycle = this.#lifecycles.get(adapter.id);
|
|
636
|
+
return Object.freeze({
|
|
637
|
+
runs: this.runs,
|
|
638
|
+
runControl: this.#controllers.get(adapter.id),
|
|
639
|
+
panes: this.#panes,
|
|
640
|
+
process: this.#process,
|
|
641
|
+
bridge: this.bridge.hostFor(adapter.id),
|
|
642
|
+
resources: this.resources.forAdapter(adapter.id),
|
|
643
|
+
logger: this.#logger,
|
|
644
|
+
health: this.#healthReporter(adapter.id),
|
|
645
|
+
options: Object.freeze({ ...options }),
|
|
646
|
+
signal: lifecycle.abort.signal,
|
|
647
|
+
inbox: this.inbox.projectorFor(adapter.id),
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
#healthReporter(adapterId) {
|
|
651
|
+
return Object.freeze({
|
|
652
|
+
report: (update) => (this.#report(adapterId, update)),
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
#report(adapterId, update) {
|
|
656
|
+
const key = `${adapterId}\0${update.capability ?? ''}`;
|
|
657
|
+
this.#health.set(key, { adapterId, ...structuredClone(update) });
|
|
658
|
+
}
|
|
659
|
+
#trackedController(adapter, raw) {
|
|
660
|
+
return Object.freeze({
|
|
661
|
+
attach: async (candidate) => (this.#track(adapter, await raw.attach(candidate), candidate)),
|
|
662
|
+
associateSession: async (lease, sessionId) => {
|
|
663
|
+
const ref = await raw.associateSession(lease, sessionId);
|
|
664
|
+
const tracked = this.#tracked.get(ref.runId);
|
|
665
|
+
if (tracked && tracked.lease === lease)
|
|
666
|
+
tracked.candidate.sessionId = sessionId;
|
|
667
|
+
return ref;
|
|
668
|
+
},
|
|
669
|
+
replace: async (current, candidate, reason) => (this.#track(adapter, await raw.replace(current, candidate, reason), candidate)),
|
|
670
|
+
revoke: (lease, reason) => raw.revoke(lease, reason),
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
#track(adapter, lease, candidate) {
|
|
674
|
+
const tracked = { adapter, lease, candidate: cloneCandidate(candidate) };
|
|
675
|
+
this.#tracked.set(lease.ref.runId, tracked);
|
|
676
|
+
const remove = () => {
|
|
677
|
+
if (this.#tracked.get(lease.ref.runId) === tracked)
|
|
678
|
+
this.#tracked.delete(lease.ref.runId);
|
|
679
|
+
};
|
|
680
|
+
if (lease.signal.aborted)
|
|
681
|
+
remove();
|
|
682
|
+
else
|
|
683
|
+
lease.signal.addEventListener('abort', remove, { once: true });
|
|
684
|
+
return lease;
|
|
685
|
+
}
|
|
686
|
+
async #authorize(adapterId, candidate, generation) {
|
|
687
|
+
if (!this.#started || this.#closed)
|
|
688
|
+
throw new Error('AgentRuntime is unavailable');
|
|
689
|
+
const previous = this.#authorizationQueues.get(candidate.paneId) ?? Promise.resolve();
|
|
690
|
+
let release;
|
|
691
|
+
const current = new Promise((resolve) => { release = resolve; });
|
|
692
|
+
const queued = previous.catch(() => { }).then(() => current);
|
|
693
|
+
this.#authorizationQueues.set(candidate.paneId, queued);
|
|
694
|
+
await previous.catch(() => { });
|
|
695
|
+
try {
|
|
696
|
+
return await this.#authorizeGeneration(adapterId, candidate, generation);
|
|
697
|
+
}
|
|
698
|
+
finally {
|
|
699
|
+
release();
|
|
700
|
+
if (this.#authorizationQueues.get(candidate.paneId) === queued) {
|
|
701
|
+
this.#authorizationQueues.delete(candidate.paneId);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
async #authorizeGeneration(adapterId, candidate, generation) {
|
|
706
|
+
const controller = this.runControlFor(adapterId);
|
|
707
|
+
const current = this.runs.currentForPane(candidate.paneId);
|
|
708
|
+
if (!generation || !current || current.ref.agentId !== adapterId) {
|
|
709
|
+
const lease = await controller.attach(candidate);
|
|
710
|
+
if (generation)
|
|
711
|
+
this.#tracked.get(lease.ref.runId).bridgeGenerationId = generation.id;
|
|
712
|
+
return lease;
|
|
713
|
+
}
|
|
714
|
+
const tracked = this.#tracked.get(current.ref.runId);
|
|
715
|
+
if (tracked?.bridgeGenerationId === generation.id) {
|
|
716
|
+
return controller.attach(candidate);
|
|
717
|
+
}
|
|
718
|
+
const lease = await controller.replace(current, candidate, 'session_replaced');
|
|
719
|
+
this.#tracked.get(lease.ref.runId).bridgeGenerationId = generation.id;
|
|
720
|
+
return lease;
|
|
721
|
+
}
|
|
722
|
+
async #bridgeConnected(lease, connection) {
|
|
723
|
+
const binding = this.#bindings.get(lease.ref.agentId);
|
|
724
|
+
if (!binding?.onBridgeConnected || !this.#startedBindings.has(lease.ref.agentId))
|
|
725
|
+
return;
|
|
726
|
+
try {
|
|
727
|
+
await binding.onBridgeConnected(lease, connection);
|
|
728
|
+
this.#report(lease.ref.agentId, {
|
|
729
|
+
capability: 'bridge', availability: 'ready', lastSuccessAt: Date.now(),
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
catch (error) {
|
|
733
|
+
this.#report(lease.ref.agentId, {
|
|
734
|
+
capability: 'bridge', availability: 'degraded',
|
|
735
|
+
message: error instanceof Error ? error.message : String(error),
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
async #verifyAttachment(adapter, candidate) {
|
|
740
|
+
let panes;
|
|
741
|
+
try {
|
|
742
|
+
panes = await this.#panes.list();
|
|
743
|
+
}
|
|
744
|
+
catch {
|
|
745
|
+
return false;
|
|
746
|
+
}
|
|
747
|
+
const pane = panes.find((value) => value.paneId === candidate.paneId);
|
|
748
|
+
return pane ? (await this.#verifyAgainst(adapter, candidate, pane)) === 'valid' : false;
|
|
749
|
+
}
|
|
750
|
+
async #verifyAgainst(adapter, candidate, pane) {
|
|
751
|
+
let inspected;
|
|
752
|
+
const context = {
|
|
753
|
+
inspectForeground: () => {
|
|
754
|
+
inspected ??= this.#process.inspectForeground(pane);
|
|
755
|
+
return inspected;
|
|
756
|
+
},
|
|
757
|
+
};
|
|
758
|
+
try {
|
|
759
|
+
return await lifecycleWithin((async () => {
|
|
760
|
+
const identity = await resolveAgentIdentity(pane, this.adapters, context, {
|
|
761
|
+
verifyTimeoutMs: this.#verifyTimeoutMs,
|
|
762
|
+
});
|
|
763
|
+
if (identity.kind === 'unknown')
|
|
764
|
+
return 'unknown';
|
|
765
|
+
if (identity.kind !== 'matched' || identity.adapter.id !== adapter.id)
|
|
766
|
+
return 'invalid';
|
|
767
|
+
const foreground = await context.inspectForeground(pane);
|
|
768
|
+
if (foreground === null)
|
|
769
|
+
return 'unknown';
|
|
770
|
+
return sameProcess(candidate, pane, foreground) ? 'valid' : 'invalid';
|
|
771
|
+
})(), this.#verifyTimeoutMs, 'Agent process verification');
|
|
772
|
+
}
|
|
773
|
+
catch {
|
|
774
|
+
return 'unknown';
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
async #reconcile(snapshot) {
|
|
778
|
+
const panes = new Map(snapshot.map((pane) => [pane.paneId, pane]));
|
|
779
|
+
for (const tracked of [...this.#tracked.values()]) {
|
|
780
|
+
if (this.#closed)
|
|
781
|
+
return;
|
|
782
|
+
if (tracked.lease.signal.aborted || this.runs.resolve(tracked.lease.ref) !== tracked.lease)
|
|
783
|
+
continue;
|
|
784
|
+
const pane = panes.get(tracked.lease.ref.paneId);
|
|
785
|
+
const verdict = pane
|
|
786
|
+
? await this.#verifyAgainst(tracked.adapter, tracked.candidate, pane)
|
|
787
|
+
: 'invalid';
|
|
788
|
+
if (verdict === 'invalid' && this.runs.resolve(tracked.lease.ref) === tracked.lease) {
|
|
789
|
+
await this.runs.revokePane(tracked.lease.ref.paneId, pane ? 'process_exit' : 'pane_detached');
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|