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.
Files changed (327) hide show
  1. package/README.md +31 -8
  2. package/README.zh-CN.md +31 -8
  3. package/bin/handmux.js +14 -688
  4. package/bin/node-version.js +20 -0
  5. package/dist/bin/handmux.js +1015 -0
  6. package/dist/connectors/bridgeClient.js +385 -0
  7. package/dist/connectors/claude/index.js +476 -0
  8. package/dist/connectors/pi/bridgeClient.js +8 -0
  9. package/dist/connectors/pi/index.js +851 -0
  10. package/dist/hooks/handmux-notify.sh +64 -0
  11. package/{hooks → dist/hooks}/handmux-statusline.cjs +28 -6
  12. package/dist/hooks/handmux-write.cjs +264 -0
  13. package/dist/package.json +127 -0
  14. package/dist/public/assets/index-CTrqTqtT.css +32 -0
  15. package/dist/public/assets/index-DmjeMSQA.js +346 -0
  16. package/dist/public/assets/pcm-worklet-CxcDy7PB.js +1 -0
  17. package/{public → dist/public}/index.html +5 -4
  18. package/dist/src/agent-runtime/adapter.js +204 -0
  19. package/dist/src/agent-runtime/bridge.js +1006 -0
  20. package/dist/src/agent-runtime/bridgeStore.js +25 -0
  21. package/dist/src/agent-runtime/bridgeTransport.js +619 -0
  22. package/dist/src/agent-runtime/bridgeTypes.js +13 -0
  23. package/dist/src/agent-runtime/builtinRuntime.js +193 -0
  24. package/dist/src/agent-runtime/conversation.js +2179 -0
  25. package/dist/src/agent-runtime/conversationActivation.js +75 -0
  26. package/dist/src/agent-runtime/conversationActivity.js +69 -0
  27. package/dist/src/agent-runtime/conversationControls.js +206 -0
  28. package/dist/src/agent-runtime/conversationLiveHub.js +276 -0
  29. package/dist/src/agent-runtime/conversationStore.js +352 -0
  30. package/dist/src/agent-runtime/conversationTypes.js +1 -0
  31. package/dist/src/agent-runtime/conversationValidation.js +339 -0
  32. package/dist/src/agent-runtime/inbox.js +973 -0
  33. package/dist/src/agent-runtime/inboxPushProjection.js +69 -0
  34. package/dist/src/agent-runtime/inboxStore.js +25 -0
  35. package/dist/src/agent-runtime/inboxTypes.js +1 -0
  36. package/dist/src/agent-runtime/interaction.js +829 -0
  37. package/dist/src/agent-runtime/interactionLiveHub.js +225 -0
  38. package/dist/src/agent-runtime/interactionStore.js +18 -0
  39. package/dist/src/agent-runtime/interactionTypes.js +1 -0
  40. package/dist/src/agent-runtime/legacyInboxProjection.js +70 -0
  41. package/dist/src/agent-runtime/migrateLegacyCodexOutbox.js +336 -0
  42. package/dist/src/agent-runtime/resourceTypes.js +1 -0
  43. package/dist/src/agent-runtime/resources.js +245 -0
  44. package/dist/src/agent-runtime/run.js +328 -0
  45. package/dist/src/agent-runtime/runtime.js +793 -0
  46. package/dist/src/agent-runtime/sessionControl.js +164 -0
  47. package/dist/src/agent-runtime/subscriptionUsage.js +190 -0
  48. package/dist/src/agent-runtime/subscriptionUsageLegacy.js +18 -0
  49. package/dist/src/agent-runtime/tmuxRuntime.js +156 -0
  50. package/dist/src/agents/claude.js +203 -0
  51. package/dist/src/agents/claudeConversation.js +362 -0
  52. package/dist/src/agents/claudeInteraction.js +134 -0
  53. package/dist/src/agents/claudeSubscriptionUsage.js +61 -0
  54. package/dist/src/agents/codex.js +262 -0
  55. package/dist/src/agents/codexConversation.js +1103 -0
  56. package/dist/src/agents/codexConversationActivation.js +121 -0
  57. package/dist/src/agents/codexConversationControls.js +256 -0
  58. package/dist/src/agents/codexInteraction.js +306 -0
  59. package/dist/src/agents/codexSessionControl.js +118 -0
  60. package/dist/src/agents/codexSubscriptionUsage.js +313 -0
  61. package/dist/src/agents/codexTerminalControl.js +23 -0
  62. package/dist/src/agents/conversationProjectionSafety.js +416 -0
  63. package/dist/src/agents/index.js +39 -0
  64. package/dist/src/agents/nativeInbox.js +264 -0
  65. package/dist/src/agents/nativeInboxSources.js +115 -0
  66. package/dist/src/agents/pi.js +28 -0
  67. package/dist/src/agents/piConversation.js +523 -0
  68. package/dist/src/agents/piConversationHistory.js +502 -0
  69. package/dist/src/agents/piInboxBridge.js +368 -0
  70. package/dist/src/agents/piSessionControl.js +32 -0
  71. package/dist/src/agents/processIdentity.js +94 -0
  72. package/dist/src/agents/scanUtils.js +262 -0
  73. package/dist/src/apiAccountProviders.js +194 -0
  74. package/dist/src/apiAccounts.js +592 -0
  75. package/dist/src/apiErrors.js +86 -0
  76. package/{src → dist/src}/appName.js +12 -13
  77. package/dist/src/asr/iflyConfig.js +7 -0
  78. package/dist/src/asr/iflySign.js +14 -0
  79. package/dist/src/auth.js +27 -0
  80. package/dist/src/browser/bootstrap.js +72 -0
  81. package/dist/src/browser/cookieProfiles.js +413 -0
  82. package/dist/src/browser/coordinator.js +51 -0
  83. package/dist/src/browser/hammerheadCompat.js +104 -0
  84. package/dist/src/browser/hammerheadRedirectCompat.js +37 -0
  85. package/dist/src/browser/manager.js +560 -0
  86. package/dist/src/browser/originLabel.js +15 -0
  87. package/dist/src/browser/profilePersistence.js +235 -0
  88. package/dist/src/browser/publicProxy.js +176 -0
  89. package/dist/src/browser/routes.js +159 -0
  90. package/dist/src/browser/sessionStore.js +72 -0
  91. package/dist/src/browser/siteVersion.js +121 -0
  92. package/dist/src/browser/targetPolicy.js +163 -0
  93. package/{src → dist/src}/browser/worker.js +14 -18
  94. package/dist/src/browser/workerClient.js +286 -0
  95. package/dist/src/browser/workerServer.js +91 -0
  96. package/dist/src/captureBackground.js +95 -0
  97. package/dist/src/claudeEvents.js +525 -0
  98. package/dist/src/cli/agentIntegration.js +131 -0
  99. package/dist/src/cli/agentLauncher.js +26 -0
  100. package/dist/src/cli/claudeHooks.js +290 -0
  101. package/{src → dist/src}/cli/cloudflareUrl.js +2 -3
  102. package/dist/src/cli/cloudflared.js +106 -0
  103. package/dist/src/cli/codexManaged.js +80 -0
  104. package/{src → dist/src}/cli/cpolarUrl.js +8 -9
  105. package/dist/src/cli/drivers.js +98 -0
  106. package/dist/src/cli/hookScaffold.js +39 -0
  107. package/dist/src/cli/i18n/en.js +392 -0
  108. package/{src → dist/src}/cli/i18n/index.js +27 -25
  109. package/dist/src/cli/i18n/zh.js +391 -0
  110. package/dist/src/cli/legacyCodexHooks.js +52 -0
  111. package/{src → dist/src}/cli/natappUrl.js +2 -3
  112. package/dist/src/cli/options.js +245 -0
  113. package/dist/src/cli/piExtension.js +155 -0
  114. package/dist/src/cli/probe.js +16 -0
  115. package/{src → dist/src}/cli/prompt.js +4 -6
  116. package/dist/src/cli/pushCmd.js +102 -0
  117. package/{src → dist/src}/cli/qr.js +19 -19
  118. package/dist/src/cli/service.js +154 -0
  119. package/dist/src/cli/setupModel.js +194 -0
  120. package/dist/src/cli/setupWizard.js +647 -0
  121. package/dist/src/cli/shortcutEditor.js +351 -0
  122. package/dist/src/cli/sshTunnel.js +18 -0
  123. package/dist/src/cli/state.js +117 -0
  124. package/dist/src/cli/statusLine.js +115 -0
  125. package/dist/src/cli/supervisor.js +278 -0
  126. package/dist/src/cli/supervisorHealth.js +21 -0
  127. package/dist/src/cli/supervisorLaunch.js +148 -0
  128. package/dist/src/cli/supervisorProcesses.js +62 -0
  129. package/dist/src/cli/supervisorState.js +62 -0
  130. package/dist/src/cli/tmuxVersion.js +53 -0
  131. package/{src → dist/src}/cli/tunlite.js +7 -10
  132. package/dist/src/cli/tunnelClients.js +72 -0
  133. package/{src → dist/src}/cli/updateCheck.js +98 -69
  134. package/{src → dist/src}/cli/urlHost.js +8 -4
  135. package/dist/src/cli/workspaceCmd.js +347 -0
  136. package/dist/src/codexAppServer.js +3436 -0
  137. package/dist/src/codexConversationProjection.js +199 -0
  138. package/dist/src/codexDiff.js +118 -0
  139. package/dist/src/codexMessageIdentity.js +18 -0
  140. package/dist/src/codexPlan.js +30 -0
  141. package/dist/src/codexQueueProtocol.js +137 -0
  142. package/dist/src/codexStreamProtocol.js +207 -0
  143. package/dist/src/codexToolProtocol.js +68 -0
  144. package/dist/src/codexTranscriptParse.js +835 -0
  145. package/dist/src/codexUsageSnapshot.js +228 -0
  146. package/dist/src/config.js +8 -0
  147. package/{src → dist/src}/docPath.js +22 -24
  148. package/dist/src/docs.js +337 -0
  149. package/dist/src/git.js +284 -0
  150. package/dist/src/healthProtocol.js +97 -0
  151. package/dist/src/httpApi.js +96 -0
  152. package/dist/src/jsonStore.js +15 -0
  153. package/{src → dist/src}/keyNames.js +3 -3
  154. package/dist/src/notifications.js +111 -0
  155. package/dist/src/orphans.js +167 -0
  156. package/dist/src/paneInput.js +42 -0
  157. package/dist/src/pendingPrompt.js +194 -0
  158. package/dist/src/previewServer.js +202 -0
  159. package/dist/src/previews.js +179 -0
  160. package/dist/src/privateStateStore.js +124 -0
  161. package/dist/src/projectTask/backup.js +224 -0
  162. package/dist/src/projectTask/lock.js +108 -0
  163. package/dist/src/projectTask/migrations.js +90 -0
  164. package/dist/src/projectTask/routes.js +155 -0
  165. package/dist/src/projectTask/runtime.js +182 -0
  166. package/dist/src/projectTask/schema.js +56 -0
  167. package/dist/src/projectTask/store.js +457 -0
  168. package/dist/src/push.js +224 -0
  169. package/dist/src/routes/agents.js +631 -0
  170. package/dist/src/routes/apiAccounts.js +82 -0
  171. package/dist/src/routes/files.js +230 -0
  172. package/dist/src/routes/git.js +89 -0
  173. package/dist/src/routes/health.js +16 -0
  174. package/dist/src/routes/notifications.js +16 -0
  175. package/dist/src/routes/previews.js +43 -0
  176. package/dist/src/routes/push.js +145 -0
  177. package/dist/src/routes/sessions.js +244 -0
  178. package/dist/src/routes/system.js +220 -0
  179. package/dist/src/routes/terminal.js +244 -0
  180. package/dist/src/routes/workspace.js +267 -0
  181. package/dist/src/server.js +315 -0
  182. package/dist/src/shortcutConfig.js +83 -0
  183. package/{src → dist/src}/staticCache.js +5 -3
  184. package/{src → dist/src}/staticCompression.js +5 -6
  185. package/dist/src/terminalStream.js +441 -0
  186. package/{src → dist/src}/tmux/commands.js +185 -172
  187. package/dist/src/tmux/format.js +47 -0
  188. package/dist/src/transcriptParse.js +261 -0
  189. package/dist/src/transcriptReader.js +209 -0
  190. package/{src → dist/src}/trimCapture.js +11 -11
  191. package/dist/src/uploadTypes.js +31 -0
  192. package/dist/src/urlPolicy.js +18 -0
  193. package/dist/src/usagePaths.js +15 -0
  194. package/dist/src/workspace/agentRunner.js +301 -0
  195. package/dist/src/workspace/atomicJson.js +31 -0
  196. package/dist/src/workspace/capture.js +139 -0
  197. package/dist/src/workspace/checkpointer.js +228 -0
  198. package/dist/src/workspace/environment.js +71 -0
  199. package/dist/src/workspace/lock.js +137 -0
  200. package/dist/src/workspace/mapping.js +128 -0
  201. package/dist/src/workspace/operations.js +271 -0
  202. package/dist/src/workspace/paths.js +15 -0
  203. package/dist/src/workspace/planner.js +292 -0
  204. package/dist/src/workspace/restore.js +363 -0
  205. package/dist/src/workspace/runtime.js +318 -0
  206. package/dist/src/workspace/schema.js +156 -0
  207. package/dist/src/workspace/store.js +464 -0
  208. package/dist/src/workspace/tmuxAdapter.js +704 -0
  209. package/dist/src/workspaceProtocol.js +158 -0
  210. package/package.json +30 -17
  211. package/hooks/handmux-notify.sh +0 -22
  212. package/hooks/handmux-write.cjs +0 -78
  213. package/public/assets/index-C3N7OkyP.css +0 -32
  214. package/public/assets/index-D1DWcC5u.js +0 -337
  215. package/src/.gitkeep +0 -0
  216. package/src/agents/claude.js +0 -159
  217. package/src/agents/codex.js +0 -168
  218. package/src/agents/index.js +0 -21
  219. package/src/agents/processIdentity.js +0 -72
  220. package/src/agents/scanUtils.js +0 -225
  221. package/src/asr/iflyConfig.js +0 -10
  222. package/src/asr/iflySign.js +0 -16
  223. package/src/auth.js +0 -30
  224. package/src/browser/bootstrap.js +0 -68
  225. package/src/browser/cookieProfiles.js +0 -403
  226. package/src/browser/coordinator.js +0 -47
  227. package/src/browser/hammerheadCompat.js +0 -86
  228. package/src/browser/hammerheadRedirectCompat.js +0 -38
  229. package/src/browser/manager.js +0 -556
  230. package/src/browser/originLabel.js +0 -17
  231. package/src/browser/profilePersistence.js +0 -241
  232. package/src/browser/publicProxy.js +0 -175
  233. package/src/browser/routes.js +0 -154
  234. package/src/browser/sessionStore.js +0 -78
  235. package/src/browser/siteVersion.js +0 -124
  236. package/src/browser/targetPolicy.js +0 -137
  237. package/src/browser/workerClient.js +0 -253
  238. package/src/browser/workerServer.js +0 -98
  239. package/src/captureBackground.js +0 -93
  240. package/src/claudeEvents.js +0 -399
  241. package/src/cli/claudeHooks.js +0 -206
  242. package/src/cli/cloudflared.js +0 -99
  243. package/src/cli/codexManaged.js +0 -77
  244. package/src/cli/drivers.js +0 -97
  245. package/src/cli/hookScaffold.js +0 -33
  246. package/src/cli/i18n/en.js +0 -385
  247. package/src/cli/i18n/zh.js +0 -384
  248. package/src/cli/legacyCodexHooks.js +0 -49
  249. package/src/cli/options.js +0 -211
  250. package/src/cli/probe.js +0 -16
  251. package/src/cli/pushCmd.js +0 -60
  252. package/src/cli/service.js +0 -126
  253. package/src/cli/setupModel.js +0 -154
  254. package/src/cli/setupWizard.js +0 -506
  255. package/src/cli/shortcutEditor.js +0 -288
  256. package/src/cli/sshTunnel.js +0 -12
  257. package/src/cli/state.js +0 -90
  258. package/src/cli/statusLine.js +0 -90
  259. package/src/cli/supervisor.js +0 -183
  260. package/src/cli/supervisorProcesses.js +0 -57
  261. package/src/cli/tmuxVersion.js +0 -49
  262. package/src/cli/tunnelClients.js +0 -62
  263. package/src/cli/workspaceCmd.js +0 -300
  264. package/src/codexAppServer.js +0 -1620
  265. package/src/codexDiff.js +0 -108
  266. package/src/codexTranscriptParse.js +0 -413
  267. package/src/codexUsageSnapshot.cjs +0 -129
  268. package/src/config.js +0 -9
  269. package/src/docs.js +0 -228
  270. package/src/git.js +0 -208
  271. package/src/httpApi.js +0 -72
  272. package/src/jsonStore.js +0 -24
  273. package/src/notifications.js +0 -68
  274. package/src/orphans.js +0 -152
  275. package/src/pendingPrompt.js +0 -163
  276. package/src/previewServer.js +0 -168
  277. package/src/previews.js +0 -116
  278. package/src/push.js +0 -183
  279. package/src/routes/codex.js +0 -473
  280. package/src/routes/files.js +0 -172
  281. package/src/routes/git.js +0 -57
  282. package/src/routes/notifications.js +0 -19
  283. package/src/routes/previews.js +0 -35
  284. package/src/routes/push.js +0 -119
  285. package/src/routes/sessions.js +0 -168
  286. package/src/routes/system.js +0 -132
  287. package/src/routes/terminal.js +0 -210
  288. package/src/routes/transcript.js +0 -171
  289. package/src/routes/workspace.js +0 -267
  290. package/src/server.js +0 -186
  291. package/src/shortcutConfig.js +0 -75
  292. package/src/terminalStream.js +0 -391
  293. package/src/tmux/format.js +0 -45
  294. package/src/transcriptParse.js +0 -180
  295. package/src/transcriptReader.js +0 -101
  296. package/src/uploadTypes.js +0 -30
  297. package/src/urlPolicy.js +0 -14
  298. package/src/usage.js +0 -260
  299. package/src/workspace/agentRunner.js +0 -188
  300. package/src/workspace/atomicJson.js +0 -32
  301. package/src/workspace/capture.js +0 -139
  302. package/src/workspace/checkpointer.js +0 -187
  303. package/src/workspace/environment.js +0 -75
  304. package/src/workspace/lock.js +0 -128
  305. package/src/workspace/mapping.js +0 -124
  306. package/src/workspace/operations.js +0 -252
  307. package/src/workspace/paths.js +0 -16
  308. package/src/workspace/planner.js +0 -247
  309. package/src/workspace/restore.js +0 -279
  310. package/src/workspace/runtime.js +0 -314
  311. package/src/workspace/schema.js +0 -136
  312. package/src/workspace/store.js +0 -382
  313. package/src/workspace/tmuxAdapter.js +0 -554
  314. /package/{public → dist/public}/fonts/JetBrainsMonoNerdFontMono-Regular.woff2 +0 -0
  315. /package/{public → dist/public}/fonts/TWUnifont.woff2 +0 -0
  316. /package/{public → dist/public}/icons/apple-touch-icon.png +0 -0
  317. /package/{public → dist/public}/icons/badge-96.png +0 -0
  318. /package/{public → dist/public}/icons/icon-192-maskable.png +0 -0
  319. /package/{public → dist/public}/icons/icon-192.png +0 -0
  320. /package/{public → dist/public}/icons/icon-512-maskable.png +0 -0
  321. /package/{public → dist/public}/icons/icon-512.png +0 -0
  322. /package/{public → dist/public}/icons/logo.svg +0 -0
  323. /package/{public → dist/public}/manifest.webmanifest +0 -0
  324. /package/{public → dist/public}/offline.html +0 -0
  325. /package/{public → dist/public}/sw.js +0 -0
  326. /package/{src → dist/src}/browser/protocol.js +0 -0
  327. /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
+ }