handmux 0.23.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (327) hide show
  1. package/README.md +31 -8
  2. package/README.zh-CN.md +31 -8
  3. package/bin/handmux.js +14 -688
  4. package/bin/node-version.js +20 -0
  5. package/dist/bin/handmux.js +1015 -0
  6. package/dist/connectors/bridgeClient.js +385 -0
  7. package/dist/connectors/claude/index.js +476 -0
  8. package/dist/connectors/pi/bridgeClient.js +8 -0
  9. package/dist/connectors/pi/index.js +851 -0
  10. package/dist/hooks/handmux-notify.sh +64 -0
  11. package/{hooks → dist/hooks}/handmux-statusline.cjs +28 -6
  12. package/dist/hooks/handmux-write.cjs +264 -0
  13. package/dist/package.json +127 -0
  14. package/dist/public/assets/index-CTrqTqtT.css +32 -0
  15. package/dist/public/assets/index-DmjeMSQA.js +346 -0
  16. package/dist/public/assets/pcm-worklet-CxcDy7PB.js +1 -0
  17. package/{public → dist/public}/index.html +5 -4
  18. package/dist/src/agent-runtime/adapter.js +204 -0
  19. package/dist/src/agent-runtime/bridge.js +1006 -0
  20. package/dist/src/agent-runtime/bridgeStore.js +25 -0
  21. package/dist/src/agent-runtime/bridgeTransport.js +619 -0
  22. package/dist/src/agent-runtime/bridgeTypes.js +13 -0
  23. package/dist/src/agent-runtime/builtinRuntime.js +193 -0
  24. package/dist/src/agent-runtime/conversation.js +2179 -0
  25. package/dist/src/agent-runtime/conversationActivation.js +75 -0
  26. package/dist/src/agent-runtime/conversationActivity.js +69 -0
  27. package/dist/src/agent-runtime/conversationControls.js +206 -0
  28. package/dist/src/agent-runtime/conversationLiveHub.js +276 -0
  29. package/dist/src/agent-runtime/conversationStore.js +352 -0
  30. package/dist/src/agent-runtime/conversationTypes.js +1 -0
  31. package/dist/src/agent-runtime/conversationValidation.js +339 -0
  32. package/dist/src/agent-runtime/inbox.js +973 -0
  33. package/dist/src/agent-runtime/inboxPushProjection.js +69 -0
  34. package/dist/src/agent-runtime/inboxStore.js +25 -0
  35. package/dist/src/agent-runtime/inboxTypes.js +1 -0
  36. package/dist/src/agent-runtime/interaction.js +829 -0
  37. package/dist/src/agent-runtime/interactionLiveHub.js +225 -0
  38. package/dist/src/agent-runtime/interactionStore.js +18 -0
  39. package/dist/src/agent-runtime/interactionTypes.js +1 -0
  40. package/dist/src/agent-runtime/legacyInboxProjection.js +70 -0
  41. package/dist/src/agent-runtime/migrateLegacyCodexOutbox.js +336 -0
  42. package/dist/src/agent-runtime/resourceTypes.js +1 -0
  43. package/dist/src/agent-runtime/resources.js +245 -0
  44. package/dist/src/agent-runtime/run.js +328 -0
  45. package/dist/src/agent-runtime/runtime.js +793 -0
  46. package/dist/src/agent-runtime/sessionControl.js +164 -0
  47. package/dist/src/agent-runtime/subscriptionUsage.js +190 -0
  48. package/dist/src/agent-runtime/subscriptionUsageLegacy.js +18 -0
  49. package/dist/src/agent-runtime/tmuxRuntime.js +156 -0
  50. package/dist/src/agents/claude.js +203 -0
  51. package/dist/src/agents/claudeConversation.js +362 -0
  52. package/dist/src/agents/claudeInteraction.js +134 -0
  53. package/dist/src/agents/claudeSubscriptionUsage.js +61 -0
  54. package/dist/src/agents/codex.js +262 -0
  55. package/dist/src/agents/codexConversation.js +1103 -0
  56. package/dist/src/agents/codexConversationActivation.js +121 -0
  57. package/dist/src/agents/codexConversationControls.js +256 -0
  58. package/dist/src/agents/codexInteraction.js +306 -0
  59. package/dist/src/agents/codexSessionControl.js +118 -0
  60. package/dist/src/agents/codexSubscriptionUsage.js +313 -0
  61. package/dist/src/agents/codexTerminalControl.js +23 -0
  62. package/dist/src/agents/conversationProjectionSafety.js +416 -0
  63. package/dist/src/agents/index.js +39 -0
  64. package/dist/src/agents/nativeInbox.js +264 -0
  65. package/dist/src/agents/nativeInboxSources.js +115 -0
  66. package/dist/src/agents/pi.js +28 -0
  67. package/dist/src/agents/piConversation.js +523 -0
  68. package/dist/src/agents/piConversationHistory.js +502 -0
  69. package/dist/src/agents/piInboxBridge.js +368 -0
  70. package/dist/src/agents/piSessionControl.js +32 -0
  71. package/dist/src/agents/processIdentity.js +94 -0
  72. package/dist/src/agents/scanUtils.js +262 -0
  73. package/dist/src/apiAccountProviders.js +194 -0
  74. package/dist/src/apiAccounts.js +592 -0
  75. package/dist/src/apiErrors.js +86 -0
  76. package/{src → dist/src}/appName.js +12 -13
  77. package/dist/src/asr/iflyConfig.js +7 -0
  78. package/dist/src/asr/iflySign.js +14 -0
  79. package/dist/src/auth.js +27 -0
  80. package/dist/src/browser/bootstrap.js +72 -0
  81. package/dist/src/browser/cookieProfiles.js +413 -0
  82. package/dist/src/browser/coordinator.js +51 -0
  83. package/dist/src/browser/hammerheadCompat.js +104 -0
  84. package/dist/src/browser/hammerheadRedirectCompat.js +37 -0
  85. package/dist/src/browser/manager.js +560 -0
  86. package/dist/src/browser/originLabel.js +15 -0
  87. package/dist/src/browser/profilePersistence.js +235 -0
  88. package/dist/src/browser/publicProxy.js +176 -0
  89. package/dist/src/browser/routes.js +159 -0
  90. package/dist/src/browser/sessionStore.js +72 -0
  91. package/dist/src/browser/siteVersion.js +121 -0
  92. package/dist/src/browser/targetPolicy.js +163 -0
  93. package/{src → dist/src}/browser/worker.js +14 -18
  94. package/dist/src/browser/workerClient.js +286 -0
  95. package/dist/src/browser/workerServer.js +91 -0
  96. package/dist/src/captureBackground.js +95 -0
  97. package/dist/src/claudeEvents.js +525 -0
  98. package/dist/src/cli/agentIntegration.js +131 -0
  99. package/dist/src/cli/agentLauncher.js +26 -0
  100. package/dist/src/cli/claudeHooks.js +290 -0
  101. package/{src → dist/src}/cli/cloudflareUrl.js +2 -3
  102. package/dist/src/cli/cloudflared.js +106 -0
  103. package/dist/src/cli/codexManaged.js +80 -0
  104. package/{src → dist/src}/cli/cpolarUrl.js +8 -9
  105. package/dist/src/cli/drivers.js +98 -0
  106. package/dist/src/cli/hookScaffold.js +39 -0
  107. package/dist/src/cli/i18n/en.js +392 -0
  108. package/{src → dist/src}/cli/i18n/index.js +27 -25
  109. package/dist/src/cli/i18n/zh.js +391 -0
  110. package/dist/src/cli/legacyCodexHooks.js +52 -0
  111. package/{src → dist/src}/cli/natappUrl.js +2 -3
  112. package/dist/src/cli/options.js +245 -0
  113. package/dist/src/cli/piExtension.js +155 -0
  114. package/dist/src/cli/probe.js +16 -0
  115. package/{src → dist/src}/cli/prompt.js +4 -6
  116. package/dist/src/cli/pushCmd.js +102 -0
  117. package/{src → dist/src}/cli/qr.js +19 -19
  118. package/dist/src/cli/service.js +154 -0
  119. package/dist/src/cli/setupModel.js +194 -0
  120. package/dist/src/cli/setupWizard.js +647 -0
  121. package/dist/src/cli/shortcutEditor.js +351 -0
  122. package/dist/src/cli/sshTunnel.js +18 -0
  123. package/dist/src/cli/state.js +117 -0
  124. package/dist/src/cli/statusLine.js +115 -0
  125. package/dist/src/cli/supervisor.js +278 -0
  126. package/dist/src/cli/supervisorHealth.js +21 -0
  127. package/dist/src/cli/supervisorLaunch.js +148 -0
  128. package/dist/src/cli/supervisorProcesses.js +62 -0
  129. package/dist/src/cli/supervisorState.js +62 -0
  130. package/dist/src/cli/tmuxVersion.js +53 -0
  131. package/{src → dist/src}/cli/tunlite.js +7 -10
  132. package/dist/src/cli/tunnelClients.js +72 -0
  133. package/{src → dist/src}/cli/updateCheck.js +98 -69
  134. package/{src → dist/src}/cli/urlHost.js +8 -4
  135. package/dist/src/cli/workspaceCmd.js +347 -0
  136. package/dist/src/codexAppServer.js +3436 -0
  137. package/dist/src/codexConversationProjection.js +199 -0
  138. package/dist/src/codexDiff.js +118 -0
  139. package/dist/src/codexMessageIdentity.js +18 -0
  140. package/dist/src/codexPlan.js +30 -0
  141. package/dist/src/codexQueueProtocol.js +137 -0
  142. package/dist/src/codexStreamProtocol.js +207 -0
  143. package/dist/src/codexToolProtocol.js +68 -0
  144. package/dist/src/codexTranscriptParse.js +835 -0
  145. package/dist/src/codexUsageSnapshot.js +228 -0
  146. package/dist/src/config.js +8 -0
  147. package/{src → dist/src}/docPath.js +22 -24
  148. package/dist/src/docs.js +337 -0
  149. package/dist/src/git.js +284 -0
  150. package/dist/src/healthProtocol.js +97 -0
  151. package/dist/src/httpApi.js +96 -0
  152. package/dist/src/jsonStore.js +15 -0
  153. package/{src → dist/src}/keyNames.js +3 -3
  154. package/dist/src/notifications.js +111 -0
  155. package/dist/src/orphans.js +167 -0
  156. package/dist/src/paneInput.js +42 -0
  157. package/dist/src/pendingPrompt.js +194 -0
  158. package/dist/src/previewServer.js +202 -0
  159. package/dist/src/previews.js +179 -0
  160. package/dist/src/privateStateStore.js +124 -0
  161. package/dist/src/projectTask/backup.js +224 -0
  162. package/dist/src/projectTask/lock.js +108 -0
  163. package/dist/src/projectTask/migrations.js +90 -0
  164. package/dist/src/projectTask/routes.js +155 -0
  165. package/dist/src/projectTask/runtime.js +182 -0
  166. package/dist/src/projectTask/schema.js +56 -0
  167. package/dist/src/projectTask/store.js +457 -0
  168. package/dist/src/push.js +224 -0
  169. package/dist/src/routes/agents.js +631 -0
  170. package/dist/src/routes/apiAccounts.js +82 -0
  171. package/dist/src/routes/files.js +230 -0
  172. package/dist/src/routes/git.js +89 -0
  173. package/dist/src/routes/health.js +16 -0
  174. package/dist/src/routes/notifications.js +16 -0
  175. package/dist/src/routes/previews.js +43 -0
  176. package/dist/src/routes/push.js +145 -0
  177. package/dist/src/routes/sessions.js +244 -0
  178. package/dist/src/routes/system.js +220 -0
  179. package/dist/src/routes/terminal.js +244 -0
  180. package/dist/src/routes/workspace.js +267 -0
  181. package/dist/src/server.js +315 -0
  182. package/dist/src/shortcutConfig.js +83 -0
  183. package/{src → dist/src}/staticCache.js +5 -3
  184. package/{src → dist/src}/staticCompression.js +5 -6
  185. package/dist/src/terminalStream.js +441 -0
  186. package/{src → dist/src}/tmux/commands.js +185 -172
  187. package/dist/src/tmux/format.js +47 -0
  188. package/dist/src/transcriptParse.js +261 -0
  189. package/dist/src/transcriptReader.js +209 -0
  190. package/{src → dist/src}/trimCapture.js +11 -11
  191. package/dist/src/uploadTypes.js +31 -0
  192. package/dist/src/urlPolicy.js +18 -0
  193. package/dist/src/usagePaths.js +15 -0
  194. package/dist/src/workspace/agentRunner.js +301 -0
  195. package/dist/src/workspace/atomicJson.js +31 -0
  196. package/dist/src/workspace/capture.js +139 -0
  197. package/dist/src/workspace/checkpointer.js +228 -0
  198. package/dist/src/workspace/environment.js +71 -0
  199. package/dist/src/workspace/lock.js +137 -0
  200. package/dist/src/workspace/mapping.js +128 -0
  201. package/dist/src/workspace/operations.js +271 -0
  202. package/dist/src/workspace/paths.js +15 -0
  203. package/dist/src/workspace/planner.js +292 -0
  204. package/dist/src/workspace/restore.js +363 -0
  205. package/dist/src/workspace/runtime.js +318 -0
  206. package/dist/src/workspace/schema.js +156 -0
  207. package/dist/src/workspace/store.js +464 -0
  208. package/dist/src/workspace/tmuxAdapter.js +704 -0
  209. package/dist/src/workspaceProtocol.js +158 -0
  210. package/package.json +30 -17
  211. package/hooks/handmux-notify.sh +0 -22
  212. package/hooks/handmux-write.cjs +0 -78
  213. package/public/assets/index-C3N7OkyP.css +0 -32
  214. package/public/assets/index-D1DWcC5u.js +0 -337
  215. package/src/.gitkeep +0 -0
  216. package/src/agents/claude.js +0 -159
  217. package/src/agents/codex.js +0 -168
  218. package/src/agents/index.js +0 -21
  219. package/src/agents/processIdentity.js +0 -72
  220. package/src/agents/scanUtils.js +0 -225
  221. package/src/asr/iflyConfig.js +0 -10
  222. package/src/asr/iflySign.js +0 -16
  223. package/src/auth.js +0 -30
  224. package/src/browser/bootstrap.js +0 -68
  225. package/src/browser/cookieProfiles.js +0 -403
  226. package/src/browser/coordinator.js +0 -47
  227. package/src/browser/hammerheadCompat.js +0 -86
  228. package/src/browser/hammerheadRedirectCompat.js +0 -38
  229. package/src/browser/manager.js +0 -556
  230. package/src/browser/originLabel.js +0 -17
  231. package/src/browser/profilePersistence.js +0 -241
  232. package/src/browser/publicProxy.js +0 -175
  233. package/src/browser/routes.js +0 -154
  234. package/src/browser/sessionStore.js +0 -78
  235. package/src/browser/siteVersion.js +0 -124
  236. package/src/browser/targetPolicy.js +0 -137
  237. package/src/browser/workerClient.js +0 -253
  238. package/src/browser/workerServer.js +0 -98
  239. package/src/captureBackground.js +0 -93
  240. package/src/claudeEvents.js +0 -399
  241. package/src/cli/claudeHooks.js +0 -206
  242. package/src/cli/cloudflared.js +0 -99
  243. package/src/cli/codexManaged.js +0 -77
  244. package/src/cli/drivers.js +0 -97
  245. package/src/cli/hookScaffold.js +0 -33
  246. package/src/cli/i18n/en.js +0 -385
  247. package/src/cli/i18n/zh.js +0 -384
  248. package/src/cli/legacyCodexHooks.js +0 -49
  249. package/src/cli/options.js +0 -211
  250. package/src/cli/probe.js +0 -16
  251. package/src/cli/pushCmd.js +0 -60
  252. package/src/cli/service.js +0 -126
  253. package/src/cli/setupModel.js +0 -154
  254. package/src/cli/setupWizard.js +0 -506
  255. package/src/cli/shortcutEditor.js +0 -288
  256. package/src/cli/sshTunnel.js +0 -12
  257. package/src/cli/state.js +0 -90
  258. package/src/cli/statusLine.js +0 -90
  259. package/src/cli/supervisor.js +0 -183
  260. package/src/cli/supervisorProcesses.js +0 -57
  261. package/src/cli/tmuxVersion.js +0 -49
  262. package/src/cli/tunnelClients.js +0 -62
  263. package/src/cli/workspaceCmd.js +0 -300
  264. package/src/codexAppServer.js +0 -1620
  265. package/src/codexDiff.js +0 -108
  266. package/src/codexTranscriptParse.js +0 -413
  267. package/src/codexUsageSnapshot.cjs +0 -129
  268. package/src/config.js +0 -9
  269. package/src/docs.js +0 -228
  270. package/src/git.js +0 -208
  271. package/src/httpApi.js +0 -72
  272. package/src/jsonStore.js +0 -24
  273. package/src/notifications.js +0 -68
  274. package/src/orphans.js +0 -152
  275. package/src/pendingPrompt.js +0 -163
  276. package/src/previewServer.js +0 -168
  277. package/src/previews.js +0 -116
  278. package/src/push.js +0 -183
  279. package/src/routes/codex.js +0 -473
  280. package/src/routes/files.js +0 -172
  281. package/src/routes/git.js +0 -57
  282. package/src/routes/notifications.js +0 -19
  283. package/src/routes/previews.js +0 -35
  284. package/src/routes/push.js +0 -119
  285. package/src/routes/sessions.js +0 -168
  286. package/src/routes/system.js +0 -132
  287. package/src/routes/terminal.js +0 -210
  288. package/src/routes/transcript.js +0 -171
  289. package/src/routes/workspace.js +0 -267
  290. package/src/server.js +0 -186
  291. package/src/shortcutConfig.js +0 -75
  292. package/src/terminalStream.js +0 -391
  293. package/src/tmux/format.js +0 -45
  294. package/src/transcriptParse.js +0 -180
  295. package/src/transcriptReader.js +0 -101
  296. package/src/uploadTypes.js +0 -30
  297. package/src/urlPolicy.js +0 -14
  298. package/src/usage.js +0 -260
  299. package/src/workspace/agentRunner.js +0 -188
  300. package/src/workspace/atomicJson.js +0 -32
  301. package/src/workspace/capture.js +0 -139
  302. package/src/workspace/checkpointer.js +0 -187
  303. package/src/workspace/environment.js +0 -75
  304. package/src/workspace/lock.js +0 -128
  305. package/src/workspace/mapping.js +0 -124
  306. package/src/workspace/operations.js +0 -252
  307. package/src/workspace/paths.js +0 -16
  308. package/src/workspace/planner.js +0 -247
  309. package/src/workspace/restore.js +0 -279
  310. package/src/workspace/runtime.js +0 -314
  311. package/src/workspace/schema.js +0 -136
  312. package/src/workspace/store.js +0 -382
  313. package/src/workspace/tmuxAdapter.js +0 -554
  314. /package/{public → dist/public}/fonts/JetBrainsMonoNerdFontMono-Regular.woff2 +0 -0
  315. /package/{public → dist/public}/fonts/TWUnifont.woff2 +0 -0
  316. /package/{public → dist/public}/icons/apple-touch-icon.png +0 -0
  317. /package/{public → dist/public}/icons/badge-96.png +0 -0
  318. /package/{public → dist/public}/icons/icon-192-maskable.png +0 -0
  319. /package/{public → dist/public}/icons/icon-192.png +0 -0
  320. /package/{public → dist/public}/icons/icon-512-maskable.png +0 -0
  321. /package/{public → dist/public}/icons/icon-512.png +0 -0
  322. /package/{public → dist/public}/icons/logo.svg +0 -0
  323. /package/{public → dist/public}/manifest.webmanifest +0 -0
  324. /package/{public → dist/public}/offline.html +0 -0
  325. /package/{public → dist/public}/sw.js +0 -0
  326. /package/{src → dist/src}/browser/protocol.js +0 -0
  327. /package/{src → dist/src}/cli/cfNamed.js +0 -0
@@ -0,0 +1,631 @@
1
+ import express from 'express';
2
+ import { ConversationContractError } from '../agent-runtime/conversation.js';
3
+ import { ConversationActivationError } from '../agent-runtime/conversationActivation.js';
4
+ import { ConversationControlError } from '../agent-runtime/conversationControls.js';
5
+ import { ConversationLiveHub, ConversationLiveHubError, } from '../agent-runtime/conversationLiveHub.js';
6
+ import { InteractionContractError } from '../agent-runtime/interaction.js';
7
+ import { InteractionLiveHub, InteractionLiveHubError, } from '../agent-runtime/interactionLiveHub.js';
8
+ import { AgentResourceError } from '../agent-runtime/resources.js';
9
+ import { SessionControlContractError } from '../agent-runtime/sessionControl.js';
10
+ const RESOURCE_ID_RE = /^[a-zA-Z0-9_-]{16,256}$/;
11
+ function record(value) {
12
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
13
+ ? value : null;
14
+ }
15
+ function text(value, max) {
16
+ return typeof value === 'string' && value.length > 0 && value.length <= max;
17
+ }
18
+ function notificationIds(value) {
19
+ if (!Array.isArray(value) || value.length === 0 || value.length > 256
20
+ || value.some((item) => !text(item, 256)))
21
+ return null;
22
+ return value;
23
+ }
24
+ function session(value) {
25
+ const item = record(value);
26
+ return item && text(item.agentId, 64) && text(item.sessionId, 1024)
27
+ ? { agentId: item.agentId, sessionId: item.sessionId } : null;
28
+ }
29
+ function run(value) {
30
+ const item = record(value);
31
+ if (!item || !text(item.agentId, 64) || !text(item.paneId, 256) || !text(item.runId, 256)
32
+ || (item.sessionId !== undefined && !text(item.sessionId, 1024)))
33
+ return null;
34
+ return {
35
+ agentId: item.agentId,
36
+ paneId: item.paneId,
37
+ runId: item.runId,
38
+ ...(item.sessionId === undefined ? {} : { sessionId: item.sessionId }),
39
+ };
40
+ }
41
+ // Auth is applied by the parent /api router. This is Handmux's own app facade, not a public Connector
42
+ // protocol: it exposes normalized Core objects and never socket paths, credentials, native payloads, or wire.
43
+ export function agentRoutes({ runtime }) {
44
+ const router = express.Router();
45
+ let conversationLive;
46
+ let interactionLive;
47
+ router.get('/agents/capabilities', (_req, res) => res.json({
48
+ adapters: runtime.capabilities(),
49
+ health: runtime.health(),
50
+ runs: runtime.activeRuns(),
51
+ }));
52
+ router.get('/agents/inbox', (_req, res) => res.json(runtime.inbox.read()));
53
+ router.get('/agents/usage', async (req, res, next) => {
54
+ if (!runtime.subscriptionUsage) {
55
+ return res.status(503).json({ error: 'subscription usage unsupported', code: 'unsupported' });
56
+ }
57
+ const refresh = req.query.refresh;
58
+ if (refresh !== undefined && refresh !== 'true' && refresh !== 'false') {
59
+ return res.status(400).json({ error: 'invalid subscription usage refresh' });
60
+ }
61
+ const target = req.query.target;
62
+ if (target !== undefined && (typeof target !== 'string' || refresh !== 'true'
63
+ || !/^[a-z][a-z0-9._-]{0,127}$/.test(target))) {
64
+ return res.status(400).json({ error: 'invalid subscription usage target' });
65
+ }
66
+ try {
67
+ return res.json({ agents: await runtime.subscriptionUsage.snapshots({
68
+ refresh: refresh === 'true',
69
+ ...(target === undefined ? {} : { targetAgentId: target }),
70
+ }) });
71
+ }
72
+ catch (error) {
73
+ if (error instanceof RangeError && target !== undefined) {
74
+ return res.status(400).json({ error: 'invalid subscription usage target' });
75
+ }
76
+ return next(error);
77
+ }
78
+ });
79
+ router.get('/agents/session-control/model', async (req, res, next) => {
80
+ try {
81
+ if (!runtime.sessionControl) {
82
+ return res.status(503).json({ error: 'session control unsupported', code: 'unsupported' });
83
+ }
84
+ const ref = run(req.query);
85
+ const lease = ref ? runtime.runs.resolve(ref) : null;
86
+ if (!ref || !lease)
87
+ return res.status(ref ? 409 : 400).json({
88
+ error: ref ? 'stale agent run' : 'invalid agent run',
89
+ });
90
+ const refresh = req.query.refresh;
91
+ if (refresh !== undefined && refresh !== 'true' && refresh !== 'false') {
92
+ return res.status(400).json({ error: 'invalid session control refresh' });
93
+ }
94
+ const control = await runtime.sessionControl.readModelControl(lease, {
95
+ refresh: refresh === 'true',
96
+ });
97
+ return res.json({ control });
98
+ }
99
+ catch (error) {
100
+ if (error instanceof SessionControlContractError && error.code === 'unsupported') {
101
+ return res.status(409).json({ error: 'session control unsupported', code: 'unsupported' });
102
+ }
103
+ return next(error);
104
+ }
105
+ });
106
+ router.patch('/agents/session-control/model', async (req, res, next) => {
107
+ try {
108
+ if (!runtime.sessionControl) {
109
+ return res.status(503).json({ error: 'session control unsupported', code: 'unsupported' });
110
+ }
111
+ const ref = run(req.body?.run);
112
+ const lease = ref ? runtime.runs.resolve(ref) : null;
113
+ const patch = record(req.body?.patch);
114
+ if (!ref || !lease || !patch)
115
+ return res.status(ref && patch ? 409 : 400).json({
116
+ error: ref && patch ? 'stale agent run' : 'invalid session control update',
117
+ });
118
+ return res.json({
119
+ control: await runtime.sessionControl.updateModelControl(lease, patch),
120
+ });
121
+ }
122
+ catch (error) {
123
+ if (error instanceof SessionControlContractError) {
124
+ if (error.code === 'invalid_request') {
125
+ return res.status(400).json({ error: 'invalid session control update' });
126
+ }
127
+ if (error.code === 'unsupported') {
128
+ return res.status(409).json({ error: 'session control unsupported', code: 'unsupported' });
129
+ }
130
+ }
131
+ return next(error);
132
+ }
133
+ });
134
+ router.post('/agents/inbox/notifications/read', async (req, res, next) => {
135
+ const ids = notificationIds(req.body?.notificationIds);
136
+ if (!ids)
137
+ return res.status(400).json({ error: 'invalid terminal notification ids' });
138
+ try {
139
+ return res.json(await runtime.inbox.markTerminalRead(ids));
140
+ }
141
+ catch (error) {
142
+ return next(error);
143
+ }
144
+ });
145
+ const controlRun = (value) => {
146
+ const ref = run(value);
147
+ return { ref, lease: ref ? runtime.runs.resolve(ref) : null };
148
+ };
149
+ const controlError = (error, res) => {
150
+ if (!(error instanceof ConversationControlError))
151
+ return false;
152
+ const status = error.code === 'invalid_request' ? 400
153
+ : error.code === 'unsupported' ? 409 : 502;
154
+ res.status(status).json({ error: error.message, code: error.code });
155
+ return true;
156
+ };
157
+ const activationError = (error, res) => {
158
+ if (!(error instanceof ConversationActivationError))
159
+ return false;
160
+ const status = error.code === 'unsupported' ? 409
161
+ : error.code === 'in_progress' ? 409
162
+ : error.code === 'contract_violation' ? 502 : 503;
163
+ res.status(status).json({ error: error.message, code: error.code });
164
+ return true;
165
+ };
166
+ router.get('/agents/conversation-activation', async (req, res, next) => {
167
+ try {
168
+ if (!runtime.conversationActivation) {
169
+ return res.status(409).json({ error: 'conversation activation unsupported', code: 'unsupported' });
170
+ }
171
+ const { ref, lease } = controlRun(req.query);
172
+ if (!ref || !lease)
173
+ return res.status(ref ? 409 : 400).json({
174
+ error: ref ? 'stale agent run' : 'invalid agent run',
175
+ ...(ref ? { code: 'stale_run' } : {}),
176
+ });
177
+ return res.json({ descriptor: await runtime.conversationActivation.describe(lease) });
178
+ }
179
+ catch (error) {
180
+ if (activationError(error, res))
181
+ return;
182
+ return next(error);
183
+ }
184
+ });
185
+ router.post('/agents/conversation-activation', async (req, res, next) => {
186
+ try {
187
+ if (!runtime.conversationActivation) {
188
+ return res.status(409).json({ error: 'conversation activation unsupported', code: 'unsupported' });
189
+ }
190
+ const { ref, lease } = controlRun(req.body?.run);
191
+ if (!ref || !lease)
192
+ return res.status(ref ? 409 : 400).json({
193
+ error: ref ? 'stale agent run' : 'invalid agent run',
194
+ ...(ref ? { code: 'stale_run' } : {}),
195
+ });
196
+ const operation = new AbortController();
197
+ const cancel = () => {
198
+ if (!res.writableEnded)
199
+ operation.abort(new Error('Activation request disconnected'));
200
+ };
201
+ req.once('aborted', cancel);
202
+ res.once('close', cancel);
203
+ try {
204
+ await runtime.conversationActivation.activate(lease, operation.signal);
205
+ }
206
+ finally {
207
+ req.removeListener('aborted', cancel);
208
+ res.removeListener('close', cancel);
209
+ }
210
+ return res.status(202).json({ accepted: true });
211
+ }
212
+ catch (error) {
213
+ if (activationError(error, res))
214
+ return;
215
+ return next(error);
216
+ }
217
+ });
218
+ router.get('/agents/conversation-controls', async (req, res, next) => {
219
+ try {
220
+ if (!runtime.conversationControls && !runtime.conversation) {
221
+ return res.status(409).json({ error: 'conversation controls unsupported', code: 'unsupported' });
222
+ }
223
+ const { ref, lease } = controlRun(req.query);
224
+ if (!ref || !lease)
225
+ return res.status(ref ? 409 : 400).json({
226
+ error: ref ? 'stale agent run' : 'invalid agent run',
227
+ });
228
+ let controls = {};
229
+ if (runtime.conversationControls) {
230
+ try {
231
+ controls = await runtime.conversationControls.read(lease);
232
+ }
233
+ catch (error) {
234
+ // Queue is a public Conversation capability. An Agent without optional Goal/Plan/Context
235
+ // controls must still reach the shared Queue snapshot; every other control failure remains fatal.
236
+ if (!(runtime.conversation && error instanceof ConversationControlError
237
+ && error.code === 'unsupported'))
238
+ throw error;
239
+ }
240
+ }
241
+ const queue = runtime.conversation ? await runtime.conversation.queueSnapshot(lease) : undefined;
242
+ return res.json({ controls: { ...controls, ...(queue ? { queue, submissions: queue.submissions } : {}) } });
243
+ }
244
+ catch (error) {
245
+ if (controlError(error, res))
246
+ return;
247
+ return next(error);
248
+ }
249
+ });
250
+ router.post('/agents/conversation-queue/action', async (req, res, next) => {
251
+ try {
252
+ if (!runtime.conversation)
253
+ throw new ConversationControlError('Conversation queue unsupported', 'unsupported');
254
+ const { ref, lease } = controlRun(req.body?.run);
255
+ const request = record(req.body?.request);
256
+ if (!ref || !lease || !request)
257
+ return res.status(ref && request ? 409 : 400).json({
258
+ error: ref && request ? 'stale agent run' : 'invalid conversation queue action',
259
+ });
260
+ return res.json(await runtime.conversation.queueAction(lease, request));
261
+ }
262
+ catch (error) {
263
+ if (controlError(error, res))
264
+ return;
265
+ return next(error);
266
+ }
267
+ });
268
+ router.post('/agents/conversation-goal/action', async (req, res, next) => {
269
+ try {
270
+ if (!runtime.conversationControls)
271
+ throw new ConversationControlError('Conversation goal unsupported', 'unsupported');
272
+ const { ref, lease } = controlRun(req.body?.run);
273
+ const request = record(req.body?.request);
274
+ if (!ref || !lease || !request)
275
+ return res.status(ref && request ? 409 : 400).json({
276
+ error: ref && request ? 'stale agent run' : 'invalid conversation goal action',
277
+ });
278
+ return res.json({ goal: await runtime.conversationControls.goalAction(lease, request) });
279
+ }
280
+ catch (error) {
281
+ if (controlError(error, res))
282
+ return;
283
+ return next(error);
284
+ }
285
+ });
286
+ router.post('/agents/conversation-permission/action', async (req, res, next) => {
287
+ try {
288
+ if (!runtime.conversationControls)
289
+ throw new ConversationControlError('Conversation permission unsupported', 'unsupported');
290
+ const { ref, lease } = controlRun(req.body?.run);
291
+ const request = record(req.body?.request);
292
+ if (!ref || !lease || !request)
293
+ return res.status(ref && request ? 409 : 400).json({
294
+ error: ref && request ? 'stale agent run' : 'invalid conversation permission action',
295
+ });
296
+ return res.json({ permission: await runtime.conversationControls.permissionAction(lease, request) });
297
+ }
298
+ catch (error) {
299
+ if (controlError(error, res))
300
+ return;
301
+ return next(error);
302
+ }
303
+ });
304
+ router.post('/agents/conversation-command', async (req, res, next) => {
305
+ try {
306
+ if (!runtime.conversationControls)
307
+ throw new ConversationControlError('Conversation command unsupported', 'unsupported');
308
+ const { ref, lease } = controlRun(req.body?.run);
309
+ if (!ref || !lease)
310
+ return res.status(ref ? 409 : 400).json({
311
+ error: ref ? 'stale agent run' : 'invalid agent run',
312
+ });
313
+ await runtime.conversationControls.command(lease, req.body?.command);
314
+ return res.json({ ok: true });
315
+ }
316
+ catch (error) {
317
+ if (controlError(error, res))
318
+ return;
319
+ return next(error);
320
+ }
321
+ });
322
+ router.post('/agents/conversation/discover', async (req, res, next) => {
323
+ try {
324
+ if (!runtime.conversation)
325
+ return res.status(409).json({ error: 'conversation unsupported' });
326
+ const ref = run(req.body?.target);
327
+ const lease = ref ? runtime.runs.resolve(ref) : null;
328
+ if (ref && !lease)
329
+ return res.status(409).json({ error: 'stale agent run' });
330
+ const target = lease?.ref ?? session(req.body?.target);
331
+ if (!target)
332
+ return res.status(400).json({ error: 'invalid agent target' });
333
+ return res.json({ descriptor: await runtime.conversation.discover(target) });
334
+ }
335
+ catch (error) {
336
+ return next(error);
337
+ }
338
+ });
339
+ router.post('/agents/conversation/page', async (req, res, next) => {
340
+ try {
341
+ if (!runtime.conversation)
342
+ return res.status(409).json({ error: 'conversation unsupported' });
343
+ const ref = run(req.body?.run);
344
+ const lease = ref ? runtime.runs.resolve(ref) : null;
345
+ if (ref && !lease)
346
+ return res.status(409).json({ error: 'stale agent run' });
347
+ const target = lease?.ref ?? session(req.body?.session);
348
+ const request = record(req.body?.request);
349
+ if (!target || !request)
350
+ return res.status(400).json({ error: 'invalid conversation page request' });
351
+ return res.json(await runtime.conversation.readPage(target, request));
352
+ }
353
+ catch (error) {
354
+ if (error instanceof ConversationContractError) {
355
+ if (error.code === 'invalid_request') {
356
+ return res.status(400).json({ error: 'invalid conversation page request' });
357
+ }
358
+ // Page cursors and view epochs are intentionally process-local. A Server restart or a concurrent
359
+ // history change is a recoverable rebase signal for the client, not an internal error/log storm.
360
+ if (error.code === 'page_stale') {
361
+ return res.status(409).json({ error: 'conversation page stale' });
362
+ }
363
+ if (error.code === 'session_unavailable') {
364
+ res.set('Retry-After', '1');
365
+ return res.status(503).json({
366
+ error: 'conversation session unavailable',
367
+ code: 'conversation_session_unavailable',
368
+ });
369
+ }
370
+ }
371
+ return next(error);
372
+ }
373
+ });
374
+ router.get('/agents/conversation/resource', async (req, res, next) => {
375
+ const owner = session(req.query);
376
+ const resourceId = typeof req.query.resourceId === 'string' ? req.query.resourceId : '';
377
+ if (!owner || !RESOURCE_ID_RE.test(resourceId)) {
378
+ return res.status(400).json({ error: 'invalid conversation resource request' });
379
+ }
380
+ try {
381
+ const content = await runtime.resources.read(owner, resourceId);
382
+ // Owner mismatch, expiry, revocation, and an unknown opaque ID are deliberately indistinguishable.
383
+ if (!content)
384
+ return res.status(404).json({ error: 'conversation resource unavailable' });
385
+ res.set({
386
+ 'Cache-Control': 'private, no-store',
387
+ 'X-Content-Type-Options': 'nosniff',
388
+ 'Content-Length': String(content.size),
389
+ });
390
+ res.attachment(content.name ?? 'agent-resource');
391
+ res.type(content.mediaType);
392
+ return res.status(200).send(Buffer.from(content.data));
393
+ }
394
+ catch (error) {
395
+ if (error instanceof AgentResourceError && error.code === 'resource-changed') {
396
+ return res.status(404).json({ error: 'conversation resource unavailable' });
397
+ }
398
+ return next(error);
399
+ }
400
+ });
401
+ router.post('/agents/conversation/send', async (req, res, next) => {
402
+ try {
403
+ if (!runtime.conversation)
404
+ return res.status(409).json({ error: 'conversation unsupported' });
405
+ const ref = run(req.body?.run);
406
+ const lease = ref ? runtime.runs.resolve(ref) : null;
407
+ const request = record(req.body?.request);
408
+ if (!ref || !lease || !request)
409
+ return res.status(ref && request ? 409 : 400).json({
410
+ error: ref && request ? 'stale agent run' : 'invalid conversation send request',
411
+ });
412
+ return res.json(await runtime.conversation.send(lease, request));
413
+ }
414
+ catch (error) {
415
+ return next(error);
416
+ }
417
+ });
418
+ router.post('/agents/conversation/submission/query', (req, res, next) => {
419
+ try {
420
+ if (!runtime.conversation)
421
+ return res.status(409).json({ error: 'conversation unsupported' });
422
+ const ref = run(req.body?.run);
423
+ const lease = ref ? runtime.runs.resolve(ref) : null;
424
+ const submissionId = req.body?.submissionId;
425
+ const actionId = req.body?.actionId;
426
+ if (!ref || !lease || !text(submissionId, 256)
427
+ || (actionId !== undefined && !text(actionId, 256))) {
428
+ return res.status(ref && submissionId ? 409 : 400).json({
429
+ error: ref && submissionId ? 'stale agent run' : 'invalid conversation submission query',
430
+ });
431
+ }
432
+ return res.json(runtime.conversation.querySubmission(lease, submissionId, actionId));
433
+ }
434
+ catch (error) {
435
+ return next(error);
436
+ }
437
+ });
438
+ router.post('/agents/conversation/interrupt', async (req, res, next) => {
439
+ try {
440
+ if (!runtime.conversation)
441
+ return res.status(409).json({ error: 'conversation unsupported' });
442
+ const ref = run(req.body?.run);
443
+ const lease = ref ? runtime.runs.resolve(ref) : null;
444
+ if (!ref || !lease)
445
+ return res.status(ref ? 409 : 400).json({
446
+ error: ref ? 'stale agent run' : 'invalid agent run',
447
+ });
448
+ return res.json(await runtime.conversation.interrupt(lease));
449
+ }
450
+ catch (error) {
451
+ return next(error);
452
+ }
453
+ });
454
+ router.get('/agents/conversation/live', async (req, res, next) => {
455
+ let subscription;
456
+ let keepalive;
457
+ let closed = false;
458
+ const close = () => {
459
+ if (closed)
460
+ return;
461
+ closed = true;
462
+ if (keepalive)
463
+ clearInterval(keepalive);
464
+ subscription?.close();
465
+ };
466
+ req.once('aborted', close);
467
+ res.once('close', close);
468
+ try {
469
+ if (!runtime.conversation)
470
+ return res.status(409).json({ error: 'conversation unsupported' });
471
+ conversationLive ??= new ConversationLiveHub({ conversation: runtime.conversation });
472
+ const ref = run(req.query);
473
+ const lease = ref ? runtime.runs.resolve(ref) : null;
474
+ if (!ref || !lease)
475
+ return res.status(ref ? 409 : 400).json({
476
+ error: ref ? 'stale agent run' : 'invalid agent run',
477
+ });
478
+ const expectedViewId = typeof req.query.expectedViewId === 'string'
479
+ ? req.query.expectedViewId : undefined;
480
+ if (expectedViewId !== undefined && !text(expectedViewId, 1024)) {
481
+ return res.status(400).json({ error: 'invalid conversation view' });
482
+ }
483
+ subscription = await conversationLive.subscribe(lease, {
484
+ ...(expectedViewId === undefined ? {} : { expectedViewId }),
485
+ });
486
+ if (closed) {
487
+ subscription.close();
488
+ return;
489
+ }
490
+ res.status(200).set({
491
+ 'Content-Type': 'text/event-stream; charset=utf-8',
492
+ 'Cache-Control': 'no-cache, no-transform',
493
+ Connection: 'keep-alive',
494
+ 'X-Accel-Buffering': 'no',
495
+ });
496
+ res.flushHeaders();
497
+ res.write(`data: ${JSON.stringify({ type: 'ready', checkpoint: subscription.checkpoint })}\n\n`);
498
+ keepalive = setInterval(() => {
499
+ if (!closed)
500
+ res.write(`: keepalive ${Date.now()}\n\n`);
501
+ }, 20_000);
502
+ keepalive.unref?.();
503
+ for await (const event of subscription) {
504
+ if (closed)
505
+ break;
506
+ const writable = res.write(`data: ${JSON.stringify({ type: 'event', event })}\n\n`);
507
+ if (!writable) {
508
+ await new Promise((resolve) => {
509
+ const done = () => {
510
+ res.off('drain', done);
511
+ res.off('close', done);
512
+ resolve();
513
+ };
514
+ res.once('drain', done);
515
+ res.once('close', done);
516
+ });
517
+ }
518
+ }
519
+ close();
520
+ return res.end();
521
+ }
522
+ catch (error) {
523
+ close();
524
+ if (res.headersSent) {
525
+ res.write(`data: ${JSON.stringify({ type: 'error', message: 'conversation live stream unavailable' })}\n\n`);
526
+ return res.end();
527
+ }
528
+ if (error instanceof ConversationLiveHubError || error instanceof ConversationContractError) {
529
+ return res.status(409).json({ error: 'conversation live stream unavailable' });
530
+ }
531
+ return next(error);
532
+ }
533
+ });
534
+ router.post('/agents/interaction/respond', async (req, res, next) => {
535
+ try {
536
+ if (!runtime.interaction)
537
+ return res.status(409).json({ error: 'interaction unsupported' });
538
+ const ref = run(req.body?.run);
539
+ const lease = ref ? runtime.runs.resolve(ref) : null;
540
+ const request = record(req.body?.request);
541
+ if (!ref || !lease || !request)
542
+ return res.status(ref && request ? 409 : 400).json({
543
+ error: ref && request ? 'stale agent run' : 'invalid interaction response',
544
+ });
545
+ const result = await runtime.interaction.respond(lease, request);
546
+ return res.status(result.status === 'stale_run' ? 409 : 200).json(result);
547
+ }
548
+ catch (error) {
549
+ if (error instanceof InteractionContractError) {
550
+ return res.status(409).json({
551
+ error: 'interaction response unavailable', code: 'interaction_unavailable',
552
+ });
553
+ }
554
+ return next(error);
555
+ }
556
+ });
557
+ router.get('/agents/interaction/live', async (req, res, next) => {
558
+ let subscription;
559
+ let keepalive;
560
+ let closed = false;
561
+ const close = () => {
562
+ if (closed)
563
+ return;
564
+ closed = true;
565
+ if (keepalive)
566
+ clearInterval(keepalive);
567
+ subscription?.close();
568
+ };
569
+ req.once('aborted', close);
570
+ res.once('close', close);
571
+ try {
572
+ if (!runtime.interaction)
573
+ return res.status(409).json({ error: 'interaction unsupported' });
574
+ interactionLive ??= new InteractionLiveHub({ interaction: runtime.interaction });
575
+ const ref = run(req.query);
576
+ const lease = ref ? runtime.runs.resolve(ref) : null;
577
+ if (!ref || !lease)
578
+ return res.status(ref ? 409 : 400).json({
579
+ error: ref ? 'stale agent run' : 'invalid agent run',
580
+ });
581
+ subscription = await interactionLive.subscribe(lease);
582
+ if (closed) {
583
+ subscription.close();
584
+ return;
585
+ }
586
+ res.status(200).set({
587
+ 'Content-Type': 'text/event-stream; charset=utf-8',
588
+ 'Cache-Control': 'no-cache, no-transform',
589
+ Connection: 'keep-alive',
590
+ 'X-Accel-Buffering': 'no',
591
+ });
592
+ res.flushHeaders();
593
+ res.write(`data: ${JSON.stringify({ type: 'ready', checkpoint: subscription.checkpoint })}\n\n`);
594
+ keepalive = setInterval(() => {
595
+ if (!closed)
596
+ res.write(`: keepalive ${Date.now()}\n\n`);
597
+ }, 20_000);
598
+ keepalive.unref?.();
599
+ for await (const event of subscription) {
600
+ if (closed)
601
+ break;
602
+ const writable = res.write(`data: ${JSON.stringify({ type: 'event', event })}\n\n`);
603
+ if (!writable) {
604
+ await new Promise((resolve) => {
605
+ const done = () => {
606
+ res.off('drain', done);
607
+ res.off('close', done);
608
+ resolve();
609
+ };
610
+ res.once('drain', done);
611
+ res.once('close', done);
612
+ });
613
+ }
614
+ }
615
+ close();
616
+ return res.end();
617
+ }
618
+ catch (error) {
619
+ close();
620
+ if (res.headersSent) {
621
+ res.write(`data: ${JSON.stringify({ type: 'error', message: 'interaction live stream unavailable' })}\n\n`);
622
+ return res.end();
623
+ }
624
+ if (error instanceof InteractionLiveHubError || error instanceof InteractionContractError) {
625
+ return res.status(409).json({ error: 'interaction live stream unavailable' });
626
+ }
627
+ return next(error);
628
+ }
629
+ });
630
+ return router;
631
+ }