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,523 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ import { PiConversationHistory } from './piConversationHistory.js';
4
+ import { sanitizeConversationToolDraft, sanitizeConversationToolItem, } from './conversationProjectionSafety.js';
5
+ const ID_RE = /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,255}$/;
6
+ const LEAF_RE = /^[a-zA-Z0-9_-]{1,128}$/;
7
+ const CURRENT_PI_IMPLEMENTATION_VERSION = 6;
8
+ const MIN_MUTATION_COMPATIBLE_PI_IMPLEMENTATION_VERSION = 4;
9
+ const MIN_STEER_COMPATIBLE_PI_IMPLEMENTATION_VERSION = 6;
10
+ const MAX_SNAPSHOT_ACTIVE_TOOLS = 256;
11
+ function isRecord(value) {
12
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
13
+ }
14
+ function isRun(target) {
15
+ return 'runId' in target;
16
+ }
17
+ function snapshotActiveTool(value) {
18
+ if (!isRecord(value) || typeof value.provisionalId !== 'string'
19
+ || !ID_RE.test(value.provisionalId) || !isRecord(value.draft)
20
+ || value.draft.kind !== 'tool_call' || typeof value.draft.callId !== 'string'
21
+ || !ID_RE.test(value.draft.callId) || typeof value.draft.name !== 'string'
22
+ || value.draft.name.length === 0 || value.draft.name.length > 256)
23
+ return null;
24
+ let settlement;
25
+ if (value.settlement !== undefined) {
26
+ if (!isRecord(value.settlement) || typeof value.settlement.durableItemId !== 'string'
27
+ || !ID_RE.test(value.settlement.durableItemId) || !isRecord(value.settlement.item))
28
+ return null;
29
+ settlement = {
30
+ durableItemId: value.settlement.durableItemId,
31
+ item: sanitizeConversationToolItem(value.settlement.item),
32
+ };
33
+ }
34
+ if (value.committedLeafId !== undefined && (typeof value.committedLeafId !== 'string'
35
+ || !LEAF_RE.test(value.committedLeafId) || settlement === undefined))
36
+ return null;
37
+ return {
38
+ provisionalId: value.provisionalId,
39
+ draft: sanitizeConversationToolDraft(value.draft),
40
+ ...(settlement === undefined ? {} : { settlement }),
41
+ ...(value.committedLeafId === undefined ? {} : { committedLeafId: value.committedLeafId }),
42
+ };
43
+ }
44
+ function snapshot(value, sessionId) {
45
+ if (!isRecord(value) || value.sessionId !== sessionId
46
+ || typeof value.leafId !== 'string' || !LEAF_RE.test(value.leafId)
47
+ || typeof value.viewId !== 'string' || !ID_RE.test(value.viewId)
48
+ || (value.implementationVersion !== undefined
49
+ && (!Number.isSafeInteger(value.implementationVersion)
50
+ || value.implementationVersion <= 0))
51
+ || (value.sessionFile !== undefined && (typeof value.sessionFile !== 'string'
52
+ || !path.isAbsolute(value.sessionFile) || value.sessionFile.length > 4096))
53
+ || (value.pendingItems !== undefined && !Array.isArray(value.pendingItems))
54
+ || (value.activeTools !== undefined && (!Array.isArray(value.activeTools)
55
+ || value.activeTools.length > MAX_SNAPSHOT_ACTIVE_TOOLS)))
56
+ return null;
57
+ const activeTools = value.activeTools === undefined ? undefined
58
+ : value.activeTools.map(snapshotActiveTool);
59
+ if (activeTools?.some((tool) => tool === null))
60
+ return null;
61
+ const parsedActiveTools = activeTools;
62
+ if (parsedActiveTools
63
+ && new Set(parsedActiveTools.map((tool) => tool.provisionalId)).size !== parsedActiveTools.length) {
64
+ return null;
65
+ }
66
+ return {
67
+ // Missing means a pre-handshake Connector. It remains readable, but discovery marks it as stale
68
+ // and disables broken send controls until the user runs Pi's documented `/reload` command.
69
+ implementationVersion: value.implementationVersion === undefined
70
+ ? 1 : value.implementationVersion,
71
+ sessionId, leafId: value.leafId, viewId: value.viewId,
72
+ ...(value.sessionFile === undefined ? {} : { sessionFile: value.sessionFile }),
73
+ ...(value.pendingItems === undefined ? {} : {
74
+ pendingItems: value.pendingItems.map(sanitizeConversationToolItem),
75
+ }),
76
+ ...(parsedActiveTools === undefined ? {} : { activeTools: parsedActiveTools }),
77
+ };
78
+ }
79
+ function payload(value) {
80
+ if (!isRecord(value) || typeof value.type !== 'string')
81
+ return null;
82
+ if (value.type === 'stream.gap')
83
+ return { type: value.type };
84
+ if (value.type === 'history.changed') {
85
+ return typeof value.leafId === 'string' && LEAF_RE.test(value.leafId)
86
+ && typeof value.viewId === 'string' && ID_RE.test(value.viewId)
87
+ ? { type: value.type, leafId: value.leafId, viewId: value.viewId } : null;
88
+ }
89
+ if (!ID_RE.test(String(value.provisionalId)))
90
+ return null;
91
+ const provisionalId = value.provisionalId;
92
+ if (value.type === 'item.opened' && isRecord(value.draft)) {
93
+ return {
94
+ type: value.type, provisionalId,
95
+ draft: sanitizeConversationToolDraft(value.draft),
96
+ };
97
+ }
98
+ if (value.type === 'item.delta' && isRecord(value.delta)) {
99
+ if (value.delta.op === 'text.append' && value.delta.target === 'tool_result.content') {
100
+ // Sanitizing chunks independently is unsafe: a credential or absolute path can span chunks.
101
+ // Keep the running provisional and wait for a complete sanitized replace/settlement.
102
+ return { type: 'tool_result.delta_ignored', provisionalId };
103
+ }
104
+ const delta = value.delta.op === 'item.replace' && isRecord(value.delta.draft)
105
+ ? {
106
+ ...value.delta,
107
+ draft: sanitizeConversationToolDraft(value.delta.draft),
108
+ }
109
+ : value.delta;
110
+ return { type: value.type, provisionalId, delta };
111
+ }
112
+ if (value.type === 'item.settled') {
113
+ if (value.durableItemId !== undefined && !ID_RE.test(String(value.durableItemId)))
114
+ return null;
115
+ if (value.item !== undefined && !isRecord(value.item))
116
+ return null;
117
+ return {
118
+ type: value.type,
119
+ provisionalId,
120
+ ...(value.durableItemId === undefined ? {} : { durableItemId: value.durableItemId }),
121
+ ...(value.item === undefined ? {} : {
122
+ item: sanitizeConversationToolItem(value.item),
123
+ }),
124
+ };
125
+ }
126
+ if (value.type === 'item.cancelled') {
127
+ const reasons = ['interrupted', 'superseded', 'provider_error', 'stream_reset'];
128
+ if (value.reason !== undefined && !reasons.includes(String(value.reason)))
129
+ return null;
130
+ return {
131
+ type: value.type,
132
+ provisionalId,
133
+ ...(value.reason === undefined ? {} : {
134
+ reason: value.reason,
135
+ }),
136
+ };
137
+ }
138
+ return null;
139
+ }
140
+ const PI_SEND_REASONS = Object.freeze({
141
+ invalid_request: 'invalid_request',
142
+ unsupported: 'unsupported',
143
+ unsupported_delivery: 'unsupported',
144
+ stale_run: 'stale_run',
145
+ conflict: 'conflict',
146
+ agent_busy: 'temporarily_unavailable',
147
+ provider_rejected: 'provider_rejected',
148
+ temporarily_unavailable: 'temporarily_unavailable',
149
+ delivery_unconfirmed: 'delivery_unconfirmed',
150
+ native_delivery_unconfirmed: 'delivery_unconfirmed',
151
+ });
152
+ function dispatchReceipt(value) {
153
+ if (!isRecord(value) || !['accepted', 'queued', 'rejected', 'unknown'].includes(String(value.status))) {
154
+ return { outcome: 'unknown', nativeMutation: 'unknown', reason: 'delivery_unconfirmed' };
155
+ }
156
+ const status = value.status;
157
+ if (status === 'accepted' || status === 'queued')
158
+ return {
159
+ outcome: 'accepted',
160
+ ...(typeof value.nativeId === 'string' && value.nativeId ? { nativeId: value.nativeId } : {}),
161
+ };
162
+ if (status === 'rejected' && value.reason === 'agent_busy') {
163
+ return { outcome: 'busy', nativeMutation: false };
164
+ }
165
+ const reason = value.reason === undefined
166
+ ? undefined : typeof value.reason === 'string' && value.reason
167
+ ? PI_SEND_REASONS[value.reason] ?? (status === 'rejected'
168
+ ? 'provider_rejected' : 'delivery_unconfirmed')
169
+ : status === 'rejected' ? 'provider_rejected' : 'delivery_unconfirmed';
170
+ if (status === 'rejected')
171
+ return {
172
+ outcome: 'rejected', nativeMutation: false, reason: reason ?? 'provider_rejected',
173
+ };
174
+ return { outcome: 'unknown', nativeMutation: 'unknown', reason: reason ?? 'delivery_unconfirmed' };
175
+ }
176
+ function interruptReceipt(value) {
177
+ if (!isRecord(value) || !['accepted', 'rejected', 'unknown'].includes(String(value.status))) {
178
+ return { status: 'unknown', reason: 'temporarily_unavailable' };
179
+ }
180
+ const status = value.status;
181
+ const reason = status === 'accepted' || value.reason === undefined
182
+ ? undefined : typeof value.reason === 'string' && value.reason
183
+ && PI_SEND_REASONS[value.reason]
184
+ ? PI_SEND_REASONS[value.reason] : 'temporarily_unavailable';
185
+ return {
186
+ status,
187
+ ...(reason === undefined ? {} : { reason }),
188
+ };
189
+ }
190
+ export function createPiConversationActivityReader(host) {
191
+ return {
192
+ async read(run) {
193
+ const value = await host.request(run, 'conversation', 'activity', {});
194
+ if (!isRecord(value) || !['idle', 'working', 'waiting', 'compacting', 'unknown']
195
+ .includes(String(value.activity)))
196
+ return { activity: 'unknown', activeTurn: { state: 'unknown' } };
197
+ const active = isRecord(value.activeTurn) ? value.activeTurn : {};
198
+ return {
199
+ activity: value.activity,
200
+ activeTurn: active.state === 'active' && typeof active.nativeTurnId === 'string'
201
+ ? { state: 'active', nativeTurnId: active.nativeTurnId }
202
+ : active.state === 'none' ? { state: 'none' } : { state: 'unknown' },
203
+ ...(typeof value.completionToken === 'string' && value.completionToken
204
+ ? { completionToken: value.completionToken } : {}),
205
+ };
206
+ },
207
+ };
208
+ }
209
+ export function createPiConversationAdapter({ host, sessionsRoot, history = new PiConversationHistory({
210
+ sessionsRoot: sessionsRoot ?? path.join(os.homedir(), '.pi', 'agent', 'sessions'),
211
+ }), }) {
212
+ if (!host || typeof host.openChannel !== 'function' || typeof host.request !== 'function') {
213
+ throw new TypeError('Pi Conversation adapter requires LocalAgentBridgeHost');
214
+ }
215
+ return {
216
+ apiVersion: 1,
217
+ async discoverNative(target) {
218
+ if (target.agentId !== 'pi' || !target.sessionId)
219
+ return null;
220
+ if (isRun(target))
221
+ history.beginLive(target.sessionId, target.runId);
222
+ const discovered = await history.discover(target.sessionId);
223
+ if (!discovered)
224
+ return null;
225
+ const implementationVersion = isRun(target)
226
+ ? target.implementationVersion : discovered.implementationVersion;
227
+ const reloadRequired = isRun(target)
228
+ && (implementationVersion ?? 1) < CURRENT_PI_IMPLEMENTATION_VERSION;
229
+ const mutationCompatible = isRun(target)
230
+ && (implementationVersion ?? 1) >= MIN_MUTATION_COMPATIBLE_PI_IMPLEMENTATION_VERSION;
231
+ const steerCompatible = isRun(target)
232
+ && (implementationVersion ?? 1) >= MIN_STEER_COMPATIBLE_PI_IMPLEMENTATION_VERSION;
233
+ return {
234
+ session: { agentId: 'pi', sessionId: target.sessionId },
235
+ ...(isRun(target) ? { run: target } : {}),
236
+ sourceViewId: discovered.sourceViewId,
237
+ capabilities: mutationCompatible ? {
238
+ history: true, live: 'delta',
239
+ sendable: true, send: ['prompt'], interrupt: true, branching: true,
240
+ ...(steerCompatible ? { steer: true } : {}),
241
+ } : isRun(target)
242
+ ? { history: true, live: 'delta', branching: true }
243
+ : { history: true, live: 'poll', branching: true },
244
+ ...(isRun(target) ? {
245
+ implementation: {
246
+ version: implementationVersion ?? 1,
247
+ ...(reloadRequired ? { reloadRequired: true } : {}),
248
+ },
249
+ } : {}),
250
+ };
251
+ },
252
+ readNativePage: (session, request) => history.readPage(session, request),
253
+ async observeNative(run, sink) {
254
+ const sessionId = run.ref.sessionId;
255
+ if (!sessionId)
256
+ throw new Error('Pi run has no session');
257
+ history.beginLive(sessionId, run.ref.runId);
258
+ let phase = 'opening';
259
+ let handle;
260
+ let bridgeSequence = 0;
261
+ let sourceSequence = 0;
262
+ let activeLeaf = '';
263
+ let activeView = '';
264
+ let snapshotToolIds = new Set();
265
+ const recoverableToolIds = new Set();
266
+ const provisionalLifecycle = new Map();
267
+ const buffered = [];
268
+ let tail = Promise.resolve();
269
+ const emit = async (operation) => {
270
+ await sink({ ...operation, sourceSequence: ++sourceSequence });
271
+ };
272
+ const acceptOperation = async (operation) => {
273
+ if (operation.type === 'tool_result.delta_ignored')
274
+ return;
275
+ if (operation.type === 'item.opened') {
276
+ const lifecycle = provisionalLifecycle.get(operation.provisionalId);
277
+ if (lifecycle !== undefined && recoverableToolIds.has(operation.provisionalId))
278
+ return;
279
+ provisionalLifecycle.set(operation.provisionalId, 'open');
280
+ }
281
+ else if (operation.type === 'item.settled') {
282
+ const lifecycle = provisionalLifecycle.get(operation.provisionalId);
283
+ if ((lifecycle === 'settled' || lifecycle === 'cancelled')
284
+ && recoverableToolIds.has(operation.provisionalId))
285
+ return;
286
+ provisionalLifecycle.set(operation.provisionalId, 'settled');
287
+ }
288
+ else if (operation.type === 'item.cancelled') {
289
+ const lifecycle = provisionalLifecycle.get(operation.provisionalId);
290
+ if ((lifecycle === 'settled' || lifecycle === 'cancelled')
291
+ && recoverableToolIds.has(operation.provisionalId))
292
+ return;
293
+ provisionalLifecycle.set(operation.provisionalId, 'cancelled');
294
+ }
295
+ await emit(operation);
296
+ };
297
+ const restoreSnapshotTools = async (tools, historyLeaf, historicalToolCallIds = new Set()) => {
298
+ snapshotToolIds = new Set(tools.map((tool) => tool.provisionalId));
299
+ for (const tool of tools) {
300
+ recoverableToolIds.add(tool.provisionalId);
301
+ const lifecycle = provisionalLifecycle.get(tool.provisionalId);
302
+ if (lifecycle === undefined) {
303
+ // The Connector keeps a committed tombstone until the next native turn so a live
304
+ // observer can finish its provisional before history advances. A fresh observer already
305
+ // reads that leaf from history and must not recreate the completed tool beside it.
306
+ if (tool.settlement && (tool.committedLeafId === historyLeaf
307
+ || (tool.draft.kind === 'tool_call'
308
+ && historicalToolCallIds.has(tool.draft.callId)))) {
309
+ provisionalLifecycle.set(tool.provisionalId, 'settled');
310
+ continue;
311
+ }
312
+ provisionalLifecycle.set(tool.provisionalId, 'open');
313
+ await emit({
314
+ type: 'item.opened', provisionalId: tool.provisionalId, draft: tool.draft,
315
+ });
316
+ }
317
+ if (tool.settlement && provisionalLifecycle.get(tool.provisionalId) === 'open') {
318
+ provisionalLifecycle.set(tool.provisionalId, 'settled');
319
+ await emit({
320
+ type: 'item.settled', provisionalId: tool.provisionalId,
321
+ durableItemId: tool.settlement.durableItemId,
322
+ item: tool.settlement.item,
323
+ });
324
+ }
325
+ }
326
+ };
327
+ const reconcileToolsBeforeHistory = async () => {
328
+ for (const provisionalId of recoverableToolIds) {
329
+ if (snapshotToolIds.has(provisionalId)
330
+ || provisionalLifecycle.get(provisionalId) !== 'open')
331
+ continue;
332
+ provisionalLifecycle.set(provisionalId, 'cancelled');
333
+ await emit({ type: 'item.cancelled', provisionalId, reason: 'superseded' });
334
+ }
335
+ };
336
+ const emitGap = async () => {
337
+ const afterSourceSequence = sourceSequence;
338
+ await emit({ type: 'stream.gap', afterSourceSequence });
339
+ };
340
+ const process = async (event) => {
341
+ if (phase === 'closed')
342
+ return;
343
+ if (event.type === 'gap') {
344
+ bridgeSequence = Math.max(bridgeSequence, event.afterSequence);
345
+ await emitGap();
346
+ return;
347
+ }
348
+ if (event.type === 'snapshot') {
349
+ const next = snapshot(event.value, sessionId);
350
+ if (!next || event.sequence <= bridgeSequence)
351
+ throw new Error('Invalid Pi Conversation snapshot');
352
+ bridgeSequence = event.sequence;
353
+ const sameView = next.viewId === activeView;
354
+ const sameLeaf = next.leafId === activeLeaf;
355
+ const previousHistoryToken = sameView && sameLeaf
356
+ ? (await history.readPage({ agentId: 'pi', sessionId }, { limit: 1 })).sourceHistoryToken
357
+ : undefined;
358
+ history.setLiveSnapshot(sessionId, run.ref.runId, next.leafId, {
359
+ ...(next.sessionFile === undefined ? {} : { sessionFile: next.sessionFile }),
360
+ implementationVersion: next.implementationVersion,
361
+ ...(next.pendingItems === undefined ? {} : { items: next.pendingItems }),
362
+ }, next.viewId !== activeView ? next.viewId : undefined);
363
+ if (next.viewId !== activeView) {
364
+ activeLeaf = next.leafId;
365
+ activeView = next.viewId;
366
+ await emitGap();
367
+ }
368
+ else if (next.leafId !== activeLeaf) {
369
+ if (next.activeTools)
370
+ await restoreSnapshotTools(next.activeTools, next.leafId);
371
+ activeLeaf = next.leafId;
372
+ const page = await history.readPage({ agentId: 'pi', sessionId }, { limit: 1 });
373
+ if (next.activeTools)
374
+ await reconcileToolsBeforeHistory();
375
+ await emit({
376
+ type: 'history.committed',
377
+ sourceViewId: page.sourceViewId, sourceHistoryToken: page.sourceHistoryToken,
378
+ });
379
+ }
380
+ else {
381
+ if (next.activeTools)
382
+ await restoreSnapshotTools(next.activeTools, next.leafId);
383
+ const page = await history.readPage({ agentId: 'pi', sessionId }, { limit: 1 });
384
+ if (page.sourceHistoryToken !== previousHistoryToken) {
385
+ if (next.activeTools)
386
+ await reconcileToolsBeforeHistory();
387
+ await emit({
388
+ type: 'history.committed',
389
+ sourceViewId: page.sourceViewId, sourceHistoryToken: page.sourceHistoryToken,
390
+ });
391
+ }
392
+ }
393
+ return;
394
+ }
395
+ if (event.event.sequence <= bridgeSequence)
396
+ throw new Error('Pi Conversation event is out of order');
397
+ bridgeSequence = event.event.sequence;
398
+ const operation = payload(event.event.payload);
399
+ if (!operation)
400
+ throw new Error('Invalid Pi Conversation event');
401
+ if (operation.type === 'stream.gap') {
402
+ await emitGap();
403
+ return;
404
+ }
405
+ if (operation.type === 'history.changed') {
406
+ history.setLiveSnapshot(sessionId, run.ref.runId, operation.leafId, {}, operation.viewId !== activeView ? operation.viewId : undefined);
407
+ activeLeaf = operation.leafId;
408
+ if (operation.viewId !== activeView) {
409
+ activeView = operation.viewId;
410
+ await emitGap();
411
+ return;
412
+ }
413
+ const page = await history.readPage({ agentId: 'pi', sessionId }, { limit: 1 });
414
+ await reconcileToolsBeforeHistory();
415
+ await emit({
416
+ type: 'history.committed',
417
+ sourceViewId: page.sourceViewId, sourceHistoryToken: page.sourceHistoryToken,
418
+ });
419
+ return;
420
+ }
421
+ await acceptOperation(operation);
422
+ };
423
+ const failClosed = async () => {
424
+ if (phase === 'closed')
425
+ return;
426
+ try {
427
+ await emitGap();
428
+ }
429
+ catch { /* Core may already have closed the stream */ }
430
+ handle?.close();
431
+ phase = 'closed';
432
+ };
433
+ handle = await host.openChannel(run, 'conversation', (event) => {
434
+ if (phase === 'opening') {
435
+ buffered.push(structuredClone(event));
436
+ return;
437
+ }
438
+ tail = tail.then(() => process(event)).catch(failClosed);
439
+ return tail;
440
+ });
441
+ if (handle.snapshotAvailability !== 'ready') {
442
+ handle.close();
443
+ throw new Error('Pi Conversation snapshot is unavailable');
444
+ }
445
+ const baseline = snapshot(handle.snapshot, sessionId);
446
+ if (!baseline) {
447
+ handle.close();
448
+ throw new Error('Pi Conversation snapshot is invalid');
449
+ }
450
+ history.setLiveSnapshot(sessionId, run.ref.runId, baseline.leafId, {
451
+ ...(baseline.sessionFile === undefined ? {} : { sessionFile: baseline.sessionFile }),
452
+ implementationVersion: baseline.implementationVersion,
453
+ ...(baseline.pendingItems === undefined ? {} : { items: baseline.pendingItems }),
454
+ });
455
+ activeLeaf = baseline.leafId;
456
+ activeView = baseline.viewId;
457
+ bridgeSequence = handle.streamSequence;
458
+ const discovered = await history.discover(sessionId);
459
+ if (!discovered) {
460
+ handle.close();
461
+ throw new Error('Pi Conversation snapshot is not readable yet');
462
+ }
463
+ // A durability wait may time out just before Pi flushes JSONL. On a later observation the
464
+ // Connector snapshot can still contain an uncommitted settlement even though native history
465
+ // already owns that call. Read the authoritative branch once and suppress only those settled
466
+ // synthetic tools whose callId is now present there; running tools always remain provisional.
467
+ const openingHistory = await history.readPage({ agentId: 'pi', sessionId }, { limit: Number.MAX_SAFE_INTEGER });
468
+ const historicalToolCallIds = new Set(openingHistory.items.flatMap((item) => (item.kind === 'tool_call' ? [item.callId] : [])));
469
+ if (baseline.activeTools) {
470
+ await restoreSnapshotTools(baseline.activeTools, baseline.leafId, historicalToolCallIds);
471
+ }
472
+ phase = 'live';
473
+ for (const event of buffered.splice(0)) {
474
+ if (event.type === 'event' && event.event.sequence <= bridgeSequence)
475
+ continue;
476
+ if (event.type === 'snapshot' && event.sequence <= bridgeSequence)
477
+ continue;
478
+ tail = tail.then(() => process(event));
479
+ }
480
+ tail = tail.catch(failClosed);
481
+ return {
482
+ checkpoint: {
483
+ sourceViewId: discovered.sourceViewId,
484
+ // Snapshot-restored provisional items are opening suffix events, not part of history.
485
+ // Returning zero lets Conversation Core apply any number of them without inventing source
486
+ // sequence values that could collide with the next Bridge operation.
487
+ sourceSequence: 0,
488
+ },
489
+ close() {
490
+ if (phase === 'closed')
491
+ return;
492
+ phase = 'closed';
493
+ buffered.splice(0);
494
+ handle?.close();
495
+ },
496
+ };
497
+ },
498
+ async dispatchPrompt(run, request) {
499
+ const response = await host.request(run, 'conversation', 'send', {
500
+ clientRequestId: request.clientRequestId,
501
+ text: request.text,
502
+ delivery: 'prompt',
503
+ });
504
+ return dispatchReceipt(response);
505
+ },
506
+ async dispatchSteer(run, request) {
507
+ if ((run.ref.implementationVersion ?? 1) < MIN_STEER_COMPATIBLE_PI_IMPLEMENTATION_VERSION) {
508
+ return { outcome: 'rejected', nativeMutation: false, reason: 'unsupported' };
509
+ }
510
+ const response = await host.request(run, 'conversation', 'send', {
511
+ clientRequestId: request.clientRequestId,
512
+ text: request.text,
513
+ origin: 'steer',
514
+ delivery: request.plan.kind === 'steer-active-turn' ? 'steer' : 'prompt',
515
+ plan: request.plan,
516
+ });
517
+ return dispatchReceipt(response);
518
+ },
519
+ async dispatchInterrupt(run) {
520
+ return interruptReceipt(await host.request(run, 'conversation', 'interrupt', {}));
521
+ },
522
+ };
523
+ }