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
@@ -1,93 +0,0 @@
1
- const CSI = /\x1b\[([0-9;]*)m/g;
2
- const ANSI = /\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1b\\))/g;
3
-
4
- function backgroundAfter(line, initial) {
5
- let background = initial;
6
- const sgr = new RegExp(CSI.source, 'g');
7
- let match = sgr.exec(line);
8
- while (match) {
9
- const params = match[1] === '' ? [0] : match[1].split(';').map(Number);
10
- for (let i = 0; i < params.length; i += 1) {
11
- const value = params[i];
12
- if (value === 0 || value === 49) background = false;
13
- else if ((value >= 40 && value <= 47) || (value >= 100 && value <= 107)) background = true;
14
- else if (value === 38 || value === 48) {
15
- const isBackground = value === 48;
16
- if (params[i + 1] === 5) {
17
- i += 2;
18
- if (isBackground) background = true;
19
- } else if (params[i + 1] === 2) {
20
- i += 4;
21
- if (isBackground) background = true;
22
- }
23
- }
24
- }
25
- match = sgr.exec(line);
26
- }
27
- return background;
28
- }
29
-
30
- function paintsBackground(line) {
31
- let background = false;
32
- let offset = 0;
33
- const sgr = new RegExp(CSI.source, 'g');
34
- let match = sgr.exec(line);
35
- while (match) {
36
- if (background && match.index > offset) return true;
37
- background = backgroundAfter(match[0], background);
38
- offset = sgr.lastIndex;
39
- match = sgr.exec(line);
40
- }
41
- return background && offset < line.length;
42
- }
43
-
44
- const isBlank = (line) => line.replace(ANSI, '').trim() === '';
45
-
46
- // `capture-pane -e -N` compresses SGR state across newlines. A default blank row after a shaded
47
- // row (Claude) and a genuinely shaded padding row (Codex) therefore look identical in the combined
48
- // capture. Capturing just that row makes tmux emit the row's real starting attributes. Only those
49
- // ambiguous rows need the extra read; ordinary rows keep the single fast combined capture.
50
- export function ambiguousBackgroundRows(lines) {
51
- const indexes = [];
52
- let background = false;
53
- let inBlankRun = false;
54
- for (let index = 0; index < lines.length; index += 1) {
55
- const line = lines[index];
56
- const ambiguous = background && isBlank(line);
57
- // One row resolves the whole trailing run: default closes it before the first LF; shaded is the
58
- // one real padding row the client preserves and seals before clearing any additional blanks.
59
- if (ambiguous && !inBlankRun) indexes.push(index);
60
- inBlankRun = ambiguous;
61
- background = backgroundAfter(line, background);
62
- }
63
- return indexes;
64
- }
65
-
66
- export async function restoreBlankRowBackgrounds(lines, historyLines, readRow) {
67
- const restored = [...lines];
68
- const indexes = ambiguousBackgroundRows(lines);
69
- await Promise.all(indexes.map(async (index) => {
70
- const row = index - historyLines;
71
- const exact = await readRow(row);
72
- // A separately-captured default row has no background SGR. Explicitly close the combined
73
- // stream's inherited background before replaying it. A real padding row starts with its own
74
- // background SGR and is preserved verbatim.
75
- restored[index] = paintsBackground(exact) ? exact : `\x1b[49m${exact}`;
76
- }));
77
- return restored;
78
- }
79
-
80
- export async function restoreCaptureBackgrounds(capture, paneHeight, readRow) {
81
- const trailingNewline = capture.endsWith('\n');
82
- const body = trailingNewline ? capture.slice(0, -1) : capture;
83
- const lines = body.split('\n');
84
- const historyLines = Math.max(0, lines.length - paneHeight);
85
- const restored = await restoreBlankRowBackgrounds(lines, historyLines, async (row) => {
86
- const exact = await readRow(row);
87
- return exact.endsWith('\n') ? exact.slice(0, -1) : exact;
88
- });
89
- return {
90
- ansi: restored.join('\n') + (trailingNewline ? '\n' : ''),
91
- historyLines,
92
- };
93
- }
@@ -1,399 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
- import { getAgent, agentForProc } from './agents/index.js';
5
- import { resolveVersionedComms } from './agents/claude.js';
6
- import { resolveCodexComms } from './agents/codex.js';
7
- import { defaultRun } from './agents/scanUtils.js';
8
- import { claude } from './agents/claude.js';
9
-
10
- const here = path.dirname(fileURLToPath(import.meta.url));
11
- // The hook-maintained state file: ONE JSON object keyed by tmux pane id, each value the pane's latest
12
- // event { ts, src, host, payload }. The hook writes it (handmux-write.js); the server only reads it.
13
- // Default lives under server/data (gitignored runtime data); override with CLAUDE_STATE_FILE.
14
- export const DEFAULT_STATE_FILE = process.env.CLAUDE_STATE_FILE || path.resolve(here, '../data/claude-state.json');
15
-
16
- // Classify a Claude hook event → inbox kind. Re-exported because tests and callers import this path.
17
- export const classifyEvent = claude.classify;
18
-
19
- // Which display VIEW a kind pushes as — and so what the device notification fires for. permission→需要你,
20
- // done→已完成. 已完成 fires at the COMPLETION MOMENT (done) only. The trailing idle reminder (~60s "still
21
- // waiting") is dropped UPSTREAM at the hook (handmux-write.cjs), so neither push nor the inbox ever sees
22
- // it — that's the single source of truth. The idle NO-OP in the loop below is defense-in-depth for one
23
- // that ever slips through. working / end / unclassifiable map to undefined → no push + re-arm the dedup.
24
- const PUSH_VIEW = { permission: 'needs', done: 'done' };
25
- const VIEW_LABEL = { needs: '需要你', done: '已完成' };
26
-
27
- // The dedup key for a pane's current push view. `needs` is view-only: Claude signals one permission gate via
28
- // TWO hooks (permreq then permission_prompt) at different ts, and both must collapse to a SINGLE 需要你. But
29
- // `done` is ts-sensitive: each finished turn is a fresh "已完成 / 该你了". For Claude, two dones are normally
30
- // separated by a 进行中 that re-arms the dedup; the timestamp also protects unusual missing-start sequences.
31
- function pushKey(view, ts) { return view === 'done' ? `done:${ts}` : view; }
32
-
33
- // A 进行中 (working) is a LATCHED state: set by UserPromptSubmit, normally closed by Stop. But an ESC
34
- // interrupt / walk-away fires NO hook at all (verified across all 26 hook event types), so working never
35
- // gets closed and the blue dot would stick forever. There's no event signal for the interrupt — so we
36
- // expire working purely by age: latched longer than this with no done/needs ⇒ no-longer-working, drop it.
37
- // Generous (2h) so a genuinely long-running task keeps its dot for its whole run; a real turn re-lights on
38
- // its next event, and a new prompt resets the clock.
39
- const WORKING_TTL_MS = 2 * 60 * 60 * 1000;
40
-
41
- // 压缩中 (compacting) clears three ways: a SUCCESSFUL compaction fires PostCompact (src 'compact' → cleared
42
- // the instant it finishes); a NO-OP /compact ("Not enough messages to compact") fires no PostCompact but
43
- // writes its <local-command-stdout> immediately, which the transcript-tail check below catches within a poll;
44
- // and this TTL is the ultimate backstop for a genuine crash/abort mid-compaction where neither signal comes.
45
- // It must stay WELL above a real compaction's duration (routinely 1-2min) so it never truncates the animation
46
- // of a compaction that's actually still running — real ones are cleared by PostCompact, not by this timer.
47
- const COMPACTING_TTL_MS = 5 * 60 * 1000;
48
-
49
- // A pane latched in `permission` (需要你) has NO hook event to close it when the user resolves the prompt:
50
- // approving a normal tool (Bash/Edit/…) doesn't hit our PostToolUse matcher (only AskUserQuestion|ExitPlanMode
51
- // do), and DENYING or ESC-interrupting fires no hook at all (verified across all hook events). So the prompt
52
- // would read 需要你 until the turn's eventual Stop — or FOREVER after an ESC, which never Stops. Detect the
53
- // resolution out-of-band: while the prompt is pending Claude is blocked and appends nothing to its session
54
- // transcript, so the transcript's mtime sits at (or before) the permission event's ts; the instant the user
55
- // resolves it — approve → tool_result, deny → denial result, ESC → a `[Request interrupted by user]` line —
56
- // the transcript grows and its mtime jumps past it. mtime beyond the event ts (+ a guard for the near-
57
- // simultaneous permreq/tool-write case) ⇒ resolved ⇒ drop the stale 需要你 (the pane falls back to neutral
58
- // process-presence). The guard must clear the gap between the tool_use write and the permreq/notification
59
- // firing; a real resolution always lands well beyond it (the user takes seconds to answer).
60
- const PERM_RESOLVED_GUARD_MS = 1500;
61
-
62
- // Read a transcript file's mtime in ms, or null if it's missing/unreadable (→ can't tell, keep 需要你).
63
- function defaultStatMtime(p) { try { return fs.statSync(p).mtimeMs; } catch { return null; } }
64
-
65
- // Read the LAST complete JSON line of a (possibly multi-MB) transcript, or null if unreadable. Reads only a
66
- // bounded tail so cost is one small read regardless of transcript size; a last line longer than the window
67
- // won't parse cleanly, but the only line we care to recognise (the interrupt marker) is tiny.
68
- function defaultReadTail(p) {
69
- try {
70
- const fd = fs.openSync(p, 'r');
71
- try {
72
- const size = fs.fstatSync(fd).size;
73
- const len = Math.min(size, 65536);
74
- const buf = Buffer.alloc(len);
75
- fs.readSync(fd, buf, 0, len, size - len);
76
- const lines = buf.toString('utf8').split('\n').filter((l) => l.trim());
77
- return lines.length ? lines[lines.length - 1] : null;
78
- } finally { fs.closeSync(fd); }
79
- } catch { return null; }
80
- }
81
-
82
- // Pure: has the user resolved the permission prompt recorded by `rec`, judged by its transcript mtime?
83
- // True only once the transcript has grown past the event ts by more than the guard. Exported for testing.
84
- export function permissionResolved(rec, mtimeMs, guard = PERM_RESOLVED_GUARD_MS) {
85
- return typeof mtimeMs === 'number' && mtimeMs > (rec.ts || 0) + guard;
86
- }
87
-
88
- // Pure: once resolved, the transcript's last line tells a RESUME from an INTERRUPT. Approving (yes) or
89
- // denying-with-feedback appends a tool_result / Claude's next message → Claude is working again → 进行中.
90
- // Pressing ESC appends a `[Request interrupted by user…]` marker → the turn ended, back to idle → drop the
91
- // pane to neutral (null). An unparseable/absent tail is treated as a resume (clears the stale 需要你 and
92
- // shows active — a truncated huge last line is a big tool_result, never the tiny interrupt marker).
93
- export function resolvedPermissionKind(lastLine) {
94
- if (isInterruptTail(lastLine)) return null; // ESC → neutral
95
- return { kind: 'working', msg: '' }; // approve / deny → 进行中
96
- }
97
-
98
- // Pure: does a transcript's last line mark a user ESC-interrupt? Matches both the plain and the
99
- // "…for tool use" forms. Used to un-stick a 进行中 pane the instant the user aborts a turn.
100
- export function isInterruptTail(lastLine) {
101
- return !!(lastLine && /Request interrupted by user/.test(lastLine));
102
- }
103
-
104
- // Pure: does a transcript's last line mark a /compact having RESOLVED — i.e. Claude wrote the command's
105
- // <local-command-stdout>? A NO-OP /compact ("Not enough messages to compact") writes it IMMEDIATELY, whereas
106
- // a real compaction stays silent for its whole (often 1-2min) run and writes it only at the very end — the
107
- // same moment PostCompact already clears us. So a fresh stdout tail while still 压缩中 ⇒ the compaction is
108
- // over (nothing to do / done) ⇒ drop the stuck state. Recognised by the system entry's subtype or the tag.
109
- export function isLocalCommandStdout(lastLine) {
110
- if (!lastLine) return false;
111
- try {
112
- const o = JSON.parse(lastLine);
113
- if (o && o.subtype === 'local_command') return true;
114
- const c = o && o.message && o.message.content;
115
- if (typeof c === 'string' && /<local-command-stdout>/.test(c)) return true;
116
- } catch { /* not JSON → fall through to a raw tag match */ }
117
- return /<local-command-stdout>/.test(lastLine);
118
- }
119
-
120
- // Truncate a Claude message to a notification-friendly one-liner.
121
- function summarize(msg) {
122
- const oneLine = (msg || '').replace(/\s+/g, ' ').trim();
123
- return oneLine.length > 120 ? `${oneLine.slice(0, 117)}…` : oneLine;
124
- }
125
-
126
- // Read the hook's JSON state file. Tolerant of a missing / corrupt / half-written file (returns {}),
127
- // never throws — the hook replaces it atomically, but a read can still land on a transient state.
128
- function readStateFile(file) {
129
- try {
130
- const obj = JSON.parse(fs.readFileSync(file, 'utf8'));
131
- return obj && typeof obj === 'object' && !Array.isArray(obj) ? obj : {};
132
- } catch { return {}; }
133
- }
134
-
135
- // Per-process event reader. Deps injected for testability:
136
- // commands.listLivePanes() → [{ id, cmd, tty, session, window, windowName }] (one tmux call)
137
- // run(cmd, args) → stdout (used to prove ambiguous version/node commands by real exe; default defaultRun)
138
- // push.sendToSession(session, payload, {ttl, urgency, topic})
139
- // file: the hook-maintained JSON state file (DEFAULT_STATE_FILE).
140
- // The hook is the sole writer; the server reads the file fresh on every getStates and on every file
141
- // change (the watcher, for push). No persisted state of our own — the file IS the persistence.
142
- export function createClaudeEvents({
143
- commands,
144
- push,
145
- codexApp = null,
146
- file = DEFAULT_STATE_FILE,
147
- now = () => Date.now(),
148
- statMtime = defaultStatMtime,
149
- readTail = defaultReadTail,
150
- run = defaultRun,
151
- onStateChange = () => {},
152
- watch = fs.watch,
153
- mkdir = fs.mkdirSync,
154
- setTimer = setTimeout,
155
- clearTimer = clearTimeout,
156
- } = {}) {
157
- // Short-lived executable-identity verdicts. Each poll still checks the foreground pid signature; the
158
- // cache only avoids repeating lsof while that exact process set is unchanged.
159
- const commVerdicts = new Map();
160
- const lastPushed = {}; // pane → 'needs' | 'done' | null (in-process push-transition dedup, by display view)
161
- // The dedup above is in-process ONLY: a restart (e.g. ./deploy.sh) wipes it while the hook's state
162
- // file on disk keeps every pane's latest 需要你/已完成. Without priming, the first read after boot
163
- // would see an empty dedup and re-push every resting pane — a flood of "historical" notifications on
164
- // every redeploy. prime() (run from start(), before any request is served) adopts the file's current
165
- // resting states as already-notified, so only transitions that happen AFTER boot push.
166
- function prime() {
167
- const recorded = readStateFile(file);
168
- for (const [pane, r] of Object.entries(recorded)) {
169
- if (r?.agent === 'codex') continue; // legacy rows are never a Codex state source
170
- const c = r && typeof r.src === 'string' ? getAgent(r.agent).classify(r.src, r.payload || {}) : null;
171
- const view = c ? PUSH_VIEW[c.kind] : undefined;
172
- if (view) lastPushed[pane] = pushKey(view, r.ts); // resting 需要你/已完成 → treat as seen, don't replay
173
- }
174
- }
175
-
176
- // Notify the device that a pane entered 需要你 / 已完成. Title carries the state label + session so the
177
- // user can tell the two apart at a glance (the inbox shows the same label as a chip); body is the
178
- // pane's one-line message, falling back to the bare label. 需要你 is high-urgency (wake the phone).
179
- async function sendPush(pane, view, c, lp) {
180
- const label = VIEW_LABEL[view];
181
- const body = summarize(c.msg) || label;
182
- const payload = { title: `${label} · ${lp.session}`, body, tag: `pane-${pane}`, data: { session: lp.session, window: lp.window, pane } };
183
- // 可靠优先 TTL: a phone in Doze / briefly offline still gets the LATEST per pane when it wakes (the
184
- // pane-topic collapses older ones, so no pileup). 需要你 holds for hours (you're being waited on);
185
- // 已完成 ~30min (a stale "done" is less useful). A force-stopped app still can't be reached at all —
186
- // that's an OS limit, not a TTL one.
187
- const opts = { topic: `pane-${pane}`, ttl: view === 'needs' ? 14400 : 1800, urgency: view === 'needs' ? 'high' : 'normal' };
188
- try { await push.sendToSession(lp.session, payload, opts); } catch { /* best effort */ }
189
- }
190
-
191
- async function updatePush(pane, c, ts, lp, { gone = false, suppressPush = false } = {}) {
192
- const kind = c ? c.kind : null;
193
- const view = PUSH_VIEW[kind];
194
- const key = view ? pushKey(view, ts) : undefined;
195
- if (kind === 'idle') return;
196
- if (gone || !view) {
197
- lastPushed[pane] = null;
198
- } else if (suppressPush) {
199
- lastPushed[pane] = key;
200
- } else if (lastPushed[pane] !== key && lp?.session) {
201
- lastPushed[pane] = key;
202
- await sendPush(pane, view, c, lp);
203
- }
204
- }
205
-
206
- // Agent identity is part of pane navigation, not inbox state. Resolve it from the pane's live foreground
207
- // process so the phone can choose terminal/chat during the initial /panes load instead of waiting for the
208
- // slower /states reconciliation pass. Work on copies because executable corroboration normalizes `cmd`.
209
- async function identifyPaneAgents(panes = []) {
210
- const livePanes = panes.map((pane) => ({
211
- ...pane,
212
- cmd: typeof pane?.cmd === 'string' ? pane.cmd : (pane?.command || ''),
213
- }));
214
- try { await resolveVersionedComms(livePanes, run, commVerdicts); } catch { /* exact names still work */ }
215
- try { await resolveCodexComms(livePanes, run, commVerdicts); } catch { /* exact names still work */ }
216
- return Object.fromEntries(livePanes.flatMap((pane) => {
217
- const agent = agentForProc(pane.cmd);
218
- return agent ? [[pane.id, agent.id]] : [];
219
- }));
220
- }
221
-
222
- // Read the state file, reconcile every recorded pane against live tmux, and in ONE pass: (1) fire push
223
- // for 需要你/已完成 transitions (deduped, global — independent of any caller's session filter), and
224
- // (2) build the pane→state roster the inbox shows. A pane is dropped from the roster when its latest
225
- // event is `end`/unclassifiable, or when tmux says it's gone or no longer running claude (hard kill /
226
- // crash / Ctrl-C-out with no SessionEnd). `allowedSessions` (session NAMES, or null) scopes only the
227
- // OUTPUT — push and reconciliation run over every pane. On a tmux failure we degrade: no location, no
228
- // reconciliation, no push (we'd have no session name to route to), roster returned best-effort.
229
- async function _getStates(allowedSessions = null) {
230
- const allow = allowedSessions == null ? null : new Set(allowedSessions);
231
- const recorded = readStateFile(file);
232
- let live = null;
233
- let managedCodex = {};
234
- try {
235
- const panes = await commands.listLivePanes();
236
- const agents = await identifyPaneAgents(panes);
237
- for (const pane of panes) if (agents[pane.id]) pane.cmd = agents[pane.id];
238
- live = new Map(panes.map((p) => [p.id, p]));
239
- managedCodex = codexApp?.inboxStates ? await codexApp.inboxStates(panes) : {};
240
- } catch { /* tmux down */ }
241
-
242
- const out = {};
243
- for (const [pane, rec] of Object.entries(recorded)) {
244
- // Older Handmux versions wrote Codex events into this Claude Hook state file. Codex is now strictly
245
- // App Server-backed, so even an unmanaged pane must ignore those stale rows instead of reviving an
246
- // approximate status or notification.
247
- if (rec?.agent === 'codex') continue;
248
- const agent = getAgent(rec && rec.agent);
249
- let c = rec && typeof rec.src === 'string' ? agent.classify(rec.src, rec.payload || {}) : null;
250
- // A 需要你 the user already resolved leaves no closing hook (see PERM_RESOLVED_GUARD_MS). statMtime
251
- // gates the cheap "still pending" path; only once the transcript has grown past the event do we pay the
252
- // bounded tail read to tell a RESUME (approve/deny → 进行中) from an INTERRUPT (ESC → neutral present).
253
- if (c && c.kind === 'permission') {
254
- const tp = rec.payload && rec.payload.transcript_path;
255
- if (tp && permissionResolved(rec, statMtime(tp))) c = resolvedPermissionKind(readTail(tp));
256
- } else if (c && c.kind === 'working') {
257
- // ESC-interrupt during a turn leaves the last hook as the stale 'prompt' (working) — no Stop fires,
258
- // so working would otherwise stick until WORKING_TTL_MS (2h), pinning the composer's send→stop toggle.
259
- // Once the transcript has grown past the prompt event, a bounded tail read settles it: an interrupt
260
- // marker → neutral (un-stick now); any other line → Claude is still producing, stay 进行中. Content is
261
- // definitive so no guard window is needed; an unreadable stat/tail can't tell → keep working.
262
- const tp = rec.payload && rec.payload.transcript_path;
263
- if (tp && statMtime(tp) > (rec.ts || 0) && isInterruptTail(readTail(tp))) c = null;
264
- } else if (c && c.kind === 'compacting') {
265
- // A no-op /compact fires no PostCompact; it writes its <local-command-stdout> at once. Once the
266
- // transcript has grown past the PreCompact event and its tail is that stdout, the /compact is done
267
- // (nothing to compact) → drop 压缩中. A real compaction stays silent until PostCompact, so it keeps
268
- // showing for its whole run. Unreadable stat/tail → can't tell → keep 压缩中 (the TTL is the backstop).
269
- const tp = rec.payload && rec.payload.transcript_path;
270
- if (tp && statMtime(tp) > (rec.ts || 0) && isLocalCommandStdout(readTail(tp))) c = null;
271
- }
272
- const lp = live ? live.get(pane) : null;
273
- // Dropped when tmux says the pane is gone or no longer running THIS agent (hard kill / crash /
274
- // Ctrl-C-out with no clean-exit event). A pane keyed by a legacy entry (no agent field) → Claude.
275
- const gone = live ? (!lp || !agent.procNames.includes(lp.cmd)) : false;
276
-
277
- // (1) push side-effect — runs for every pane regardless of the output filter. Push fires on entry
278
- // into a 需要你 (permission) / 已完成 (done) view, deduped so a stay-put doesn't re-push. The idle
279
- // reminder that trails a done is a NO-OP: 已完成 pings at the completion moment (done) only, so the
280
- // 60s "still waiting" idle neither pushes nor disturbs the dedup (the pane is still in the same
281
- // resting state). working / end / gone / unclassifiable re-arm the dedup for the next entry.
282
- if (live) {
283
- await updatePush(pane, c, rec.ts, lp, { gone });
284
- }
285
-
286
- // (2) roster — drop ended / dead / claude-exited panes; resolve location from the live tmux row.
287
- if (!c || c.kind === 'end' || gone) continue;
288
- // Expire a 进行中 latched past the TTL (an ESC-interrupt / walk-away that never got a Stop): drop it
289
- // from the roster so the stuck working pane goes away. See WORKING_TTL_MS.
290
- if (c.kind === 'working' && now() - (rec.ts || 0) > WORKING_TTL_MS) continue;
291
- if (c.kind === 'compacting' && now() - (rec.ts || 0) > COMPACTING_TTL_MS) continue;
292
- const loc = lp ? { session: lp.session, window: lp.window, windowName: lp.windowName } : {};
293
- if (allow && !allow.has(loc.session)) continue;
294
- out[pane] = { ...loc, kind: c.kind, msg: c.msg || '', ts: rec.ts || 0, agent: agent.id };
295
- }
296
-
297
- // App Server is the sole Codex state source. A neutral state still leaves process presence below to
298
- // identify the pane as Codex without guessing its activity.
299
- if (live) {
300
- for (const [pane, state] of Object.entries(managedCodex)) {
301
- const lp = live.get(pane);
302
- if (!lp) continue;
303
- const c = state.kind ? { kind: state.kind, msg: state.msg || '' } : null;
304
- await updatePush(pane, c, state.ts || 0, lp, { suppressPush: state.suppressPush });
305
- if (allow && !allow.has(lp.session)) continue;
306
- out[pane] = {
307
- session: lp.session, window: lp.window, windowName: lp.windowName,
308
- kind: c?.kind || null, msg: c?.msg || '', ...(c ? { ts: state.ts || 0 } : {}), agent: 'codex',
309
- };
310
- }
311
- }
312
-
313
- // (3) process presence — a pane whose FOREGROUND program IS a coding agent reads as "agent here" even
314
- // with no active turn on record: a fresh session that hasn't prompted yet, or one right after `/clear`
315
- // (whose SessionEnd dropped its roster entry above though the agent is still running). This is what the
316
- // phone's per-window agent icon and the dock's default mode key off — process liveness, not the volatile
317
- // activity roster. Emitted with kind:null so the inbox (inboxRows keys off VIEW[kind]) skips it and no
318
- // push fires, and with NO ts so it stays inert to the done/read high-water marks (maxTs, the seen-mark
319
- // effect). An active roster entry from (2) always wins (out[id] set → skip). agentForProc matches the
320
- // canonical procName (never the ambiguous 'node'), so a plain node process is not mistaken for an agent.
321
- if (live) {
322
- for (const lp of live.values()) {
323
- if (out[lp.id]) continue;
324
- const a = agentForProc(lp.cmd);
325
- if (!a) continue;
326
- if (allow && !allow.has(lp.session)) continue;
327
- out[lp.id] = { session: lp.session, window: lp.window, windowName: lp.windowName, kind: null, msg: '', agent: a.id };
328
- }
329
- }
330
- return out;
331
- }
332
-
333
- // Run getStates run-to-completion: the /states poll and the file-watcher both call it, and it mutates the
334
- // shared `lastPushed` dedup and `await sendPush` mid-loop — overlapping runs could interleave on that state.
335
- // Serialize on a tail promise so calls queue instead of racing; each caller still gets its own filtered
336
- // roster (allowedSessions differs per call), and the tmux read isn't duplicated concurrently.
337
- let tail = Promise.resolve();
338
- function getStates(allowedSessions = null) {
339
- const run = tail.then(() => _getStates(allowedSessions), () => _getStates(allowedSessions));
340
- tail = run.catch(() => {});
341
- return run;
342
- }
343
-
344
- // Watch the state file's directory (the hook replaces the file via rename, which changes the inode, so
345
- // watching the file itself would go deaf after the first write — watch the dir and filter by name).
346
- // On a change, re-run getStates so 需要你/已完成 push fires even when NO client is polling — that's
347
- // the whole point of push (notify you while you're away). Debounced so a burst of writes pumps once.
348
- let watcher = null;
349
- let deb = null;
350
- function start() {
351
- if (watcher) return;
352
- prime(); // boot baseline: don't replay the file's resting 需要你/已完成 as fresh push (see prime())
353
- const dir = path.dirname(file);
354
- const base = path.basename(file);
355
- try { mkdir(dir, { recursive: true }); } catch { /* ignore */ }
356
- try {
357
- watcher = watch(dir, (_evt, fname) => {
358
- if (fname && fname !== base) return;
359
- clearTimer(deb);
360
- deb = setTimer(() => {
361
- getStates()
362
- .then(() => onStateChange())
363
- .catch(() => {});
364
- }, 120);
365
- });
366
- } catch { /* fs.watch unsupported → push falls back to evaluation on each /states poll */ }
367
- }
368
- function stop() { if (watcher) { watcher.close(); watcher = null; } clearTimer(deb); }
369
-
370
- // The chat lens's pane→session bind: the hook state file records THIS pane's exact session (session_id +
371
- // transcript_path), authoritative over the terminal-side cwd→newest-jsonl guess (which collapses distinct
372
- // sessions that happen to share a cwd — see transcript.js). Returns null when hooks are off / the pane
373
- // isn't a Claude pane / the recorded payload carries no session info, so callers can fall back cleanly.
374
- function paneSession(pane) {
375
- const rec = readStateFile(file)[pane];
376
- if (rec?.agent === 'codex') return null;
377
- const p = rec && rec.payload;
378
- if (!p || typeof p !== 'object') return null;
379
- const transcriptPath = typeof p.transcript_path === 'string' ? p.transcript_path : null;
380
- const sessionId = typeof p.session_id === 'string' ? p.session_id : null;
381
- const cwd = typeof p.cwd === 'string' ? p.cwd : null;
382
- if (!transcriptPath && !sessionId) return null;
383
- return {
384
- sessionId, transcriptPath, cwd,
385
- agent: typeof rec.agent === 'string' ? rec.agent : undefined,
386
- };
387
- }
388
-
389
- // Agent identity is needed independently of session metadata so the chat lens chooses the matching
390
- // backend (Claude transcript or Codex App Server). Untagged legacy hook rows are Claude.
391
- function paneAgent(pane) {
392
- const rec = readStateFile(file)[pane];
393
- if (!rec || typeof rec !== 'object') return null;
394
- if (rec.agent === 'codex') return null;
395
- return typeof rec.agent === 'string' && rec.agent ? rec.agent : 'claude';
396
- }
397
-
398
- return { getStates, identifyPaneAgents, start, stop, paneSession, paneAgent };
399
- }