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,368 @@
1
+ const STATES = new Set(['working', 'waiting', 'done', 'error']);
2
+ const AVAILABILITIES = new Set(['ready', 'degraded', 'unavailable']);
3
+ const EVENT_ID_RE = /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,255}$/;
4
+ export class PiInboxBridgeError extends Error {
5
+ constructor(message) {
6
+ super(message);
7
+ this.name = 'PiInboxBridgeError';
8
+ }
9
+ }
10
+ function isControlOperation(operation) {
11
+ return operation.kind === 'superseded' || operation.kind === 'gap';
12
+ }
13
+ function isRecord(value) {
14
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
15
+ }
16
+ function optionalText(value, max, nullable = false) {
17
+ return value === undefined || (nullable && value === null)
18
+ || (typeof value === 'string' && value.length > 0 && value.length <= max);
19
+ }
20
+ function optionalEventId(value) {
21
+ return value === undefined || (typeof value === 'string' && EVENT_ID_RE.test(value));
22
+ }
23
+ function optionalTime(value) {
24
+ return value === undefined || (typeof value === 'number' && Number.isFinite(value));
25
+ }
26
+ function copyRun(ref) {
27
+ return {
28
+ agentId: ref.agentId,
29
+ paneId: ref.paneId,
30
+ runId: ref.runId,
31
+ ...(ref.sessionId === undefined ? {} : { sessionId: ref.sessionId }),
32
+ };
33
+ }
34
+ function sameRun(first, second) {
35
+ return first.agentId === second.agentId
36
+ && first.paneId === second.paneId
37
+ && first.runId === second.runId
38
+ && first.sessionId === second.sessionId;
39
+ }
40
+ function parseItem(value) {
41
+ if (!isRecord(value) || typeof value.state !== 'string' || !STATES.has(value.state)
42
+ || !optionalText(value.message, 4096) || !optionalText(value.reason, 1024)
43
+ || !optionalText(value.correlationId, 256) || !optionalEventId(value.eventId)
44
+ || !optionalTime(value.sourceOccurredAt))
45
+ return null;
46
+ return {
47
+ state: value.state,
48
+ ...(value.message === undefined ? {} : { message: value.message }),
49
+ ...(value.reason === undefined ? {} : { reason: value.reason }),
50
+ ...(value.correlationId === undefined ? {} : { correlationId: value.correlationId }),
51
+ ...(value.eventId === undefined ? {} : { eventId: value.eventId }),
52
+ ...(value.sourceOccurredAt === undefined ? {} : { sourceOccurredAt: value.sourceOccurredAt }),
53
+ };
54
+ }
55
+ function parseSnapshot(value, availability, run, sourceId, sequence, label = 'Pi') {
56
+ if (availability === 'unavailable') {
57
+ return {
58
+ current: undefined,
59
+ result: { availability: 'unavailable', message: `${label} Inbox snapshot is unavailable` },
60
+ };
61
+ }
62
+ if (!isRecord(value) || typeof value.availability !== 'string'
63
+ || !AVAILABILITIES.has(value.availability)
64
+ || !optionalText(value.message, 1024)) {
65
+ throw new PiInboxBridgeError(`${label} Inbox Bridge returned an invalid snapshot`);
66
+ }
67
+ const snapshotAvailability = value.availability;
68
+ if (snapshotAvailability === 'unavailable' && value.current !== undefined) {
69
+ throw new PiInboxBridgeError(`Unavailable ${label} Inbox snapshot cannot contain current state`);
70
+ }
71
+ const current = value.current === undefined ? undefined : parseItem(value.current);
72
+ if (value.current !== undefined && !current) {
73
+ throw new PiInboxBridgeError(`${label} Inbox Bridge snapshot contains an invalid current state`);
74
+ }
75
+ const baseline = current ? [{
76
+ run: copyRun(run),
77
+ source: { sourceId, cursor: `bridge:${sequence}` },
78
+ ...current,
79
+ }] : [];
80
+ return {
81
+ current: current ?? undefined,
82
+ result: {
83
+ availability: snapshotAvailability,
84
+ ...(snapshotAvailability === 'unavailable' ? {} : { snapshot: baseline }),
85
+ ...(value.message === undefined ? {} : { message: value.message }),
86
+ },
87
+ };
88
+ }
89
+ function parseOperation(envelope, sourceId) {
90
+ const value = envelope.payload;
91
+ if (!isRecord(value)
92
+ || (value.kind !== 'set' && value.kind !== 'clear'
93
+ && value.kind !== 'superseded' && value.kind !== 'gap')
94
+ || !optionalText(value.correlationId, 256) || !optionalEventId(value.eventId)
95
+ || !optionalTime(value.sourceOccurredAt)
96
+ || (envelope.eventId !== undefined && value.eventId !== undefined
97
+ && envelope.eventId !== value.eventId))
98
+ return null;
99
+ const eventId = typeof value.eventId === 'string'
100
+ ? value.eventId : envelope.eventId;
101
+ const base = {
102
+ source: { sourceId, cursor: `bridge:${envelope.sequence}` },
103
+ ...(value.correlationId === undefined ? {} : { correlationId: value.correlationId }),
104
+ ...(eventId === undefined ? {} : { eventId }),
105
+ ...(value.sourceOccurredAt === undefined ? {} : { sourceOccurredAt: value.sourceOccurredAt }),
106
+ };
107
+ if (value.kind === 'superseded' || value.kind === 'gap') {
108
+ if (!eventId)
109
+ return null;
110
+ return { kind: value.kind, ...base, eventId };
111
+ }
112
+ if (value.kind === 'clear')
113
+ return { kind: 'clear', ...base };
114
+ if (typeof value.state !== 'string' || !STATES.has(value.state)
115
+ || !optionalText(value.message, 4096, true) || !optionalText(value.reason, 1024, true))
116
+ return null;
117
+ const state = value.state;
118
+ if ((state === 'waiting' || state === 'done' || state === 'error') && eventId === undefined)
119
+ return null;
120
+ return {
121
+ kind: 'set',
122
+ state,
123
+ ...base,
124
+ ...(value.message === undefined ? {} : { message: value.message }),
125
+ ...(value.reason === undefined ? {} : { reason: value.reason }),
126
+ };
127
+ }
128
+ // Owns only Bridge source ordering and semantic arbitration. Bridge sequence remains a source cursor;
129
+ // Inbox Core allocates the public run-scoped sequence and authoritative acceptedAt.
130
+ export class BridgeInboxCoordinator {
131
+ #host;
132
+ #projector;
133
+ #agentId;
134
+ #sourceId;
135
+ #label;
136
+ #bindings = new Map();
137
+ #stopConsumer;
138
+ #restoreTail = Promise.resolve();
139
+ #closed = false;
140
+ constructor({ host, projector, agentId = 'pi', sourceId = 'pi.bridge.inbox', label = 'Pi', }) {
141
+ this.#host = host;
142
+ this.#projector = projector;
143
+ this.#agentId = agentId;
144
+ this.#sourceId = sourceId;
145
+ this.#label = label;
146
+ }
147
+ start() {
148
+ if (this.#closed)
149
+ throw new PiInboxBridgeError(`${this.#label} Inbox Bridge coordinator is closed`);
150
+ if (this.#stopConsumer)
151
+ return;
152
+ this.#stopConsumer = this.#host.consumeDurableReplays('inbox', (replay) => this.#consumeDurable(replay));
153
+ }
154
+ async bind(lease) {
155
+ if (this.#closed || lease.signal.aborted || lease.ref.agentId !== this.#agentId) {
156
+ throw new PiInboxBridgeError(`${this.#label} Inbox Bridge requires a live run lease`);
157
+ }
158
+ this.start();
159
+ const existing = this.#bindings.get(lease.ref.runId);
160
+ if (existing) {
161
+ if (existing.lease !== lease || existing.phase === 'closed') {
162
+ throw new PiInboxBridgeError(`${this.#label} Inbox Bridge runId is already bound to another lease`);
163
+ }
164
+ return existing.exposed;
165
+ }
166
+ const binding = {};
167
+ const exposed = Object.freeze({
168
+ run: copyRun(lease.ref),
169
+ close: () => this.#closeBinding(binding),
170
+ });
171
+ Object.assign(binding, {
172
+ lease,
173
+ projector: this.#projector.forRun(lease),
174
+ exposed,
175
+ phase: 'opening',
176
+ baselineSequence: 0,
177
+ baseline: undefined,
178
+ baselineReady: false,
179
+ handle: undefined,
180
+ buffered: [],
181
+ tail: Promise.resolve(),
182
+ abort: new AbortController(),
183
+ detachAbort: () => { },
184
+ });
185
+ const onAbort = () => this.#closeBinding(binding);
186
+ lease.signal.addEventListener('abort', onAbort, { once: true });
187
+ binding.detachAbort = () => lease.signal.removeEventListener('abort', onAbort);
188
+ this.#bindings.set(lease.ref.runId, binding);
189
+ try {
190
+ const handle = await this.#host.openChannel(lease, 'inbox', async (event) => {
191
+ if (binding.phase === 'closed')
192
+ return;
193
+ if (binding.phase !== 'live') {
194
+ binding.buffered.push(event);
195
+ return;
196
+ }
197
+ try {
198
+ await this.#enqueue(binding, () => this.#processHostEvent(binding, event));
199
+ }
200
+ catch (error) {
201
+ this.#closeBinding(binding);
202
+ throw error;
203
+ }
204
+ });
205
+ binding.handle = handle;
206
+ binding.baselineSequence = handle.streamSequence;
207
+ binding.baseline = parseSnapshot(handle.snapshot, handle.snapshotAvailability, lease.ref, this.#sourceId, handle.streamSequence, this.#label);
208
+ binding.phase = 'prebaseline';
209
+ await this.#host.drainDurable(lease.ref, 'inbox', handle.streamSequence, {
210
+ signal: binding.abort.signal,
211
+ });
212
+ if (lease.signal.aborted || this.#bindings.get(lease.ref.runId) !== binding) {
213
+ throw new PiInboxBridgeError(`${this.#label} Inbox Bridge binding closed during restore`);
214
+ }
215
+ binding.baselineReady = true;
216
+ await this.#restoreBindings();
217
+ const buffered = binding.buffered.splice(0);
218
+ let initialization = binding.tail;
219
+ for (const event of buffered) {
220
+ initialization = initialization.then(() => this.#processHostEvent(binding, event));
221
+ }
222
+ binding.tail = initialization.catch(() => { });
223
+ binding.phase = 'live';
224
+ await initialization;
225
+ return exposed;
226
+ }
227
+ catch (error) {
228
+ this.#closeBinding(binding);
229
+ throw error;
230
+ }
231
+ }
232
+ close() {
233
+ if (this.#closed)
234
+ return;
235
+ this.#closed = true;
236
+ this.#stopConsumer?.();
237
+ this.#stopConsumer = undefined;
238
+ for (const binding of [...this.#bindings.values()])
239
+ this.#closeBinding(binding);
240
+ }
241
+ async #consumeDurable(replay) {
242
+ const operation = parseOperation(replay.event, this.#sourceId);
243
+ if (!operation)
244
+ return 'invalid';
245
+ if (isControlOperation(operation)) {
246
+ return replay.runStatus === 'current' ? 'accepted' : 'invalid';
247
+ }
248
+ if (replay.runStatus === 'revoked') {
249
+ if (operation.kind !== 'set' || (operation.state !== 'done' && operation.state !== 'error')
250
+ || !operation.eventId)
251
+ return 'invalid';
252
+ const result = await this.#projector.submitTerminalReplay({
253
+ run: replay.run,
254
+ source: operation.source,
255
+ state: operation.state,
256
+ eventId: operation.eventId,
257
+ ...(typeof operation.message === 'string' ? { message: operation.message } : {}),
258
+ ...(typeof operation.reason === 'string' ? { reason: operation.reason } : {}),
259
+ ...(operation.correlationId === undefined ? {} : { correlationId: operation.correlationId }),
260
+ ...(operation.sourceOccurredAt === undefined ? {} : { sourceOccurredAt: operation.sourceOccurredAt }),
261
+ });
262
+ return this.#replayResult(result.accepted, result.reason);
263
+ }
264
+ const binding = this.#bindings.get(replay.run.runId);
265
+ if (!binding || !sameRun(binding.lease.ref, replay.run) || binding.phase === 'opening'
266
+ || binding.phase === 'closed')
267
+ return 'retry';
268
+ if (binding.phase === 'prebaseline') {
269
+ if (replay.event.sequence > binding.baselineSequence)
270
+ return 'retry';
271
+ if (operation.kind === 'set' && operation.state === 'waiting'
272
+ && binding.baseline?.result.availability === 'ready') {
273
+ const current = binding.baseline.current;
274
+ if (!current || current.state !== 'waiting' || current.eventId !== operation.eventId)
275
+ return 'invalid';
276
+ }
277
+ }
278
+ const result = await binding.projector.submit(operation);
279
+ return this.#replayResult(result.accepted, result.reason);
280
+ }
281
+ #replayResult(accepted, reason) {
282
+ if (accepted)
283
+ return 'accepted';
284
+ if (reason === 'persistence_failed' || reason === 'stale_lease')
285
+ return 'retry';
286
+ return 'invalid';
287
+ }
288
+ #enqueue(binding, operation) {
289
+ const pending = binding.tail.then(operation);
290
+ binding.tail = pending.catch(() => { });
291
+ return pending;
292
+ }
293
+ #restore(result) {
294
+ const pending = this.#restoreTail.then(() => this.#projector.restore(result()).then(() => undefined));
295
+ this.#restoreTail = pending.catch(() => { });
296
+ return pending;
297
+ }
298
+ #restoreBindings() {
299
+ return this.#restore(() => {
300
+ const bindings = [...this.#bindings.values()].filter((binding) => binding.phase !== 'closed');
301
+ const available = bindings.filter((binding) => (binding.baselineReady && binding.baseline?.result.availability !== 'unavailable'));
302
+ const pending = bindings.filter((binding) => !binding.baselineReady || !binding.baseline);
303
+ const unavailable = bindings.filter((binding) => (binding.baselineReady && binding.baseline?.result.availability === 'unavailable'));
304
+ if ((pending.length || unavailable.length) && available.length === 0) {
305
+ return {
306
+ availability: 'unavailable',
307
+ message: unavailable[0]?.baseline?.result.message
308
+ ?? `${this.#label} Inbox baseline is still restoring`,
309
+ };
310
+ }
311
+ const baselines = available.map((binding) => binding.baseline);
312
+ const degraded = baselines.find((baseline) => baseline.result.availability === 'degraded');
313
+ return {
314
+ availability: degraded || pending.length || unavailable.length ? 'degraded' : 'ready',
315
+ snapshot: baselines.flatMap((baseline) => baseline.result.snapshot ?? []),
316
+ ...((degraded?.result.message ?? unavailable[0]?.baseline?.result.message) === undefined
317
+ ? {} : { message: degraded?.result.message ?? unavailable[0].baseline.result.message }),
318
+ };
319
+ });
320
+ }
321
+ async #processHostEvent(binding, event) {
322
+ if (binding.phase === 'closed')
323
+ return;
324
+ if (event.type === 'gap') {
325
+ await this.#restore(() => ({
326
+ availability: 'unavailable',
327
+ message: `${this.#label} Inbox stream gap after Bridge sequence ${event.afterSequence}`,
328
+ }));
329
+ this.#closeBinding(binding);
330
+ throw new PiInboxBridgeError(`${this.#label} Inbox Bridge stream continuity was lost`);
331
+ }
332
+ if (event.type === 'snapshot') {
333
+ const snapshot = parseSnapshot(event.value, 'ready', binding.lease.ref, this.#sourceId, event.sequence, this.#label);
334
+ binding.baseline = snapshot;
335
+ binding.baselineReady = true;
336
+ await this.#restoreBindings();
337
+ return;
338
+ }
339
+ const operation = parseOperation(event.event, this.#sourceId);
340
+ if (!operation)
341
+ throw new PiInboxBridgeError(`${this.#label} Inbox Bridge returned an invalid live event`);
342
+ if (isControlOperation(operation))
343
+ return;
344
+ const result = await binding.projector.submit(operation);
345
+ if (!result.accepted) {
346
+ throw new PiInboxBridgeError(`${this.#label} Inbox live operation rejected: ${result.reason ?? 'unknown'}`);
347
+ }
348
+ }
349
+ #closeBinding(binding) {
350
+ if (!binding || binding.phase === 'closed')
351
+ return;
352
+ binding.phase = 'closed';
353
+ binding.abort.abort('binding_closed');
354
+ binding.detachAbort();
355
+ binding.handle?.close();
356
+ binding.buffered = [];
357
+ if (this.#bindings.get(binding.lease.ref.runId) === binding) {
358
+ this.#bindings.delete(binding.lease.ref.runId);
359
+ if (!this.#closed)
360
+ void this.#restoreBindings().catch(() => { });
361
+ }
362
+ }
363
+ }
364
+ export class PiInboxBridgeCoordinator extends BridgeInboxCoordinator {
365
+ constructor(options) {
366
+ super({ ...options, agentId: 'pi', label: 'Pi' });
367
+ }
368
+ }
@@ -0,0 +1,32 @@
1
+ export const PI_SESSION_CONTROL_IMPLEMENTATION_VERSION = 3;
2
+ function isSnapshot(value) {
3
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
4
+ }
5
+ export function createPiSessionControlAdapter({ host, }) {
6
+ if (!host || typeof host.request !== 'function') {
7
+ throw new TypeError('Pi Session Control adapter requires LocalAgentBridgeHost');
8
+ }
9
+ const read = async (run, refresh) => {
10
+ // Connector v2 knows nothing about Session Control. Treat absence as an optional capability instead
11
+ // of probing an unregistered Bridge method and delaying or disabling its otherwise healthy chat.
12
+ if ((run.ref.implementationVersion ?? 1) < PI_SESSION_CONTROL_IMPLEMENTATION_VERSION)
13
+ return null;
14
+ const value = await host.request(run, 'session-control', 'read', { refresh }, { timeoutMs: refresh ? 30_000 : 8_000, signal: run.signal });
15
+ if (!isSnapshot(value))
16
+ throw new Error('Pi Extension returned an invalid Session Control snapshot');
17
+ return value;
18
+ };
19
+ return {
20
+ apiVersion: 1,
21
+ readModelControl: (run, options) => read(run, options?.refresh === true),
22
+ async updateModelControl(run, patch) {
23
+ if ((run.ref.implementationVersion ?? 1) < PI_SESSION_CONTROL_IMPLEMENTATION_VERSION) {
24
+ throw new Error('Pi Session Control requires /reload');
25
+ }
26
+ const value = await host.request(run, 'session-control', 'update', patch, { timeoutMs: 8_000, signal: run.signal });
27
+ if (!isSnapshot(value))
28
+ throw new Error('Pi Extension returned an invalid Session Control snapshot');
29
+ return value;
30
+ },
31
+ };
32
+ }
@@ -0,0 +1,94 @@
1
+ import { promises as fsp } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { normTty } from './scanUtils.js';
4
+ const SUCCESS_TTL_MS = 30_000;
5
+ const FAILURE_TTL_MS = 3_000;
6
+ // Real executable path of a pid. lsof resolves launch symlinks on macOS; /proc is the cheap Linux path.
7
+ // A transient lsof failure is inconclusive, not a negative identity verdict that should escape the call.
8
+ export async function executablePath(run, pid) {
9
+ let out = '';
10
+ try {
11
+ out = await run('lsof', ['-a', '-p', String(pid), '-d', 'txt', '-Fn']);
12
+ }
13
+ catch { /* try /proc */ }
14
+ for (const line of String(out).split('\n'))
15
+ if (line[0] === 'n')
16
+ return line.slice(1).trim();
17
+ try {
18
+ return await fsp.readlink(`/proc/${pid}/exe`);
19
+ }
20
+ catch {
21
+ return '';
22
+ }
23
+ }
24
+ function parseForegroundProcesses(out) {
25
+ const rows = [];
26
+ for (const line of String(out).split('\n')) {
27
+ const m = line.match(/^\s*(\S+)\s+(\d+)\s+(\S+)\s*$/);
28
+ if (!m)
29
+ continue;
30
+ const [, rawTty, pid, stat] = m;
31
+ if (!rawTty || !pid || !stat)
32
+ continue;
33
+ const tty = normTty(rawTty);
34
+ if (tty && stat.includes('+'))
35
+ rows.push({ tty, pid });
36
+ }
37
+ return rows;
38
+ }
39
+ async function foregroundProcesses(run, ttys) {
40
+ let targeted = '';
41
+ try {
42
+ targeted = await run('ps', ['-t', ttys.join(','), '-o', 'tty=,pid=,stat=']);
43
+ }
44
+ catch { /* fall back to the portable full snapshot below */ }
45
+ if (String(targeted).trim())
46
+ return parseForegroundProcesses(targeted);
47
+ return parseForegroundProcesses(await run('ps', ['-Ao', 'tty=,pid=,stat=']));
48
+ }
49
+ // Normalize ambiguous tmux pane_current_command values only after tying the pane's TTY to a foreground
50
+ // process whose REAL executable proves the agent identity. Every call refreshes the candidate TTYs with ps;
51
+ // the cache only skips lsof while the exact foreground pid set is unchanged and its short TTL is live.
52
+ // This makes TTY reuse/process replacement invalidate immediately, and failed probes retry quickly. If a
53
+ // platform rejects the targeted query (defaultRun returns ''), retain the old full-system scan as fallback.
54
+ export async function resolveByExecutable(panes, run, verdicts, { candidate, normalized, matches, now = () => Date.now(), successTtlMs = SUCCESS_TTL_MS, failureTtlMs = FAILURE_TTL_MS, }) {
55
+ const candidates = panes.filter((p) => p && p.tty && candidate(p.cmd || ''));
56
+ if (!candidates.length)
57
+ return panes;
58
+ const ttys = [...new Set(candidates.map((pane) => normTty(pane.tty)).filter(Boolean))];
59
+ if (!ttys.length)
60
+ return panes;
61
+ const rows = await foregroundProcesses(run, ttys);
62
+ for (const pane of candidates) {
63
+ const tty = normTty(pane.tty);
64
+ const procs = rows.filter((r) => r.tty === tty);
65
+ const signature = procs.map((r) => r.pid).sort().join(',');
66
+ const key = `${normalized}|${tty}|${pane.cmd}`;
67
+ const cached = verdicts.get(key);
68
+ if (cached && cached.signature === signature && cached.expiresAt > now()) {
69
+ if (cached.ok)
70
+ pane.cmd = normalized;
71
+ continue;
72
+ }
73
+ let ok = false;
74
+ let matchedPid = null;
75
+ for (const proc of procs) {
76
+ const exe = await executablePath(run, proc.pid);
77
+ if (matches(exe, pane.cmd || '')) {
78
+ ok = true;
79
+ matchedPid = proc.pid;
80
+ break;
81
+ }
82
+ }
83
+ verdicts.set(key, {
84
+ ok,
85
+ pid: matchedPid,
86
+ signature,
87
+ expiresAt: now() + (ok ? successTtlMs : failureTtlMs),
88
+ });
89
+ if (ok)
90
+ pane.cmd = normalized;
91
+ }
92
+ return panes;
93
+ }
94
+ export const executableBasename = (file) => path.basename(String(file || ''));