@xenosystem/agent-interface-host 0.1.3

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 (482) hide show
  1. package/LICENSE +67 -0
  2. package/dist/acp/AcpAgentRegistryService.d.ts +77 -0
  3. package/dist/acp/AcpAgentRegistryService.d.ts.map +1 -0
  4. package/dist/acp/AcpAgentRegistryService.js +754 -0
  5. package/dist/acp/AcpAgentRegistryService.js.map +1 -0
  6. package/dist/acp/AcpAgentTurnExecutionAdapter.d.ts +56 -0
  7. package/dist/acp/AcpAgentTurnExecutionAdapter.d.ts.map +1 -0
  8. package/dist/acp/AcpAgentTurnExecutionAdapter.js +323 -0
  9. package/dist/acp/AcpAgentTurnExecutionAdapter.js.map +1 -0
  10. package/dist/acp/AcpApprovalPortAdapter.d.ts +41 -0
  11. package/dist/acp/AcpApprovalPortAdapter.d.ts.map +1 -0
  12. package/dist/acp/AcpApprovalPortAdapter.js +82 -0
  13. package/dist/acp/AcpApprovalPortAdapter.js.map +1 -0
  14. package/dist/acp/AcpCoordinationBridge.d.ts +13 -0
  15. package/dist/acp/AcpCoordinationBridge.d.ts.map +1 -0
  16. package/dist/acp/AcpCoordinationBridge.js +13 -0
  17. package/dist/acp/AcpCoordinationBridge.js.map +1 -0
  18. package/dist/acp/AcpDiagnosticsService.d.ts +61 -0
  19. package/dist/acp/AcpDiagnosticsService.d.ts.map +1 -0
  20. package/dist/acp/AcpDiagnosticsService.js +203 -0
  21. package/dist/acp/AcpDiagnosticsService.js.map +1 -0
  22. package/dist/acp/AcpExternalCertificationHarness.d.ts +47 -0
  23. package/dist/acp/AcpExternalCertificationHarness.d.ts.map +1 -0
  24. package/dist/acp/AcpExternalCertificationHarness.js +176 -0
  25. package/dist/acp/AcpExternalCertificationHarness.js.map +1 -0
  26. package/dist/acp/AcpHostBridge.d.ts +81 -0
  27. package/dist/acp/AcpHostBridge.d.ts.map +1 -0
  28. package/dist/acp/AcpHostBridge.js +101 -0
  29. package/dist/acp/AcpHostBridge.js.map +1 -0
  30. package/dist/acp/AcpInteractivePermissionBridge.d.ts +77 -0
  31. package/dist/acp/AcpInteractivePermissionBridge.d.ts.map +1 -0
  32. package/dist/acp/AcpInteractivePermissionBridge.js +127 -0
  33. package/dist/acp/AcpInteractivePermissionBridge.js.map +1 -0
  34. package/dist/acp/AcpPermissionBridge.d.ts +19 -0
  35. package/dist/acp/AcpPermissionBridge.d.ts.map +1 -0
  36. package/dist/acp/AcpPermissionBridge.js +110 -0
  37. package/dist/acp/AcpPermissionBridge.js.map +1 -0
  38. package/dist/acp/AcpPermissionPolicy.d.ts +111 -0
  39. package/dist/acp/AcpPermissionPolicy.d.ts.map +1 -0
  40. package/dist/acp/AcpPermissionPolicy.js +167 -0
  41. package/dist/acp/AcpPermissionPolicy.js.map +1 -0
  42. package/dist/acp/AcpProcessSupervisor.d.ts +134 -0
  43. package/dist/acp/AcpProcessSupervisor.d.ts.map +1 -0
  44. package/dist/acp/AcpProcessSupervisor.js +674 -0
  45. package/dist/acp/AcpProcessSupervisor.js.map +1 -0
  46. package/dist/acp/AcpProviderApprovalService.d.ts +27 -0
  47. package/dist/acp/AcpProviderApprovalService.d.ts.map +1 -0
  48. package/dist/acp/AcpProviderApprovalService.js +128 -0
  49. package/dist/acp/AcpProviderApprovalService.js.map +1 -0
  50. package/dist/acp/AcpProviderCatalogService.d.ts +69 -0
  51. package/dist/acp/AcpProviderCatalogService.d.ts.map +1 -0
  52. package/dist/acp/AcpProviderCatalogService.js +707 -0
  53. package/dist/acp/AcpProviderCatalogService.js.map +1 -0
  54. package/dist/acp/AcpProviderModelService.d.ts +6 -0
  55. package/dist/acp/AcpProviderModelService.d.ts.map +1 -0
  56. package/dist/acp/AcpProviderModelService.js +274 -0
  57. package/dist/acp/AcpProviderModelService.js.map +1 -0
  58. package/dist/acp/AcpProviderVersionReadiness.d.ts +6 -0
  59. package/dist/acp/AcpProviderVersionReadiness.d.ts.map +1 -0
  60. package/dist/acp/AcpProviderVersionReadiness.js +100 -0
  61. package/dist/acp/AcpProviderVersionReadiness.js.map +1 -0
  62. package/dist/acp/AcpRuntimeEventMapper.d.ts +16 -0
  63. package/dist/acp/AcpRuntimeEventMapper.d.ts.map +1 -0
  64. package/dist/acp/AcpRuntimeEventMapper.js +326 -0
  65. package/dist/acp/AcpRuntimeEventMapper.js.map +1 -0
  66. package/dist/acp/AcpSessionService.d.ts +65 -0
  67. package/dist/acp/AcpSessionService.d.ts.map +1 -0
  68. package/dist/acp/AcpSessionService.js +931 -0
  69. package/dist/acp/AcpSessionService.js.map +1 -0
  70. package/dist/acp/DurableAcpPermissionPolicyStore.d.ts +25 -0
  71. package/dist/acp/DurableAcpPermissionPolicyStore.d.ts.map +1 -0
  72. package/dist/acp/DurableAcpPermissionPolicyStore.js +100 -0
  73. package/dist/acp/DurableAcpPermissionPolicyStore.js.map +1 -0
  74. package/dist/acp/acpCommandSafety.d.ts +63 -0
  75. package/dist/acp/acpCommandSafety.d.ts.map +1 -0
  76. package/dist/acp/acpCommandSafety.js +90 -0
  77. package/dist/acp/acpCommandSafety.js.map +1 -0
  78. package/dist/acp/acpErrors.d.ts +47 -0
  79. package/dist/acp/acpErrors.d.ts.map +1 -0
  80. package/dist/acp/acpErrors.js +93 -0
  81. package/dist/acp/acpErrors.js.map +1 -0
  82. package/dist/acp/acpRedaction.d.ts +9 -0
  83. package/dist/acp/acpRedaction.d.ts.map +1 -0
  84. package/dist/acp/acpRedaction.js +69 -0
  85. package/dist/acp/acpRedaction.js.map +1 -0
  86. package/dist/acp/acpSessionResume.d.ts +8 -0
  87. package/dist/acp/acpSessionResume.d.ts.map +1 -0
  88. package/dist/acp/acpSessionResume.js +31 -0
  89. package/dist/acp/acpSessionResume.js.map +1 -0
  90. package/dist/acp/acpTypes.d.ts +467 -0
  91. package/dist/acp/acpTypes.d.ts.map +1 -0
  92. package/dist/acp/acpTypes.js +2 -0
  93. package/dist/acp/acpTypes.js.map +1 -0
  94. package/dist/agent/chooseDirectoryBridge.d.ts +29 -0
  95. package/dist/agent/chooseDirectoryBridge.d.ts.map +1 -0
  96. package/dist/agent/chooseDirectoryBridge.js +116 -0
  97. package/dist/agent/chooseDirectoryBridge.js.map +1 -0
  98. package/dist/agent/permissionProfiles.d.ts +29 -0
  99. package/dist/agent/permissionProfiles.d.ts.map +1 -0
  100. package/dist/agent/permissionProfiles.js +107 -0
  101. package/dist/agent/permissionProfiles.js.map +1 -0
  102. package/dist/agent/runtimeAuthorityPrompt.d.ts +13 -0
  103. package/dist/agent/runtimeAuthorityPrompt.d.ts.map +1 -0
  104. package/dist/agent/runtimeAuthorityPrompt.js +45 -0
  105. package/dist/agent/runtimeAuthorityPrompt.js.map +1 -0
  106. package/dist/agent/runtimeModeOutputGuard.d.ts +6 -0
  107. package/dist/agent/runtimeModeOutputGuard.d.ts.map +1 -0
  108. package/dist/agent/runtimeModeOutputGuard.js +40 -0
  109. package/dist/agent/runtimeModeOutputGuard.js.map +1 -0
  110. package/dist/agent/workspacePlacementBridge.d.ts +29 -0
  111. package/dist/agent/workspacePlacementBridge.d.ts.map +1 -0
  112. package/dist/agent/workspacePlacementBridge.js +143 -0
  113. package/dist/agent/workspacePlacementBridge.js.map +1 -0
  114. package/dist/agentSearch/AgentSearchService.d.ts +81 -0
  115. package/dist/agentSearch/AgentSearchService.d.ts.map +1 -0
  116. package/dist/agentSearch/AgentSearchService.js +1038 -0
  117. package/dist/agentSearch/AgentSearchService.js.map +1 -0
  118. package/dist/agentSearch/JsonInvertedIndexAdapter.d.ts +44 -0
  119. package/dist/agentSearch/JsonInvertedIndexAdapter.d.ts.map +1 -0
  120. package/dist/agentSearch/JsonInvertedIndexAdapter.js +307 -0
  121. package/dist/agentSearch/JsonInvertedIndexAdapter.js.map +1 -0
  122. package/dist/agentSearch/SearchIndexAdapter.d.ts +33 -0
  123. package/dist/agentSearch/SearchIndexAdapter.d.ts.map +1 -0
  124. package/dist/agentSearch/SearchIndexAdapter.js +2 -0
  125. package/dist/agentSearch/SearchIndexAdapter.js.map +1 -0
  126. package/dist/agentSearch/SearchSourceWatcher.d.ts +29 -0
  127. package/dist/agentSearch/SearchSourceWatcher.d.ts.map +1 -0
  128. package/dist/agentSearch/SearchSourceWatcher.js +150 -0
  129. package/dist/agentSearch/SearchSourceWatcher.js.map +1 -0
  130. package/dist/agentSearch/SqliteFtsIndexAdapter.d.ts +58 -0
  131. package/dist/agentSearch/SqliteFtsIndexAdapter.d.ts.map +1 -0
  132. package/dist/agentSearch/SqliteFtsIndexAdapter.js +420 -0
  133. package/dist/agentSearch/SqliteFtsIndexAdapter.js.map +1 -0
  134. package/dist/agentSearch/conversationIndexer.d.ts +69 -0
  135. package/dist/agentSearch/conversationIndexer.d.ts.map +1 -0
  136. package/dist/agentSearch/conversationIndexer.js +313 -0
  137. package/dist/agentSearch/conversationIndexer.js.map +1 -0
  138. package/dist/agentSearch/searchTypes.d.ts +264 -0
  139. package/dist/agentSearch/searchTypes.d.ts.map +1 -0
  140. package/dist/agentSearch/searchTypes.js +2 -0
  141. package/dist/agentSearch/searchTypes.js.map +1 -0
  142. package/dist/agentSearch/searchUtils.d.ts +12 -0
  143. package/dist/agentSearch/searchUtils.d.ts.map +1 -0
  144. package/dist/agentSearch/searchUtils.js +125 -0
  145. package/dist/agentSearch/searchUtils.js.map +1 -0
  146. package/dist/agentSearch/workspaceMetadataIndexer.d.ts +3 -0
  147. package/dist/agentSearch/workspaceMetadataIndexer.d.ts.map +1 -0
  148. package/dist/agentSearch/workspaceMetadataIndexer.js +125 -0
  149. package/dist/agentSearch/workspaceMetadataIndexer.js.map +1 -0
  150. package/dist/cli/CliControlPlaneService.d.ts +269 -0
  151. package/dist/cli/CliControlPlaneService.d.ts.map +1 -0
  152. package/dist/cli/CliControlPlaneService.js +1874 -0
  153. package/dist/cli/CliControlPlaneService.js.map +1 -0
  154. package/dist/cloud/XenoCloudAgentTurnExecutionAdapter.d.ts +91 -0
  155. package/dist/cloud/XenoCloudAgentTurnExecutionAdapter.d.ts.map +1 -0
  156. package/dist/cloud/XenoCloudAgentTurnExecutionAdapter.js +379 -0
  157. package/dist/cloud/XenoCloudAgentTurnExecutionAdapter.js.map +1 -0
  158. package/dist/codebase/CodebaseIntelligenceService.d.ts +68 -0
  159. package/dist/codebase/CodebaseIntelligenceService.d.ts.map +1 -0
  160. package/dist/codebase/CodebaseIntelligenceService.js +1717 -0
  161. package/dist/codebase/CodebaseIntelligenceService.js.map +1 -0
  162. package/dist/codebase/LanguageServerProcessManager.d.ts +85 -0
  163. package/dist/codebase/LanguageServerProcessManager.d.ts.map +1 -0
  164. package/dist/codebase/LanguageServerProcessManager.js +607 -0
  165. package/dist/codebase/LanguageServerProcessManager.js.map +1 -0
  166. package/dist/codebase/codeTypes.d.ts +485 -0
  167. package/dist/codebase/codeTypes.d.ts.map +1 -0
  168. package/dist/codebase/codeTypes.js +2 -0
  169. package/dist/codebase/codeTypes.js.map +1 -0
  170. package/dist/codebase/codeUtils.d.ts +24 -0
  171. package/dist/codebase/codeUtils.d.ts.map +1 -0
  172. package/dist/codebase/codeUtils.js +177 -0
  173. package/dist/codebase/codeUtils.js.map +1 -0
  174. package/dist/codebase/contextPacks.d.ts +18 -0
  175. package/dist/codebase/contextPacks.d.ts.map +1 -0
  176. package/dist/codebase/contextPacks.js +180 -0
  177. package/dist/codebase/contextPacks.js.map +1 -0
  178. package/dist/codebase/dependencyGraph.d.ts +3 -0
  179. package/dist/codebase/dependencyGraph.d.ts.map +1 -0
  180. package/dist/codebase/dependencyGraph.js +209 -0
  181. package/dist/codebase/dependencyGraph.js.map +1 -0
  182. package/dist/codebase/fileIndexer.d.ts +27 -0
  183. package/dist/codebase/fileIndexer.d.ts.map +1 -0
  184. package/dist/codebase/fileIndexer.js +167 -0
  185. package/dist/codebase/fileIndexer.js.map +1 -0
  186. package/dist/codebase/symbolIndexer.d.ts +3 -0
  187. package/dist/codebase/symbolIndexer.d.ts.map +1 -0
  188. package/dist/codebase/symbolIndexer.js +184 -0
  189. package/dist/codebase/symbolIndexer.js.map +1 -0
  190. package/dist/codebase/whatChanged.d.ts +4 -0
  191. package/dist/codebase/whatChanged.d.ts.map +1 -0
  192. package/dist/codebase/whatChanged.js +138 -0
  193. package/dist/codebase/whatChanged.js.map +1 -0
  194. package/dist/coordination/CoordinationService.d.ts +166 -0
  195. package/dist/coordination/CoordinationService.d.ts.map +1 -0
  196. package/dist/coordination/CoordinationService.js +247 -0
  197. package/dist/coordination/CoordinationService.js.map +1 -0
  198. package/dist/coordination/claims.d.ts +122 -0
  199. package/dist/coordination/claims.d.ts.map +1 -0
  200. package/dist/coordination/claims.js +196 -0
  201. package/dist/coordination/claims.js.map +1 -0
  202. package/dist/coordination/coordinationTools.d.ts +40 -0
  203. package/dist/coordination/coordinationTools.d.ts.map +1 -0
  204. package/dist/coordination/coordinationTools.js +92 -0
  205. package/dist/coordination/coordinationTools.js.map +1 -0
  206. package/dist/coordination/laneBridge.d.ts +84 -0
  207. package/dist/coordination/laneBridge.d.ts.map +1 -0
  208. package/dist/coordination/laneBridge.js +86 -0
  209. package/dist/coordination/laneBridge.js.map +1 -0
  210. package/dist/coordination/messages.d.ts +225 -0
  211. package/dist/coordination/messages.d.ts.map +1 -0
  212. package/dist/coordination/messages.js +335 -0
  213. package/dist/coordination/messages.js.map +1 -0
  214. package/dist/coordination/permissionWaitRegistry.d.ts +32 -0
  215. package/dist/coordination/permissionWaitRegistry.d.ts.map +1 -0
  216. package/dist/coordination/permissionWaitRegistry.js +59 -0
  217. package/dist/coordination/permissionWaitRegistry.js.map +1 -0
  218. package/dist/coordination/query.d.ts +187 -0
  219. package/dist/coordination/query.d.ts.map +1 -0
  220. package/dist/coordination/query.js +138 -0
  221. package/dist/coordination/query.js.map +1 -0
  222. package/dist/coordination/renderInbox.d.ts +15 -0
  223. package/dist/coordination/renderInbox.d.ts.map +1 -0
  224. package/dist/coordination/renderInbox.js +19 -0
  225. package/dist/coordination/renderInbox.js.map +1 -0
  226. package/dist/coordination/runtimeEvents.d.ts +53 -0
  227. package/dist/coordination/runtimeEvents.d.ts.map +1 -0
  228. package/dist/coordination/runtimeEvents.js +112 -0
  229. package/dist/coordination/runtimeEvents.js.map +1 -0
  230. package/dist/detachedHostService.d.ts +77 -0
  231. package/dist/detachedHostService.d.ts.map +1 -0
  232. package/dist/detachedHostService.js +66 -0
  233. package/dist/detachedHostService.js.map +1 -0
  234. package/dist/detachedHostSupervisor.d.ts +102 -0
  235. package/dist/detachedHostSupervisor.d.ts.map +1 -0
  236. package/dist/detachedHostSupervisor.js +168 -0
  237. package/dist/detachedHostSupervisor.js.map +1 -0
  238. package/dist/durableRuntimeEventStore.d.ts +49 -0
  239. package/dist/durableRuntimeEventStore.d.ts.map +1 -0
  240. package/dist/durableRuntimeEventStore.js +364 -0
  241. package/dist/durableRuntimeEventStore.js.map +1 -0
  242. package/dist/engineering/EngineeringWorkbenchService.d.ts +142 -0
  243. package/dist/engineering/EngineeringWorkbenchService.d.ts.map +1 -0
  244. package/dist/engineering/EngineeringWorkbenchService.js +2301 -0
  245. package/dist/engineering/EngineeringWorkbenchService.js.map +1 -0
  246. package/dist/engineering/LandingService.d.ts +112 -0
  247. package/dist/engineering/LandingService.d.ts.map +1 -0
  248. package/dist/engineering/LandingService.js +239 -0
  249. package/dist/engineering/LandingService.js.map +1 -0
  250. package/dist/engineering/engineeringTypes.d.ts +2 -0
  251. package/dist/engineering/engineeringTypes.d.ts.map +1 -0
  252. package/dist/engineering/engineeringTypes.js +2 -0
  253. package/dist/engineering/engineeringTypes.js.map +1 -0
  254. package/dist/engineering/patchReview.d.ts +11 -0
  255. package/dist/engineering/patchReview.d.ts.map +1 -0
  256. package/dist/engineering/patchReview.js +174 -0
  257. package/dist/engineering/patchReview.js.map +1 -0
  258. package/dist/index.d.ts +834 -0
  259. package/dist/index.d.ts.map +1 -0
  260. package/dist/index.js +3242 -0
  261. package/dist/index.js.map +1 -0
  262. package/dist/intake/IntakeService.d.ts +84 -0
  263. package/dist/intake/IntakeService.d.ts.map +1 -0
  264. package/dist/intake/IntakeService.js +117 -0
  265. package/dist/intake/IntakeService.js.map +1 -0
  266. package/dist/intake/RepositoryTodoIntakeSource.d.ts +72 -0
  267. package/dist/intake/RepositoryTodoIntakeSource.d.ts.map +1 -0
  268. package/dist/intake/RepositoryTodoIntakeSource.js +168 -0
  269. package/dist/intake/RepositoryTodoIntakeSource.js.map +1 -0
  270. package/dist/intake/WorkspaceTodoIntakeSource.d.ts +53 -0
  271. package/dist/intake/WorkspaceTodoIntakeSource.d.ts.map +1 -0
  272. package/dist/intake/WorkspaceTodoIntakeSource.js +81 -0
  273. package/dist/intake/WorkspaceTodoIntakeSource.js.map +1 -0
  274. package/dist/isolation/CommandExecution.d.ts +132 -0
  275. package/dist/isolation/CommandExecution.d.ts.map +1 -0
  276. package/dist/isolation/CommandExecution.js +48 -0
  277. package/dist/isolation/CommandExecution.js.map +1 -0
  278. package/dist/isolation/LocalCommandExecutor.d.ts +27 -0
  279. package/dist/isolation/LocalCommandExecutor.d.ts.map +1 -0
  280. package/dist/isolation/LocalCommandExecutor.js +97 -0
  281. package/dist/isolation/LocalCommandExecutor.js.map +1 -0
  282. package/dist/isolation/RunExecutionEnvironment.d.ts +135 -0
  283. package/dist/isolation/RunExecutionEnvironment.d.ts.map +1 -0
  284. package/dist/isolation/RunExecutionEnvironment.js +206 -0
  285. package/dist/isolation/RunExecutionEnvironment.js.map +1 -0
  286. package/dist/isolation/RunIsolationService.d.ts +90 -0
  287. package/dist/isolation/RunIsolationService.d.ts.map +1 -0
  288. package/dist/isolation/RunIsolationService.js +86 -0
  289. package/dist/isolation/RunIsolationService.js.map +1 -0
  290. package/dist/isolation/SandboxCommandExecutor.d.ts +93 -0
  291. package/dist/isolation/SandboxCommandExecutor.d.ts.map +1 -0
  292. package/dist/isolation/SandboxCommandExecutor.js +189 -0
  293. package/dist/isolation/SandboxCommandExecutor.js.map +1 -0
  294. package/dist/knowledge/WorkspaceKnowledgeService.d.ts +71 -0
  295. package/dist/knowledge/WorkspaceKnowledgeService.d.ts.map +1 -0
  296. package/dist/knowledge/WorkspaceKnowledgeService.js +103 -0
  297. package/dist/knowledge/WorkspaceKnowledgeService.js.map +1 -0
  298. package/dist/localHostAuthority.d.ts +24 -0
  299. package/dist/localHostAuthority.d.ts.map +1 -0
  300. package/dist/localHostAuthority.js +87 -0
  301. package/dist/localHostAuthority.js.map +1 -0
  302. package/dist/localHostLease.d.ts +51 -0
  303. package/dist/localHostLease.d.ts.map +1 -0
  304. package/dist/localHostLease.js +167 -0
  305. package/dist/localHostLease.js.map +1 -0
  306. package/dist/localHostPaths.d.ts +30 -0
  307. package/dist/localHostPaths.d.ts.map +1 -0
  308. package/dist/localHostPaths.js +60 -0
  309. package/dist/localHostPaths.js.map +1 -0
  310. package/dist/localRpcTransport.d.ts +21 -0
  311. package/dist/localRpcTransport.d.ts.map +1 -0
  312. package/dist/localRpcTransport.js +423 -0
  313. package/dist/localRpcTransport.js.map +1 -0
  314. package/dist/node.d.ts +104 -0
  315. package/dist/node.d.ts.map +1 -0
  316. package/dist/node.js +103 -0
  317. package/dist/node.js.map +1 -0
  318. package/dist/observation/DirectoryBaselineScanner.d.ts +44 -0
  319. package/dist/observation/DirectoryBaselineScanner.d.ts.map +1 -0
  320. package/dist/observation/DirectoryBaselineScanner.js +112 -0
  321. package/dist/observation/DirectoryBaselineScanner.js.map +1 -0
  322. package/dist/observation/MountContentReader.d.ts +63 -0
  323. package/dist/observation/MountContentReader.d.ts.map +1 -0
  324. package/dist/observation/MountContentReader.js +158 -0
  325. package/dist/observation/MountContentReader.js.map +1 -0
  326. package/dist/observation/RunObservationService.d.ts +229 -0
  327. package/dist/observation/RunObservationService.d.ts.map +1 -0
  328. package/dist/observation/RunObservationService.js +248 -0
  329. package/dist/observation/RunObservationService.js.map +1 -0
  330. package/dist/observation/XenoUseObservationSource.d.ts +42 -0
  331. package/dist/observation/XenoUseObservationSource.d.ts.map +1 -0
  332. package/dist/observation/XenoUseObservationSource.js +104 -0
  333. package/dist/observation/XenoUseObservationSource.js.map +1 -0
  334. package/dist/observation/XenoUseRunObserver.d.ts +140 -0
  335. package/dist/observation/XenoUseRunObserver.d.ts.map +1 -0
  336. package/dist/observation/XenoUseRunObserver.js +256 -0
  337. package/dist/observation/XenoUseRunObserver.js.map +1 -0
  338. package/dist/pluginMcp/PluginMcpManagerService.d.ts +141 -0
  339. package/dist/pluginMcp/PluginMcpManagerService.d.ts.map +1 -0
  340. package/dist/pluginMcp/PluginMcpManagerService.js +1821 -0
  341. package/dist/pluginMcp/PluginMcpManagerService.js.map +1 -0
  342. package/dist/pluginMcp/pluginMcpTypes.d.ts +349 -0
  343. package/dist/pluginMcp/pluginMcpTypes.d.ts.map +1 -0
  344. package/dist/pluginMcp/pluginMcpTypes.js +2 -0
  345. package/dist/pluginMcp/pluginMcpTypes.js.map +1 -0
  346. package/dist/policy/WorkspacePolicyService.d.ts +83 -0
  347. package/dist/policy/WorkspacePolicyService.d.ts.map +1 -0
  348. package/dist/policy/WorkspacePolicyService.js +809 -0
  349. package/dist/policy/WorkspacePolicyService.js.map +1 -0
  350. package/dist/policy/policyGuard.d.ts +27 -0
  351. package/dist/policy/policyGuard.d.ts.map +1 -0
  352. package/dist/policy/policyGuard.js +143 -0
  353. package/dist/policy/policyGuard.js.map +1 -0
  354. package/dist/policy/policyTypes.d.ts +190 -0
  355. package/dist/policy/policyTypes.d.ts.map +1 -0
  356. package/dist/policy/policyTypes.js +2 -0
  357. package/dist/policy/policyTypes.js.map +1 -0
  358. package/dist/runtime/RuntimeEventStore.d.ts +42 -0
  359. package/dist/runtime/RuntimeEventStore.d.ts.map +1 -0
  360. package/dist/runtime/RuntimeEventStore.js +412 -0
  361. package/dist/runtime/RuntimeEventStore.js.map +1 -0
  362. package/dist/runtime/RuntimeEventStoreRepositoryAdapter.d.ts +21 -0
  363. package/dist/runtime/RuntimeEventStoreRepositoryAdapter.d.ts.map +1 -0
  364. package/dist/runtime/RuntimeEventStoreRepositoryAdapter.js +41 -0
  365. package/dist/runtime/RuntimeEventStoreRepositoryAdapter.js.map +1 -0
  366. package/dist/runtime/RuntimeTimelineProjection.d.ts +5 -0
  367. package/dist/runtime/RuntimeTimelineProjection.d.ts.map +1 -0
  368. package/dist/runtime/RuntimeTimelineProjection.js +705 -0
  369. package/dist/runtime/RuntimeTimelineProjection.js.map +1 -0
  370. package/dist/runtime/runtimeEventTypes.d.ts +73 -0
  371. package/dist/runtime/runtimeEventTypes.d.ts.map +1 -0
  372. package/dist/runtime/runtimeEventTypes.js +2 -0
  373. package/dist/runtime/runtimeEventTypes.js.map +1 -0
  374. package/dist/sdk/SdkNativeAgentTurnExecutionAdapter.d.ts +117 -0
  375. package/dist/sdk/SdkNativeAgentTurnExecutionAdapter.d.ts.map +1 -0
  376. package/dist/sdk/SdkNativeAgentTurnExecutionAdapter.js +214 -0
  377. package/dist/sdk/SdkNativeAgentTurnExecutionAdapter.js.map +1 -0
  378. package/dist/session/AgentSessionService.d.ts +285 -0
  379. package/dist/session/AgentSessionService.d.ts.map +1 -0
  380. package/dist/session/AgentSessionService.js +444 -0
  381. package/dist/session/AgentSessionService.js.map +1 -0
  382. package/dist/session/bufferedSessionPty.d.ts +27 -0
  383. package/dist/session/bufferedSessionPty.d.ts.map +1 -0
  384. package/dist/session/bufferedSessionPty.js +49 -0
  385. package/dist/session/bufferedSessionPty.js.map +1 -0
  386. package/dist/session/renderSupport.d.ts +53 -0
  387. package/dist/session/renderSupport.d.ts.map +1 -0
  388. package/dist/session/renderSupport.js +41 -0
  389. package/dist/session/renderSupport.js.map +1 -0
  390. package/dist/session/runtimeEventChannel.d.ts +56 -0
  391. package/dist/session/runtimeEventChannel.d.ts.map +1 -0
  392. package/dist/session/runtimeEventChannel.js +53 -0
  393. package/dist/session/runtimeEventChannel.js.map +1 -0
  394. package/dist/state/JsonAgentStateRepository.d.ts +29 -0
  395. package/dist/state/JsonAgentStateRepository.d.ts.map +1 -0
  396. package/dist/state/JsonAgentStateRepository.js +165 -0
  397. package/dist/state/JsonAgentStateRepository.js.map +1 -0
  398. package/dist/state/SqliteAgentStateRepository.d.ts +58 -0
  399. package/dist/state/SqliteAgentStateRepository.d.ts.map +1 -0
  400. package/dist/state/SqliteAgentStateRepository.js +777 -0
  401. package/dist/state/SqliteAgentStateRepository.js.map +1 -0
  402. package/dist/state/createAgentStateRepository.d.ts +4 -0
  403. package/dist/state/createAgentStateRepository.d.ts.map +1 -0
  404. package/dist/state/createAgentStateRepository.js +12 -0
  405. package/dist/state/createAgentStateRepository.js.map +1 -0
  406. package/dist/state/stateSanitizers.d.ts +7 -0
  407. package/dist/state/stateSanitizers.d.ts.map +1 -0
  408. package/dist/state/stateSanitizers.js +87 -0
  409. package/dist/state/stateSanitizers.js.map +1 -0
  410. package/dist/subagents/AcpSubagentExecutionAdapter.d.ts +12 -0
  411. package/dist/subagents/AcpSubagentExecutionAdapter.d.ts.map +1 -0
  412. package/dist/subagents/AcpSubagentExecutionAdapter.js +119 -0
  413. package/dist/subagents/AcpSubagentExecutionAdapter.js.map +1 -0
  414. package/dist/subagents/CliSubagentExecutionAdapter.d.ts +13 -0
  415. package/dist/subagents/CliSubagentExecutionAdapter.d.ts.map +1 -0
  416. package/dist/subagents/CliSubagentExecutionAdapter.js +217 -0
  417. package/dist/subagents/CliSubagentExecutionAdapter.js.map +1 -0
  418. package/dist/subagents/HostTurnSubagentExecutionAdapter.d.ts +78 -0
  419. package/dist/subagents/HostTurnSubagentExecutionAdapter.d.ts.map +1 -0
  420. package/dist/subagents/HostTurnSubagentExecutionAdapter.js +160 -0
  421. package/dist/subagents/HostTurnSubagentExecutionAdapter.js.map +1 -0
  422. package/dist/subagents/SubagentRunService.d.ts +103 -0
  423. package/dist/subagents/SubagentRunService.d.ts.map +1 -0
  424. package/dist/subagents/SubagentRunService.js +1183 -0
  425. package/dist/subagents/SubagentRunService.js.map +1 -0
  426. package/dist/subagents/SubagentScheduler.d.ts +167 -0
  427. package/dist/subagents/SubagentScheduler.d.ts.map +1 -0
  428. package/dist/subagents/SubagentScheduler.js +530 -0
  429. package/dist/subagents/SubagentScheduler.js.map +1 -0
  430. package/dist/subagents/TeamAgentRouter.d.ts +104 -0
  431. package/dist/subagents/TeamAgentRouter.d.ts.map +1 -0
  432. package/dist/subagents/TeamAgentRouter.js +435 -0
  433. package/dist/subagents/TeamAgentRouter.js.map +1 -0
  434. package/dist/subagents/capabilityMatcher.d.ts +3 -0
  435. package/dist/subagents/capabilityMatcher.d.ts.map +1 -0
  436. package/dist/subagents/capabilityMatcher.js +127 -0
  437. package/dist/subagents/capabilityMatcher.js.map +1 -0
  438. package/dist/subagents/subagentRunPort.d.ts +43 -0
  439. package/dist/subagents/subagentRunPort.d.ts.map +1 -0
  440. package/dist/subagents/subagentRunPort.js +24 -0
  441. package/dist/subagents/subagentRunPort.js.map +1 -0
  442. package/dist/subagents/subagentTypes.d.ts +330 -0
  443. package/dist/subagents/subagentTypes.d.ts.map +1 -0
  444. package/dist/subagents/subagentTypes.js +2 -0
  445. package/dist/subagents/subagentTypes.js.map +1 -0
  446. package/dist/subagents/subagentWorkspaceAuthority.d.ts +29 -0
  447. package/dist/subagents/subagentWorkspaceAuthority.d.ts.map +1 -0
  448. package/dist/subagents/subagentWorkspaceAuthority.js +173 -0
  449. package/dist/subagents/subagentWorkspaceAuthority.js.map +1 -0
  450. package/dist/support/SupportDiagnosticsService.d.ts +87 -0
  451. package/dist/support/SupportDiagnosticsService.d.ts.map +1 -0
  452. package/dist/support/SupportDiagnosticsService.js +1199 -0
  453. package/dist/support/SupportDiagnosticsService.js.map +1 -0
  454. package/dist/turns/DurableInterruptedTurnStore.d.ts +14 -0
  455. package/dist/turns/DurableInterruptedTurnStore.d.ts.map +1 -0
  456. package/dist/turns/DurableInterruptedTurnStore.js +63 -0
  457. package/dist/turns/DurableInterruptedTurnStore.js.map +1 -0
  458. package/dist/turns/interruptedTurns.d.ts +50 -0
  459. package/dist/turns/interruptedTurns.d.ts.map +1 -0
  460. package/dist/turns/interruptedTurns.js +54 -0
  461. package/dist/turns/interruptedTurns.js.map +1 -0
  462. package/dist/turns/turnAdmission.d.ts +113 -0
  463. package/dist/turns/turnAdmission.d.ts.map +1 -0
  464. package/dist/turns/turnAdmission.js +136 -0
  465. package/dist/turns/turnAdmission.js.map +1 -0
  466. package/dist/workspace/WorkspaceHostService.d.ts +126 -0
  467. package/dist/workspace/WorkspaceHostService.d.ts.map +1 -0
  468. package/dist/workspace/WorkspaceHostService.js +841 -0
  469. package/dist/workspace/WorkspaceHostService.js.map +1 -0
  470. package/dist/workspace/WorkspaceShellService.d.ts +71 -0
  471. package/dist/workspace/WorkspaceShellService.d.ts.map +1 -0
  472. package/dist/workspace/WorkspaceShellService.js +273 -0
  473. package/dist/workspace/WorkspaceShellService.js.map +1 -0
  474. package/dist/workspace/fileAssociations.d.ts +12 -0
  475. package/dist/workspace/fileAssociations.d.ts.map +1 -0
  476. package/dist/workspace/fileAssociations.js +202 -0
  477. package/dist/workspace/fileAssociations.js.map +1 -0
  478. package/dist/workspace/scrollbackBuffer.d.ts +48 -0
  479. package/dist/workspace/scrollbackBuffer.d.ts.map +1 -0
  480. package/dist/workspace/scrollbackBuffer.js +94 -0
  481. package/dist/workspace/scrollbackBuffer.js.map +1 -0
  482. package/package.json +42 -0
package/dist/index.js ADDED
@@ -0,0 +1,3242 @@
1
+ import { AgentHostProtocolError, XENO_AGENT_HOST_MIN_PROTOCOL_VERSION, XENO_AGENT_HOST_PROTOCOL_VERSION, isAgentHostClientIdentity, isAgentProviderListResult, defaultRunIdOf, isAgentRuntimeEventEnvelope, } from '@xenosystem/agent-interface-contract';
2
+ /**
3
+ * Bounds on the instruction queue. Unbounded queues are how a stuck loop turns
4
+ * into unbounded memory; the coordination spec applies the same discipline to
5
+ * inter-run messages.
6
+ */
7
+ /** A refusal is cheap to produce and a host can live for days. */
8
+ const MAX_FLEET_COLLISIONS = 500;
9
+ const MAX_QUEUED_INSTRUCTIONS = 50;
10
+ const MAX_QUEUED_INSTRUCTION_LENGTH = 20_000;
11
+ import { cloneProvider, isDevelopmentProviderReady, missingDevelopmentCapabilities, resolveAgentRuntimeSelection, renderWorkspaceKnowledge, selectAvailableProviders, withHostBackedCapabilities, planFanOut, routeByCapability, } from '@xenosystem/agent-interface-core';
12
+ import { InMemoryInterruptedTurnStore, } from './turns/interruptedTurns.js';
13
+ import { evaluateTurnAdmission, resolveTurnAdmissionPolicy, } from './turns/turnAdmission.js';
14
+ import { boundSubagentWorkspace } from './subagents/subagentWorkspaceAuthority.js';
15
+ import { summariseSubagentRun } from './subagents/subagentRunPort.js';
16
+ import { describeClaimScope } from './coordination/claims.js';
17
+ import { CoordinationService } from './coordination/CoordinationService.js';
18
+ import { renderInbox } from './coordination/renderInbox.js';
19
+ import { coordinationRuntimeEventDraft } from './coordination/runtimeEvents.js';
20
+ import { RunObservationService } from './observation/RunObservationService.js';
21
+ import { RunIsolationService } from './isolation/RunIsolationService.js';
22
+ import { IntakeService } from './intake/IntakeService.js';
23
+ import { WorkspaceKnowledgeService } from './knowledge/WorkspaceKnowledgeService.js';
24
+ import { AgentSessionService } from './session/AgentSessionService.js';
25
+ import { createBufferedSessionPty } from './session/bufferedSessionPty.js';
26
+ import { renderSupportFor } from './session/renderSupport.js';
27
+ import { createRuntimeEventSessionChannel } from './session/runtimeEventChannel.js';
28
+ import { answerChooseDirectoryRequest } from './agent/chooseDirectoryBridge.js';
29
+ import { answerWorkspacePlacementRequest } from './agent/workspacePlacementBridge.js';
30
+ /**
31
+ * How long the host waits for an adapter to notice an abort before it stops counting the turn.
32
+ *
33
+ * 🔴 This exists because the host used to wait FOREVER. `activeTurns` is cleared in the
34
+ * `.finally()` of the adapter's execute promise, so an adapter that ignores `signal` — or simply
35
+ * hangs — kept its entry indefinitely. Measured 2026-08-17: `turn.cancel` returned
36
+ * `success: true` while the turn stayed active and stayed counted, and with turn admission in
37
+ * place the slot never came back. Sixteen such turns wedge a default host, recoverable only by
38
+ * restarting it.
39
+ *
40
+ * Generous, because a slow-but-honest adapter finishing its cleanup is the common case and
41
+ * abandoning it early would be worse than waiting. Bounded, because "wait for cooperation that
42
+ * may never come" is not a policy.
43
+ */
44
+ export const DEFAULT_TURN_ABANDON_GRACE_MS = 5_000;
45
+ export class AgentHostCoordinator {
46
+ options;
47
+ listeners = new Set();
48
+ providers = new Map();
49
+ providerCatalogLoader;
50
+ runtimeEvents = new Map();
51
+ runtimeEventRepository;
52
+ stateRepository;
53
+ workspaceService;
54
+ interactionService;
55
+ permissionPolicyService;
56
+ interruptedTurnStore;
57
+ sdkControlService;
58
+ engineeringControlService;
59
+ coordinationService;
60
+ /**
61
+ * Run id -> the conversation that run belongs to.
62
+ *
63
+ * Gate 8 needs coordination on the RUN TIMELINE, and a timeline entry is
64
+ * addressed by conversation and request. Only the host knows that mapping;
65
+ * the coordination service deals in run ids alone, which is what keeps it
66
+ * testable without a store.
67
+ */
68
+ runContexts = new Map();
69
+ coordinationEventSequence = 0;
70
+ /** Collisions seen since this host started — see the sink above for why this
71
+ * is the only place they exist. */
72
+ fleetCollisions = [];
73
+ runObservationService;
74
+ runIsolationService;
75
+ runObservationBinder;
76
+ sessionService;
77
+ intakeService;
78
+ knowledgeService;
79
+ acpApprovalService;
80
+ /**
81
+ * Subagent runs — ADE §5.2. A PORT spanning `SubagentRunService` (create/list) and
82
+ * `SubagentScheduler` (cancel), because cancelling a launched run must reach the adapter.
83
+ */
84
+ subagentRuns;
85
+ resolveRunId;
86
+ connectedClients = new Map();
87
+ /** What each connected client declared it can do, keyed the same way. */
88
+ clientCapabilities = new Map();
89
+ turnExecutionAdapters = new Map();
90
+ turnAdmissionPolicy;
91
+ turnAbandonGraceMs;
92
+ /**
93
+ * Timers owned by a live turn: its deadline, and any abandonment grace already started.
94
+ * Cleared when the turn settles, so a completed turn cannot be abandoned retroactively.
95
+ */
96
+ turnTimers = new Map();
97
+ activeTurns = new Map();
98
+ commandResults = new Map();
99
+ /**
100
+ * Instructions stacked while a turn is running, per conversation.
101
+ *
102
+ * Held by the HOST, not by an adapter: that is what makes queueing work at
103
+ * every structure tier (ADE spec §2.9), including a third-party agent running
104
+ * its own TUI where there is no protocol to hand a steer to.
105
+ *
106
+ * The host does NOT auto-start the queued turn. It holds the instruction and
107
+ * signals when the active turn ends; the client starts the next turn, because
108
+ * the client is where the user's permission context lives. Auto-drain would
109
+ * convert a queue into unattended execution, which is a different decision.
110
+ */
111
+ queuedInstructions = new Map();
112
+ queuedInstructionSequence = 0;
113
+ sequence = 0;
114
+ catalogRevision = 0;
115
+ providerCatalogFingerprint = '';
116
+ providerCatalogRefresh = null;
117
+ constructor(options) {
118
+ const { providers, providerCatalogLoader, runtimeEvents, runtimeEventRepository, stateRepository, workspaceService, interactionService, permissionPolicyService, interruptedTurnStore, turnAdmissionPolicy, turnAbandonGraceMs, sdkControlService, engineeringControlService, turnExecutionAdapters, coordinationService, runObservationService, runIsolationService, runObservationBinder, sessionService, intakeService, knowledgeService, acpApprovalService, subagentRuns, resolveRunId, ...hostOptions } = options;
119
+ if (runtimeEventRepository && runtimeEvents && runtimeEvents.length > 0) {
120
+ throw new Error('Seeded runtimeEvents cannot be combined with a durable runtimeEventRepository.');
121
+ }
122
+ this.options = { ...hostOptions };
123
+ this.runtimeEventRepository = runtimeEventRepository;
124
+ this.providerCatalogLoader = providerCatalogLoader;
125
+ this.stateRepository = stateRepository;
126
+ this.workspaceService = workspaceService;
127
+ this.interactionService = interactionService;
128
+ this.permissionPolicyService = permissionPolicyService;
129
+ this.interruptedTurnStore = interruptedTurnStore || new InMemoryInterruptedTurnStore();
130
+ this.turnAdmissionPolicy = turnAdmissionPolicy ?? resolveTurnAdmissionPolicy();
131
+ this.turnAbandonGraceMs = turnAbandonGraceMs ?? DEFAULT_TURN_ABANDON_GRACE_MS;
132
+ this.sdkControlService = sdkControlService;
133
+ this.engineeringControlService = engineeringControlService;
134
+ this.coordinationService = coordinationService;
135
+ // A turn's run identity, if the request carries one. `subagentRunId` first:
136
+ // a subagent IS a distinct run with its own claims, so folding it into its
137
+ // parent conversation would let two runs share one lock holder.
138
+ // 🔴 `defaultRunIdOf`, not a hand-written `??` — a session filters runtime
139
+ // EVENTS by the same rule, and the two must not drift. See its doc.
140
+ this.resolveRunId = resolveRunId ?? ((request) => defaultRunIdOf(request));
141
+ this.workspaceService?.setEventSink((type, payload) => this.publish(type, payload));
142
+ // Coordination reaches surfaces through the event feed and nothing else.
143
+ // Published as `redactionState: 'safe'` like every host event — which holds
144
+ // because a coordination event carries ids, scopes and outcomes, never
145
+ // message TEXT. That is a property of the CoordinationEvent union, so it is
146
+ // asserted in a test rather than trusted to stay true.
147
+ this.coordinationService?.setEventSink((event) => {
148
+ // 🔴 Recorded HERE, not projected from the runtime-event cache.
149
+ //
150
+ // The first version of `fleet.view` projected collisions from
151
+ // `this.runtimeEvents`, which looked right and was empty in the app:
152
+ // `publishPersistedRuntimeEvent` publishes a persisted event to
153
+ // subscribers and does NOT add it to that map, so with a repository
154
+ // configured — which is every real composition — coordination events
155
+ // never reached it. The view would have reported "no collisions" forever.
156
+ //
157
+ // The claim registry keeps no record of a refusal (it refuses and moves
158
+ // on), so this sink is the only place a collision exists at all. Bounded,
159
+ // because a long-lived host must not accumulate one entry per refusal
160
+ // without limit.
161
+ if (event.type === 'coordination.claim.refused') {
162
+ this.fleetCollisions.push({
163
+ // `describeClaimScope`, because a scope is a structured value and the fleet view
164
+ // reports the unit of work a person recognises — a path as itself, a branch permit
165
+ // as `branch:<name>` so the two axes stay distinguishable in one timeline.
166
+ scope: describeClaimScope(event.scope),
167
+ wantedByRunId: event.runId,
168
+ heldByRunId: event.heldByRunId,
169
+ at: new Date().toISOString(),
170
+ });
171
+ if (this.fleetCollisions.length > MAX_FLEET_COLLISIONS)
172
+ this.fleetCollisions.shift();
173
+ }
174
+ this.publish('coordination.event', event);
175
+ });
176
+ // The OTHER half of gate 8: the durable record, which carries the message
177
+ // text the live feed deliberately does not.
178
+ this.coordinationService?.setTimelineSink((event, text) => {
179
+ void this.recordCoordinationEvent(event, text);
180
+ });
181
+ this.runObservationService = runObservationService;
182
+ // Always present, never optional. A host that could not answer "is this
183
+ // contained?" would leave the surface guessing, and the honest answer with
184
+ // no source is `none` rather than silence.
185
+ this.runIsolationService = runIsolationService ?? new RunIsolationService();
186
+ this.runObservationBinder = runObservationBinder;
187
+ // Counts and completeness only — see the contract's `run.observation`.
188
+ this.runObservationService?.setEventSink((summary) => this.publish('run.observation', summary));
189
+ this.sessionService = sessionService;
190
+ this.intakeService = intakeService;
191
+ this.knowledgeService = knowledgeService;
192
+ // Session lifecycle on the feed: ids, modes and viewer counts — never PTY
193
+ // BYTES. Terminal output reaches a surface by attaching to the session,
194
+ // which is a subscription per viewer, not a broadcast to every client.
195
+ this.sessionService?.setEventSink((event) => this.publish('session.event', event));
196
+ this.acpApprovalService = acpApprovalService;
197
+ this.subagentRuns = subagentRuns;
198
+ for (const provider of providers ?? []) {
199
+ this.setProvider(provider, false);
200
+ }
201
+ this.providerCatalogFingerprint = fingerprintProviders([...this.providers.values()]);
202
+ for (const event of runtimeEvents ?? []) {
203
+ this.appendRuntimeEvent(event, false);
204
+ }
205
+ for (const adapter of turnExecutionAdapters ?? []) {
206
+ this.registerTurnExecutionAdapter(adapter);
207
+ }
208
+ }
209
+ /**
210
+ * The mounted coordination service, or `undefined` when the host has none.
211
+ *
212
+ * Deliberately NOT reachable through `request()`. A run coordinates with
213
+ * another run; a client surface watches. Exposing send/claim as host methods
214
+ * would let whatever is holding a transport act *as* a run, which is exactly
215
+ * the boundary coordination spec §4 draws — and `AgentHostTransport` is a
216
+ * protected cross-repo interface whose consumers (Hub, Shell) would all need
217
+ * coordinating for a capability neither of them is asking for.
218
+ */
219
+ get coordination() {
220
+ return this.coordinationService;
221
+ }
222
+ /**
223
+ * Puts what the workspace knows into the turn — ADE §5.8's gate.
224
+ *
225
+ * 🔴 **In the coordinator, before the adapter is invoked, so it cannot be
226
+ * per-provider.** The gate is that a fact learned by one provider reaches a
227
+ * later run on a DIFFERENT one; injecting inside an adapter would make that a
228
+ * property each lane had to remember, and the lane that forgot would fail
229
+ * silently — the prompt would simply be missing knowledge, which looks exactly
230
+ * like an agent that never learned anything.
231
+ *
232
+ * 🔴 **Prepended to `systemPrompt`, never replacing it.** Standing context
233
+ * about the codebase is what a system prompt is for, and every lane already
234
+ * carries one — so this reaches all of them without touching three adapters.
235
+ * A caller's own system prompt is preserved below it.
236
+ *
237
+ * A failure here does NOT fail the turn. Knowledge is an enrichment; the run
238
+ * is the user's actual work, and a broken store must not be able to stop it.
239
+ * The turn simply proceeds without the block, which is the same behaviour as a
240
+ * workspace that has learned nothing yet.
241
+ */
242
+ async injectWorkspaceKnowledge(request) {
243
+ const service = this.knowledgeService;
244
+ if (!service || !request.workspaceId)
245
+ return;
246
+ try {
247
+ const selection = await service.context(request.workspaceId);
248
+ const block = renderWorkspaceKnowledge(selection);
249
+ // An empty block is not injected at all — not as an empty string, not as a
250
+ // stray blank line. Prompt space spent saying nothing is prompt space
251
+ // taken from something.
252
+ if (!block)
253
+ return;
254
+ const heading = 'What this workspace knows:';
255
+ request.systemPrompt = request.systemPrompt
256
+ ? `${heading}\n${block}\n\n${request.systemPrompt}`
257
+ : `${heading}\n${block}`;
258
+ }
259
+ catch {
260
+ // Deliberately silent to the TURN. The knowledge service reports its own
261
+ // problems through `knowledge.context`, which a surface can show.
262
+ }
263
+ }
264
+ /** Workspace knowledge — ADE §5.8. Thin: the rules live in `core`. */
265
+ async listKnowledge(input) {
266
+ const service = this.knowledgeService;
267
+ if (!service) {
268
+ return {
269
+ available: false,
270
+ entries: [],
271
+ durable: false,
272
+ reason: 'This host is not keeping workspace knowledge.',
273
+ };
274
+ }
275
+ return {
276
+ available: true,
277
+ entries: (await service.list(input.workspaceId)).map(summariseKnowledge),
278
+ durable: service.durable,
279
+ };
280
+ }
281
+ async recordKnowledge(input) {
282
+ const service = this.knowledgeService;
283
+ if (!service)
284
+ return { ok: false, reason: 'This host is not keeping workspace knowledge.' };
285
+ const subject = String(input.subject ?? '').trim();
286
+ const fact = String(input.fact ?? '').trim();
287
+ if (!subject || !fact) {
288
+ // Refused rather than stored empty: a blank fact occupies prompt budget
289
+ // and tells an agent nothing.
290
+ return { ok: false, reason: 'A fact needs both a subject and a statement.' };
291
+ }
292
+ const entry = await service.record({
293
+ workspaceId: input.workspaceId,
294
+ subject,
295
+ fact,
296
+ ...(input.pinned ? { pinned: true } : {}),
297
+ runId: input.runId,
298
+ ...(input.providerId ? { providerId: input.providerId } : {}),
299
+ });
300
+ return { ok: true, entry: summariseKnowledge(entry) };
301
+ }
302
+ async forgetKnowledge(input) {
303
+ const service = this.knowledgeService;
304
+ if (!service)
305
+ return { ok: false, reason: 'This host is not keeping workspace knowledge.' };
306
+ const forgotten = await service.forget(input.workspaceId, input.entryId);
307
+ return forgotten ? { ok: true } : { ok: false, reason: 'No such knowledge entry.' };
308
+ }
309
+ async knowledgeContext(input) {
310
+ const service = this.knowledgeService;
311
+ if (!service) {
312
+ return {
313
+ available: false,
314
+ text: '',
315
+ entries: [],
316
+ omitted: [],
317
+ // NOT complete. Nothing was read, and a caller treating this as a
318
+ // complete empty context would inject nothing and believe it had
319
+ // everything.
320
+ complete: false,
321
+ reason: 'This host is not keeping workspace knowledge.',
322
+ };
323
+ }
324
+ const selection = await service.context(input.workspaceId, input.maxChars === undefined ? {} : { maxChars: input.maxChars });
325
+ return {
326
+ available: true,
327
+ text: renderWorkspaceKnowledge(selection),
328
+ entries: selection.entries.map(summariseKnowledge),
329
+ omitted: selection.omitted,
330
+ complete: selection.complete,
331
+ };
332
+ }
333
+ /** Intake — ADE §5.1. Thin: every rule lives in `core/intakeClaim`. */
334
+ listIntakeSources() {
335
+ const service = this.intakeService;
336
+ if (!service) {
337
+ return { available: false, sources: [], reason: 'This host has no intake sources configured.' };
338
+ }
339
+ return {
340
+ available: true,
341
+ sources: service.sources().map((source) => ({
342
+ sourceId: source.sourceId,
343
+ label: source.label,
344
+ healthy: source.healthy,
345
+ ...(source.problem ? { problem: source.problem } : {}),
346
+ })),
347
+ };
348
+ }
349
+ async listIntakeItems(input) {
350
+ const service = this.intakeService;
351
+ if (!service) {
352
+ return {
353
+ available: false,
354
+ items: [],
355
+ // NOT `complete: true`. Nothing was read, so nothing is known — and an
356
+ // unreadable inbox reporting completeness reads as "no work".
357
+ complete: false,
358
+ failedSources: [],
359
+ reason: 'This host has no intake sources configured.',
360
+ };
361
+ }
362
+ const inbox = await service.inbox(input.sourceId ? { sourceId: input.sourceId } : {});
363
+ return {
364
+ available: true,
365
+ items: inbox.items.map((item) => ({
366
+ id: item.id,
367
+ sourceId: item.source.sourceId,
368
+ externalId: item.source.externalId,
369
+ title: item.title,
370
+ ...(item.detail === undefined ? {} : { detail: item.detail }),
371
+ ...(item.createdAt === undefined ? {} : { createdAt: item.createdAt }),
372
+ ...(item.url === undefined ? {} : { url: item.url }),
373
+ claimState: item.claim.state,
374
+ ...(item.claim.state === 'unclaimed' ? {} : { claimedBy: item.claim.byClientId }),
375
+ ...(item.claim.state === 'dispatched' ? { runId: item.claim.runId } : {}),
376
+ })),
377
+ complete: inbox.complete,
378
+ failedSources: inbox.failedSources,
379
+ };
380
+ }
381
+ claimIntakeItem(input) {
382
+ const service = this.intakeService;
383
+ if (!service)
384
+ return { ok: false, reason: 'This host has no intake sources configured.' };
385
+ const result = service.claim(input.itemId, input.clientId);
386
+ return intakeClaimResponse(result, service.claimState(input.itemId).state);
387
+ }
388
+ releaseIntakeItem(input) {
389
+ const service = this.intakeService;
390
+ if (!service)
391
+ return { ok: false, reason: 'This host has no intake sources configured.' };
392
+ const released = service.release(input.itemId, input.clientId);
393
+ if (released)
394
+ return { ok: true, claimState: 'unclaimed' };
395
+ // Named rather than a bare false: "you do not hold this" and "nobody does"
396
+ // call for different next moves.
397
+ const state = service.claimState(input.itemId);
398
+ return {
399
+ ok: false,
400
+ claimState: state.state,
401
+ ...(state.state === 'unclaimed'
402
+ ? { reason: 'Nobody holds this item.' }
403
+ : { heldBy: state.byClientId, reason: `${state.byClientId} holds this item.` }),
404
+ };
405
+ }
406
+ dispatchIntakeItem(input) {
407
+ const service = this.intakeService;
408
+ if (!service)
409
+ return { ok: false, reason: 'This host has no intake sources configured.' };
410
+ const result = service.dispatch(input.itemId, input.clientId, input.runId);
411
+ return intakeClaimResponse(result, service.claimState(input.itemId).state);
412
+ }
413
+ /**
414
+ * Sessions over the protocol — ADE §5.5, build-order step 4.
415
+ *
416
+ * The substrate has had attach, takeover and input routing all along; none of
417
+ * it was reachable from a surface, which is the same "built, tested,
418
+ * unreachable" shape this repo has shipped repeatedly. These methods are thin
419
+ * on purpose: every rule worth trusting lives in `AgentSessionService` and
420
+ * `writeLease`, where it is tested without a host.
421
+ */
422
+ listSessions(input) {
423
+ const service = this.sessionService;
424
+ if (!service) {
425
+ // Distinct from "no sessions". A surface that cannot tell them apart
426
+ // waits for sessions that can never appear.
427
+ return { available: false, sessions: [], reason: 'This host has no session substrate mounted.' };
428
+ }
429
+ const sessions = service.sessions()
430
+ .filter((session) => !input.runId || session.runId === input.runId)
431
+ .map((session) => summariseSession(session));
432
+ return { available: true, sessions };
433
+ }
434
+ attachSession(input) {
435
+ const service = this.sessionService;
436
+ if (!service)
437
+ return { ok: false, reason: 'This host has no session substrate mounted.' };
438
+ /**
439
+ * 🔴 The `onData` sink is deliberately EMPTY.
440
+ *
441
+ * Attaching registers a viewer and makes the session's viewer count true;
442
+ * it does not stream bytes to this caller, because the host protocol is
443
+ * request/response and its event feed is a broadcast that must never carry
444
+ * PTY output. Output is read with `session.snapshot`.
445
+ *
446
+ * The retention buffer is subscribed at OPEN, not here, so a viewer that
447
+ * attaches late still sees what happened while nobody was watching.
448
+ */
449
+ const ok = service.attach(input.sessionId, input.viewerId, () => { });
450
+ if (!ok) {
451
+ return {
452
+ ok: false,
453
+ reason: service.session(input.sessionId)
454
+ ? 'That viewer is already attached to this session.'
455
+ : 'No such session.',
456
+ };
457
+ }
458
+ const session = service.session(input.sessionId);
459
+ return { ok: true, ...(session ? { session: summariseSession(session) } : {}) };
460
+ }
461
+ detachSession(input) {
462
+ const service = this.sessionService;
463
+ if (!service)
464
+ return { ok: false, reason: 'This host has no session substrate mounted.' };
465
+ const ok = service.detach(input.sessionId, input.viewerId);
466
+ // A session outlives its viewers — detaching is not closing.
467
+ if (!ok)
468
+ return { ok: false, reason: 'That viewer is not attached to this session.' };
469
+ const session = service.session(input.sessionId);
470
+ return { ok: true, ...(session ? { session: summariseSession(session) } : {}) };
471
+ }
472
+ snapshotSession(input) {
473
+ const service = this.sessionService;
474
+ const snapshot = service?.snapshot(input.sessionId);
475
+ if (!snapshot) {
476
+ return {
477
+ available: false,
478
+ output: '',
479
+ // NOT `truncated: false`. Nothing was read, so nothing is known about
480
+ // whether output was dropped, and a false here would be a claim.
481
+ truncated: false,
482
+ reason: service ? 'No such session.' : 'This host has no session substrate mounted.',
483
+ };
484
+ }
485
+ return { available: true, output: snapshot.output, truncated: snapshot.truncated };
486
+ }
487
+ /**
488
+ * Hands a run what its peers said — ADE §5.2b's `message` primitive.
489
+ *
490
+ * 🔴 Nothing called `deliverTo`. Messages were sent, journalled, reported
491
+ * `delivered` to the sender — and sat in the bus forever, because the only
492
+ * thing that empties an inbox had no caller anywhere in the repo. The
493
+ * primitive was half a pipe: every agent could send and no agent could
494
+ * receive, while the sender was told it had been delivered.
495
+ *
496
+ * At TURN START and in the coordinator, for the same reason §5.8's knowledge
497
+ * injection is: a push that each lane had to remember would fail silently in
498
+ * the lane that forgot, and a prompt merely missing its messages looks exactly
499
+ * like a run nobody wrote to.
500
+ *
501
+ * Prepended to `systemPrompt` alongside knowledge rather than appended to the
502
+ * user's prompt — what a peer said is standing context for this turn, not
503
+ * something the user typed, and putting it in their words would misattribute
504
+ * it.
505
+ *
506
+ * ⚠️ Draining also sets the run's CAUSE, which is what makes gate 8
507
+ * attribution and §8 loop detection work at all: a message this run sends
508
+ * while handling another inherits the chain. Both were inert for the same
509
+ * reason.
510
+ */
511
+ deliverCoordinationMessages(request) {
512
+ const service = this.coordinationService;
513
+ const runId = this.resolveRunId(request);
514
+ if (!service || !runId)
515
+ return;
516
+ try {
517
+ const block = renderInbox(service.deliverTo(runId));
518
+ // An empty inbox injects NOTHING — not a heading, not a blank line. Prompt
519
+ // space spent saying "no messages" is prompt space taken from something.
520
+ if (!block)
521
+ return;
522
+ const heading = 'Messages from other runs in this fleet:';
523
+ request.systemPrompt = request.systemPrompt
524
+ ? `${heading}\n${block}\n\n${request.systemPrompt}`
525
+ : `${heading}\n${block}`;
526
+ }
527
+ catch {
528
+ // A failure here does NOT fail the turn, matching the knowledge
529
+ // injection: the run is the user's actual work. But note the asymmetry —
530
+ // a dropped message is a LOSS, not a missing enrichment, and the drain
531
+ // already published `coordination.message.delivered` to the timeline, so
532
+ // a reader can still see what arrived.
533
+ }
534
+ }
535
+ /**
536
+ * A run's activity changed — §7's run state and last decision.
537
+ *
538
+ * 🔴 `awaiting-approval` had no producer at all, and the gap is not
539
+ * cosmetic: a peer deciding whether to wait for a run or take the work itself
540
+ * needs "blocked on a human" to look different from "busy". A run reported
541
+ * `running` while it sits on a permission prompt is the one case where
542
+ * waiting is guaranteed to be wrong.
543
+ *
544
+ * Public because the signal originates in a turn adapter, which the
545
+ * composition builds BEFORE this host exists — the same ordering
546
+ * `setEventSink` already solves for coordination events.
547
+ */
548
+ reportRunActivity(signal) {
549
+ this.registerCoordinationRun(signal.runId, signal.state, signal.lastDecision);
550
+ }
551
+ /**
552
+ * Puts a run on the coordination roster — ADE §5.2b.
553
+ *
554
+ * 🔴 Nothing called `registerRun`. The primitives were reachable from both
555
+ * lanes and INERT: `peers()` returns an empty list for a caller it has never
556
+ * heard of, so every agent asking who else was working got the same answer as
557
+ * an agent genuinely alone. The build was complete and the feature did
558
+ * nothing — the shape this repo keeps finding one level further in.
559
+ *
560
+ * An UPSERT, called at both ends of a turn, because a run spans many turns and
561
+ * its state changes: `running` while a turn is in flight, `idle` between them.
562
+ * A run is never removed here; `endRun` belongs to whoever created the run,
563
+ * and claims have lease expiry as the backstop for a host that never got the
564
+ * news.
565
+ *
566
+ * ⚠️ `complete: false`, always, and honestly. This host observes FILES — the
567
+ * process sensor lives behind `xeno-use` and only the file-change port is
568
+ * consumed here — so `commandsRun` is empty because commands are UNOBSERVED,
569
+ * not because none ran. A peer must be able to tell those apart.
570
+ */
571
+ registerCoordinationRun(runId, state, lastDecision) {
572
+ const service = this.coordinationService;
573
+ // \u{1F534} Read from `runContexts`, not from a request handed in. A state change
574
+ // arrives mid-turn from the permission bridge, which has no turn request —
575
+ // and a version of this that needed one could only ever be called at the
576
+ // two moments a turn begins and ends, which is exactly the granularity that
577
+ // made `awaiting-approval` unreachable.
578
+ const context = this.runContexts.get(runId);
579
+ if (!service || !context)
580
+ return;
581
+ // Live, from tier-1 observation, rather than a list this method maintains:
582
+ // a second copy would disagree with the diff a reviewer is looking at.
583
+ const observed = this.runObservationService?.snapshot(runId);
584
+ // Carried forward when this update does not bring a new one: `undefined`
585
+ // means "no change", and overwriting a real decision with it would erase
586
+ // §7's fourth field every time the run merely went idle.
587
+ const previous = service.runSummary(runId, runId);
588
+ const decision = lastDecision ?? previous?.lastDecision;
589
+ service.registerRun({
590
+ runId,
591
+ // §3: human-readable, defaulting from the workspace. The run id is the
592
+ // honest fallback — it is at least the thing a peer would address.
593
+ name: context.workspaceId || runId,
594
+ state,
595
+ // '' when the request names no workspace, which `canSee` treats as
596
+ // matching NOTHING rather than matching every other unnamed run.
597
+ workspace: context.workspaceId ?? '',
598
+ filesTouched: (observed?.changes ?? []).map((change) => change.path),
599
+ commandsRun: [],
600
+ complete: false,
601
+ ...(decision === undefined ? {} : { lastDecision: decision }),
602
+ // §6: the input to `deriveInboundPolicy`.
603
+ //
604
+ // Per-TURN, and that is correct rather than incidental: `upsertRun`
605
+ // REPLACES the record, and `runContexts` is rewritten at each turn start,
606
+ // so a turn that declares no capabilities leaves the posture unknown again
607
+ // instead of inheriting the last turn's. A posture is "what this run is
608
+ // executing with *now*" — carrying a stale one forward would judge a
609
+ // message against authority the run no longer has.
610
+ //
611
+ // Spread-omitted rather than set to `undefined` because the field is
612
+ // declared optional under `exactOptionalPropertyTypes`.
613
+ ...(context.posture === undefined ? {} : { posture: context.posture }),
614
+ });
615
+ }
616
+ /**
617
+ * Gives a run a session — ADE §2.8, and the step §9 has been stuck on.
618
+ *
619
+ * Opened at TURN START rather than on demand, because a session is where a
620
+ * person looks to see what the agent is doing, and one that only exists after
621
+ * someone asks for it is not there during the minute they most want it.
622
+ *
623
+ * 🔴 Every ingredient is derived, none is asserted:
624
+ *
625
+ * - the render support comes from the ADAPTERS (`renderSupportFor`), so a
626
+ * provider nothing can execute is refused by name instead of opening a
627
+ * session that would render nothing;
628
+ * - the protocol channel is the run's OWN runtime-event stream, already
629
+ * redacted and already durable, so the terminal is a rendering of the run
630
+ * rather than a second opinion about it;
631
+ * - the PTY has no process behind it, because in `acp` there is nothing to
632
+ * attach to — and `createBufferedSessionPty` throws rather than silently
633
+ * swallowing a write, so a broken mode rule surfaces instead of losing
634
+ * what someone typed.
635
+ *
636
+ * Failures are contained for the same reason the observation binder's are: a
637
+ * turn is the user's actual work, and a session is an observability surface
638
+ * over it. A run without a session is honest — `session.list` simply will not
639
+ * show it — whereas a turn that failed to start because a terminal could not
640
+ * be opened would be absurd.
641
+ *
642
+ * ⚠️ One session per RUN, not per turn. A second turn on the same run finds
643
+ * the session already open and leaves it alone; re-opening would discard the
644
+ * output a viewer is reading mid-conversation.
645
+ */
646
+ openSessionForRun(runId, provider) {
647
+ const service = this.sessionService;
648
+ // No "already open?" check: `open` REFUSES `session_exists`, and a second
649
+ // statement of that rule here would be free to drift from it. Removing this
650
+ // guard changed no test, which is how it was found.
651
+ if (!service)
652
+ return;
653
+ try {
654
+ const support = renderSupportFor(provider, { adapters: [...this.turnExecutionAdapters.values()] });
655
+ const protocol = createRuntimeEventSessionChannel({
656
+ runId,
657
+ subscribe: (listener) => this.subscribe((event) => {
658
+ if (event.type !== 'runtime.event.appended')
659
+ return;
660
+ const payload = event.payload;
661
+ if (isAgentRuntimeEventEnvelope(payload?.event))
662
+ listener(payload.event);
663
+ }),
664
+ });
665
+ const opened = service.open({
666
+ sessionId: runId,
667
+ runId,
668
+ support,
669
+ pty: createBufferedSessionPty(),
670
+ protocol,
671
+ });
672
+ // A refusal is the substrate doing its job — an unexecutable provider has
673
+ // nothing to render. Close the channel we built for it rather than leaving
674
+ // a subscription attached to a session that never existed.
675
+ if (!opened.ok)
676
+ protocol.close();
677
+ }
678
+ catch {
679
+ // Contained: see above. The run proceeds without a session.
680
+ }
681
+ }
682
+ async routeSessionInput(input) {
683
+ const service = this.sessionService;
684
+ if (!service)
685
+ return { ok: false, reason: 'This host has no session substrate mounted.' };
686
+ // \u{1F534} DERIVED here, never accepted from the caller. Both facts are the
687
+ // host's: it holds the active turns, and it knows whether the run's adapter
688
+ // can steer — it already refuses `steer_unsupported` on that same basis.
689
+ const runId = service.session(input.sessionId)?.runId;
690
+ const active = runId ? this.activeTurnForRun(runId) : undefined;
691
+ const result = service.routeInput(input.sessionId, input.viewerId, input.data, {
692
+ turnInFlight: active !== undefined,
693
+ acceptsMidTurnSteering: active?.adapter.steer !== undefined,
694
+ });
695
+ if (!result.ok) {
696
+ // Who holds it, by name. "Someone else is typing" leaves the reader with
697
+ // no next action; naming the surface tells them whom to ask.
698
+ return result.code === 'lease_held'
699
+ ? {
700
+ ok: false,
701
+ code: 'lease_held',
702
+ reason: `${result.holder} has taken over this session.`,
703
+ }
704
+ : { ok: false, code: 'no_such_session', reason: 'No such session.' };
705
+ }
706
+ const owed = await this.deliverOwedSessionInput(input.sessionId, input.command, input.data, result.delivery);
707
+ return {
708
+ ok: true,
709
+ disposition: result.routing.disposition,
710
+ delivered: owed.delivered,
711
+ sink: result.delivery.sink,
712
+ ...(owed.queued === undefined ? {} : { queued: owed.queued }),
713
+ ...(owed.reason === undefined ? {} : { reason: owed.reason }),
714
+ };
715
+ }
716
+ /**
717
+ * The turn a run is executing right now, if any.
718
+ *
719
+ * §2.8's routing needs two facts — is a turn in flight, and can this agent
720
+ * take a correction mid-turn — and both live here. `activeTurns` is keyed by
721
+ * conversation and request, so finding a RUN's turn means asking
722
+ * `resolveRunId` about each one, the same derivation everything else uses.
723
+ */
724
+ activeTurnForRun(runId) {
725
+ for (const active of this.activeTurns.values()) {
726
+ if (this.resolveRunId(active.request) === runId)
727
+ return active;
728
+ }
729
+ return undefined;
730
+ }
731
+ /**
732
+ * Delivers what the substrate could not, through the ONE path that can.
733
+ *
734
+ * §2.8 promises typing never falls into a void, and `AgentSessionService`
735
+ * keeps its half by refusing to write into a PTY with no process behind it. It
736
+ * deliberately stops there: an `acp` steer travels `turn.steer`, where
737
+ * acknowledgement, idempotency and the named refusal already live, and a
738
+ * second delivery path from inside the substrate would be the unacknowledged
739
+ * one §5.5 measured to be wrong.
740
+ *
741
+ * So the host — which owns execution — closes the loop here, for BOTH input
742
+ * doors. Splitting it would leave the promise held at one entrance again.
743
+ *
744
+ * A failure is REPORTED, never swallowed: `turn_not_active` in particular is
745
+ * an ordinary outcome (someone typed between turns) and its message already
746
+ * tells the reader to queue instead. Reporting `delivered: true` there would
747
+ * be the precise lie this whole chain exists to prevent.
748
+ */
749
+ async deliverOwedSessionInput(sessionId, command, data, delivery) {
750
+ if (delivery.delivered || delivery.sink === 'pty')
751
+ return { delivered: delivery.delivered };
752
+ const runId = this.sessionService?.session(sessionId)?.runId;
753
+ const context = runId ? this.runContexts.get(runId) : undefined;
754
+ if (!context) {
755
+ return {
756
+ delivered: false,
757
+ reason: delivery.sink === 'turn.queue'
758
+ ? 'That session has no run to queue against. Start a turn so the instruction has somewhere to go.'
759
+ : 'That session has no run to steer. Start a turn so the instruction has somewhere to go.',
760
+ };
761
+ }
762
+ /**
763
+ * 🔴 The `turn.queue` sink, SERVICED.
764
+ *
765
+ * `routeHumanInput` returns `disposition: 'queue'` when a turn is in flight
766
+ * and the agent does not take mid-turn steering, and its own reason string
767
+ * promises the input is "delivered as the next turn". Nothing kept that
768
+ * promise: this method returned early for every sink but `turn.steer`, so
769
+ * the keystrokes were dropped — the void `AgentSessionInputResult` says
770
+ * deliberately does not exist ("there is deliberately no `drop`: typing
771
+ * never falls into a void").
772
+ *
773
+ * It also left the host's whole turn queue with no writer, which is why
774
+ * `turn.queue` / `.list` / `.drop` were built, tested and reachable by
775
+ * nothing.
776
+ *
777
+ * `delivered` stays FALSE on purpose: the agent has not seen this. Queued
778
+ * is not delivered, and the id is what says the queue took it.
779
+ */
780
+ if (delivery.sink === 'turn.queue') {
781
+ const queued = this.queueTurnInstruction({
782
+ command,
783
+ conversationId: context.conversationId,
784
+ instruction: data,
785
+ });
786
+ if (!queued.success)
787
+ return { delivered: false, reason: queued.safeError };
788
+ const entry = queued.queue[queued.queue.length - 1];
789
+ return {
790
+ delivered: false,
791
+ // The conversation rides along because that is what `turn.queue.drop`
792
+ // is keyed by, and the session only knows its run. Without it the id is
793
+ // evidence a surface can render and not act on.
794
+ ...(entry
795
+ ? { queued: { instructionId: entry.id, conversationId: context.conversationId } }
796
+ : {}),
797
+ };
798
+ }
799
+ const steer = await this.steerTurn({
800
+ command,
801
+ conversationId: context.conversationId,
802
+ requestId: context.requestId,
803
+ instruction: data,
804
+ });
805
+ return steer.success ? { delivered: true } : { delivered: false, reason: steer.safeError };
806
+ }
807
+ async takeOverSession(input) {
808
+ const service = this.sessionService;
809
+ if (!service)
810
+ return { ok: false, reason: 'This host has no session substrate mounted.' };
811
+ if (!service.session(input.sessionId))
812
+ return { ok: false, reason: 'No such session.' };
813
+ const outcome = service.takeOver(input.sessionId, input.viewerId, input.data);
814
+ if (!outcome) {
815
+ /**
816
+ * 🔴 The session EXISTS — checked immediately above — so `undefined` here
817
+ * means REFUSED, not missing.
818
+ *
819
+ * `takeOver` returns `undefined` for both, which is fine inside the
820
+ * substrate (the caller there has the session in hand) and is exactly the
821
+ * conflation a protocol surface must not pass on: "no such session" sends
822
+ * someone to re-open a terminal, while "someone else is typing" tells them
823
+ * to go talk to a colleague. The pre-check is what keeps them apart.
824
+ */
825
+ const current = service.session(input.sessionId)?.lease;
826
+ const holder = current && current.holder.kind === 'human' ? current.holder.viewerId : null;
827
+ return {
828
+ ok: false,
829
+ ...(current ? { lease: summariseLease(current) } : {}),
830
+ reason: holder
831
+ ? `Another person is typing in this session (${holder}).`
832
+ : 'The write lease could not be taken.',
833
+ };
834
+ }
835
+ const owed = await this.deliverOwedSessionInput(input.sessionId, input.command, input.data, outcome.delivery);
836
+ return {
837
+ ok: true,
838
+ lease: summariseLease(outcome.lease),
839
+ delivered: owed.delivered,
840
+ sink: outcome.delivery.sink,
841
+ ...(owed.reason === undefined ? {} : { reason: owed.reason }),
842
+ };
843
+ }
844
+ releaseSession(input) {
845
+ const service = this.sessionService;
846
+ if (!service)
847
+ return { ok: false, reason: 'This host has no session substrate mounted.' };
848
+ const ok = service.releaseTakeOver(input.sessionId, input.viewerId);
849
+ if (!ok)
850
+ return { ok: false, reason: 'That viewer does not hold the write lease.' };
851
+ const session = service.session(input.sessionId);
852
+ return { ok: true, ...(session ? { session: summariseSession(session) } : {}) };
853
+ }
854
+ /**
855
+ * Every run this host knows about — ADE §9 step 6.
856
+ *
857
+ * Composed from three subsystems rather than a fourth record, because a
858
+ * separate fleet ledger would be a second copy of facts that already exist and
859
+ * would disagree with them the first time one was updated and the other was
860
+ * not.
861
+ *
862
+ * 🔴 The completeness flags are the load-bearing part. A fleet view showing
863
+ * three collisions when five happened, or four changed files when a drain was
864
+ * dropped, is worse than no fleet view: it is the shape a person uses to
865
+ * decide the work is safe to land.
866
+ */
867
+ /**
868
+ * Plans a fan-out WITHOUT starting anything — ADE §5.2's scheduler.
869
+ *
870
+ * Deliberately separate from dispatch. A plan is the reviewable artifact: it
871
+ * shows which agent gets which task, which tasks nothing can run and why, and
872
+ * how the load actually spread — all before a single agent touches a
873
+ * repository. Fusing plan and execute would remove the only moment at which
874
+ * any of that can be checked.
875
+ */
876
+ planFleetFanOut(input) {
877
+ const plan = planFanOut([...this.providers.values()], (input.tasks ?? []).map((task) => ({
878
+ taskId: task.taskId,
879
+ ...(task.requiredCapabilities === undefined
880
+ ? {}
881
+ : { requiredCapabilities: task.requiredCapabilities }),
882
+ })));
883
+ return {
884
+ assignments: plan.assignments.map((assignment) => ({
885
+ taskId: assignment.taskId,
886
+ providerId: assignment.providerId,
887
+ degraded: [...assignment.degraded],
888
+ })),
889
+ refusals: plan.refusals.map((refusal) => ({ ...refusal })),
890
+ loadByProvider: { ...plan.loadByProvider },
891
+ };
892
+ }
893
+ /**
894
+ * Which agents could run a task requiring these capabilities — ADE §5.2.
895
+ *
896
+ * A pure QUERY over the live provider catalog: it starts nothing, so a
897
+ * surface or a scheduler can ask before committing to anything.
898
+ *
899
+ * 🔴 The refusal REASON is the deliverable, not the boolean. §5.2's gate is
900
+ * that a task requiring an undeclared capability is "refused with a named
901
+ * reason" — so this reports which capability nobody declares, and
902
+ * distinguishes that from every capable agent merely being down. Those two
903
+ * send a user to completely different places.
904
+ *
905
+ * The matching rules live in `routeByCapability` (provider-neutral, in
906
+ * `core`) rather than here: this method's job is to supply the catalog and
907
+ * shape the answer for the wire.
908
+ */
909
+ routeByCapabilities(input) {
910
+ const decision = routeByCapability([...this.providers.values()], input.requiredCapabilities ?? []);
911
+ if (decision.routable) {
912
+ return {
913
+ routable: true,
914
+ candidates: decision.matches.map((match) => ({
915
+ providerId: match.providerId,
916
+ degraded: [...match.degraded],
917
+ })),
918
+ rejected: decision.rejected.map((entry) => ({
919
+ providerId: entry.providerId,
920
+ ...(entry.capability === undefined ? {} : { capability: entry.capability }),
921
+ reason: entry.reason,
922
+ })),
923
+ };
924
+ }
925
+ return {
926
+ routable: false,
927
+ candidates: [],
928
+ rejected: decision.rejected.map((entry) => ({
929
+ providerId: entry.providerId,
930
+ ...(entry.capability === undefined ? {} : { capability: entry.capability }),
931
+ reason: entry.reason,
932
+ })),
933
+ reason: decision.reason,
934
+ };
935
+ }
936
+ readFleetView(input) {
937
+ const problems = [];
938
+ // 🔴 Claims come from the SERVICE and collisions from the timeline, and the
939
+ // split is not an inconsistency — the two answer different questions.
940
+ //
941
+ // A claim is current state: the registry knows what is held right now,
942
+ // including leases that have since expired and runs that never started a
943
+ // turn. Projecting claims from events instead would MISS a run that claimed
944
+ // a path without a conversation — which is exactly the run worth seeing,
945
+ // because it has locked something and is about to collide with someone.
946
+ //
947
+ // A collision is history: the registry refuses the second claimant and keeps
948
+ // no record of the refusal, so the timeline is the only place it survives.
949
+ // `fleetProjection` retains them deliberately — "nobody collided" and "a
950
+ // collision happened and was handled" are different histories.
951
+ const liveClaims = this.coordinationService?.claims() ?? [];
952
+ const heldScopes = new Set(liveClaims.map((claim) => describeClaimScope(claim.scope)));
953
+ // 🔴 The collision record is NEVER complete today, and this says so.
954
+ //
955
+ // The first version of this line read `this.runtimeEventRepository ===
956
+ // undefined`, which was exactly backwards: with no repository the host
957
+ // records no coordination timeline at all, so collisions are always empty —
958
+ // and it reported that as COMPLETE. An empty list plus a complete flag is
959
+ // read as "nobody collided", which is the false-clean failure this whole
960
+ // family of flags exists to prevent. Found by running the test, not by
961
+ // reading the code.
962
+ //
963
+ // With a repository it is still partial: the collisions are projected from
964
+ // an in-memory cache of what this host has seen since it started, not from
965
+ // the durable timeline. Both cases are false; they differ in WHY, and the
966
+ // reason is what a reader can act on.
967
+ const coordinationComplete = false;
968
+ problems.push('Collisions cover what this host has seen since it started; earlier ones are not held anywhere.');
969
+ const claimsByRun = new Map();
970
+ for (const claim of liveClaims) {
971
+ claimsByRun.set(claim.runId, [...(claimsByRun.get(claim.runId) ?? []), describeClaimScope(claim.scope)]);
972
+ }
973
+ // §5.1 provenance, read per RUN. Absent when the host has no intake mounted
974
+ // OR when this run was not dispatched from an item — both mean "no intake
975
+ // origin to show", and neither is a claim that one is unknown.
976
+ const intakeOriginFor = (runId) => this.intakeService?.originFor(runId);
977
+ const overlaps = this.runObservationService?.landingOverlaps(input.claimedPathsByRun ?? {});
978
+ const contestedByRun = new Map();
979
+ for (const overlap of overlaps?.overlaps ?? []) {
980
+ for (const runId of overlap.runIds) {
981
+ contestedByRun.set(runId, [...(contestedByRun.get(runId) ?? []), overlap.path]);
982
+ }
983
+ }
984
+ // Every run anything knows about, not just the observed ones: a run with a
985
+ // claim and no observation is exactly the run a person needs to see.
986
+ const runIds = new Set([
987
+ ...this.runContexts.keys(),
988
+ ...claimsByRun.keys(),
989
+ ...this.fleetCollisions.flatMap((collision) => [collision.wantedByRunId, collision.heldByRunId]),
990
+ ...(this.runObservationService?.observedRuns() ?? []),
991
+ ]);
992
+ // One lookup built once: `runSummary(runId, runId)` is how a run reads its
993
+ // OWN record, which is the only view guaranteed visible regardless of
994
+ // workspace.
995
+ const service = this.coordinationService;
996
+ const rosterState = service
997
+ ? (runId) => service.runSummary(runId, runId)?.state
998
+ : undefined;
999
+ const workingRuns = new Set();
1000
+ for (const active of this.activeTurns.values()) {
1001
+ const runId = this.resolveRunId(active.request);
1002
+ if (runId)
1003
+ workingRuns.add(runId);
1004
+ }
1005
+ /**
1006
+ * 🔴 Starts FALSE when nothing is observing.
1007
+ *
1008
+ * It used to start `true` unconditionally and only go false when a snapshot
1009
+ * existed and reported a hole — so a host with no observation substrate,
1010
+ * where `snapshot` is always undefined, announced that every diff was the
1011
+ * whole story. Exactly the shape already fixed once in
1012
+ * `coordinationComplete`, which is why the fix is the same: say why.
1013
+ */
1014
+ let observationComplete = this.runObservationService !== undefined;
1015
+ if (!observationComplete) {
1016
+ problems.push('No run observation is configured, so no run\'s changes are known.');
1017
+ }
1018
+ const runs = [...runIds].sort().map((runId) => {
1019
+ const context = this.runContexts.get(runId);
1020
+ const snapshot = this.runObservationService?.snapshot(runId);
1021
+ const pinnedInboundPolicy = service?.inboundPolicyFor(runId).explicit;
1022
+ const isolation = this.runIsolationService.report(runId);
1023
+ // A run nobody observed is not a complete picture either. Absent and
1024
+ // incomplete are different facts about the RUN — see `diffComplete` —
1025
+ // but for the FLEET both mean the same thing: this view cannot be read as
1026
+ // the whole story.
1027
+ if (!snapshot && this.runObservationService) {
1028
+ observationComplete = false;
1029
+ problems.push(`${runId}: not observed, so its changes are unknown.`);
1030
+ }
1031
+ if (snapshot && !snapshot.complete) {
1032
+ observationComplete = false;
1033
+ for (const problem of snapshot.problems)
1034
+ problems.push(`${runId}: ${problem}`);
1035
+ }
1036
+ return {
1037
+ runId,
1038
+ ...(context?.conversationId ? { conversationId: context.conversationId } : {}),
1039
+ ...(context?.workspaceId ? { workspaceId: context.workspaceId } : {}),
1040
+ ...(context?.agentId ? { agentId: context.agentId } : {}),
1041
+ working: workingRuns.has(runId),
1042
+ claims: (claimsByRun.get(runId) ?? []).sort(),
1043
+ ...(snapshot
1044
+ ? {
1045
+ changes: countChangeKinds(snapshot.changes),
1046
+ // Present ONLY when observed. Absent and `false` are different
1047
+ // facts: "nobody watched" versus "watched, with a hole".
1048
+ diffComplete: snapshot.complete,
1049
+ }
1050
+ : {}),
1051
+ ...(intakeOriginFor(runId) ? { intakeItemId: intakeOriginFor(runId) } : {}),
1052
+ contestedPaths: (contestedByRun.get(runId) ?? []).sort(),
1053
+ // From the ROSTER, the same state a peer sees. Absent without a
1054
+ // coordination service: no roster means no answer, and `false` would
1055
+ // assert the run is not waiting.
1056
+ ...(rosterState
1057
+ ? { awaitingApproval: rosterState(runId) === 'awaiting-approval' }
1058
+ : {}),
1059
+ // §6 `hold`, as a COUNT. Absent without a coordination service for the
1060
+ // same reason as the line above: no roster, no answer.
1061
+ ...(service ? { heldMessages: service.heldFor(runId).length } : {}),
1062
+ // §6's EXPLICIT override only — absent when the derivation decides.
1063
+ //
1064
+ // Deliberately not the effective policy: §6 derives comparatively, so
1065
+ // "what happens to a message" depends on who sends it and has no single
1066
+ // value to show on a row. Reporting one would invent an answer for a
1067
+ // question the row cannot ask. What a human needs here is whether THEY
1068
+ // pinned something, because that is the only part they can undo.
1069
+ // Read ONCE into a local: two calls cannot be narrowed by the compiler,
1070
+ // and a value read twice is a value that can disagree with itself.
1071
+ ...(pinnedInboundPolicy === undefined ? {} : { inboundPolicy: pinnedInboundPolicy }),
1072
+ // 🔴 ALWAYS present, unlike every other optional field on this row.
1073
+ // §5.3 requires the user to always know whether the thing running is
1074
+ // contained, and "always" is the word that forbids omitting it when the
1075
+ // answer is unflattering. `none` IS the answer today.
1076
+ isolation: isolation.isolation,
1077
+ ...(isolation.mechanism === undefined ? {} : { isolationMechanism: isolation.mechanism }),
1078
+ ...(isolation.reason === undefined ? {} : { isolationReason: isolation.reason }),
1079
+ };
1080
+ });
1081
+ return {
1082
+ runs,
1083
+ collisions: this.fleetCollisions.map((collision) => ({
1084
+ ...collision,
1085
+ // Recomputed against the CURRENT claim map rather than stored: a
1086
+ // collision whose holder has since released is history, not something a
1087
+ // person still has to act on, and only the live registry knows which.
1088
+ stillHeld: heldScopes.has(collision.scope),
1089
+ })),
1090
+ coordinationComplete,
1091
+ observationComplete,
1092
+ problems,
1093
+ };
1094
+ }
1095
+ /**
1096
+ * A run's observed diff, for review.
1097
+ *
1098
+ * 🔴 Unavailable is reported as `available: false` with a reason, never as an
1099
+ * empty change list. They are different facts — "this run changed nothing" and
1100
+ * "nobody was watching" — and a surface that cannot tell them apart will show
1101
+ * a clean review for an unobserved run.
1102
+ */
1103
+ /**
1104
+ * What a run is not being told yet — coordination §6 `hold`.
1105
+ *
1106
+ * 🔴 `available: false` when this host has no coordination service, so a
1107
+ * surface can tell "this host does not coordinate" from "nothing is
1108
+ * held". Reporting an empty list for both would make an unmounted service
1109
+ * look like a quiet one.
1110
+ */
1111
+ listHeldMessages(input) {
1112
+ const service = this.coordinationService;
1113
+ if (!service) {
1114
+ return {
1115
+ available: false,
1116
+ runId: input.runId,
1117
+ held: [],
1118
+ reason: 'This host is not coordinating runs.',
1119
+ };
1120
+ }
1121
+ return {
1122
+ available: true,
1123
+ runId: input.runId,
1124
+ held: service.heldFor(input.runId).map((message) => ({
1125
+ id: message.id,
1126
+ fromRunId: message.fromRunId,
1127
+ text: message.text,
1128
+ sentAt: message.sentAt,
1129
+ })),
1130
+ };
1131
+ }
1132
+ /** A human approves one held message. §6's approval step, made reachable. */
1133
+ releaseHeldMessage(input) {
1134
+ const service = this.coordinationService;
1135
+ if (!service) {
1136
+ return {
1137
+ status: 'expired',
1138
+ messageId: input.messageId,
1139
+ reason: 'This host is not coordinating runs.',
1140
+ };
1141
+ }
1142
+ const outcome = service.releaseHeld(input.runId, input.messageId);
1143
+ return {
1144
+ status: outcome.status,
1145
+ messageId: outcome.messageId,
1146
+ ...('reason' in outcome && outcome.reason ? { reason: outcome.reason } : {}),
1147
+ };
1148
+ }
1149
+ /**
1150
+ * A human pins or releases a run's inbound policy — §6's override, made
1151
+ * reachable.
1152
+ *
1153
+ * 🔴 Deliberately a HOST method and not a lane call. `CoordinationLanePort`
1154
+ * exposes exactly four calls and this is not one of them, because an agent
1155
+ * that could set its receiver's policy could accept its own messages past the
1156
+ * rule that a message never moves trust upward.
1157
+ */
1158
+ setCoordinationInboundPolicy(input) {
1159
+ const service = this.coordinationService;
1160
+ if (!service) {
1161
+ return { ok: false, reason: 'This host is not coordinating runs.' };
1162
+ }
1163
+ if (input.policy === null)
1164
+ service.clearInboundPolicy(input.runId);
1165
+ else
1166
+ service.setInboundPolicy(input.runId, input.policy);
1167
+ // Read back rather than echo the request: the surface must render what the
1168
+ // host now holds, and a result that restated the input would look identical
1169
+ // whether or not the write landed.
1170
+ const { explicit } = service.inboundPolicyFor(input.runId);
1171
+ return { ok: true, ...(explicit === undefined ? {} : { explicit }) };
1172
+ }
1173
+ async readRunObservation(input) {
1174
+ const unavailable = (reason) => ({
1175
+ available: false,
1176
+ runId: input.runId,
1177
+ changes: [],
1178
+ // `complete: false` on the unavailable path, deliberately. An unobserved
1179
+ // run's diff is not complete; it does not exist. Reporting `true` here
1180
+ // would let a landing gate that only reads completeness clear it.
1181
+ complete: false,
1182
+ droppedEvents: 0,
1183
+ problems: [],
1184
+ reason,
1185
+ });
1186
+ const service = this.runObservationService;
1187
+ if (!service)
1188
+ return unavailable('This host is not observing runs.');
1189
+ const snapshot = input.poll === true
1190
+ ? await service.poll(input.runId)
1191
+ : service.snapshot(input.runId);
1192
+ if (!snapshot)
1193
+ return unavailable('This run is not being observed.');
1194
+ return {
1195
+ available: true,
1196
+ runId: snapshot.runId,
1197
+ changes: snapshot.changes.map((change) => ({ path: change.path, kind: change.kind })),
1198
+ complete: snapshot.complete,
1199
+ droppedEvents: snapshot.droppedEvents,
1200
+ problems: snapshot.problems,
1201
+ };
1202
+ }
1203
+ /**
1204
+ * Content for one changed path — §2.9 hunk-level review.
1205
+ *
1206
+ * The service refuses a path the run did not change, so this cannot be used to
1207
+ * browse the sandbox: that is a different capability with a different consent
1208
+ * story, and a review pane is not a licence to read arbitrary files.
1209
+ */
1210
+ async readRunObservationChange(input) {
1211
+ const service = this.runObservationService;
1212
+ const content = service ? await service.readChange(input.runId, input.path) : undefined;
1213
+ if (!content) {
1214
+ return {
1215
+ available: false,
1216
+ path: input.path,
1217
+ binary: false,
1218
+ truncated: false,
1219
+ refusal: service
1220
+ ? 'This run has no observed change at that path, or no content reader is configured.'
1221
+ : 'This host is not observing runs.',
1222
+ };
1223
+ }
1224
+ return {
1225
+ available: true,
1226
+ path: content.path,
1227
+ ...(content.before === undefined ? {} : { before: content.before }),
1228
+ ...(content.after === undefined ? {} : { after: content.after }),
1229
+ binary: content.binary,
1230
+ truncated: content.truncated,
1231
+ ...(content.refusal === undefined ? {} : { refusal: content.refusal }),
1232
+ };
1233
+ }
1234
+ /** Where the observed runs collide — §5.7, while both are still alive. */
1235
+ readRunObservationOverlaps(input) {
1236
+ const service = this.runObservationService;
1237
+ if (!service) {
1238
+ return {
1239
+ available: false,
1240
+ overlaps: [],
1241
+ incompleteRuns: [],
1242
+ // Not complete: nothing was measured. An `available: false` result that
1243
+ // claimed completeness would read as "checked, nothing found".
1244
+ complete: false,
1245
+ reason: 'This host is not observing runs, so overlaps cannot be detected.',
1246
+ };
1247
+ }
1248
+ /**
1249
+ * The host tells the service which runs it knows about; the service owns the
1250
+ * rule that an unobserved one makes the report incomplete.
1251
+ *
1252
+ * 🔴 That rule used to live HERE, and the shipping landing gate bypassed
1253
+ * it by calling `landingOverlaps` directly. A check that only one caller
1254
+ * performs is a check the other caller does not have.
1255
+ */
1256
+ const report = service.landingOverlaps(input.claimedPathsByRun ?? {}, [
1257
+ ...this.runContexts.keys(),
1258
+ ...(this.coordinationService?.claims() ?? []).map((claim) => claim.runId),
1259
+ ]);
1260
+ return {
1261
+ available: true,
1262
+ overlaps: report.overlaps.map((overlap) => ({
1263
+ path: overlap.path,
1264
+ runIds: overlap.runIds,
1265
+ kind: overlap.kind,
1266
+ ...(overlap.unclaimed === undefined ? {} : { unclaimed: overlap.unclaimed }),
1267
+ })),
1268
+ incompleteRuns: report.incompleteRuns,
1269
+ complete: report.complete,
1270
+ };
1271
+ }
1272
+ /**
1273
+ * How many turns are executing right now.
1274
+ *
1275
+ * Exposed so a composition can answer "would quitting destroy work?" — which
1276
+ * it cannot ask any other way, and which is the difference between an app that
1277
+ * closes cleanly and one that silently ends a run the user is waiting on.
1278
+ *
1279
+ * A COUNT, not the turns themselves: a caller deciding whether to quit has no
1280
+ * business with the requests, and handing them out would make an internal map
1281
+ * part of the surface.
1282
+ */
1283
+ get activeTurnCount() {
1284
+ return this.activeTurns.size;
1285
+ }
1286
+ /**
1287
+ * The mounted run-observation service, or `undefined` when unmounted.
1288
+ *
1289
+ * The full change set is pulled through here rather than pushed on the feed:
1290
+ * a diff is large and mostly unchanged between polls, while the notification
1291
+ * that it moved is small. Same reason it is not on `AgentHostTransport` —
1292
+ * that is a protected cross-repo interface (root ABSOLUTE RULE §4).
1293
+ */
1294
+ get runObservation() {
1295
+ return this.runObservationService;
1296
+ }
1297
+ /**
1298
+ * The mounted session substrate, or `undefined` when unmounted.
1299
+ *
1300
+ * Reached by composition, not through `request()`: attaching to a session
1301
+ * means subscribing to a byte stream, which is not a request/response shape —
1302
+ * and `AgentHostTransport` is protected (root ABSOLUTE RULE §4).
1303
+ */
1304
+ get sessions() {
1305
+ return this.sessionService;
1306
+ }
1307
+ async request(method, input) {
1308
+ if (method === 'host.handshake') {
1309
+ return this.handshake(input);
1310
+ }
1311
+ if (method === 'providers.list') {
1312
+ await this.refreshProviderCatalog();
1313
+ return this.listProviders(input);
1314
+ }
1315
+ if (method === 'providers.refresh') {
1316
+ await this.refreshProviderCatalog();
1317
+ return this.listProviders(input);
1318
+ }
1319
+ if (method === 'runtime.events.list') {
1320
+ return await this.listRuntimeEvents(input);
1321
+ }
1322
+ if (method === 'state.workspaces.load') {
1323
+ return await this.requireStateRepository().loadWorkspaces();
1324
+ }
1325
+ if (method === 'state.workspaces.save') {
1326
+ return await this.requireStateRepository().saveWorkspaces(input);
1327
+ }
1328
+ if (method === 'state.conversations.load') {
1329
+ return await this.requireStateRepository().loadConversations();
1330
+ }
1331
+ if (method === 'state.conversations.save') {
1332
+ return await this.requireStateRepository().saveConversation(input);
1333
+ }
1334
+ if (method === 'state.conversations.delete') {
1335
+ return await this.requireStateRepository().deleteConversation(input);
1336
+ }
1337
+ if (method === 'workspace.access.grant') {
1338
+ return await this.requireWorkspaceService().grantDirectory(input);
1339
+ }
1340
+ if (method === 'coordination.held.list') {
1341
+ return this.listHeldMessages(input);
1342
+ }
1343
+ if (method === 'coordination.held.release') {
1344
+ return this.releaseHeldMessage(input);
1345
+ }
1346
+ if (method === 'coordination.policy.set') {
1347
+ return this.setCoordinationInboundPolicy(input);
1348
+ }
1349
+ if (method === 'workspace.application.grant') {
1350
+ // Deliberately NOT behind `assertWorkspaceClient`, matching
1351
+ // `workspace.access.grant` beside it: granting is how a client comes to
1352
+ // have access at all, so requiring existing access to ask for it would
1353
+ // make the first grant impossible.
1354
+ return await this.requireWorkspaceService().grantApplication(input);
1355
+ }
1356
+ if (method === 'workspace.directory.list') {
1357
+ this.assertWorkspaceClient(input);
1358
+ return await this.requireWorkspaceService().listDirectory(input);
1359
+ }
1360
+ if (method === 'workspace.file.read') {
1361
+ this.assertWorkspaceClient(input);
1362
+ return await this.requireWorkspaceService().readFile(input);
1363
+ }
1364
+ if (method === 'workspace.file.write') {
1365
+ this.assertWorkspaceClient(input);
1366
+ return await this.requireWorkspaceService().writeFile(input);
1367
+ }
1368
+ if (method === 'workspace.command.run') {
1369
+ this.assertWorkspaceClient(input);
1370
+ return await this.requireWorkspaceService().runCommand(input);
1371
+ }
1372
+ if (method === 'workspace.terminal.start') {
1373
+ this.assertWorkspaceClient(input);
1374
+ return await this.requireWorkspaceService().startTerminal(input);
1375
+ }
1376
+ if (method === 'workspace.terminal.stop') {
1377
+ this.assertWorkspaceClient(input);
1378
+ return await this.requireWorkspaceService().stopTerminal(input);
1379
+ }
1380
+ if (method === 'workspace.pty.start') {
1381
+ this.assertWorkspaceClient(input);
1382
+ return await this.requireWorkspaceService().startPty(input);
1383
+ }
1384
+ if (method === 'workspace.pty.snapshot') {
1385
+ this.assertWorkspaceClient(input);
1386
+ return await this.requireWorkspaceService().snapshotPty(input);
1387
+ }
1388
+ if (method === 'workspace.pty.input') {
1389
+ this.assertWorkspaceClient(input);
1390
+ return await this.requireWorkspaceService().inputPty(input);
1391
+ }
1392
+ if (method === 'workspace.pty.resize') {
1393
+ this.assertWorkspaceClient(input);
1394
+ return await this.requireWorkspaceService().resizePty(input);
1395
+ }
1396
+ if (method === 'workspace.pty.clear') {
1397
+ this.assertWorkspaceClient(input);
1398
+ return await this.requireWorkspaceService().clearPty(input);
1399
+ }
1400
+ if (method === 'workspace.pty.stop') {
1401
+ this.assertWorkspaceClient(input);
1402
+ return await this.requireWorkspaceService().stopPty(input);
1403
+ }
1404
+ if (method === 'turn.start') {
1405
+ return await this.startTurn(input);
1406
+ }
1407
+ if (method === 'acp.provider.approve') {
1408
+ return await this.approveAcpProvider(input);
1409
+ }
1410
+ if (method === 'turn.resume') {
1411
+ return await this.resumeTurn(input);
1412
+ }
1413
+ if (method === 'turn.cancel') {
1414
+ return await this.cancelTurn(input);
1415
+ }
1416
+ if (method === 'turn.steer') {
1417
+ return await this.steerTurn(input);
1418
+ }
1419
+ if (method === 'turn.queue') {
1420
+ return this.queueTurnInstruction(input);
1421
+ }
1422
+ if (method === 'turn.queue.list') {
1423
+ return this.listQueuedInstructions(input);
1424
+ }
1425
+ if (method === 'run.observation.snapshot') {
1426
+ return await this.readRunObservation(input);
1427
+ }
1428
+ if (method === 'run.observation.change') {
1429
+ return await this.readRunObservationChange(input);
1430
+ }
1431
+ if (method === 'run.observation.overlaps') {
1432
+ return this.readRunObservationOverlaps(input);
1433
+ }
1434
+ if (method === 'subagent.run.create') {
1435
+ return this.createSubagentRun(input);
1436
+ }
1437
+ if (method === 'subagent.run.list') {
1438
+ return this.listSubagentRuns(input);
1439
+ }
1440
+ if (method === 'subagent.run.cancel') {
1441
+ return this.cancelSubagentRun(input);
1442
+ }
1443
+ if (method === 'fleet.view') {
1444
+ return this.readFleetView(input);
1445
+ }
1446
+ if (method === 'fleet.route') {
1447
+ return this.routeByCapabilities(input);
1448
+ }
1449
+ if (method === 'fleet.plan') {
1450
+ return this.planFleetFanOut(input);
1451
+ }
1452
+ if (method === 'session.list') {
1453
+ return this.listSessions(input);
1454
+ }
1455
+ if (method === 'session.attach') {
1456
+ return this.attachSession(input);
1457
+ }
1458
+ if (method === 'session.detach') {
1459
+ return this.detachSession(input);
1460
+ }
1461
+ if (method === 'session.snapshot') {
1462
+ return this.snapshotSession(input);
1463
+ }
1464
+ if (method === 'session.input') {
1465
+ return await this.routeSessionInput(input);
1466
+ }
1467
+ if (method === 'session.takeover') {
1468
+ return await this.takeOverSession(input);
1469
+ }
1470
+ if (method === 'session.release') {
1471
+ return this.releaseSession(input);
1472
+ }
1473
+ if (method === 'intake.sources.list') {
1474
+ return this.listIntakeSources();
1475
+ }
1476
+ if (method === 'intake.items.list') {
1477
+ return await this.listIntakeItems(input);
1478
+ }
1479
+ if (method === 'intake.item.claim') {
1480
+ return this.claimIntakeItem(input);
1481
+ }
1482
+ if (method === 'intake.item.release') {
1483
+ return this.releaseIntakeItem(input);
1484
+ }
1485
+ if (method === 'intake.item.dispatch') {
1486
+ return this.dispatchIntakeItem(input);
1487
+ }
1488
+ if (method === 'knowledge.list') {
1489
+ return await this.listKnowledge(input);
1490
+ }
1491
+ if (method === 'knowledge.record') {
1492
+ return await this.recordKnowledge(input);
1493
+ }
1494
+ if (method === 'knowledge.forget') {
1495
+ return await this.forgetKnowledge(input);
1496
+ }
1497
+ if (method === 'knowledge.context') {
1498
+ return await this.knowledgeContext(input);
1499
+ }
1500
+ if (method === 'turn.queue.drop') {
1501
+ return this.dropQueuedInstruction(input);
1502
+ }
1503
+ if (method === 'elicitation.choose-directory.answer') {
1504
+ return answerChooseDirectoryRequest(input);
1505
+ }
1506
+ if (method === 'elicitation.workspace-placement.answer') {
1507
+ return answerWorkspacePlacementRequest(input);
1508
+ }
1509
+ if (method === 'elicitation.permission.answer') {
1510
+ return await this.requireInteractionService().answerPermission(input);
1511
+ }
1512
+ if (method === 'permissions.rules.list') {
1513
+ return await this.requirePermissionPolicyService().listRules();
1514
+ }
1515
+ if (method === 'permissions.rules.revoke') {
1516
+ return await this.requirePermissionPolicyService().revokeRule(input);
1517
+ }
1518
+ if (method === 'elicitation.ask-user.answer') {
1519
+ return await this.requireInteractionService().answerAskUser(input);
1520
+ }
1521
+ if (method === 'sdk.sessions.list') {
1522
+ this.assertSdkControlClient(input);
1523
+ return await this.requireSdkControlService().listSessions(input);
1524
+ }
1525
+ if (method === 'sdk.sessions.show') {
1526
+ this.assertSdkControlClient(input);
1527
+ return await this.requireSdkControlService().showSession(input);
1528
+ }
1529
+ if (method === 'sdk.sessions.attach') {
1530
+ this.assertSdkControlClient(input);
1531
+ return await this.requireSdkControlService().attachSession(input);
1532
+ }
1533
+ if (method === 'sdk.sessions.detach') {
1534
+ this.assertSdkControlClient(input);
1535
+ return await this.requireSdkControlService().detachSession(input);
1536
+ }
1537
+ if (method === 'sdk.sessions.delete') {
1538
+ this.assertSdkControlClient(input);
1539
+ return await this.requireSdkControlService().deleteSession(input);
1540
+ }
1541
+ if (method === 'sdk.sessions.clean') {
1542
+ this.assertSdkControlClient(input);
1543
+ return await this.requireSdkControlService().cleanSessions(input);
1544
+ }
1545
+ if (method === 'sdk.checkpoints.list') {
1546
+ this.assertSdkControlClient(input);
1547
+ return await this.requireSdkControlService().listCheckpoints(input);
1548
+ }
1549
+ if (method === 'sdk.checkpoints.restore') {
1550
+ this.assertSdkControlClient(input);
1551
+ return await this.requireSdkControlService().restoreCheckpoint(input);
1552
+ }
1553
+ if (method === 'sdk.checkpoints.delete') {
1554
+ this.assertSdkControlClient(input);
1555
+ return await this.requireSdkControlService().deleteCheckpoint(input);
1556
+ }
1557
+ if (method === 'sdk.tasks.output') {
1558
+ this.assertSdkControlClient(input);
1559
+ return await this.requireSdkControlService().readTaskOutput(input);
1560
+ }
1561
+ if (method === 'sdk.tasks.stop') {
1562
+ this.assertSdkControlClient(input);
1563
+ return await this.requireSdkControlService().stopTask(input);
1564
+ }
1565
+ if (String(method).startsWith('engineering.')) {
1566
+ this.assertEngineeringControlClient(input);
1567
+ const engineeringMethod = method;
1568
+ return await this.requireEngineeringControlService().request(engineeringMethod, input);
1569
+ }
1570
+ throw new AgentHostProtocolError('invalid_response', `Unsupported XENO Agent host method: ${String(method)}`);
1571
+ }
1572
+ subscribe(listener) {
1573
+ this.listeners.add(listener);
1574
+ return () => {
1575
+ this.listeners.delete(listener);
1576
+ };
1577
+ }
1578
+ /**
1579
+ * Forget a client whose connection is gone.
1580
+ *
1581
+ * 🔴 Before this there was no `connectedClients.delete` anywhere except a wholesale clear, so
1582
+ * the set only ever grew within a session. That is invisible until something asks a question of
1583
+ * it — and `canPresentElicitations` is exactly such a question, asked immediately before the
1584
+ * host puts a prompt to a person and waits with no timeout.
1585
+ */
1586
+ releaseClient(instanceId) {
1587
+ this.connectedClients.delete(instanceId);
1588
+ this.clientCapabilities.delete(instanceId);
1589
+ }
1590
+ /**
1591
+ * Is there a connected client that can put a question to a person and answer it?
1592
+ *
1593
+ * ⚠️ Subscribers are NOT the signal, and that is the trap this method exists to avoid: the RPC
1594
+ * server subscribes exactly ONE listener and fans out to whatever sockets it has, so a listener
1595
+ * count of 1 is equally consistent with ten attached clients and none. Only a completed
1596
+ * handshake says someone is there, and only a declared capability says they can answer.
1597
+ */
1598
+ hasElicitationCapableClient() {
1599
+ for (const [instanceId, capabilities] of this.clientCapabilities) {
1600
+ if (!this.connectedClients.has(instanceId))
1601
+ continue;
1602
+ if (capabilities.canPresentElicitations === true)
1603
+ return true;
1604
+ }
1605
+ return false;
1606
+ }
1607
+ registerProvider(provider) {
1608
+ this.setProvider(provider, true);
1609
+ this.providerCatalogFingerprint = fingerprintProviders([...this.providers.values()]);
1610
+ }
1611
+ registerTurnExecutionAdapter(adapter) {
1612
+ if (!adapter.id.trim())
1613
+ throw new Error('Turn execution adapter id must not be empty.');
1614
+ if (this.turnExecutionAdapters.has(adapter.id)) {
1615
+ throw new Error(`Turn execution adapter ${adapter.id} is already registered.`);
1616
+ }
1617
+ this.turnExecutionAdapters.set(adapter.id, adapter);
1618
+ }
1619
+ async close() {
1620
+ for (const active of this.activeTurns.values()) {
1621
+ active.controller.abort('The XENO Agent host is shutting down.');
1622
+ }
1623
+ this.activeTurns.clear();
1624
+ await Promise.all([...this.turnExecutionAdapters.values()].map(async (adapter) => {
1625
+ await adapter.close?.();
1626
+ }));
1627
+ await this.sdkControlService?.close?.();
1628
+ await this.engineeringControlService?.close?.();
1629
+ await this.workspaceService?.close?.();
1630
+ await this.stateRepository?.close?.();
1631
+ // Before the listeners go: each session's protocol channel is subscribed to
1632
+ // this host's event feed, and closing the session is what releases it.
1633
+ for (const session of this.sessionService?.sessions() ?? []) {
1634
+ this.sessionService?.close(session.sessionId);
1635
+ }
1636
+ this.connectedClients.clear();
1637
+ // Cleared together, always: a capability outliving its client would answer "yes, someone can
1638
+ // be asked" on behalf of a connection that no longer exists.
1639
+ this.clientCapabilities.clear();
1640
+ this.listeners.clear();
1641
+ }
1642
+ removeProvider(providerId) {
1643
+ const removed = this.providers.delete(providerId);
1644
+ if (removed) {
1645
+ this.catalogRevision += 1;
1646
+ this.providerCatalogFingerprint = fingerprintProviders([...this.providers.values()]);
1647
+ this.publishCatalogChange();
1648
+ }
1649
+ return removed;
1650
+ }
1651
+ async recordRuntimeEvent(event) {
1652
+ assertRuntimeEventForHost(event);
1653
+ if (this.runtimeEventRepository) {
1654
+ const result = await this.runtimeEventRepository.append(structuredClone(event));
1655
+ if (result.inserted) {
1656
+ this.publish('runtime.event.appended', {
1657
+ requestId: result.event.requestId,
1658
+ event: structuredClone(result.event),
1659
+ });
1660
+ }
1661
+ return result.inserted;
1662
+ }
1663
+ return this.appendRuntimeEvent(event, true);
1664
+ }
1665
+ publishPersistedRuntimeEvent(event) {
1666
+ assertRuntimeEventForHost(event);
1667
+ this.publish('runtime.event.appended', {
1668
+ requestId: event.requestId,
1669
+ event: structuredClone(event),
1670
+ });
1671
+ }
1672
+ createElicitationSurfaceMessenger() {
1673
+ return {
1674
+ send: (channel, payload) => {
1675
+ if (channel === 'agent:choose-directory-request') {
1676
+ this.publish('elicitation.choose-directory.requested', structuredClone(payload));
1677
+ return;
1678
+ }
1679
+ if (channel === 'agent:workspace-placement-request') {
1680
+ this.publish('elicitation.workspace-placement.requested', structuredClone(payload));
1681
+ return;
1682
+ }
1683
+ if (channel === 'agent:permission-request') {
1684
+ this.publish('elicitation.permission.requested', structuredClone(payload));
1685
+ return;
1686
+ }
1687
+ if (channel === 'agent:ask-user-request') {
1688
+ this.publish('elicitation.ask-user.requested', structuredClone(payload));
1689
+ return;
1690
+ }
1691
+ if (channel === 'agent:task-event') {
1692
+ this.publish('task.changed', structuredClone(payload));
1693
+ return;
1694
+ }
1695
+ throw new AgentHostProtocolError('invalid_response', `Unsupported Agent elicitation channel: ${channel}`);
1696
+ },
1697
+ };
1698
+ }
1699
+ /**
1700
+ * Enqueue a subagent run — ADE §5.2, the join between `fleet.plan` and `SubagentScheduler`.
1701
+ *
1702
+ * ⚠️ Reached through a PORT spanning two objects on purpose. `createRun`/`listRuns` live on
1703
+ * `SubagentRunService` and `cancelRun` on `SubagentScheduler` — and cancel must go through the
1704
+ * SCHEDULER, because cancelling a run whose adapter has already launched it has to tell the
1705
+ * adapter. Handing the host either object alone would push that distinction onto every caller.
1706
+ *
1707
+ * 🔴 Refuses with a NAMED reason when no scheduler is mounted, rather than pretending. A host
1708
+ * with no scheduler is a normal composition (the standalone app had none until this landed),
1709
+ * and a create that silently succeeded against nothing would queue work no one will ever run.
1710
+ */
1711
+ createSubagentRun(input) {
1712
+ if (!this.isConnectedClient(input.command.client)) {
1713
+ return { success: false, safeError: 'The requesting surface must complete the host handshake first.' };
1714
+ }
1715
+ const commandKey = this.commandKey('subagent.run.create', input.command);
1716
+ const fingerprint = commandFingerprint(input);
1717
+ const prior = this.commandResults.get(commandKey);
1718
+ if (prior) {
1719
+ // 🔴 The reason this verb takes a command envelope at all: a client that retries an enqueue
1720
+ // it is unsure about must NOT get a second child doing identical work.
1721
+ if (prior.method !== 'subagent.run.create' || prior.fingerprint !== fingerprint) {
1722
+ return { success: false, safeError: 'This idempotency key was already used for a different command.' };
1723
+ }
1724
+ return structuredClone(prior.result);
1725
+ }
1726
+ if (!this.subagentRuns) {
1727
+ // Deliberately NOT remembered: a refusal caused by this host's shape must not freeze into
1728
+ // the command ledger, or mounting a scheduler later would replay the refusal forever.
1729
+ return { success: false, safeError: 'This Agent host has no subagent scheduler mounted.' };
1730
+ }
1731
+ /**
1732
+ * 🔴 A CHILD MAY NOT OUTRANK ITS PARENT. The requested workspace is bounded by the workspace
1733
+ * of the turn that spawned this run, and a request that cannot be bounded is REFUSED rather
1734
+ * than quietly narrowed — a caller told it got a workspace it did not get would debug the
1735
+ * wrong thing.
1736
+ *
1737
+ * ⚠️ Refusals here are NOT remembered in the command ledger, for the same reason as the
1738
+ * missing-scheduler refusal above: the parent turn may simply not have started yet, and
1739
+ * freezing that answer would replay it forever against a retry that would now succeed.
1740
+ */
1741
+ const bound = boundSubagentWorkspace({
1742
+ ...(input.workspace === undefined ? {} : { requested: input.workspace }),
1743
+ ...(this.parentTurnWorkspace(input) === undefined
1744
+ ? {}
1745
+ : { parent: this.parentTurnWorkspace(input) }),
1746
+ });
1747
+ if (!bound.granted)
1748
+ return { success: false, safeError: bound.safeError };
1749
+ const created = this.subagentRuns.createRun({
1750
+ ...(bound.workspace === undefined ? {} : { workspace: bound.workspace }),
1751
+ parentConversationId: input.parentConversationId,
1752
+ ...(input.parentRequestId === undefined ? {} : { parentRequestId: input.parentRequestId }),
1753
+ parentAgentId: input.parentAgentId,
1754
+ childConversationId: input.childConversationId,
1755
+ childAgentId: input.childAgentId,
1756
+ workspaceId: input.workspaceId,
1757
+ teamId: input.teamId,
1758
+ objective: input.objective,
1759
+ ...(input.role === undefined ? {} : { role: input.role }),
1760
+ ...(input.requiredCapabilities === undefined
1761
+ ? {}
1762
+ : { requiredCapabilities: [...input.requiredCapabilities] }),
1763
+ });
1764
+ const result = created.success && created.run
1765
+ ? { success: true, run: summariseSubagentRun(created.run) }
1766
+ : { success: false, safeError: created.error ?? 'The subagent run could not be created.' };
1767
+ this.commandResults.set(commandKey, {
1768
+ method: 'subagent.run.create',
1769
+ fingerprint,
1770
+ result: structuredClone(result),
1771
+ });
1772
+ return result;
1773
+ }
1774
+ /**
1775
+ * What the turn that spawned this run is allowed to reach.
1776
+ *
1777
+ * Resolved from `runContexts`, which the host populates at `turn.start` and keeps for the life
1778
+ * of the run. Absent means the host genuinely does not know — a run created outside a turn, or
1779
+ * by a surface that declared no workspace — and `boundSubagentWorkspace` treats that as
1780
+ * authorising nothing rather than as permission.
1781
+ */
1782
+ parentTurnWorkspace(input) {
1783
+ for (const context of this.runContexts.values()) {
1784
+ if (context.conversationId !== input.parentConversationId)
1785
+ continue;
1786
+ if (input.parentRequestId !== undefined && context.requestId !== input.parentRequestId)
1787
+ continue;
1788
+ if (context.workspace)
1789
+ return context.workspace;
1790
+ }
1791
+ return undefined;
1792
+ }
1793
+ listSubagentRuns(input) {
1794
+ if (!this.subagentRuns) {
1795
+ return { success: false, runs: [], safeError: 'This Agent host has no subagent scheduler mounted.' };
1796
+ }
1797
+ const listed = this.subagentRuns.listRuns({
1798
+ ...(input.parentConversationId === undefined ? {} : { parentConversationId: input.parentConversationId }),
1799
+ ...(input.childAgentId === undefined ? {} : { childAgentId: input.childAgentId }),
1800
+ ...(input.workspaceId === undefined ? {} : { workspaceId: input.workspaceId }),
1801
+ ...(input.teamId === undefined ? {} : { teamId: input.teamId }),
1802
+ ...(input.status === undefined ? {} : { status: input.status }),
1803
+ ...(input.activeOnly === undefined ? {} : { activeOnly: input.activeOnly }),
1804
+ });
1805
+ return listed.success
1806
+ ? { success: true, runs: listed.runs.map(summariseSubagentRun) }
1807
+ : { success: false, runs: [], safeError: listed.error ?? 'Subagent runs could not be listed.' };
1808
+ }
1809
+ cancelSubagentRun(input) {
1810
+ if (!this.isConnectedClient(input.command.client)) {
1811
+ return { success: false, safeError: 'The requesting surface must complete the host handshake first.' };
1812
+ }
1813
+ if (!this.subagentRuns) {
1814
+ return { success: false, safeError: 'This Agent host has no subagent scheduler mounted.' };
1815
+ }
1816
+ const cancelled = this.subagentRuns.cancelRun({
1817
+ id: input.runId,
1818
+ ...(input.reason === undefined ? {} : { reason: input.reason }),
1819
+ });
1820
+ return cancelled.success && cancelled.run
1821
+ ? { success: true, run: summariseSubagentRun(cancelled.run) }
1822
+ : { success: false, safeError: cancelled.error ?? 'The subagent run could not be cancelled.' };
1823
+ }
1824
+ /** Remember a timer owned by this turn, so settling can cancel it. */
1825
+ trackTurnTimer(activeKey, timer) {
1826
+ const timers = this.turnTimers.get(activeKey);
1827
+ if (timers)
1828
+ timers.push(timer);
1829
+ else
1830
+ this.turnTimers.set(activeKey, [timer]);
1831
+ }
1832
+ /**
1833
+ * Drop every timer for a turn. Called when it settles, so a turn that finished normally can
1834
+ * never be abandoned afterwards by a timer that was already in flight.
1835
+ */
1836
+ clearTurnTimers(activeKey) {
1837
+ for (const timer of this.turnTimers.get(activeKey) ?? [])
1838
+ clearTimeout(timer);
1839
+ this.turnTimers.delete(activeKey);
1840
+ }
1841
+ /**
1842
+ * Abort a turn, then STOP COUNTING IT if the adapter does not settle in time.
1843
+ *
1844
+ * 🔴 The honesty this preserves is the whole point. The host cannot make a third-party adapter
1845
+ * stop — it can only ask, and then decide what to believe. Waiting forever means one
1846
+ * uncooperative adapter permanently consumes an admission slot; pretending the abort worked
1847
+ * means reporting a success we do not have, which this repo's own rules forbid. So the host
1848
+ * abandons ITS OWN record and says so: `turn.execution.abandoned` states that the host gave up
1849
+ * tracking, NOT that the work stopped.
1850
+ */
1851
+ abortAndAbandon(activeKey, reason) {
1852
+ const active = this.activeTurns.get(activeKey);
1853
+ if (!active)
1854
+ return;
1855
+ const { controller, request, adapter } = active;
1856
+ controller.abort(reason);
1857
+ const timer = setTimeout(() => {
1858
+ const stillActive = this.activeTurns.get(activeKey);
1859
+ // Only abandon the turn we aborted: a newer turn may legitimately hold this key.
1860
+ if (!stillActive || stillActive.controller !== controller)
1861
+ return;
1862
+ this.activeTurns.delete(activeKey);
1863
+ this.clearTurnTimers(activeKey);
1864
+ this.publish('turn.execution.abandoned', {
1865
+ conversationId: request.conversationId,
1866
+ requestId: request.requestId,
1867
+ adapterId: adapter.id,
1868
+ graceMs: this.turnAbandonGraceMs,
1869
+ safeError: 'The runtime adapter did not stop within the grace period. The XENO Agent host has '
1870
+ + 'stopped tracking this turn and released its capacity. The adapter may still be '
1871
+ + 'running.',
1872
+ });
1873
+ }, this.turnAbandonGraceMs);
1874
+ timer.unref?.();
1875
+ this.trackTurnTimer(activeKey, timer);
1876
+ }
1877
+ async startTurn(input) {
1878
+ const invalidReason = validateTurnStartRequest(input);
1879
+ if (invalidReason) {
1880
+ return turnStartFailure('invalid_request', invalidReason, input);
1881
+ }
1882
+ if (!this.isConnectedClient(input.command.client)) {
1883
+ return turnStartFailure('client_not_connected', 'The requesting surface must complete the host handshake before starting a turn.', input);
1884
+ }
1885
+ const commandKey = this.commandKey('turn.start', input.command);
1886
+ const fingerprint = commandFingerprint(input);
1887
+ const prior = this.commandResults.get(commandKey);
1888
+ if (prior) {
1889
+ if (prior.method !== 'turn.start' || prior.fingerprint !== fingerprint) {
1890
+ return turnStartFailure('idempotency_conflict', 'This idempotency key was already used for a different command.', input);
1891
+ }
1892
+ const result = prior.result;
1893
+ return result.success
1894
+ ? structuredClone({ ...result, status: 'replayed' })
1895
+ : structuredClone(result);
1896
+ }
1897
+ const provider = this.providers.get(input.selection.providerId);
1898
+ if (!provider) {
1899
+ return this.rememberTurnStartFailure(commandKey, fingerprint, turnStartFailure('provider_not_found', 'The selected runtime provider is no longer in the catalog.', input));
1900
+ }
1901
+ if (provider.catalogRevision !== input.selection.catalogRevision) {
1902
+ return this.rememberTurnStartFailure(commandKey, fingerprint, turnStartFailure('catalog_revision_mismatch', 'The provider catalog changed. Refresh the model selection before starting this turn.', input));
1903
+ }
1904
+ const resolved = resolveAgentRuntimeSelection([provider], {
1905
+ providerId: input.selection.providerId,
1906
+ ...(input.selection.modelId ? { modelId: input.selection.modelId } : {}),
1907
+ ...(input.selection.reasoningOptionId ? { reasoningOptionId: input.selection.reasoningOptionId } : {}),
1908
+ });
1909
+ if (!resolved.ok) {
1910
+ return this.rememberTurnStartFailure(commandKey, fingerprint, turnStartFailure(resolved.code, resolved.safeReason, input));
1911
+ }
1912
+ if (input.mode === 'development' && !isDevelopmentProviderReady(provider)) {
1913
+ const missing = missingDevelopmentCapabilities(provider);
1914
+ return this.rememberTurnStartFailure(commandKey, fingerprint, turnStartFailure('development_capability_missing', `The selected provider cannot run an engineering turn because it lacks: ${missing.join(', ')}.`, input));
1915
+ }
1916
+ if (input.execution?.toolExecution === 'disabled') {
1917
+ // 🔴 The declaration is what makes the request honest, so it is checked BEFORE the turn is
1918
+ // accepted rather than discovered at execution time. A lane that cannot hold an agent to a
1919
+ // tool-free turn must not be handed one: the request and the audit record would both read as
1920
+ // though the restriction had been honoured.
1921
+ //
1922
+ // ⚠️ `available` only. For most capabilities `degraded` means "works, with caveats"; a tool
1923
+ // policy that works with caveats is not a tool policy, and there is no partial credit for
1924
+ // mostly not executing tools.
1925
+ const declared = provider.capabilities?.['turn.tools-disabled'];
1926
+ if (declared?.status !== 'available') {
1927
+ return this.rememberTurnStartFailure(commandKey, fingerprint, turnStartFailure('development_capability_missing', declared?.reason
1928
+ ?? 'The selected provider cannot guarantee a turn that executes no tools.', input));
1929
+ }
1930
+ }
1931
+ const adapter = [...this.turnExecutionAdapters.values()].find((candidate) => {
1932
+ try {
1933
+ return candidate.supports(provider);
1934
+ }
1935
+ catch {
1936
+ return false;
1937
+ }
1938
+ });
1939
+ if (!adapter) {
1940
+ return this.rememberTurnStartFailure(commandKey, fingerprint, turnStartFailure('adapter_unavailable', 'No runnable host adapter is registered for the selected provider.', input));
1941
+ }
1942
+ const activeKey = turnKey(input.conversationId, input.requestId);
1943
+ if (this.activeTurns.has(activeKey)) {
1944
+ return this.rememberTurnStartFailure(commandKey, fingerprint, turnStartFailure('request_already_active', 'A turn with this conversation and request ID is already active.', input));
1945
+ }
1946
+ // 🔴 ADMISSION — and deliberately the LAST gate.
1947
+ //
1948
+ // Everything above is a PERMANENT property of the request: malformed, unknown provider,
1949
+ // stale catalog, no adapter, already active. Those must be reported regardless of load,
1950
+ // because the caller has to change something. Capacity is TRANSIENT, so it is checked only
1951
+ // once the request is known to be otherwise runnable — telling someone they are over the
1952
+ // limit when their provider does not exist sends them to fix the wrong thing.
1953
+ //
1954
+ // Placed before `structuredClone` because nothing below is free of side effects: run
1955
+ // contexts, observation binding, sessions and the coordination roster all begin there.
1956
+ //
1957
+ // It READS `activeTurns` rather than keeping its own count. That map's cleanup runs in a
1958
+ // `finally` and is guarded against evicting a newer turn, so deriving the decision from it
1959
+ // makes a leaked slot unrepresentable — there is no second ledger to drift out of sync.
1960
+ const admission = evaluateTurnAdmission({
1961
+ surface: input.command.client.surface,
1962
+ providerId: input.selection.providerId,
1963
+ active: [...this.activeTurns.values()].map((turn) => ({
1964
+ surface: turn.request.command.client.surface,
1965
+ providerId: turn.request.selection.providerId,
1966
+ })),
1967
+ policy: this.turnAdmissionPolicy,
1968
+ });
1969
+ if (!admission.admitted) {
1970
+ // 🔴 NOT remembered, and that is the whole difference between this refusal and the ones
1971
+ // above it. `rememberTurnStartFailure` freezes a result into the command ledger so a
1972
+ // retried command replays it — correct for a PERMANENT fault, and wrong here. Capacity is
1973
+ // transient: caching it would mean a surface that waits and retries the same command gets
1974
+ // a stale "at capacity" forever, with no way back except inventing a new command id.
1975
+ //
1976
+ // Idempotency exists to stop duplicate EXECUTION. This refusal executed nothing, so there
1977
+ // is nothing to deduplicate — re-evaluating on retry is the correct behaviour.
1978
+ return turnStartFailure('concurrency_limit_reached', admission.safeError, input);
1979
+ }
1980
+ const request = structuredClone(input);
1981
+ // Recorded BEFORE execution, so a coordination event arriving in the first
1982
+ // moments of a turn still finds its conversation.
1983
+ const contextRunId = this.resolveRunId(request);
1984
+ if (contextRunId) {
1985
+ this.runContexts.set(contextRunId, {
1986
+ conversationId: request.conversationId,
1987
+ requestId: request.requestId,
1988
+ ...(request.workspaceId === undefined ? {} : { workspaceId: request.workspaceId }),
1989
+ ...(request.agentId === undefined ? {} : { agentId: request.agentId }),
1990
+ // §6's derivation input. Captured from the turn that is starting,
1991
+ // because "the capabilities this run is executing with" is a property of
1992
+ // the turn — and omitted entirely when the request declares none, so an
1993
+ // undeclared posture stays UNKNOWN rather than becoming an empty set
1994
+ // that would read as "maximally constrained".
1995
+ ...(request.execution?.capabilities === undefined
1996
+ ? {}
1997
+ : { posture: request.execution.capabilities }),
1998
+ // The bound for any subagent this turn spawns. Captured from the turn that is starting
1999
+ // for the same reason as `posture`: it is a property of the turn, and a child created
2000
+ // later must be measured against what its parent was actually granted — not against
2001
+ // whatever the child asks for.
2002
+ ...(request.workspace === undefined ? {} : { workspace: structuredClone(request.workspace) }),
2003
+ });
2004
+ // Fire and forget, and guarded: attaching a sandbox is slow and the turn
2005
+ // is the user's actual work. A binder that throws must not be able to fail
2006
+ // a turn — the run simply goes unobserved, which every consumer of the
2007
+ // observation service already reports honestly.
2008
+ try {
2009
+ this.runObservationBinder?.begin({
2010
+ runId: contextRunId,
2011
+ conversationId: request.conversationId,
2012
+ ...(request.workspace?.primaryDirectory === undefined
2013
+ ? {}
2014
+ : { primaryDirectory: request.workspace.primaryDirectory }),
2015
+ ...(request.workspaceId === undefined ? {} : { workspaceId: request.workspaceId }),
2016
+ ...(request.agentId === undefined ? {} : { agentId: request.agentId }),
2017
+ });
2018
+ }
2019
+ catch {
2020
+ // Nothing to report to the caller: observation is not part of this
2021
+ // command's contract, and the binder owns its own problem reporting.
2022
+ }
2023
+ this.openSessionForRun(contextRunId, provider);
2024
+ this.registerCoordinationRun(contextRunId, 'running');
2025
+ }
2026
+ const controller = new AbortController();
2027
+ this.activeTurns.set(activeKey, { adapter, controller, request });
2028
+ // 🔴 The host ENFORCES the deadline it already validates. `timeoutMs` was checked on the way
2029
+ // in (1..3_600_000) and forwarded to the adapter, but nothing here ever acted on it: the
2030
+ // controller was aborted only for shutdown or an explicit cancel. A turn whose adapter
2031
+ // ignores the timeout ran forever — and, since admission counts `activeTurns`, held its
2032
+ // capacity forever with it. Client sets the deadline, the AUTHORITY enforces it.
2033
+ if (request.timeoutMs !== undefined) {
2034
+ const deadline = setTimeout(() => {
2035
+ this.abortAndAbandon(activeKey, `Turn exceeded its ${request.timeoutMs}ms deadline.`);
2036
+ }, request.timeoutMs);
2037
+ deadline.unref?.();
2038
+ this.trackTurnTimer(activeKey, deadline);
2039
+ }
2040
+ // Written BEFORE execution begins: a record created afterwards would miss
2041
+ // exactly the turns that die early, which are the ones worth reporting.
2042
+ this.interruptedTurnStore.record({
2043
+ conversationId: request.conversationId,
2044
+ requestId: request.requestId,
2045
+ adapterId: adapter.id,
2046
+ startedAt: Date.now(),
2047
+ request,
2048
+ });
2049
+ const accepted = {
2050
+ success: true,
2051
+ status: 'accepted',
2052
+ conversationId: input.conversationId,
2053
+ requestId: input.requestId,
2054
+ adapterId: adapter.id,
2055
+ selection: { ...resolved.selection },
2056
+ };
2057
+ this.commandResults.set(commandKey, {
2058
+ method: 'turn.start',
2059
+ fingerprint,
2060
+ result: structuredClone(accepted),
2061
+ });
2062
+ void Promise.resolve()
2063
+ .then(() => this.recordContextCompaction(request, provider, resolved.selection))
2064
+ // §5.8's gate lives here. Everything else in that pillar — the store, the
2065
+ // selection, the cross-provider guarantee — is inert until a fact actually
2066
+ // reaches a prompt.
2067
+ .then(() => this.injectWorkspaceKnowledge(request))
2068
+ // §5.2b's `message` primitive is a PUSH, and this is the push. Without
2069
+ // it a run could send and no run could ever receive.
2070
+ .then(() => this.deliverCoordinationMessages(request))
2071
+ .then(() => adapter.execute(request, provider, resolved.selection, {
2072
+ signal: controller.signal,
2073
+ publishPersistedRuntimeEvent: (event) => this.publishPersistedRuntimeEvent(event),
2074
+ }))
2075
+ .then(() => {
2076
+ // The turn ended WELL. Published here rather than inferred by each surface from
2077
+ // provider-specific runtime event strings — see the note on the event type itself.
2078
+ //
2079
+ // 🔴 Guarded by controller identity, because "the adapter's promise resolved" and "this
2080
+ // turn is still the host's turn" are different facts. An abandoned turn has already been
2081
+ // declared over and its capacity handed to someone else; if its adapter finally returns
2082
+ // afterwards, reporting completion would give a turn TWO terminal events and contradict
2083
+ // what the host already told every surface. Late is not the same as successful.
2084
+ if (this.activeTurns.get(activeKey)?.controller !== controller)
2085
+ return;
2086
+ this.publish('turn.execution.completed', {
2087
+ conversationId: request.conversationId,
2088
+ requestId: request.requestId,
2089
+ adapterId: adapter.id,
2090
+ });
2091
+ })
2092
+ .catch(() => {
2093
+ // Same guard, same reason: an abandoned turn whose adapter later rejects must not add a
2094
+ // second terminal event either.
2095
+ if (this.activeTurns.get(activeKey)?.controller !== controller)
2096
+ return;
2097
+ this.publish('turn.execution.failed', {
2098
+ conversationId: request.conversationId,
2099
+ requestId: request.requestId,
2100
+ adapterId: adapter.id,
2101
+ safeError: 'The selected runtime adapter failed to execute the turn.',
2102
+ });
2103
+ })
2104
+ .finally(() => {
2105
+ const active = this.activeTurns.get(activeKey);
2106
+ if (active?.controller === controller)
2107
+ this.activeTurns.delete(activeKey);
2108
+ // Before anything else: a turn that settled must not be abandoned afterwards by a timer
2109
+ // that was already in flight.
2110
+ this.clearTurnTimers(activeKey);
2111
+ // Reaching here at all means the turn ended — well or badly. Either way
2112
+ // it was not interrupted by process death, which is the only thing this
2113
+ // record exists to describe.
2114
+ this.interruptedTurnStore.clear(request.conversationId, request.requestId);
2115
+ // Gate 8: a delivered message is the receiving run's cause "until its
2116
+ // turn ends", so this is where that ends. In `finally`, because a turn
2117
+ // that FAILED still ended — leaving the cause set would attribute the
2118
+ // run's next, unrelated work to a message it already finished acting on.
2119
+ //
2120
+ // `endTurn`, never `endRun`: see `resolveRunId`. Releasing claims here
2121
+ // would drop a live run's locks every time a turn completed.
2122
+ const runId = this.resolveRunId(request);
2123
+ if (runId) {
2124
+ this.coordinationService?.endTurn(runId);
2125
+ // \u{1F534} `idle`, not gone. A run outlives its turns — the same reason
2126
+ // `endTurn` is called here and `endRun` is not — and a peer that
2127
+ // vanished between turns would be unaddressable exactly when someone
2128
+ // wanted to hand it work.
2129
+ this.registerCoordinationRun(runId, 'idle');
2130
+ }
2131
+ });
2132
+ return structuredClone(accepted);
2133
+ }
2134
+ async recordContextCompaction(request, provider, selection) {
2135
+ const compaction = request.contextCompaction;
2136
+ if (!compaction || compaction.compactedMessageCount < 1)
2137
+ return;
2138
+ const draft = {
2139
+ conversationId: request.conversationId,
2140
+ requestId: request.requestId,
2141
+ ...(request.workspaceId ? { workspaceId: request.workspaceId } : {}),
2142
+ ...(request.teamId ? { teamId: request.teamId } : {}),
2143
+ ...(request.agentId ? { agentId: request.agentId } : {}),
2144
+ ...(request.subagentRunId ? { subagentRunId: request.subagentRunId } : {}),
2145
+ providerKind: provider.kind,
2146
+ providerId: provider.id,
2147
+ source: 'agent-host',
2148
+ schemaVersion: '1',
2149
+ redactionState: 'safe',
2150
+ event: {
2151
+ id: `context-compacted-${request.requestId}-${safeEventIdSegment(compaction.contextDigest)}`,
2152
+ type: 'context.compacted',
2153
+ timestamp: new Date().toISOString(),
2154
+ strategyVersion: compaction.strategyVersion,
2155
+ rawMessageCount: compaction.rawMessageCount,
2156
+ sentMessageCount: compaction.sentMessageCount,
2157
+ compactedMessageCount: compaction.compactedMessageCount,
2158
+ estimatedRawTokens: compaction.estimatedRawTokens,
2159
+ estimatedSentTokens: compaction.estimatedSentTokens,
2160
+ retainedAnchorCount: compaction.retainedAnchorCount,
2161
+ retainedArtifactCount: compaction.retainedArtifactCount,
2162
+ retainedGrantedDirectoryCount: compaction.retainedGrantedDirectoryCount,
2163
+ contextDigest: compaction.contextDigest,
2164
+ selection: {
2165
+ providerId: selection.providerId,
2166
+ catalogRevision: selection.catalogRevision,
2167
+ ...(selection.modelId ? { modelId: selection.modelId } : {}),
2168
+ ...(selection.reasoningOptionId ? { reasoningOptionId: selection.reasoningOptionId } : {}),
2169
+ ...(selection.runtimeModelId ? { runtimeModelId: selection.runtimeModelId } : {}),
2170
+ },
2171
+ },
2172
+ };
2173
+ if (this.runtimeEventRepository) {
2174
+ const result = await this.runtimeEventRepository.appendGenerated(draft);
2175
+ if (result.inserted) {
2176
+ this.publish('runtime.event.appended', {
2177
+ requestId: result.event.requestId,
2178
+ event: structuredClone(result.event),
2179
+ });
2180
+ }
2181
+ return;
2182
+ }
2183
+ const sequence = [...this.runtimeEvents.values()]
2184
+ .filter((event) => event.conversationId === request.conversationId)
2185
+ .reduce((maximum, event) => Math.max(maximum, event.sequence), 0) + 1;
2186
+ this.appendRuntimeEvent({
2187
+ id: draft.event.id,
2188
+ type: draft.event.type,
2189
+ timestamp: draft.event.timestamp,
2190
+ sequence,
2191
+ conversationId: draft.conversationId,
2192
+ requestId: draft.requestId,
2193
+ ...(draft.workspaceId ? { workspaceId: draft.workspaceId } : {}),
2194
+ ...(draft.teamId ? { teamId: draft.teamId } : {}),
2195
+ ...(draft.agentId ? { agentId: draft.agentId } : {}),
2196
+ ...(draft.parentRunId ? { parentRunId: draft.parentRunId } : {}),
2197
+ ...(draft.subagentRunId ? { subagentRunId: draft.subagentRunId } : {}),
2198
+ ...(draft.providerKind ? { providerKind: draft.providerKind } : {}),
2199
+ ...(draft.providerId ? { providerId: draft.providerId } : {}),
2200
+ source: draft.source || 'agent-host',
2201
+ schemaVersion: draft.schemaVersion || '1',
2202
+ redactionState: draft.redactionState || 'safe',
2203
+ event: structuredClone(draft.event),
2204
+ }, true);
2205
+ }
2206
+ async cancelTurn(input) {
2207
+ const invalidReason = validateTurnCancelRequest(input);
2208
+ if (invalidReason) {
2209
+ return turnCancelFailure('invalid_request', invalidReason, input);
2210
+ }
2211
+ if (!this.isConnectedClient(input.command.client)) {
2212
+ return turnCancelFailure('client_not_connected', 'The requesting surface must complete the host handshake before cancelling a turn.', input);
2213
+ }
2214
+ const commandKey = this.commandKey('turn.cancel', input.command);
2215
+ const fingerprint = commandFingerprint(input);
2216
+ const prior = this.commandResults.get(commandKey);
2217
+ if (prior) {
2218
+ if (prior.method !== 'turn.cancel' || prior.fingerprint !== fingerprint) {
2219
+ return turnCancelFailure('idempotency_conflict', 'This idempotency key was already used for a different command.', input);
2220
+ }
2221
+ const result = prior.result;
2222
+ return result.success
2223
+ ? structuredClone({ ...result, status: 'replayed' })
2224
+ : structuredClone(result);
2225
+ }
2226
+ const active = this.activeTurns.get(turnKey(input.conversationId, input.requestId));
2227
+ if (!active) {
2228
+ const failure = turnCancelFailure('turn_not_active', 'The requested turn is not active on this host.', input);
2229
+ this.commandResults.set(commandKey, { method: 'turn.cancel', fingerprint, result: structuredClone(failure) });
2230
+ return failure;
2231
+ }
2232
+ // Abort, then stop counting it if the adapter does not comply. `turn.cancel` used to return
2233
+ // success while the turn stayed active and stayed counted against admission — reporting a
2234
+ // success the host did not have.
2235
+ this.abortAndAbandon(turnKey(input.conversationId, input.requestId), input.reason || 'Turn cancelled by the user.');
2236
+ if (active.adapter.cancel) {
2237
+ try {
2238
+ await active.adapter.cancel(structuredClone(input));
2239
+ }
2240
+ catch {
2241
+ // The shared abort signal is authoritative; provider-specific cancellation is best-effort.
2242
+ }
2243
+ }
2244
+ const result = {
2245
+ success: true,
2246
+ status: 'cancel_requested',
2247
+ conversationId: input.conversationId,
2248
+ requestId: input.requestId,
2249
+ };
2250
+ this.commandResults.set(commandKey, { method: 'turn.cancel', fingerprint, result: structuredClone(result) });
2251
+ return result;
2252
+ }
2253
+ async steerTurn(input) {
2254
+ if (!this.isConnectedClient(input.command.client)) {
2255
+ return {
2256
+ success: false,
2257
+ code: 'client_not_connected',
2258
+ safeError: 'The requesting surface must complete the host handshake before steering a turn.',
2259
+ conversationId: input.conversationId,
2260
+ requestId: input.requestId,
2261
+ };
2262
+ }
2263
+ const instruction = typeof input.instruction === 'string' ? input.instruction.trim() : '';
2264
+ if (!input.conversationId || !input.requestId || !instruction) {
2265
+ return {
2266
+ success: false,
2267
+ code: 'invalid_request',
2268
+ safeError: 'Steering requires a conversation id, a request id and non-empty text.',
2269
+ conversationId: input.conversationId,
2270
+ requestId: input.requestId,
2271
+ };
2272
+ }
2273
+ const commandKey = this.commandKey('turn.steer', input.command);
2274
+ const fingerprint = commandFingerprint(input);
2275
+ const prior = this.commandResults.get(commandKey);
2276
+ if (prior) {
2277
+ if (prior.method !== 'turn.steer' || prior.fingerprint !== fingerprint) {
2278
+ return {
2279
+ success: false,
2280
+ code: 'idempotency_conflict',
2281
+ safeError: 'This idempotency key was already used for a different command.',
2282
+ conversationId: input.conversationId,
2283
+ requestId: input.requestId,
2284
+ };
2285
+ }
2286
+ const result = prior.result;
2287
+ return result.success
2288
+ ? structuredClone({ ...result, status: 'replayed' })
2289
+ : structuredClone(result);
2290
+ }
2291
+ const active = this.activeTurns.get(turnKey(input.conversationId, input.requestId));
2292
+ if (!active) {
2293
+ // Not an error the user should be punished for: the turn most likely just
2294
+ // ended. `turn.queue` is the right destination, and the message says so.
2295
+ const failure = {
2296
+ success: false,
2297
+ code: 'turn_not_active',
2298
+ safeError: 'That turn is no longer running. Queue the instruction instead so it is delivered to the next turn.',
2299
+ conversationId: input.conversationId,
2300
+ requestId: input.requestId,
2301
+ };
2302
+ this.commandResults.set(commandKey, { method: 'turn.steer', fingerprint, result: structuredClone(failure) });
2303
+ return failure;
2304
+ }
2305
+ if (!active.adapter.steer) {
2306
+ /**
2307
+ * 🔴 THE REMEDY IS DERIVED, NOT NAMED BY HAND.
2308
+ *
2309
+ * This used to read "… or run the agent over ACP where steering is acknowledged", and it was
2310
+ * returned FOR AN ACP LANE — measured against a real host on 2026-08-18. The ACP adapter
2311
+ * implements no `steer`; the SDK-native one does. So the advice named the single lane that
2312
+ * cannot do it, to someone already on it.
2313
+ *
2314
+ * An error that misdirects is worse than a generic one: it spends the reader's time proving
2315
+ * the system wrong before they can start. Same shape as a package name retyped into a path —
2316
+ * a fact that already exists in the code, restated by hand, then drifting.
2317
+ */
2318
+ const steerable = [...this.turnExecutionAdapters.values()]
2319
+ .filter((adapter) => typeof adapter.steer === 'function')
2320
+ .map((adapter) => adapter.id);
2321
+ const failure = {
2322
+ success: false,
2323
+ code: 'steer_unsupported',
2324
+ safeError: steerable.length > 0
2325
+ ? `The '${active.adapter.id}' lane cannot accept a correction mid-turn. Queue the `
2326
+ + `instruction so it reaches the next turn, or run this agent on a lane that can: `
2327
+ + `${steerable.join(', ')}.`
2328
+ : `The '${active.adapter.id}' lane cannot accept a correction mid-turn, and no lane on `
2329
+ + 'this host can. Queue the instruction so it reaches the next turn.',
2330
+ conversationId: input.conversationId,
2331
+ requestId: input.requestId,
2332
+ };
2333
+ this.commandResults.set(commandKey, { method: 'turn.steer', fingerprint, result: structuredClone(failure) });
2334
+ return failure;
2335
+ }
2336
+ try {
2337
+ await active.adapter.steer({ ...structuredClone(input), instruction });
2338
+ }
2339
+ catch (error) {
2340
+ // A failed steer is reported, never swallowed: the user must not believe
2341
+ // they corrected an agent that never heard them.
2342
+ const failure = {
2343
+ success: false,
2344
+ code: 'steer_unsupported',
2345
+ safeError: error instanceof Error && error.message
2346
+ ? `The provider rejected the correction: ${error.message.slice(0, 300)}`
2347
+ : 'The provider rejected the correction.',
2348
+ conversationId: input.conversationId,
2349
+ requestId: input.requestId,
2350
+ };
2351
+ this.commandResults.set(commandKey, { method: 'turn.steer', fingerprint, result: structuredClone(failure) });
2352
+ return failure;
2353
+ }
2354
+ const result = {
2355
+ success: true,
2356
+ status: 'delivered',
2357
+ acknowledged: true,
2358
+ deliveredVia: 'adapter',
2359
+ conversationId: input.conversationId,
2360
+ requestId: input.requestId,
2361
+ };
2362
+ this.commandResults.set(commandKey, { method: 'turn.steer', fingerprint, result: structuredClone(result) });
2363
+ return result;
2364
+ }
2365
+ /** True while any turn is running for this conversation. */
2366
+ hasActiveTurn(conversationId) {
2367
+ for (const active of this.activeTurns.values()) {
2368
+ if (active.request.conversationId === conversationId)
2369
+ return true;
2370
+ }
2371
+ return false;
2372
+ }
2373
+ queueSnapshot(conversationId) {
2374
+ return structuredClone(this.queuedInstructions.get(conversationId) || []);
2375
+ }
2376
+ queueTurnInstruction(input) {
2377
+ if (!this.isConnectedClient(input.command.client)) {
2378
+ return {
2379
+ success: false,
2380
+ code: 'client_not_connected',
2381
+ safeError: 'The requesting surface must complete the host handshake before queueing an instruction.',
2382
+ conversationId: input.conversationId,
2383
+ };
2384
+ }
2385
+ const instruction = typeof input.instruction === 'string' ? input.instruction.trim() : '';
2386
+ if (!input.conversationId || !instruction) {
2387
+ return {
2388
+ success: false,
2389
+ code: 'invalid_request',
2390
+ safeError: 'A queued instruction requires a conversation id and non-empty text.',
2391
+ conversationId: input.conversationId,
2392
+ };
2393
+ }
2394
+ const commandKey = this.commandKey('turn.queue', input.command);
2395
+ const fingerprint = commandFingerprint(input);
2396
+ const prior = this.commandResults.get(commandKey);
2397
+ if (prior) {
2398
+ if (prior.method !== 'turn.queue' || prior.fingerprint !== fingerprint) {
2399
+ return {
2400
+ success: false,
2401
+ code: 'idempotency_conflict',
2402
+ safeError: 'This idempotency key was already used for a different command.',
2403
+ conversationId: input.conversationId,
2404
+ };
2405
+ }
2406
+ const result = prior.result;
2407
+ return result.success
2408
+ ? structuredClone({ ...result, status: 'replayed' })
2409
+ : structuredClone(result);
2410
+ }
2411
+ const existing = input.replace ? [] : (this.queuedInstructions.get(input.conversationId) || []);
2412
+ if (existing.length >= MAX_QUEUED_INSTRUCTIONS) {
2413
+ const failure = {
2414
+ success: false,
2415
+ code: 'queue_full',
2416
+ safeError: `At most ${MAX_QUEUED_INSTRUCTIONS} instructions may be queued for one conversation.`,
2417
+ conversationId: input.conversationId,
2418
+ };
2419
+ this.commandResults.set(commandKey, { method: 'turn.queue', fingerprint, result: structuredClone(failure) });
2420
+ return failure;
2421
+ }
2422
+ this.queuedInstructionSequence += 1;
2423
+ const queued = {
2424
+ id: `queued-${this.queuedInstructionSequence}`,
2425
+ instruction: instruction.slice(0, MAX_QUEUED_INSTRUCTION_LENGTH),
2426
+ queuedAt: new Date().toISOString(),
2427
+ queuedByInstanceId: input.command.client.instanceId,
2428
+ };
2429
+ this.queuedInstructions.set(input.conversationId, [...existing, queued]);
2430
+ const result = {
2431
+ success: true,
2432
+ status: 'queued',
2433
+ conversationId: input.conversationId,
2434
+ queue: this.queueSnapshot(input.conversationId),
2435
+ };
2436
+ this.commandResults.set(commandKey, { method: 'turn.queue', fingerprint, result: structuredClone(result) });
2437
+ return result;
2438
+ }
2439
+ listQueuedInstructions(input) {
2440
+ return {
2441
+ conversationId: input.conversationId,
2442
+ queue: this.queueSnapshot(input.conversationId),
2443
+ turnActive: this.hasActiveTurn(input.conversationId),
2444
+ };
2445
+ }
2446
+ dropQueuedInstruction(input) {
2447
+ if (!this.isConnectedClient(input.command.client)) {
2448
+ return {
2449
+ success: false,
2450
+ code: 'client_not_connected',
2451
+ safeError: 'The requesting surface must complete the host handshake before changing the queue.',
2452
+ conversationId: input.conversationId,
2453
+ };
2454
+ }
2455
+ if (!input.conversationId) {
2456
+ return {
2457
+ success: false,
2458
+ code: 'invalid_request',
2459
+ safeError: 'A conversation id is required.',
2460
+ };
2461
+ }
2462
+ const existing = this.queuedInstructions.get(input.conversationId) || [];
2463
+ if (input.instructionId === undefined) {
2464
+ this.queuedInstructions.delete(input.conversationId);
2465
+ return { success: true, conversationId: input.conversationId, queue: [] };
2466
+ }
2467
+ if (!existing.some((entry) => entry.id === input.instructionId)) {
2468
+ return {
2469
+ success: false,
2470
+ code: 'not_found',
2471
+ safeError: 'No queued instruction with that id exists for this conversation.',
2472
+ conversationId: input.conversationId,
2473
+ };
2474
+ }
2475
+ this.queuedInstructions.set(input.conversationId, existing.filter((entry) => entry.id !== input.instructionId));
2476
+ return {
2477
+ success: true,
2478
+ conversationId: input.conversationId,
2479
+ queue: this.queueSnapshot(input.conversationId),
2480
+ };
2481
+ }
2482
+ rememberTurnStartFailure(commandKey, fingerprint, result) {
2483
+ this.commandResults.set(commandKey, { method: 'turn.start', fingerprint, result: structuredClone(result) });
2484
+ return result;
2485
+ }
2486
+ commandKey(method, command) {
2487
+ return `${command.client.instanceId}:${method}:${command.idempotencyKey}`;
2488
+ }
2489
+ isConnectedClient(identity) {
2490
+ const connected = this.connectedClients.get(identity.instanceId);
2491
+ return !!connected
2492
+ && connected.clientId === identity.clientId
2493
+ && connected.clientVersion === identity.clientVersion
2494
+ && connected.surface === identity.surface;
2495
+ }
2496
+ handshake(input) {
2497
+ if (!isAgentHostClientIdentity(input.identity)) {
2498
+ throw new AgentHostProtocolError('invalid_handshake', 'Agent host client identity is invalid.');
2499
+ }
2500
+ if (!isProtocolRange(input.supportedProtocol)) {
2501
+ throw new AgentHostProtocolError('invalid_handshake', 'Agent host client protocol range is invalid.');
2502
+ }
2503
+ const hostRange = {
2504
+ min: XENO_AGENT_HOST_MIN_PROTOCOL_VERSION,
2505
+ max: XENO_AGENT_HOST_PROTOCOL_VERSION,
2506
+ };
2507
+ if (!rangesOverlap(hostRange, input.supportedProtocol)) {
2508
+ throw new AgentHostProtocolError('protocol_mismatch', `Client protocol ${input.supportedProtocol.min}-${input.supportedProtocol.max} is incompatible with host ${hostRange.min}-${hostRange.max}.`);
2509
+ }
2510
+ this.connectedClients.set(input.identity.instanceId, { ...input.identity });
2511
+ // Recorded per connection, and re-recorded on every handshake so a reconnecting client
2512
+ // cannot inherit what a previous connection with the same instance id claimed.
2513
+ this.clientCapabilities.set(input.identity.instanceId, {
2514
+ canPresentElicitations: input.capabilities?.canPresentElicitations === true,
2515
+ });
2516
+ return {
2517
+ protocol: {
2518
+ current: XENO_AGENT_HOST_PROTOCOL_VERSION,
2519
+ supported: hostRange,
2520
+ },
2521
+ host: {
2522
+ instanceId: this.options.hostInstanceId,
2523
+ version: this.options.hostVersion,
2524
+ authority: 'primary',
2525
+ processBoundary: this.options.processBoundary,
2526
+ },
2527
+ storage: {
2528
+ owner: 'xeno-agent-host',
2529
+ schemaVersion: this.options.storageSchemaVersion,
2530
+ eventSchemaVersion: this.options.eventSchemaVersion,
2531
+ },
2532
+ surface: { ...input.identity },
2533
+ };
2534
+ }
2535
+ listProviders(input) {
2536
+ return {
2537
+ revision: String(this.catalogRevision),
2538
+ providers: selectAvailableProviders([...this.providers.values()], input.includeUnavailable === true),
2539
+ };
2540
+ }
2541
+ async refreshProviderCatalog() {
2542
+ if (!this.providerCatalogLoader)
2543
+ return false;
2544
+ if (this.providerCatalogRefresh)
2545
+ return this.providerCatalogRefresh;
2546
+ this.providerCatalogRefresh = (async () => {
2547
+ const providers = await this.providerCatalogLoader();
2548
+ if (!isAgentProviderListResult({ revision: 'candidate', providers })) {
2549
+ throw new AgentHostProtocolError('invalid_response', 'Provider catalog source returned an invalid descriptor.');
2550
+ }
2551
+ const providerIds = new Set();
2552
+ for (const provider of providers) {
2553
+ if (providerIds.has(provider.id)) {
2554
+ throw new AgentHostProtocolError('invalid_response', `Provider catalog source returned duplicate id: ${provider.id}`);
2555
+ }
2556
+ providerIds.add(provider.id);
2557
+ }
2558
+ const nextFingerprint = fingerprintProviders(providers);
2559
+ if (nextFingerprint === this.providerCatalogFingerprint)
2560
+ return false;
2561
+ this.providers.clear();
2562
+ for (const provider of providers)
2563
+ this.providers.set(provider.id, cloneProvider(provider));
2564
+ this.providerCatalogFingerprint = nextFingerprint;
2565
+ this.catalogRevision += 1;
2566
+ this.publishCatalogChange();
2567
+ return true;
2568
+ })().finally(() => {
2569
+ this.providerCatalogRefresh = null;
2570
+ });
2571
+ return this.providerCatalogRefresh;
2572
+ }
2573
+ async listRuntimeEvents(input) {
2574
+ if (!input.conversationId.trim()) {
2575
+ throw new AgentHostProtocolError('invalid_response', 'conversationId is required to list runtime events.');
2576
+ }
2577
+ if (this.runtimeEventRepository) {
2578
+ return this.runtimeEventRepository.list(structuredClone(input));
2579
+ }
2580
+ const limit = normalizeEventLimit(input.limit);
2581
+ const events = [...this.runtimeEvents.values()]
2582
+ .filter((event) => event.conversationId === input.conversationId)
2583
+ .filter((event) => !input.requestId || event.requestId === input.requestId)
2584
+ .filter((event) => !input.subagentRunId || event.subagentRunId === input.subagentRunId)
2585
+ .filter((event) => !input.type || event.type === input.type)
2586
+ .filter((event) => input.afterSequence === undefined || event.sequence > input.afterSequence)
2587
+ .sort((left, right) => left.sequence - right.sequence || left.id.localeCompare(right.id))
2588
+ .slice(0, limit)
2589
+ .map((event) => structuredClone(event));
2590
+ return {
2591
+ events,
2592
+ nextSequence: events.length > 0 ? events[events.length - 1]?.sequence ?? null : null,
2593
+ };
2594
+ }
2595
+ requireStateRepository() {
2596
+ if (!this.stateRepository) {
2597
+ throw new AgentHostProtocolError('invalid_response', 'Host-owned Agent state persistence is unavailable.');
2598
+ }
2599
+ return this.stateRepository;
2600
+ }
2601
+ requireWorkspaceService() {
2602
+ if (!this.workspaceService) {
2603
+ throw new AgentHostProtocolError('invalid_response', 'Host-owned workspace services are unavailable.');
2604
+ }
2605
+ return this.workspaceService;
2606
+ }
2607
+ requireInteractionService() {
2608
+ if (!this.interactionService) {
2609
+ throw new AgentHostProtocolError('invalid_response', 'Host-owned Agent interaction services are unavailable.');
2610
+ }
2611
+ return this.interactionService;
2612
+ }
2613
+ /**
2614
+ * Re-issues a turn that process death interrupted (§5.5).
2615
+ *
2616
+ * ⚠️ It RESTARTS rather than resumes, and says so in the result. A turn killed
2617
+ * with the process left no continuation point inside the agent — only the
2618
+ * request that produced it. Work already done is done again, and side effects
2619
+ * already performed have already happened. Where the request carries a
2620
+ * provider continuation the session is picked up, which narrows the gap
2621
+ * without closing it.
2622
+ *
2623
+ * Everything after the lookup goes through `startTurn`, deliberately: a
2624
+ * second execution path would be free to drift from the first on provider
2625
+ * validation, catalog revision, adapter selection and idempotency — and the
2626
+ * one that runs rarely is the one that would rot.
2627
+ */
2628
+ /**
2629
+ * Approve or revoke an ACP provider's binary.
2630
+ *
2631
+ * Fails CLOSED and by name in every branch. The shipping app previously
2632
+ * rendered an enabled Approve button whose bridge answered "not available on
2633
+ * this host yet" — a message that was true of the bridge and false of the
2634
+ * host, which implements this. `unavailable` here means the host genuinely
2635
+ * has no approval service mounted, and says so.
2636
+ */
2637
+ async approveAcpProvider(input) {
2638
+ const agentId = typeof input?.agentId === 'string' ? input.agentId.trim() : '';
2639
+ if (!input?.client || !this.isConnectedClient(input.client)) {
2640
+ return {
2641
+ success: false,
2642
+ status: 'failed',
2643
+ agentId,
2644
+ safeError: 'Complete the host handshake before approving a provider.',
2645
+ };
2646
+ }
2647
+ if (!agentId) {
2648
+ return {
2649
+ success: false,
2650
+ status: 'failed',
2651
+ agentId,
2652
+ safeError: 'Provider approval needs the id of a configured ACP agent.',
2653
+ };
2654
+ }
2655
+ if (input.action !== 'approve' && input.action !== 'revoke') {
2656
+ // Not defaulted to `approve`: guessing an unrecognised action would be
2657
+ // guessing at a trust decision.
2658
+ return {
2659
+ success: false,
2660
+ status: 'failed',
2661
+ agentId,
2662
+ safeError: 'Provider approval action must be approve or revoke.',
2663
+ };
2664
+ }
2665
+ if (!this.acpApprovalService) {
2666
+ return {
2667
+ success: false,
2668
+ status: 'unavailable',
2669
+ agentId,
2670
+ safeError: 'This host has no ACP approval service mounted.',
2671
+ };
2672
+ }
2673
+ try {
2674
+ return await this.acpApprovalService.approveProvider({
2675
+ agentId,
2676
+ ...(typeof input.providerId === 'string' && input.providerId
2677
+ ? { providerId: input.providerId }
2678
+ : {}),
2679
+ action: input.action,
2680
+ });
2681
+ }
2682
+ catch {
2683
+ // The underlying service spawns a CLI. A thrown error carries a command
2684
+ // line and environment; neither belongs on a surface.
2685
+ return {
2686
+ success: false,
2687
+ status: 'failed',
2688
+ agentId,
2689
+ safeError: 'The provider approval command failed to run.',
2690
+ };
2691
+ }
2692
+ }
2693
+ async resumeTurn(input) {
2694
+ if (!input?.command?.client || !this.isConnectedClient(input.command.client)) {
2695
+ return {
2696
+ success: false,
2697
+ code: 'client_not_connected',
2698
+ safeError: 'Complete the host handshake before resuming a turn.',
2699
+ };
2700
+ }
2701
+ const conversationId = typeof input?.conversationId === 'string' ? input.conversationId.trim() : '';
2702
+ const requestId = typeof input?.requestId === 'string' ? input.requestId.trim() : '';
2703
+ if (!conversationId || !requestId) {
2704
+ return {
2705
+ success: false,
2706
+ code: 'invalid_request',
2707
+ safeError: 'A turn resume needs both a conversation id and a request id.',
2708
+ };
2709
+ }
2710
+ if (this.activeTurns.has(turnKey(conversationId, requestId))) {
2711
+ // Restarting a live turn would run it twice against the same workspace.
2712
+ return {
2713
+ success: false,
2714
+ code: 'turn_already_active',
2715
+ safeError: 'That turn is already running.',
2716
+ conversationId,
2717
+ requestId,
2718
+ };
2719
+ }
2720
+ const record = this.interruptedTurnStore.list()
2721
+ .find((entry) => entry.conversationId === conversationId && entry.requestId === requestId);
2722
+ if (!record) {
2723
+ // Absence is reported by name rather than as a generic failure: "this turn
2724
+ // was never interrupted" and "resuming it failed" are different answers.
2725
+ return {
2726
+ success: false,
2727
+ code: 'not_interrupted',
2728
+ safeError: 'No interrupted turn is recorded for that request.',
2729
+ conversationId,
2730
+ requestId,
2731
+ };
2732
+ }
2733
+ const started = await this.startTurn(record.request);
2734
+ if (!started.success) {
2735
+ return {
2736
+ success: false,
2737
+ code: 'resume_failed',
2738
+ safeError: started.safeError,
2739
+ conversationId,
2740
+ requestId,
2741
+ };
2742
+ }
2743
+ return {
2744
+ success: true,
2745
+ status: 'restarted',
2746
+ conversationId,
2747
+ requestId,
2748
+ adapterId: started.adapterId,
2749
+ };
2750
+ }
2751
+ /**
2752
+ * Reports turns that were running when the previous process ended (§5.5).
2753
+ *
2754
+ * Called by the composition after construction. Each surviving record is
2755
+ * journalled as a durable `turn.interrupted` runtime event, so the surface
2756
+ * shows "this turn was interrupted" instead of a turn that simply stops
2757
+ * mid-sentence and never says why. An absence indistinguishable from an
2758
+ * outcome is the failure this whole mechanism exists to remove.
2759
+ *
2760
+ * Records are NOT cleared: `turn.resume` needs them. They clear when the turn
2761
+ * reaches a terminal state, which a resumed turn eventually does.
2762
+ */
2763
+ async reportInterruptedTurns() {
2764
+ const records = this.interruptedTurnStore.list();
2765
+ if (records.length === 0 || !this.runtimeEventRepository)
2766
+ return 0;
2767
+ let reported = 0;
2768
+ for (const entry of records) {
2769
+ try {
2770
+ const committed = await this.runtimeEventRepository.appendGenerated({
2771
+ conversationId: entry.conversationId,
2772
+ requestId: entry.requestId,
2773
+ source: 'host',
2774
+ event: {
2775
+ id: `${entry.requestId}:turn-interrupted`,
2776
+ type: 'turn.interrupted',
2777
+ timestamp: new Date().toISOString(),
2778
+ status: 'interrupted',
2779
+ adapterId: entry.adapterId,
2780
+ startedAt: entry.startedAt,
2781
+ resumable: true,
2782
+ },
2783
+ });
2784
+ this.publishPersistedRuntimeEvent(committed.event);
2785
+ reported += 1;
2786
+ }
2787
+ catch {
2788
+ // One unreportable turn must not hide the rest.
2789
+ }
2790
+ }
2791
+ return reported;
2792
+ }
2793
+ requirePermissionPolicyService() {
2794
+ if (!this.permissionPolicyService) {
2795
+ throw new AgentHostProtocolError('invalid_response', 'Host-owned permission policy is unavailable.');
2796
+ }
2797
+ return this.permissionPolicyService;
2798
+ }
2799
+ requireSdkControlService() {
2800
+ if (!this.sdkControlService) {
2801
+ throw new AgentHostProtocolError('invalid_response', 'Host-owned SDK session controls are unavailable.');
2802
+ }
2803
+ return this.sdkControlService;
2804
+ }
2805
+ requireEngineeringControlService() {
2806
+ if (!this.engineeringControlService) {
2807
+ throw new AgentHostProtocolError('invalid_response', 'Host-owned engineering workbench controls are unavailable.');
2808
+ }
2809
+ return this.engineeringControlService;
2810
+ }
2811
+ assertWorkspaceClient(input) {
2812
+ if (!isRecord(input) || !isAgentHostClientIdentity(input.client) || !this.isConnectedClient(input.client)) {
2813
+ throw new AgentHostProtocolError('invalid_response', 'The requesting surface must complete the host handshake before using workspace services.');
2814
+ }
2815
+ }
2816
+ assertSdkControlClient(input) {
2817
+ if (!isRecord(input) || !isAgentHostClientIdentity(input.client) || !this.isConnectedClient(input.client)) {
2818
+ throw new AgentHostProtocolError('invalid_response', 'The requesting surface must complete the host handshake before using SDK session controls.');
2819
+ }
2820
+ }
2821
+ assertEngineeringControlClient(input) {
2822
+ if (!isRecord(input) || !isAgentHostClientIdentity(input.client) || !this.isConnectedClient(input.client)) {
2823
+ throw new AgentHostProtocolError('invalid_response', 'The requesting surface must complete the host handshake before using engineering workbench controls.');
2824
+ }
2825
+ }
2826
+ setProvider(provider, publish) {
2827
+ if (!provider.id.trim())
2828
+ throw new Error('Provider id must not be empty.');
2829
+ /**
2830
+ * 🔴 A provider cannot advertise a capability the HOST does not have.
2831
+ *
2832
+ * Coordination is a property of this host — whether a service is mounted —
2833
+ * and no adapter can make it true by declaring it. Registration is the right
2834
+ * place because it is the only point every provider passes through,
2835
+ * including ones a composition adds later.
2836
+ */
2837
+ const stored = cloneProvider(provider);
2838
+ stored.capabilities = withHostBackedCapabilities(stored.capabilities, {
2839
+ coordination: this.coordinationService !== undefined,
2840
+ });
2841
+ this.providers.set(provider.id, stored);
2842
+ this.catalogRevision += 1;
2843
+ if (publish)
2844
+ this.publishCatalogChange();
2845
+ }
2846
+ publishCatalogChange() {
2847
+ this.publish('provider.catalog.changed', {
2848
+ revision: String(this.catalogRevision),
2849
+ providerIds: [...this.providers.keys()].sort(),
2850
+ });
2851
+ }
2852
+ appendRuntimeEvent(event, publish) {
2853
+ assertRuntimeEventForHost(event);
2854
+ if (this.runtimeEvents.has(event.id))
2855
+ return false;
2856
+ const copy = structuredClone(event);
2857
+ this.runtimeEvents.set(copy.id, copy);
2858
+ if (publish)
2859
+ this.publish('runtime.event.appended', { requestId: copy.requestId, event: copy });
2860
+ return true;
2861
+ }
2862
+ /**
2863
+ * Writes one coordination event to the run timeline.
2864
+ *
2865
+ * Silent when the run has no known conversation — a coordination event
2866
+ * addressed to a run this host never started a turn for has no timeline to
2867
+ * join, and inventing a conversation id would put the entry somewhere a
2868
+ * reviewer would never look. It stays on the live feed either way.
2869
+ */
2870
+ async recordCoordinationEvent(event, text) {
2871
+ if (!this.runtimeEventRepository)
2872
+ return;
2873
+ const runId = coordinationSubjectRun(event);
2874
+ const context = this.runContexts.get(runId);
2875
+ if (!context)
2876
+ return;
2877
+ this.coordinationEventSequence += 1;
2878
+ const draft = coordinationRuntimeEventDraft(event, context, {
2879
+ id: `${this.options.hostInstanceId}:coordination:${this.coordinationEventSequence}`,
2880
+ timestamp: new Date().toISOString(),
2881
+ ...(text === undefined ? {} : { text }),
2882
+ });
2883
+ try {
2884
+ const { inserted, event: envelope } = await this.runtimeEventRepository.appendGenerated(draft);
2885
+ if (inserted)
2886
+ this.publishPersistedRuntimeEvent(envelope);
2887
+ }
2888
+ catch {
2889
+ // A timeline write must not take the coordination call down with it: the
2890
+ // message was already sent and the claim already taken, so throwing here
2891
+ // would report a failure for work that succeeded.
2892
+ }
2893
+ }
2894
+ publish(type, payload) {
2895
+ this.sequence += 1;
2896
+ const event = {
2897
+ id: `${this.options.hostInstanceId}:${this.sequence}`,
2898
+ sequence: this.sequence,
2899
+ timestamp: Date.now(),
2900
+ type,
2901
+ source: 'host',
2902
+ schemaVersion: 1,
2903
+ redactionState: 'safe',
2904
+ payload,
2905
+ };
2906
+ for (const listener of [...this.listeners])
2907
+ listener(event);
2908
+ }
2909
+ }
2910
+ function fingerprintProviders(providers) {
2911
+ return JSON.stringify(providers
2912
+ .map((provider) => cloneProvider(provider))
2913
+ .sort((left, right) => left.id.localeCompare(right.id)));
2914
+ }
2915
+ function assertRuntimeEventForHost(value) {
2916
+ if (!isAgentRuntimeEventEnvelope(value)) {
2917
+ throw new AgentHostProtocolError('invalid_response', 'Runtime event does not satisfy the host contract.');
2918
+ }
2919
+ if (value.redactionState === 'unsafe') {
2920
+ throw new AgentHostProtocolError('invalid_response', 'Unsafe runtime events cannot enter host history.');
2921
+ }
2922
+ }
2923
+ function rangesOverlap(left, right) {
2924
+ return left.min <= right.max && right.min <= left.max;
2925
+ }
2926
+ function isProtocolRange(value) {
2927
+ return Number.isInteger(value.min) && Number.isInteger(value.max) && value.min > 0 && value.max >= value.min;
2928
+ }
2929
+ function normalizeEventLimit(value) {
2930
+ return typeof value === 'number' && Number.isFinite(value)
2931
+ ? Math.min(5_000, Math.max(1, Math.floor(value)))
2932
+ : 500;
2933
+ }
2934
+ function validateTurnStartRequest(value) {
2935
+ if (!isRecord(value))
2936
+ return 'Turn start input must be an object.';
2937
+ const commandError = validateHostCommand(value.command);
2938
+ if (commandError)
2939
+ return commandError;
2940
+ if (!isNonEmptyString(value.conversationId))
2941
+ return 'conversationId is required.';
2942
+ if (!isNonEmptyString(value.requestId))
2943
+ return 'requestId is required.';
2944
+ if (value.mode !== 'development' && value.mode !== 'chat')
2945
+ return 'mode must be development or chat.';
2946
+ if (!isRecord(value.selection))
2947
+ return 'selection is required.';
2948
+ if (!isNonEmptyString(value.selection.providerId))
2949
+ return 'selection.providerId is required.';
2950
+ if (!isNonEmptyString(value.selection.catalogRevision))
2951
+ return 'selection.catalogRevision is required.';
2952
+ if (!isNonEmptyString(value.prompt))
2953
+ return 'prompt is required.';
2954
+ if (value.history !== undefined) {
2955
+ if (!Array.isArray(value.history))
2956
+ return 'history must be an array.';
2957
+ for (const message of value.history) {
2958
+ if (!isRecord(message)
2959
+ || (message.role !== 'user' && message.role !== 'assistant')
2960
+ || typeof message.content !== 'string') {
2961
+ return 'history contains an invalid message.';
2962
+ }
2963
+ }
2964
+ }
2965
+ if (value.workspace !== undefined) {
2966
+ if (!isRecord(value.workspace))
2967
+ return 'workspace must be an object.';
2968
+ if (value.workspace.primaryDirectory !== undefined && !isNonEmptyString(value.workspace.primaryDirectory)) {
2969
+ return 'workspace.primaryDirectory must be a non-empty path.';
2970
+ }
2971
+ if (value.workspace.additionalDirectories !== undefined
2972
+ && (!Array.isArray(value.workspace.additionalDirectories)
2973
+ || value.workspace.additionalDirectories.some((entry) => !isNonEmptyString(entry)))) {
2974
+ return 'workspace.additionalDirectories must contain non-empty paths.';
2975
+ }
2976
+ if (value.workspace.filesystem !== undefined
2977
+ && value.workspace.filesystem !== 'read-only'
2978
+ && value.workspace.filesystem !== 'workspace-write') {
2979
+ return 'workspace.filesystem is invalid.';
2980
+ }
2981
+ }
2982
+ if (value.execution !== undefined) {
2983
+ if (!isRecord(value.execution))
2984
+ return 'execution must be an object.';
2985
+ if (value.execution.maxIterations !== undefined
2986
+ && (typeof value.execution.maxIterations !== 'number'
2987
+ || !Number.isInteger(value.execution.maxIterations)
2988
+ || value.execution.maxIterations < 1
2989
+ || value.execution.maxIterations > 100)) {
2990
+ return 'execution.maxIterations must be an integer between 1 and 100.';
2991
+ }
2992
+ if (value.execution.enableThinking !== undefined
2993
+ && typeof value.execution.enableThinking !== 'boolean') {
2994
+ return 'execution.enableThinking must be a boolean.';
2995
+ }
2996
+ if (value.execution.capabilities !== undefined) {
2997
+ if (!isRecord(value.execution.capabilities)
2998
+ || Object.values(value.execution.capabilities).some((entry) => typeof entry !== 'boolean')) {
2999
+ return 'execution.capabilities must contain only boolean values.';
3000
+ }
3001
+ }
3002
+ if (value.execution.approvalRules !== undefined
3003
+ && (!Array.isArray(value.execution.approvalRules)
3004
+ || value.execution.approvalRules.some((entry) => !isRecord(entry)))) {
3005
+ return 'execution.approvalRules must contain objects.';
3006
+ }
3007
+ }
3008
+ if (value.contextCompaction !== undefined) {
3009
+ if (!isRecord(value.contextCompaction))
3010
+ return 'contextCompaction must be an object.';
3011
+ const integerFields = [
3012
+ 'strategyVersion',
3013
+ 'rawMessageCount',
3014
+ 'sentMessageCount',
3015
+ 'compactedMessageCount',
3016
+ 'estimatedRawTokens',
3017
+ 'estimatedSentTokens',
3018
+ 'retainedAnchorCount',
3019
+ 'retainedArtifactCount',
3020
+ 'retainedGrantedDirectoryCount',
3021
+ ];
3022
+ for (const field of integerFields) {
3023
+ const fieldValue = value.contextCompaction[field];
3024
+ if (typeof fieldValue !== 'number'
3025
+ || !Number.isSafeInteger(fieldValue)
3026
+ || fieldValue < (field === 'strategyVersion' ? 1 : 0)) {
3027
+ return `contextCompaction.${field} must be a non-negative safe integer.`;
3028
+ }
3029
+ }
3030
+ if (!isNonEmptyString(value.contextCompaction.contextDigest)
3031
+ || value.contextCompaction.contextDigest.length > 160
3032
+ || !/^[a-z0-9]+:[a-f0-9]+$/i.test(value.contextCompaction.contextDigest)) {
3033
+ return 'contextCompaction.contextDigest is invalid.';
3034
+ }
3035
+ const rawMessageCount = Number(value.contextCompaction.rawMessageCount);
3036
+ const sentMessageCount = Number(value.contextCompaction.sentMessageCount);
3037
+ const compactedMessageCount = Number(value.contextCompaction.compactedMessageCount);
3038
+ if (sentMessageCount > rawMessageCount
3039
+ || compactedMessageCount !== rawMessageCount - sentMessageCount) {
3040
+ return 'contextCompaction message counts are inconsistent.';
3041
+ }
3042
+ }
3043
+ if (value.timeoutMs !== undefined
3044
+ && (typeof value.timeoutMs !== 'number'
3045
+ || !Number.isFinite(value.timeoutMs)
3046
+ || value.timeoutMs < 1
3047
+ || value.timeoutMs > 3_600_000)) {
3048
+ return 'timeoutMs must be between 1 and 3600000.';
3049
+ }
3050
+ return null;
3051
+ }
3052
+ function validateTurnCancelRequest(value) {
3053
+ if (!isRecord(value))
3054
+ return 'Turn cancel input must be an object.';
3055
+ const commandError = validateHostCommand(value.command);
3056
+ if (commandError)
3057
+ return commandError;
3058
+ if (!isNonEmptyString(value.conversationId))
3059
+ return 'conversationId is required.';
3060
+ if (!isNonEmptyString(value.requestId))
3061
+ return 'requestId is required.';
3062
+ if (value.reason !== undefined && typeof value.reason !== 'string')
3063
+ return 'reason must be a string.';
3064
+ return null;
3065
+ }
3066
+ function validateHostCommand(value) {
3067
+ if (!isRecord(value))
3068
+ return 'command context is required.';
3069
+ if (value.schemaVersion !== 1)
3070
+ return 'command.schemaVersion must be 1.';
3071
+ if (!isNonEmptyString(value.commandId))
3072
+ return 'command.commandId is required.';
3073
+ if (!isNonEmptyString(value.idempotencyKey))
3074
+ return 'command.idempotencyKey is required.';
3075
+ if (!isAgentHostClientIdentity(value.client))
3076
+ return 'command.client is invalid.';
3077
+ return null;
3078
+ }
3079
+ function commandFingerprint(
3080
+ // Every command that carries an idempotency key, not just turn commands: the fingerprint is
3081
+ // what distinguishes "the same request retried" from "a different request reusing a key".
3082
+ input) {
3083
+ const command = {
3084
+ ...input.command,
3085
+ commandId: '',
3086
+ };
3087
+ return JSON.stringify({ ...input, command });
3088
+ }
3089
+ function turnKey(conversationId, requestId) {
3090
+ return `${conversationId}:${requestId}`;
3091
+ }
3092
+ function safeEventIdSegment(value) {
3093
+ return value.replace(/[^a-zA-Z0-9_-]/g, '-').slice(0, 120);
3094
+ }
3095
+ function turnStartFailure(code, safeError, input) {
3096
+ const record = isRecord(input) ? input : {};
3097
+ return {
3098
+ success: false,
3099
+ code,
3100
+ safeError,
3101
+ ...(typeof record.conversationId === 'string' ? { conversationId: record.conversationId } : {}),
3102
+ ...(typeof record.requestId === 'string' ? { requestId: record.requestId } : {}),
3103
+ };
3104
+ }
3105
+ function turnCancelFailure(code, safeError, input) {
3106
+ const record = isRecord(input) ? input : {};
3107
+ return {
3108
+ success: false,
3109
+ code,
3110
+ safeError,
3111
+ ...(typeof record.conversationId === 'string' ? { conversationId: record.conversationId } : {}),
3112
+ ...(typeof record.requestId === 'string' ? { requestId: record.requestId } : {}),
3113
+ };
3114
+ }
3115
+ function isNonEmptyString(value) {
3116
+ return typeof value === 'string' && value.trim().length > 0 && !value.includes('\0');
3117
+ }
3118
+ function isRecord(value) {
3119
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
3120
+ }
3121
+ /**
3122
+ * Coordination — the team-agent primitives (`XENO_AGENT_COORDINATION_SPEC.md`).
3123
+ *
3124
+ * Exported from the PORTABLE entry rather than `./node`: these are
3125
+ * provider-neutral rules with no Node dependency, and the host is the only party
3126
+ * that can arbitrate them because it is the only party that sees every run.
3127
+ */
3128
+ export * from './coordination/claims.js';
3129
+ export * from './coordination/messages.js';
3130
+ export * from './coordination/query.js';
3131
+ export * from './coordination/CoordinationService.js';
3132
+ export * from './coordination/laneBridge.js';
3133
+ export * from './coordination/coordinationTools.js';
3134
+ export * from './coordination/permissionWaitRegistry.js';
3135
+ export * from './coordination/renderInbox.js';
3136
+ export * from './coordination/runtimeEvents.js';
3137
+ export * from './knowledge/WorkspaceKnowledgeService.js';
3138
+ export * from './intake/IntakeService.js';
3139
+ // 🔴 The two TODO intake SOURCES are exported from `./node.js`, not here.
3140
+ // They import `node:fs/promises`, `node:path` and `node:crypto`; this barrel is
3141
+ // the browser-facing one, reached by any renderer that builds a fixture host,
3142
+ // and exporting them from here broke `apps/embed-preview`'s bundle outright
3143
+ // ("stat is not exported by __vite-browser-external"). `IntakeService` itself
3144
+ // stays here — it takes its sources by injection and touches no I/O.
3145
+ export * from './observation/RunObservationService.js';
3146
+ export * from './isolation/RunIsolationService.js';
3147
+ /**
3148
+ * The confining implementation — browser-safe, and that is not an accident.
3149
+ *
3150
+ * It reaches the substrate over `fetch` and imports nothing from `node:`, so it
3151
+ * belongs in THIS barrel while `RunExecutionEnvironment` does not: the
3152
+ * environment constructs a `LocalCommandExecutor`, which spawns processes.
3153
+ * Exporting the environment here made the browser barrel transitively reach
3154
+ * `node:child_process` — caught by `browserBarrelPurity.test.ts` rather than by
3155
+ * review, which is the whole reason that gate exists.
3156
+ */
3157
+ export * from './isolation/SandboxCommandExecutor.js';
3158
+ export * from './observation/XenoUseObservationSource.js';
3159
+ export * from './observation/XenoUseRunObserver.js';
3160
+ export * from './session/AgentSessionService.js';
3161
+ export * from './session/runtimeEventChannel.js';
3162
+ export * from './session/renderSupport.js';
3163
+ /**
3164
+ * Which run a coordination event belongs to on a timeline.
3165
+ *
3166
+ * A message has two runs; the entry belongs to the one the event is ABOUT — the
3167
+ * sender for a send, the receiver for a delivery. Writing both would double
3168
+ * every message in the record and make a reviewer counting messages wrong.
3169
+ */
3170
+ /** Counts by kind for the fleet view. Separate from the observation service's
3171
+ * `summarise`: that one shapes an EVENT payload and this one a query result,
3172
+ * and folding them together would couple two contracts that change for
3173
+ * different reasons. */
3174
+ /**
3175
+ * A session on the wire.
3176
+ *
3177
+ * The lease is FLATTENED: `holder` is a discriminated union internally, and a
3178
+ * surface that had to switch on `kind` to learn who is typing would reimplement
3179
+ * that logic in every client. `holder: null` means the agent, which is the
3180
+ * state a surface renders as "the agent is driving".
3181
+ */
3182
+ /** Knowledge on the wire. Provenance is FLATTENED so a surface can render "run
3183
+ * r7, claude, two days ago" without walking a nested object. */
3184
+ function summariseKnowledge(entry) {
3185
+ return {
3186
+ id: entry.id,
3187
+ subject: entry.subject,
3188
+ fact: entry.fact,
3189
+ ...(entry.pinned ? { pinned: true } : {}),
3190
+ runId: entry.provenance.runId,
3191
+ ...(entry.provenance.providerId ? { providerId: entry.provenance.providerId } : {}),
3192
+ at: entry.provenance.at,
3193
+ };
3194
+ }
3195
+ /** One shape for every claim outcome, so a refusal always carries its reason. */
3196
+ function intakeClaimResponse(result, claimState) {
3197
+ return {
3198
+ ok: result.ok,
3199
+ claimState,
3200
+ ...(result.reason ? { reason: result.reason } : {}),
3201
+ ...(result.heldBy ? { heldBy: result.heldBy } : {}),
3202
+ ...(result.runId ? { runId: result.runId } : {}),
3203
+ };
3204
+ }
3205
+ function summariseSession(session) {
3206
+ return {
3207
+ sessionId: session.sessionId,
3208
+ runId: session.runId,
3209
+ mode: session.mode,
3210
+ switchable: session.switchable,
3211
+ hasProtocolChannel: session.hasProtocolChannel,
3212
+ openedAt: session.openedAt,
3213
+ viewers: [...session.viewers],
3214
+ lease: summariseLease(session.lease),
3215
+ };
3216
+ }
3217
+ function summariseLease(lease) {
3218
+ const human = lease.holder.kind === 'human' ? lease.holder.viewerId ?? null : null;
3219
+ return {
3220
+ holder: human,
3221
+ // `heldSince` is null for the agent too: "since when" is a fact about a
3222
+ // human takeover, and reporting the session's open time there would read as
3223
+ // a takeover that never happened.
3224
+ heldSince: human ? lease.since : null,
3225
+ interventions: lease.interventions,
3226
+ };
3227
+ }
3228
+ function countChangeKinds(changes) {
3229
+ return {
3230
+ added: changes.filter((change) => change.kind === 'added').length,
3231
+ modified: changes.filter((change) => change.kind === 'modified').length,
3232
+ deleted: changes.filter((change) => change.kind === 'deleted').length,
3233
+ };
3234
+ }
3235
+ function coordinationSubjectRun(event) {
3236
+ if (event.type === 'coordination.message.sent')
3237
+ return event.fromRunId;
3238
+ if (event.type === 'coordination.message.delivered')
3239
+ return event.toRunId;
3240
+ return event.runId;
3241
+ }
3242
+ //# sourceMappingURL=index.js.map