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,75 @@
1
+ export class ConversationActivationError extends Error {
2
+ code;
3
+ constructor(message, code) {
4
+ super(message);
5
+ this.code = code;
6
+ this.name = 'ConversationActivationError';
7
+ }
8
+ }
9
+ export class AgentConversationActivationService {
10
+ #controllers;
11
+ #active = new Set();
12
+ #timeoutMs;
13
+ constructor(controllers, { timeoutMs = 15_000 } = {}) {
14
+ if (!controllers || typeof controllers !== 'object' || !Object.keys(controllers).length) {
15
+ throw new TypeError('AgentConversationActivationService requires controllers');
16
+ }
17
+ this.#controllers = new Map(Object.entries(controllers));
18
+ this.#timeoutMs = Math.max(1, timeoutMs);
19
+ }
20
+ async describe(run) {
21
+ const controller = this.#controllers.get(run.ref.agentId);
22
+ if (!controller)
23
+ throw new ConversationActivationError('Conversation activation unsupported', 'unsupported');
24
+ let value;
25
+ try {
26
+ value = await controller.describe(run);
27
+ }
28
+ catch {
29
+ throw new ConversationActivationError('Conversation activation is temporarily unavailable', 'unavailable');
30
+ }
31
+ if (value === null)
32
+ return null;
33
+ if (!value || value.effect !== 'replace-process-preserve-session') {
34
+ throw new ConversationActivationError('Invalid Conversation activation descriptor', 'contract_violation');
35
+ }
36
+ return structuredClone(value);
37
+ }
38
+ async activate(run, signal) {
39
+ const controller = this.#controllers.get(run.ref.agentId);
40
+ if (!controller)
41
+ throw new ConversationActivationError('Conversation activation unsupported', 'unsupported');
42
+ if (run.signal.aborted) {
43
+ throw new ConversationActivationError('The Agent run is no longer active', 'unavailable');
44
+ }
45
+ const key = `${run.ref.paneId}\0${run.ref.runId}`;
46
+ if (this.#active.has(key)) {
47
+ throw new ConversationActivationError('Conversation activation is already in progress', 'in_progress');
48
+ }
49
+ this.#active.add(key);
50
+ const operation = new AbortController();
51
+ const cancel = () => operation.abort(signal?.reason ?? new Error('Activation request cancelled'));
52
+ signal?.addEventListener('abort', cancel, { once: true });
53
+ if (signal?.aborted)
54
+ cancel();
55
+ const timer = setTimeout(() => operation.abort(new Error('Conversation activation timed out')), this.#timeoutMs);
56
+ try {
57
+ if (operation.signal.aborted)
58
+ throw operation.signal.reason;
59
+ const aborted = new Promise((_resolve, reject) => {
60
+ operation.signal.addEventListener('abort', () => reject(operation.signal.reason), { once: true });
61
+ });
62
+ await Promise.race([controller.activate(run, operation.signal), aborted]);
63
+ }
64
+ catch (error) {
65
+ if (error instanceof ConversationActivationError)
66
+ throw error;
67
+ throw new ConversationActivationError('Conversation activation could not finish; continue in the terminal or try again', 'unavailable');
68
+ }
69
+ finally {
70
+ clearTimeout(timer);
71
+ signal?.removeEventListener('abort', cancel);
72
+ this.#active.delete(key);
73
+ }
74
+ }
75
+ }
@@ -0,0 +1,69 @@
1
+ function key(run) {
2
+ return `${run.ref.agentId}\0${run.ref.sessionId ?? ''}`;
3
+ }
4
+ function validActivity(value) {
5
+ return value === 'idle' || value === 'working' || value === 'waiting'
6
+ || value === 'compacting' || value === 'unknown';
7
+ }
8
+ function normalizeActiveTurn(value) {
9
+ if (!value || typeof value !== 'object')
10
+ return { state: 'unknown' };
11
+ const record = value;
12
+ if (record.state === 'none')
13
+ return { state: 'none' };
14
+ if (record.state === 'active' && typeof record.nativeTurnId === 'string' && record.nativeTurnId) {
15
+ return { state: 'active', nativeTurnId: record.nativeTurnId };
16
+ }
17
+ return { state: 'unknown' };
18
+ }
19
+ /** Runtime-owned normalization: provider readers never own revisions or dispatch state. */
20
+ export class RuntimeConversationActivitySource {
21
+ #readers;
22
+ #snapshots = new Map();
23
+ #readGenerations = new Map();
24
+ constructor(readers) {
25
+ this.#readers = new Map(Object.entries(readers));
26
+ }
27
+ async read(run) {
28
+ const owner = key(run);
29
+ const epoch = run.ref.runId;
30
+ const generation = (this.#readGenerations.get(owner) ?? 0) + 1;
31
+ this.#readGenerations.set(owner, generation);
32
+ const reader = this.#readers.get(run.ref.agentId);
33
+ let activity = 'unknown';
34
+ let activeTurn = { state: 'unknown' };
35
+ let completionToken;
36
+ try {
37
+ const raw = await reader?.read(run);
38
+ if (raw && validActivity(raw.activity)) {
39
+ activity = raw.activity;
40
+ activeTurn = normalizeActiveTurn(raw.activeTurn);
41
+ if (typeof raw.completionToken === 'string' && raw.completionToken
42
+ && raw.completionToken.length <= 1024)
43
+ completionToken = raw.completionToken;
44
+ }
45
+ }
46
+ catch { /* an unreadable provider snapshot is explicitly unknown */ }
47
+ const current = this.#snapshots.get(owner);
48
+ if (run.signal.aborted || run.ref.runId !== epoch || key(run) !== owner
49
+ || this.#readGenerations.get(owner) !== generation) {
50
+ return structuredClone(current ?? {
51
+ activity: 'unknown', activeTurn: { state: 'unknown' }, revision: 1, epoch,
52
+ });
53
+ }
54
+ const previous = current;
55
+ const semantic = `${activity}\0${activeTurn.state}\0${activeTurn.state === 'active' ? activeTurn.nativeTurnId : ''}\0${completionToken ?? ''}`;
56
+ const previousSemantic = previous
57
+ ? `${previous.activity}\0${previous.activeTurn.state}\0${previous.activeTurn.state === 'active'
58
+ ? previous.activeTurn.nativeTurnId : ''}\0${previous.completionToken ?? ''}` : '';
59
+ const revision = previous?.epoch === epoch
60
+ ? previous.revision + (semantic === previousSemantic ? 0 : 1)
61
+ : 1;
62
+ const snapshot = {
63
+ activity, activeTurn, revision, epoch,
64
+ ...(completionToken === undefined ? {} : { completionToken }),
65
+ };
66
+ this.#snapshots.set(owner, snapshot);
67
+ return structuredClone(snapshot);
68
+ }
69
+ }
@@ -0,0 +1,206 @@
1
+ export class ConversationControlError extends Error {
2
+ code;
3
+ constructor(message, code) {
4
+ super(message);
5
+ this.code = code;
6
+ this.name = 'ConversationControlError';
7
+ }
8
+ }
9
+ const MAX_OBJECTIVE = 4_000;
10
+ function text(value, max) {
11
+ return typeof value === 'string' && value.trim().length > 0 && value.length <= max;
12
+ }
13
+ function finite(value) {
14
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0;
15
+ }
16
+ function goal(value) {
17
+ if (value === null)
18
+ return null;
19
+ if (!value || !text(value.objective, MAX_OBJECTIVE) || !text(value.status, 64))
20
+ return undefined;
21
+ const numeric = ['createdAt', 'updatedAt', 'tokensUsed', 'timeUsedSeconds'];
22
+ if (numeric.some((key) => value[key] !== undefined && !finite(value[key])))
23
+ return undefined;
24
+ if (value.tokenBudget !== undefined && value.tokenBudget !== null && !finite(value.tokenBudget)) {
25
+ return undefined;
26
+ }
27
+ return structuredClone(value);
28
+ }
29
+ function plan(value) {
30
+ if (value === null)
31
+ return null;
32
+ if (!value || !Array.isArray(value.steps) || value.steps.length > 100
33
+ || (value.explanation !== undefined && !text(value.explanation, 16_384))
34
+ || (value.turnId !== undefined && !text(value.turnId, 256))
35
+ || (value.waiting !== undefined && typeof value.waiting !== 'boolean'))
36
+ return undefined;
37
+ const statuses = new Set(['pending', 'inProgress', 'completed']);
38
+ if (value.steps.some((step) => !step || !text(step.step, 4_096) || !statuses.has(step.status))) {
39
+ return undefined;
40
+ }
41
+ return structuredClone(value);
42
+ }
43
+ function context(value) {
44
+ if (value === null)
45
+ return null;
46
+ if (!value || !['idle', 'working', 'waiting', 'compacting', 'unknown'].includes(value.activity)
47
+ || (value.usedTokens !== undefined && !finite(value.usedTokens))
48
+ || (value.totalTokens !== undefined && (!finite(value.totalTokens) || value.totalTokens <= 0))
49
+ || (value.cwd !== undefined && !text(value.cwd, 4_096))
50
+ || (value.branch !== undefined && !text(value.branch, 1_024))
51
+ || (value.access !== undefined && !['read-only', 'workspace-write', 'full-access'].includes(value.access))) {
52
+ return undefined;
53
+ }
54
+ return structuredClone(value);
55
+ }
56
+ function permission(value) {
57
+ if (value === null)
58
+ return null;
59
+ const validModes = ['default', 'auto-review', 'full-access', 'custom'];
60
+ if (!value || !validModes.includes(value.mode) || !Array.isArray(value.options)
61
+ || value.options.length === 0 || value.options.length > 3
62
+ || value.options.some((mode) => !['default', 'auto-review', 'full-access'].includes(mode))
63
+ || new Set(value.options).size !== value.options.length)
64
+ return undefined;
65
+ return structuredClone(value);
66
+ }
67
+ export class AgentConversationControlService {
68
+ #bindings;
69
+ constructor(bindings) {
70
+ if (!bindings || typeof bindings !== 'object' || !Object.keys(bindings).length) {
71
+ throw new TypeError('AgentConversationControlService requires controller bindings');
72
+ }
73
+ this.#bindings = new Map(Object.entries(bindings));
74
+ }
75
+ slots(agentId) {
76
+ const binding = this.#bindings.get(agentId);
77
+ return {
78
+ goal: binding?.goal?.apiVersion === 1,
79
+ plan: binding?.plan?.apiVersion === 1,
80
+ context: binding?.context?.apiVersion === 1,
81
+ permission: binding?.permission?.apiVersion === 1,
82
+ commands: binding?.commands?.apiVersion === 1,
83
+ };
84
+ }
85
+ async read(run) {
86
+ const binding = this.#bindings.get(run.ref.agentId);
87
+ if (!binding)
88
+ throw new ConversationControlError('Conversation controls unsupported', 'unsupported');
89
+ const [goalResult, planResult, contextResult, permissionResult] = await Promise.allSettled([
90
+ binding.goal?.read(run), binding.plan?.read(run), binding.context?.read(run),
91
+ binding.permission?.read(run),
92
+ ]);
93
+ const slotErrors = {};
94
+ const resultValue = (slot, result) => {
95
+ if (result.status === 'fulfilled')
96
+ return result.value;
97
+ // Provider failures may contain RPC payloads, credentials, or local paths. The facade exposes only a
98
+ // stable slot-level recovery signal; adapter diagnostics belong in Runtime health/logging.
99
+ slotErrors[slot] = 'temporarily unavailable';
100
+ return undefined;
101
+ };
102
+ const goalValue = resultValue('goal', goalResult);
103
+ const planValue = resultValue('plan', planResult);
104
+ const contextValue = resultValue('context', contextResult);
105
+ const permissionValue = resultValue('permission', permissionResult);
106
+ const normalizedGoal = goalValue === undefined ? undefined : goal(goalValue);
107
+ const normalizedPlan = planValue === undefined ? undefined : plan(planValue);
108
+ const normalizedContext = contextValue === undefined ? undefined : context(contextValue);
109
+ const normalizedPermission = permissionValue === undefined ? undefined : permission(permissionValue);
110
+ if (goalValue !== undefined && normalizedGoal === undefined)
111
+ slotErrors.goal = 'temporarily unavailable';
112
+ if (planValue !== undefined && normalizedPlan === undefined)
113
+ slotErrors.plan = 'temporarily unavailable';
114
+ if (contextValue !== undefined && normalizedContext === undefined) {
115
+ slotErrors.context = 'temporarily unavailable';
116
+ }
117
+ if (permissionValue !== undefined && normalizedPermission === undefined) {
118
+ slotErrors.permission = 'temporarily unavailable';
119
+ }
120
+ return {
121
+ ...(normalizedGoal === undefined ? {} : { goal: normalizedGoal }),
122
+ ...(binding.goal?.apiVersion === 1 ? { goalActions: [
123
+ ...(binding.goal.start ? ['start'] : []),
124
+ ...(binding.goal.update ? ['update'] : []),
125
+ ...(binding.goal.clear ? ['clear'] : []),
126
+ ] } : {}),
127
+ ...(normalizedPlan === undefined ? {} : { plan: normalizedPlan }),
128
+ ...(normalizedContext === undefined ? {} : { context: normalizedContext }),
129
+ ...(normalizedPermission === undefined ? {} : { permission: normalizedPermission }),
130
+ ...(binding.permission?.apiVersion === 1 ? {
131
+ permissionCanUpdate: typeof binding.permission.update === 'function',
132
+ } : {}),
133
+ ...(binding.commands?.apiVersion === 1 ? { commands: [...binding.commands.commands] } : {}),
134
+ ...(Object.keys(slotErrors).length ? { slotErrors } : {}),
135
+ };
136
+ }
137
+ async goalAction(run, request) {
138
+ const control = this.#bindings.get(run.ref.agentId)?.goal;
139
+ if (!control)
140
+ throw new ConversationControlError('Conversation goal unsupported', 'unsupported');
141
+ let raw;
142
+ if (request.action === 'clear') {
143
+ if (!control.clear)
144
+ throw new ConversationControlError('Goal clear unsupported', 'unsupported');
145
+ await control.clear(run);
146
+ return null;
147
+ }
148
+ if (request.action === 'start' && text(request.objective, MAX_OBJECTIVE)) {
149
+ if (!control.start)
150
+ throw new ConversationControlError('Goal start unsupported', 'unsupported');
151
+ raw = await control.start(run, request.objective.trim());
152
+ }
153
+ else if (request.action === 'update') {
154
+ const patch = {};
155
+ if (request.objective !== undefined) {
156
+ if (!text(request.objective, MAX_OBJECTIVE)) {
157
+ throw new ConversationControlError('Invalid goal objective', 'invalid_request');
158
+ }
159
+ patch.objective = request.objective.trim();
160
+ }
161
+ if (request.status !== undefined) {
162
+ if (request.status !== 'active' && request.status !== 'paused') {
163
+ throw new ConversationControlError('Invalid goal status', 'invalid_request');
164
+ }
165
+ patch.status = request.status;
166
+ }
167
+ if (!Object.keys(patch).length) {
168
+ throw new ConversationControlError('Empty goal update', 'invalid_request');
169
+ }
170
+ if (!control.update)
171
+ throw new ConversationControlError('Goal update unsupported', 'unsupported');
172
+ raw = await control.update(run, patch);
173
+ }
174
+ else {
175
+ throw new ConversationControlError('Invalid goal action', 'invalid_request');
176
+ }
177
+ const normalized = goal(raw);
178
+ if (!normalized)
179
+ throw new ConversationControlError('Invalid goal response', 'contract_violation');
180
+ return normalized;
181
+ }
182
+ async permissionAction(run, request) {
183
+ const control = this.#bindings.get(run.ref.agentId)?.permission;
184
+ if (!control)
185
+ throw new ConversationControlError('Conversation permission unsupported', 'unsupported');
186
+ if (!control.update) {
187
+ throw new ConversationControlError('Permission update unsupported', 'unsupported');
188
+ }
189
+ const mode = request.permissionMode;
190
+ if (request.action !== 'set_permission'
191
+ || !['default', 'auto-review', 'full-access'].includes(String(mode))) {
192
+ throw new ConversationControlError('Invalid permission action', 'invalid_request');
193
+ }
194
+ const normalized = permission(await control.update(run, mode));
195
+ if (!normalized)
196
+ throw new ConversationControlError('Invalid permission response', 'contract_violation');
197
+ return normalized;
198
+ }
199
+ async command(run, value) {
200
+ const control = this.#bindings.get(run.ref.agentId)?.commands;
201
+ if (!control || (value !== 'compact' && value !== 'clear') || !control.commands.includes(value)) {
202
+ throw new ConversationControlError('Conversation command unsupported', 'unsupported');
203
+ }
204
+ await control.execute(run, value);
205
+ }
206
+ }
@@ -0,0 +1,276 @@
1
+ const DEFAULT_MAX_BUFFERED_EVENTS = 512;
2
+ const DEFAULT_MAX_REPLAY_EVENTS = 2_048;
3
+ const DEFAULT_IDLE_GRACE_MS = 5_000;
4
+ export class ConversationLiveHubError extends Error {
5
+ code;
6
+ constructor(code, message) {
7
+ super(message);
8
+ this.code = code;
9
+ this.name = 'ConversationLiveHubError';
10
+ }
11
+ }
12
+ class EventQueue {
13
+ checkpoint;
14
+ #maxBufferedEvents;
15
+ #detach;
16
+ #values = [];
17
+ #waiting;
18
+ #closed = false;
19
+ #finishWhenEmpty = false;
20
+ #detached = false;
21
+ #lastReadSequence;
22
+ constructor(checkpoint, maxBufferedEvents, detach) {
23
+ this.checkpoint = Object.freeze(structuredClone(checkpoint));
24
+ this.#lastReadSequence = checkpoint.streamSequence;
25
+ this.#maxBufferedEvents = maxBufferedEvents;
26
+ this.#detach = detach;
27
+ }
28
+ [Symbol.asyncIterator]() { return this; }
29
+ next() {
30
+ const value = this.#values.shift();
31
+ if (value) {
32
+ this.#lastReadSequence = value.sequence;
33
+ return Promise.resolve({ value: structuredClone(value), done: false });
34
+ }
35
+ if (this.#closed || this.#finishWhenEmpty) {
36
+ this.#closed = true;
37
+ this.#detachOnce();
38
+ return Promise.resolve({ value: undefined, done: true });
39
+ }
40
+ return new Promise((resolve) => { this.#waiting = resolve; });
41
+ }
42
+ push(event, finish = false) {
43
+ if (this.#closed || this.#finishWhenEmpty)
44
+ return;
45
+ const cloned = structuredClone(event);
46
+ const waiting = this.#waiting;
47
+ if (waiting) {
48
+ this.#waiting = undefined;
49
+ this.#lastReadSequence = cloned.sequence;
50
+ if (finish) {
51
+ this.#finishWhenEmpty = true;
52
+ this.#detachOnce();
53
+ }
54
+ waiting({ value: cloned, done: false });
55
+ return;
56
+ }
57
+ if (this.#values.length >= this.#maxBufferedEvents) {
58
+ this.#values.splice(0);
59
+ this.#values.push({
60
+ type: 'stream.gap',
61
+ sequence: cloned.sequence,
62
+ afterSequence: this.#lastReadSequence,
63
+ });
64
+ this.#finishWhenEmpty = true;
65
+ this.#detachOnce();
66
+ return;
67
+ }
68
+ this.#values.push(cloned);
69
+ if (finish) {
70
+ this.#finishWhenEmpty = true;
71
+ this.#detachOnce();
72
+ }
73
+ }
74
+ finishAfterDrain() {
75
+ if (this.#closed || this.#finishWhenEmpty)
76
+ return;
77
+ this.#finishWhenEmpty = true;
78
+ this.#detachOnce();
79
+ if (!this.#values.length && this.#waiting) {
80
+ const waiting = this.#waiting;
81
+ this.#waiting = undefined;
82
+ this.#closed = true;
83
+ waiting({ value: undefined, done: true });
84
+ }
85
+ }
86
+ close() {
87
+ if (this.#closed)
88
+ return;
89
+ this.#closed = true;
90
+ this.#values.splice(0);
91
+ this.#detachOnce();
92
+ if (this.#waiting) {
93
+ const waiting = this.#waiting;
94
+ this.#waiting = undefined;
95
+ waiting({ value: undefined, done: true });
96
+ }
97
+ }
98
+ #detachOnce() {
99
+ if (this.#detached)
100
+ return;
101
+ this.#detached = true;
102
+ this.#detach();
103
+ }
104
+ }
105
+ // App-level fan-out only. ConversationService still owns provider ordering, reconciliation, and durable
106
+ // truth; this hub owns one shared observation plus bounded per-client delivery queues.
107
+ export class ConversationLiveHub {
108
+ #conversation;
109
+ #maxBufferedEvents;
110
+ #maxReplayEvents;
111
+ #idleGraceMs;
112
+ #observations = new Map();
113
+ #closed = false;
114
+ constructor({ conversation, maxBufferedEvents = DEFAULT_MAX_BUFFERED_EVENTS, maxReplayEvents = DEFAULT_MAX_REPLAY_EVENTS, idleGraceMs = DEFAULT_IDLE_GRACE_MS, }) {
115
+ if (!conversation || typeof conversation.open !== 'function'
116
+ || !Number.isSafeInteger(maxBufferedEvents) || maxBufferedEvents <= 0
117
+ || !Number.isSafeInteger(maxReplayEvents) || maxReplayEvents <= 0
118
+ || !Number.isSafeInteger(idleGraceMs) || idleGraceMs < 0) {
119
+ throw new TypeError('Conversation live hub requires a service and bounded queues');
120
+ }
121
+ this.#conversation = conversation;
122
+ this.#maxBufferedEvents = maxBufferedEvents;
123
+ this.#maxReplayEvents = maxReplayEvents;
124
+ this.#idleGraceMs = idleGraceMs;
125
+ }
126
+ async subscribe(run, request = {}) {
127
+ if (this.#closed)
128
+ throw new ConversationLiveHubError('closed', 'Conversation live hub is closed');
129
+ let shared = this.#observations.get(run.ref.runId);
130
+ if (!shared)
131
+ shared = this.#create(run);
132
+ if (shared.run !== run || shared.terminated) {
133
+ throw new ConversationLiveHubError('stale_run', 'Conversation live run is no longer current');
134
+ }
135
+ if (shared.idleTimer) {
136
+ clearTimeout(shared.idleTimer);
137
+ shared.idleTimer = undefined;
138
+ }
139
+ await shared.ready;
140
+ const checkpoint = shared.checkpoint;
141
+ const baseline = shared.baseline;
142
+ if (!checkpoint || !baseline || shared.terminated) {
143
+ throw new ConversationLiveHubError('unavailable', 'Conversation live observation is unavailable');
144
+ }
145
+ if (request.expectedViewId !== undefined && request.expectedViewId !== checkpoint.viewId) {
146
+ this.#idle(shared);
147
+ throw new ConversationLiveHubError('stale_view', 'Conversation view is stale');
148
+ }
149
+ let subscription;
150
+ subscription = new EventQueue(baseline, this.#maxBufferedEvents, () => {
151
+ this.#detach(shared, subscription);
152
+ });
153
+ shared.subscribers.add(subscription);
154
+ for (const event of shared.replay)
155
+ subscription.push(event);
156
+ return subscription;
157
+ }
158
+ close() {
159
+ if (this.#closed)
160
+ return;
161
+ this.#closed = true;
162
+ for (const shared of [...this.#observations.values()])
163
+ this.#terminate(shared, false);
164
+ }
165
+ #create(run) {
166
+ const shared = {
167
+ run,
168
+ ready: Promise.resolve(),
169
+ subscribers: new Set(),
170
+ replay: [],
171
+ idleTimer: undefined,
172
+ detachAbort: () => { },
173
+ terminated: false,
174
+ };
175
+ const abort = () => this.#terminate(shared, false);
176
+ run.signal.addEventListener('abort', abort, { once: true });
177
+ shared.detachAbort = () => run.signal.removeEventListener('abort', abort);
178
+ this.#observations.set(run.ref.runId, shared);
179
+ shared.ready = this.#conversation.open(run, {}, (event) => {
180
+ this.#publish(shared, event);
181
+ }).then((handle) => {
182
+ if (shared.terminated) {
183
+ try {
184
+ void Promise.resolve(handle.close()).catch(() => { });
185
+ }
186
+ catch { /* best effort */ }
187
+ throw new Error('Conversation live run was revoked during open');
188
+ }
189
+ shared.handle = handle;
190
+ shared.baseline = structuredClone(handle.checkpoint);
191
+ shared.checkpoint = structuredClone(handle.checkpoint);
192
+ }).catch((error) => {
193
+ this.#terminate(shared, false);
194
+ throw error;
195
+ });
196
+ return shared;
197
+ }
198
+ #publish(shared, event) {
199
+ if (shared.terminated || !shared.checkpoint
200
+ || !Number.isSafeInteger(event.sequence)
201
+ || event.sequence <= shared.checkpoint.streamSequence)
202
+ return;
203
+ const previousSequence = shared.checkpoint.streamSequence;
204
+ if (shared.replay.length >= this.#maxReplayEvents
205
+ && event.type !== 'history.changed' && event.type !== 'stream.gap') {
206
+ const gap = {
207
+ type: 'stream.gap', sequence: event.sequence, afterSequence: previousSequence,
208
+ };
209
+ shared.checkpoint = { ...shared.checkpoint, streamSequence: event.sequence };
210
+ for (const subscriber of [...shared.subscribers])
211
+ subscriber.push(gap, true);
212
+ this.#terminate(shared, true);
213
+ return;
214
+ }
215
+ shared.checkpoint = {
216
+ viewId: event.type === 'history.changed' ? event.viewId : shared.checkpoint.viewId,
217
+ historyVersion: event.type === 'history.changed'
218
+ ? event.historyVersion : shared.checkpoint.historyVersion,
219
+ streamSequence: event.sequence,
220
+ };
221
+ const terminal = event.type === 'stream.gap';
222
+ shared.replay.push(structuredClone(event));
223
+ for (const subscriber of [...shared.subscribers])
224
+ subscriber.push(event, terminal);
225
+ if (event.type === 'history.changed') {
226
+ // A durable barrier advances the baseline without ending the native observation. Existing clients
227
+ // receive the barrier and reconcile their page; later subscribers start directly from the new
228
+ // checkpoint, so pre-barrier replay can be released immediately.
229
+ shared.baseline = structuredClone(shared.checkpoint);
230
+ shared.replay = [];
231
+ }
232
+ if (terminal)
233
+ this.#terminate(shared, true);
234
+ }
235
+ #detach(shared, subscription) {
236
+ shared.subscribers.delete(subscription);
237
+ this.#idle(shared);
238
+ }
239
+ #idle(shared) {
240
+ if (shared.terminated || shared.subscribers.size || shared.idleTimer)
241
+ return;
242
+ if (this.#idleGraceMs === 0) {
243
+ this.#terminate(shared, false);
244
+ return;
245
+ }
246
+ shared.idleTimer = setTimeout(() => {
247
+ shared.idleTimer = undefined;
248
+ if (!shared.subscribers.size)
249
+ this.#terminate(shared, false);
250
+ }, this.#idleGraceMs);
251
+ shared.idleTimer.unref?.();
252
+ }
253
+ #terminate(shared, drain) {
254
+ if (shared.terminated)
255
+ return;
256
+ shared.terminated = true;
257
+ if (this.#observations.get(shared.run.ref.runId) === shared) {
258
+ this.#observations.delete(shared.run.ref.runId);
259
+ }
260
+ if (shared.idleTimer)
261
+ clearTimeout(shared.idleTimer);
262
+ shared.idleTimer = undefined;
263
+ shared.detachAbort();
264
+ for (const subscriber of [...shared.subscribers]) {
265
+ if (drain)
266
+ subscriber.finishAfterDrain();
267
+ else
268
+ subscriber.close();
269
+ }
270
+ shared.subscribers.clear();
271
+ try {
272
+ void Promise.resolve(shared.handle?.close()).catch(() => { });
273
+ }
274
+ catch { /* close is best effort */ }
275
+ }
276
+ }