@co0ontty/wand 4.106.0 → 4.106.1

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 (279) hide show
  1. package/README.md +2 -2
  2. package/dist/ai-text-runner.js +24 -14
  3. package/dist/build-info.d.ts +2 -0
  4. package/dist/build-info.js +18 -0
  5. package/dist/build-info.json +3 -3
  6. package/dist/cli.js +8 -2
  7. package/dist/daemon-maintenance-state.js +1 -1
  8. package/dist/daemon-maintenance-targets.d.ts +1 -0
  9. package/dist/daemon-maintenance-targets.js +25 -9
  10. package/dist/daemon-maintenance.d.ts +3 -0
  11. package/dist/daemon-maintenance.js +60 -13
  12. package/dist/employee-im-types.d.ts +1 -1
  13. package/dist/employee-input-scheduling.d.ts +4 -0
  14. package/dist/employee-knowledge-cli.js +5 -5
  15. package/dist/employee-knowledge.d.ts +3 -1
  16. package/dist/employee-knowledge.js +24 -8
  17. package/dist/git-quick-commit.d.ts +2 -0
  18. package/dist/git-quick-commit.js +15 -2
  19. package/dist/git-worktree-batch.d.ts +26 -0
  20. package/dist/git-worktree-batch.js +139 -0
  21. package/dist/global-knowledge.d.ts +12 -0
  22. package/dist/global-knowledge.js +124 -0
  23. package/dist/iteration-log.d.ts +13 -5
  24. package/dist/iteration-log.js +120 -59
  25. package/dist/knowledge-types.d.ts +12 -1
  26. package/dist/knowledge-types.js +4 -0
  27. package/dist/middleware/api-scope.js +1 -0
  28. package/dist/model-groups.js +6 -3
  29. package/dist/models/parsers/opencode.js +31 -1
  30. package/dist/models.js +15 -14
  31. package/dist/npm-update-utils.d.ts +5 -2
  32. package/dist/npm-update-utils.js +34 -4
  33. package/dist/opencode-cli.d.ts +6 -0
  34. package/dist/opencode-cli.js +51 -0
  35. package/dist/opencode-system-fallback.d.ts +1 -0
  36. package/dist/opencode-system-fallback.js +16 -4
  37. package/dist/pi-native-event-bridge.js +55 -3
  38. package/dist/pi-native-input.d.ts +22 -0
  39. package/dist/pi-native-input.js +77 -0
  40. package/dist/pi-tmux-host.d.ts +14 -5
  41. package/dist/pi-tmux-host.js +68 -17
  42. package/dist/process-manager.js +17 -3
  43. package/dist/pty-tmux-backend.d.ts +27 -2
  44. package/dist/pty-tmux-backend.js +471 -70
  45. package/dist/pty-tmux-log.d.ts +1 -1
  46. package/dist/pty-tmux-log.js +23 -2
  47. package/dist/retention-protection.d.ts +1 -1
  48. package/dist/retention-protection.js +3 -2
  49. package/dist/retention.d.ts +5 -0
  50. package/dist/retention.js +11 -0
  51. package/dist/server-knowledge-routes.js +2 -2
  52. package/dist/server-session-input-routes.js +38 -19
  53. package/dist/server-session-lifecycle-routes.js +14 -1
  54. package/dist/server-session-worktree-routes.js +105 -12
  55. package/dist/server-task-routes.js +41 -12
  56. package/dist/server-workspace-routes.js +77 -9
  57. package/dist/server.js +67 -35
  58. package/dist/session-registry.d.ts +17 -3
  59. package/dist/session-registry.js +66 -13
  60. package/dist/session-service.js +1 -1
  61. package/dist/storage/internal.d.ts +4 -3
  62. package/dist/storage/internal.js +3 -3
  63. package/dist/storage/knowledge-repository.d.ts +5 -0
  64. package/dist/storage/knowledge-repository.js +75 -16
  65. package/dist/storage/mappers.d.ts +1 -0
  66. package/dist/storage/mappers.js +6 -1
  67. package/dist/storage/migrations.js +60 -0
  68. package/dist/storage/session-input-repository.d.ts +19 -0
  69. package/dist/storage/session-input-repository.js +35 -0
  70. package/dist/storage/task-workflow-repository.d.ts +29 -0
  71. package/dist/storage/task-workflow-repository.js +110 -0
  72. package/dist/storage/tasks-repository.d.ts +6 -1
  73. package/dist/storage/tasks-repository.js +94 -34
  74. package/dist/storage/workspaces-repository.d.ts +7 -0
  75. package/dist/storage/workspaces-repository.js +28 -2
  76. package/dist/storage-cleanup.d.ts +7 -2
  77. package/dist/storage-cleanup.js +16 -0
  78. package/dist/storage-schema.js +2 -0
  79. package/dist/storage.d.ts +5 -1
  80. package/dist/storage.js +12 -0
  81. package/dist/structured-exec-host.d.ts +4 -0
  82. package/dist/structured-exec-pump.d.ts +1 -0
  83. package/dist/structured-exec-pump.js +18 -11
  84. package/dist/structured-opencode-adapter.d.ts +2 -1
  85. package/dist/structured-opencode-adapter.js +32 -8
  86. package/dist/structured-pi-adapter.js +13 -2
  87. package/dist/structured-runner.d.ts +6 -0
  88. package/dist/structured-session-manager-replay.d.ts +2 -0
  89. package/dist/structured-session-manager-replay.js +27 -0
  90. package/dist/structured-session-manager.d.ts +32 -21
  91. package/dist/structured-session-manager.js +395 -118
  92. package/dist/structured-tmux-host.d.ts +23 -2
  93. package/dist/structured-tmux-host.js +143 -20
  94. package/dist/task-actions.js +5 -3
  95. package/dist/task-archive-policy.js +2 -1
  96. package/dist/task-comment-dispatch.d.ts +2 -1
  97. package/dist/task-comment-dispatch.js +19 -6
  98. package/dist/task-execution-queue.d.ts +2 -0
  99. package/dist/task-execution-queue.js +10 -10
  100. package/dist/task-hierarchy.js +1 -1
  101. package/dist/task-read-model.d.ts +1 -1
  102. package/dist/task-read-model.js +2 -1
  103. package/dist/task-types.d.ts +34 -3
  104. package/dist/task-types.js +21 -5
  105. package/dist/task-workflow-cli.d.ts +2 -0
  106. package/dist/task-workflow-cli.js +29 -0
  107. package/dist/task-workflow-runner.d.ts +4 -0
  108. package/dist/task-workflow-runner.js +36 -0
  109. package/dist/task-workflow.d.ts +13 -0
  110. package/dist/task-workflow.js +48 -0
  111. package/dist/terminal-daemon-server.js +12 -4
  112. package/dist/tmux-policy.d.ts +9 -0
  113. package/dist/tmux-policy.js +46 -0
  114. package/dist/tmux-terminal-host.js +5 -2
  115. package/dist/tui/commands.js +2 -1
  116. package/dist/types.d.ts +5 -0
  117. package/dist/wand-task-sync.js +5 -0
  118. package/dist/web-ui/content/ai-teams.js +9 -9
  119. package/dist/web-ui/content/module-manifest.json +1 -1
  120. package/dist/web-ui/content/modules/{52574891ef08e29b → 014fa5c6f973badf}/host.js +3 -3
  121. package/dist/web-ui/content/modules/0818b1d31a11d006/employee-profile.js +1 -0
  122. package/dist/web-ui/content/modules/{31157ceb6d23b809 → 0a58d9ec273078e4}/chunk.js +3 -3
  123. package/dist/web-ui/content/modules/{bf3ee58e0da44822 → 0a979dcd165cd0cc}/chunk.js +1 -1
  124. package/dist/web-ui/content/modules/{5f298a8f43dd0589 → 0c143928e8f96abc}/chunk.js +1 -1
  125. package/dist/web-ui/content/modules/0e9e42a319f6b8fc/workspace-window.js +1 -0
  126. package/dist/web-ui/content/modules/{b713cad51ca4c512 → 0f0d1769b732c0bd}/chunk.js +1 -1
  127. package/dist/web-ui/content/modules/{4970319a83432ec7 → 1447886e6a1e74a4}/chunk.js +1 -1
  128. package/dist/web-ui/content/modules/151c734cbca31da5/host.js +1 -0
  129. package/dist/web-ui/content/modules/{e5174201f9d918fb → 16e4ab9c5263fcfd}/chunk.js +1 -1
  130. package/dist/web-ui/content/modules/17e752fb53952753/host.js +1 -0
  131. package/dist/web-ui/content/modules/{913187c92a98a1ac → 20ed1454fc09fa6f}/chunk.js +1 -1
  132. package/dist/web-ui/content/modules/255a3baf59d2b734/home.js +6 -0
  133. package/dist/web-ui/content/modules/{2f801e53dda62770 → 296dd81886c42b45}/chunk.js +1 -1
  134. package/dist/web-ui/content/modules/{8891ca8165435ca1 → 2a01f4e525c0725e}/chunk.js +1 -1
  135. package/dist/web-ui/content/modules/2ab4409c82b44fba/markdown.js +1 -0
  136. package/dist/web-ui/content/modules/2c955eced11610c2/chunk.js +1 -0
  137. package/dist/web-ui/content/modules/2d7ee4f9e2c0aca4/panel.js +1 -0
  138. package/dist/web-ui/content/modules/{3f8830f8ad28decc → 33ae550e4a295c85}/chunk.js +1 -1
  139. package/dist/web-ui/content/modules/35f4aa4443696bd0/chunk.js +1 -0
  140. package/dist/web-ui/content/modules/388b37ffcce9d4ff/host.js +1 -0
  141. package/dist/web-ui/content/modules/39279604dcdac7c7/runtime.js +338 -0
  142. package/dist/web-ui/content/modules/{57c357d321db5df9 → 3a3cdff2a662b7d3}/chunk.js +1 -1
  143. package/dist/web-ui/content/modules/{d49f106c4d106057 → 3ab646be55514243}/chunk.js +1 -1
  144. package/dist/web-ui/content/modules/{16dab1297efd71a7 → 3ae88a5c5e1949d2}/chunk.js +1 -1
  145. package/dist/web-ui/content/modules/3e3b0e71d6ce80d8/chunk.js +1 -0
  146. package/dist/web-ui/content/modules/{ec33e17c00c1eb24 → 434aaaaf9604dc34}/chunk.js +1 -1
  147. package/dist/web-ui/content/modules/4385cd48771e1d4f/host.js +1 -0
  148. package/dist/web-ui/content/modules/{face3dbf117d63e3 → 4638129d60f549a0}/chunk.js +1 -1
  149. package/dist/web-ui/content/modules/47407209931b7011/host.js +13 -0
  150. package/dist/web-ui/content/modules/4829dd712d8b366b/chunk.js +1 -0
  151. package/dist/web-ui/content/modules/488d5b747bf374c8/chunk.js +1 -0
  152. package/dist/web-ui/content/modules/{ed7405f064944215 → 4ab085c195f7d76a}/sha256.js +1 -1
  153. package/dist/web-ui/content/modules/{f27a5d3c6c02a71c → 4c485bd44740fec7}/chunk.js +1 -1
  154. package/dist/web-ui/content/modules/{771439a41e737ee2 → 4ca0494308ab2bb3}/chunk.js +1 -1
  155. package/dist/web-ui/content/modules/4ec571218779ba3e/composer-surface-adapter.js +1 -0
  156. package/dist/web-ui/content/modules/{5933ce8965aa4004 → 5023bf0294c2e7cb}/chunk.js +1 -1
  157. package/dist/web-ui/content/modules/{e63582a3bce48f7d → 5456e3417600bf15}/chunk.js +1 -1
  158. package/dist/web-ui/content/modules/55c588b0f5b41a52/host-bridge.js +1 -0
  159. package/dist/web-ui/content/modules/{9d941d6ff39861b8 → 5c3c8237ba7d8460}/chunk.js +1 -1
  160. package/dist/web-ui/content/modules/624b858da9834757/host.js +1 -0
  161. package/dist/web-ui/content/modules/6290be936320d4b1/host.js +1 -0
  162. package/dist/web-ui/content/modules/66be408793048cc3/progress.js +1 -0
  163. package/dist/web-ui/content/modules/67a11176a563317b/chunk.js +1 -0
  164. package/dist/web-ui/content/modules/{e060508ecd50fa7e → 68ef6ec7d26d78cd}/chunk.js +1 -1
  165. package/dist/web-ui/content/modules/{41445c6589dba325 → 6ec3e41df31927d9}/host.js +2 -2
  166. package/dist/web-ui/content/modules/{97d7f267959fd385 → 6f10f369445bbcbe}/chunk.js +1 -1
  167. package/dist/web-ui/content/modules/{d47d75a78ae598e3 → 7295d9ff92d5e1e7}/chunk.js +1 -1
  168. package/dist/web-ui/content/modules/{f383d892fef73483 → 72cd613de1a5f69b}/chunk.js +1 -1
  169. package/dist/web-ui/content/modules/73aae1daafeea7e7/workspaces-panel.js +4 -0
  170. package/dist/web-ui/content/modules/{c2f09c8a472f5d83 → 73d2689f13b9a3e9}/dialog-modal.js +1 -1
  171. package/dist/web-ui/content/modules/{2f97ddba3842adaa → 75cf0c048e9b92ab}/chunk.js +1 -1
  172. package/dist/web-ui/content/modules/{4ba1e8c74bed7ef2 → 75dabda3b141296e}/chunk.js +1 -1
  173. package/dist/web-ui/content/modules/{f3cff7792ba39df8 → 76c10b58d5f92d02}/login-visual.js +1 -1
  174. package/dist/web-ui/content/modules/{b63e075f222a1266 → 783583a12f5010f8}/library-bridge.js +1 -1
  175. package/dist/web-ui/content/modules/7e13a8a4a981f2f1/group-editor.js +1 -0
  176. package/dist/web-ui/content/modules/{0d76e582a7a71b2f → 7e3034fea34c6dae}/chunk.js +1 -1
  177. package/dist/web-ui/content/modules/80bf3291f2577a68/host.js +1 -0
  178. package/dist/web-ui/content/modules/{cba6f13c0b2aec1c → 82c3e08f34a6b828}/chunk.js +1 -1
  179. package/dist/web-ui/content/modules/{dedd4a19bb70e327 → 83d1f4cf82c65f19}/page.js +3 -3
  180. package/dist/web-ui/content/modules/844f97958f25240c/shell-file-panel-drawer.js +1 -0
  181. package/dist/web-ui/content/modules/{62e0023451f428cf → 899cdc467044d418}/chunk.js +1 -1
  182. package/dist/web-ui/content/modules/{5083a5dbb5b155b7 → 8c7ca1fe72741c07}/chunk.js +1 -1
  183. package/dist/web-ui/content/modules/8dae4ca2261525e4/task-board-host.js +27 -0
  184. package/dist/web-ui/content/modules/92195c14c74929da/list-filter-panel.js +1 -0
  185. package/dist/web-ui/content/modules/{a61393bfa976d8d5 → 93793696af76bcbf}/chunk.js +1 -1
  186. package/dist/web-ui/content/modules/{db521a246e14e5e4 → 97cd6af3da5622b8}/chunk.js +4 -4
  187. package/dist/web-ui/content/modules/{8de8368570ab089d → 9d3179258249ec77}/chunk.js +1 -1
  188. package/dist/web-ui/content/modules/{579d486763d47c0d → a234c484078c0568}/chunk.js +1 -1
  189. package/dist/web-ui/content/modules/a4d215448a089a7d/host.js +1 -0
  190. package/dist/web-ui/content/modules/{c0a446100d969f3e → ae003f89901c5687}/chunk.js +1 -1
  191. package/dist/web-ui/content/modules/ae712bdd45b9917e/employee-card.js +1 -0
  192. package/dist/web-ui/content/modules/{a2caed0139df4b21 → b2a15293f7965d09}/chunk.js +1 -1
  193. package/dist/web-ui/content/modules/b4ee65e9998f0088/host.js +61 -0
  194. package/dist/web-ui/content/modules/{bd3daf04a8bf4422 → b666137d17f83be9}/chunk.js +1 -1
  195. package/dist/web-ui/content/modules/{831b6c7b43e800be → b66beb6c0185256b}/host.js +1 -1
  196. package/dist/web-ui/content/modules/{816eb9b3a000fb2c → bb03b17e877657de}/chunk.js +1 -1
  197. package/dist/web-ui/content/modules/{1eab7410d5586038 → bc343afc991db1a1}/chunk.js +1 -1
  198. package/dist/web-ui/content/modules/bca45976ff8fe2ac/host.js +69 -0
  199. package/dist/web-ui/content/modules/{d434edbb2f4672e5 → c2715dbfc9eaf510}/chunk.js +1 -1
  200. package/dist/web-ui/content/modules/{d6fffe2c2a3a738d → c30c220281f35d74}/toast-region.js +1 -1
  201. package/dist/web-ui/content/modules/c4cc46fa9b4adddc/dropdown-surface.js +1 -0
  202. package/dist/web-ui/content/modules/c6dd19ee3d3ffd59/brand-identity.js +1 -0
  203. package/dist/web-ui/content/modules/c7ea6a114e6ff8ae/host.js +1 -0
  204. package/dist/web-ui/content/modules/{a59734be368daa7e → c916d82c8acef9c8}/chunk.js +1 -1
  205. package/dist/web-ui/content/modules/{182929e188c1ab3a → cceaedda619a737a}/host.js +1 -1
  206. package/dist/web-ui/content/modules/cf67f52b076b9d2d/panel-host.js +1 -0
  207. package/dist/web-ui/content/modules/{9e163d54ff8fd1aa → d26c86558d422313}/chunk.js +1 -1
  208. package/dist/web-ui/content/modules/d2b8c6c6f3706032/queue-view-adapter.js +1 -0
  209. package/dist/web-ui/content/modules/d52ebe2ef12a9fa8/chunk.js +3 -0
  210. package/dist/web-ui/content/modules/{3fe11a75284ad8ae → d552b3bff2151eb6}/host.js +2 -2
  211. package/dist/web-ui/content/modules/d5d6b4511ac76a6e/chunk.js +3 -0
  212. package/dist/web-ui/content/modules/{bd7dc4bb46731ee7 → df42e151d625463b}/chunk.js +1 -1
  213. package/dist/web-ui/content/modules/{2a791cd2e14aa1e0 → e0a04f5ae56c68aa}/notice-view.js +1 -1
  214. package/dist/web-ui/content/modules/{438807b8bf112cd0 → e28e607c95d6f63a}/chunk.js +1 -1
  215. package/dist/web-ui/content/modules/{50da446c1377c220 → e3880df97ea62d0c}/chunk.js +1 -1
  216. package/dist/web-ui/content/modules/{87c7f50d9455d55e → e66196f1ece3e3ad}/chunk.js +1 -1
  217. package/dist/web-ui/content/modules/{9b12c2301f3a18f3 → eb76307444ac9d71}/chunk.js +1 -1
  218. package/dist/web-ui/content/modules/{c33935155289c22d → ebff7ca22cee1468}/chunk.js +1 -1
  219. package/dist/web-ui/content/modules/{4861eca9a8a87ad0 → ed1c712c569960d6}/chunk.js +1 -1
  220. package/dist/web-ui/content/modules/ee8b9c2d4bec63d4/chunk.js +1 -0
  221. package/dist/web-ui/content/modules/f458a194ce4208e2/chunk.js +1 -0
  222. package/dist/web-ui/content/modules/{11af5d14b30e57b6 → f938e826420cfe1b}/chunk.js +1 -1
  223. package/dist/web-ui/content/modules/fa7b9bf6cc84bfc1/chat-render.js +81 -0
  224. package/dist/web-ui/content/modules/{ea8fe4b9999fd2aa → fd1fb1834f5f06df}/chunk.js +1 -1
  225. package/dist/web-ui/content/modules/fd3084e562a3dbd6/host.js +1 -0
  226. package/dist/web-ui/content/styles.css +1 -1
  227. package/dist/web-ui/embedded-assets.d.ts +416 -413
  228. package/dist/web-ui/embedded-assets.js +5 -5
  229. package/dist/web-ui/page-route.js +2 -2
  230. package/dist/web-ui/react/workspaces/types.d.ts +3 -0
  231. package/dist/web-ui/thinking-efforts.d.ts +2 -0
  232. package/dist/web-ui/thinking-efforts.js +8 -0
  233. package/package.json +1 -2
  234. package/scripts/stage-render-binaries.js +0 -1
  235. package/skills/wand-knowledge/SKILL.md +5 -3
  236. package/dist/structured-bounded-keys.d.ts +0 -19
  237. package/dist/structured-bounded-keys.js +0 -47
  238. package/dist/web-ui/content/modules/00b168a19664439e/dropdown-surface.js +0 -1
  239. package/dist/web-ui/content/modules/059c5fb7f128d340/task-board-host.js +0 -27
  240. package/dist/web-ui/content/modules/07494f5a623e4471/chat-render.js +0 -76
  241. package/dist/web-ui/content/modules/0a743ad3b4db6874/chunk.js +0 -3
  242. package/dist/web-ui/content/modules/1858670c0f84fec5/employee-profile.js +0 -1
  243. package/dist/web-ui/content/modules/20c47dc0442f5a04/host.js +0 -1
  244. package/dist/web-ui/content/modules/226241b999d12008/host.js +0 -1
  245. package/dist/web-ui/content/modules/23c4b8d223ec64c5/host.js +0 -61
  246. package/dist/web-ui/content/modules/2882c97388ff4f7b/employee-card.js +0 -1
  247. package/dist/web-ui/content/modules/2c90c3be260e6cce/progress.js +0 -1
  248. package/dist/web-ui/content/modules/2d7be9935394f397/workspace-window.js +0 -1
  249. package/dist/web-ui/content/modules/389a5112332ec23a/host.js +0 -1
  250. package/dist/web-ui/content/modules/3ea13e8b3c2deaa4/panel-host.js +0 -1
  251. package/dist/web-ui/content/modules/42e5c34002fcffb7/chunk.js +0 -1
  252. package/dist/web-ui/content/modules/4a5d669c90f545cb/host.js +0 -1
  253. package/dist/web-ui/content/modules/5705d25468274534/host.js +0 -1
  254. package/dist/web-ui/content/modules/5961ab2947c86ca1/shell-file-panel-drawer.js +0 -1
  255. package/dist/web-ui/content/modules/5b6e1369df7dea69/chunk.js +0 -1
  256. package/dist/web-ui/content/modules/6787bdcf56f743a5/host.js +0 -1
  257. package/dist/web-ui/content/modules/67c39b0398186a9f/host.js +0 -1
  258. package/dist/web-ui/content/modules/6b16f35c0627bafb/chunk.js +0 -1
  259. package/dist/web-ui/content/modules/6dd4f3e2f619f7a0/chunk.js +0 -1
  260. package/dist/web-ui/content/modules/70a2e13e0ccc8549/panel.js +0 -1
  261. package/dist/web-ui/content/modules/710db590e9707f73/runtime.js +0 -338
  262. package/dist/web-ui/content/modules/7249950f8771dbeb/workspaces-panel.js +0 -4
  263. package/dist/web-ui/content/modules/87dcd516ffd9f688/composer-surface-adapter.js +0 -1
  264. package/dist/web-ui/content/modules/8b7cd7bffdfd69e7/brand-identity.js +0 -1
  265. package/dist/web-ui/content/modules/8bf9917f6c3429f6/host.js +0 -1
  266. package/dist/web-ui/content/modules/8e612d4881fa4118/home.js +0 -6
  267. package/dist/web-ui/content/modules/8e6178262ea63dec/markdown.js +0 -1
  268. package/dist/web-ui/content/modules/96f2ae970f8c576b/host.js +0 -1
  269. package/dist/web-ui/content/modules/ba89cd97627e6682/queue-view-adapter.js +0 -1
  270. package/dist/web-ui/content/modules/c351eb11e0bdec2c/sidebar-peek.js +0 -1
  271. package/dist/web-ui/content/modules/ca7ca5902a35d26e/chunk.js +0 -1
  272. package/dist/web-ui/content/modules/d5d28ec375826ef2/chunk.js +0 -3
  273. package/dist/web-ui/content/modules/d6b16dedb9011c33/chunk.js +0 -1
  274. package/dist/web-ui/content/modules/d8a7011760f61719/group-editor.js +0 -1
  275. package/dist/web-ui/content/modules/d9e2b65e9679ad39/host.js +0 -75
  276. package/dist/web-ui/content/modules/eac72f69716dba05/chunk.js +0 -1
  277. package/dist/web-ui/content/modules/ebc53dedc3945514/host.js +0 -1
  278. package/dist/web-ui/content/modules/f43da7050badf54c/host.js +0 -1
  279. package/dist/web-ui/content/modules/fb2356eced24c4cc/host-bridge.js +0 -1
@@ -1,6 +1,6 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { execFileSync } from "node:child_process";
3
- import { chmodSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync, openSync, closeSync, readSync, statSync, renameSync, rmSync } from "node:fs";
3
+ import { chmodSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync, openSync, closeSync, readSync, statSync, renameSync, rmSync, lstatSync, existsSync } from "node:fs";
4
4
  import net from "node:net";
5
5
  import { StringDecoder } from "node:string_decoder";
6
6
  import { ptyTmuxLogWriter } from "./pty-tmux-log.js";
@@ -13,22 +13,107 @@ s.removeAllListeners('data');const r=JSON.parse(b.slice(0,n));const c=spawn(r.fi
13
13
  c.on('spawn',()=>s.end(JSON.stringify({pid:c.pid})+'\\n'));c.on('error',()=>process.exit(127));
14
14
  c.on('exit',(code,signal)=>process.exit(code===null?128:code));});s.on('error',()=>process.exit(125));`;
15
15
  function quote(value) { return `'${value.replaceAll("'", "'\\''")}'`; }
16
- /** A private tmux server owns panes; attach transports are disposable, not execution owners. */
16
+ function directoryLatestMtime(directory) {
17
+ let latest = lstatSync(directory).mtimeMs;
18
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
19
+ const child = path.join(directory, entry.name);
20
+ const stat = lstatSync(child);
21
+ if (stat.isSymbolicLink())
22
+ continue;
23
+ if (stat.isDirectory())
24
+ latest = Math.max(latest, directoryLatestMtime(child));
25
+ else
26
+ latest = Math.max(latest, stat.mtimeMs);
27
+ }
28
+ return latest;
29
+ }
30
+ function directoryBytes(directory) {
31
+ let total = 0;
32
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
33
+ const child = path.join(directory, entry.name);
34
+ const stat = lstatSync(child);
35
+ if (stat.isSymbolicLink())
36
+ continue;
37
+ if (stat.isDirectory())
38
+ total += directoryBytes(child);
39
+ else
40
+ total += stat.size;
41
+ }
42
+ return total;
43
+ }
44
+ function streamSocketPath(recordName) {
45
+ const hash = createHash("sha256").update(recordName).digest("hex").slice(0, 16);
46
+ const uid = process.getuid?.() ?? 0;
47
+ return path.join("/tmp", `wand-pts-${uid}-${hash}.sock`);
48
+ }
49
+ /** The default tmux server owns panes; attach transports are disposable, not execution owners. */
17
50
  export class PtyTmuxBackend {
18
51
  directory;
19
- socket;
52
+ legacySocket;
20
53
  constructor(configPath) {
21
54
  this.directory = path.join(path.dirname(configPath), "pty-tmux");
22
55
  mkdirSync(this.directory, { recursive: true, mode: 0o700 });
23
56
  const hash = createHash("sha256").update(path.resolve(configPath)).digest("hex").slice(0, 20);
24
- this.socket = `/tmp/wand-pty-${process.getuid?.() ?? 0}-${hash}.sock`;
57
+ this.legacySocket = `/tmp/wand-pty-${process.getuid?.() ?? 0}-${hash}.sock`;
25
58
  }
26
- tmux(...args) {
27
- return execFileSync("tmux", ["-S", this.socket, "-f", "/dev/null", ...args], {
59
+ connection(record) {
60
+ if (record.socket === "default")
61
+ return ["-L", "default"];
62
+ if (record.socket !== undefined)
63
+ throw new Error("Unknown tmux terminal owner socket");
64
+ return ["-S", this.legacySocket];
65
+ }
66
+ tmux(record, ...args) {
67
+ return execFileSync("tmux", [...this.connection(record), ...args], {
28
68
  encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 5000,
29
- env: { PATH: process.env.PATH, HOME: process.env.HOME, LANG: process.env.LANG },
69
+ env: { PATH: process.env.PATH, HOME: process.env.HOME, LANG: process.env.LANG, TMUX_TMPDIR: process.env.TMUX_TMPDIR },
30
70
  }).trim();
31
71
  }
72
+ pane(record) { return record.pane ?? `=${record.name}:0.0`; }
73
+ /**
74
+ * 只认稳定身份:会话名 + 我们创建的 pane id + 写入的 owner 标记。
75
+ * `pane_pid` 在新建后可能还没就绪,不能作为归属证据;需要时从实时结果取。
76
+ */
77
+ assertOwner(record) {
78
+ if (record.socket !== "default")
79
+ return record.panePid ?? null; // 旧私有 server 没有 owner 标记。
80
+ let fields = [];
81
+ try {
82
+ fields = this.tmux(record, "list-panes", "-t", `=${record.name}`, "-F", "#{pane_id}\t#{session_name}\t#{@wand-pty-owner}\t#{pane_pid}").split("\n")
83
+ .find(entry => entry.startsWith(`${record.pane}\t`))?.split("\t") ?? [];
84
+ }
85
+ catch (error) {
86
+ if (this.isTmuxMissing(error))
87
+ return record.panePid ?? null;
88
+ throw error;
89
+ }
90
+ if (fields.length > 0 && fields[2] === record.sessionId) {
91
+ if (fields[0] !== record.pane || fields[1] !== record.name) {
92
+ throw new Error("tmux terminal ownership changed; refusing to control another pane");
93
+ }
94
+ const pid = Number(fields[3]);
95
+ return Number.isSafeInteger(pid) && pid > 0 ? pid : null;
96
+ }
97
+ // A different owner tag is conclusive; pane missing or not published is non-fatal check.
98
+ if (fields.length > 0 && fields[2]) {
99
+ throw new Error("tmux terminal ownership changed; refusing to control another pane");
100
+ }
101
+ return record.panePid ?? null;
102
+ }
103
+ isTmuxMissing(error) {
104
+ if (!error || typeof error !== "object")
105
+ return false;
106
+ const err = error;
107
+ const text = `${typeof err.stderr === "string" ? err.stderr : ""} ${err.message ?? ""}`.toLowerCase();
108
+ return text.includes("no server running") ||
109
+ text.includes("error connecting to") ||
110
+ text.includes("failed to connect") ||
111
+ text.includes("can't find session") ||
112
+ text.includes("session not found") ||
113
+ text.includes("can't find pane") ||
114
+ text.includes("can't find window") ||
115
+ text.includes("no such file or directory");
116
+ }
32
117
  forget(sessionId) {
33
118
  for (const file of readdirSync(this.directory).filter(file => file.endsWith(".json"))) {
34
119
  const record = JSON.parse(readFileSync(path.join(this.directory, file), "utf8"));
@@ -36,14 +121,120 @@ export class PtyTmuxBackend {
36
121
  if (record.exitCode === undefined)
37
122
  throw new Error("tmux termination is not acknowledged");
38
123
  try {
39
- this.tmux("kill-session", "-t", record.name);
124
+ this.assertOwner(record);
125
+ this.tmux(record, "kill-session", "-t", `=${record.name}`);
126
+ }
127
+ catch (error) {
128
+ if (!this.isTmuxMissing(error))
129
+ throw error;
40
130
  }
41
- catch { /* Already destroyed or naturally exited. */ }
42
131
  unlinkSync(path.join(this.directory, file));
43
132
  rmSync(path.join(this.directory, `${record.name}.output`), { recursive: true, force: true });
44
133
  }
45
134
  }
46
135
  }
136
+ collectGarbage(options) {
137
+ return PtyTmuxBackend.collectGarbage(path.dirname(this.directory), options);
138
+ }
139
+ static collectGarbage(configDir, options) {
140
+ const report = { removedRecords: 0, freedBytes: 0, removedSockets: 0 };
141
+ const now = options.now ?? Date.now();
142
+ const directory = path.join(configDir, "pty-tmux");
143
+ let entries = [];
144
+ try {
145
+ entries = readdirSync(directory, { withFileTypes: true });
146
+ }
147
+ catch { /* directory not created */ }
148
+ for (const entry of entries) {
149
+ if (!entry.isFile() || !entry.name.endsWith(".json"))
150
+ continue;
151
+ const jsonPath = path.join(directory, entry.name);
152
+ let record;
153
+ try {
154
+ record = JSON.parse(readFileSync(jsonPath, "utf8"));
155
+ }
156
+ catch {
157
+ continue;
158
+ }
159
+ if (options.activeSessionIds.has(record.sessionId))
160
+ continue;
161
+ if (record.exitCode === undefined)
162
+ continue;
163
+ const outputDir = path.join(directory, `${record.name}.output`);
164
+ let latestMtime = statSync(jsonPath).mtimeMs;
165
+ let bytes = statSync(jsonPath).size;
166
+ if (existsSync(outputDir)) {
167
+ try {
168
+ latestMtime = Math.max(latestMtime, directoryLatestMtime(outputDir));
169
+ bytes += directoryBytes(outputDir);
170
+ }
171
+ catch {
172
+ continue;
173
+ }
174
+ }
175
+ if (now - latestMtime < options.retentionMs)
176
+ continue;
177
+ try {
178
+ unlinkSync(jsonPath);
179
+ if (existsSync(outputDir))
180
+ rmSync(outputDir, { recursive: true, force: true });
181
+ report.removedRecords += 1;
182
+ report.freedBytes += bytes;
183
+ }
184
+ catch { /* concurrent removal or permission error */ }
185
+ }
186
+ const uid = process.getuid?.() ?? 0;
187
+ const prefix = `wand-pty-${uid}-`;
188
+ const streamPrefix = `wand-pts-${uid}-`;
189
+ let tmpEntries = [];
190
+ try {
191
+ tmpEntries = readdirSync("/tmp");
192
+ }
193
+ catch {
194
+ tmpEntries = [];
195
+ }
196
+ for (const name of tmpEntries) {
197
+ if ((!name.startsWith(prefix) && !name.startsWith(streamPrefix)) || !name.endsWith(".sock"))
198
+ continue;
199
+ const socketPath = path.join("/tmp", name);
200
+ try {
201
+ const stat = lstatSync(socketPath);
202
+ if (!stat.isSocket())
203
+ continue;
204
+ if (name.startsWith(streamPrefix)) {
205
+ const probe = net.connect(socketPath);
206
+ probe.on("error", () => {
207
+ try {
208
+ unlinkSync(socketPath);
209
+ report.removedSockets += 1;
210
+ }
211
+ catch { /* ignore */ }
212
+ });
213
+ probe.on("connect", () => { probe.destroy(); });
214
+ continue;
215
+ }
216
+ try {
217
+ execFileSync("tmux", ["-S", socketPath, "-f", "/dev/null", "ls"], {
218
+ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 1000,
219
+ env: { PATH: process.env.PATH, HOME: process.env.HOME, LANG: process.env.LANG },
220
+ });
221
+ }
222
+ catch (probeError) {
223
+ const err = probeError;
224
+ const text = `${typeof err.stderr === "string" ? err.stderr : ""} ${err.message ?? ""}`.toLowerCase();
225
+ if (text.includes("no server running") || text.includes("error connecting to") || text.includes("failed to connect")) {
226
+ try {
227
+ unlinkSync(socketPath);
228
+ report.removedSockets += 1;
229
+ }
230
+ catch { /* ignore */ }
231
+ }
232
+ }
233
+ }
234
+ catch { /* socket vanished during iteration */ }
235
+ }
236
+ return report;
237
+ }
47
238
  recover(exclude = new Set()) {
48
239
  return readdirSync(this.directory).filter(file => file.endsWith(".json"))
49
240
  .map(file => JSON.parse(readFileSync(path.join(this.directory, file), "utf8")))
@@ -61,7 +252,7 @@ export class PtyTmuxBackend {
61
252
  if (process.platform === "win32")
62
253
  throw new Error("tmux PTY requires a POSIX host");
63
254
  execFileSync("tmux", ["-V"], { stdio: "ignore" });
64
- const name = `w${randomUUID().replaceAll("-", "")}`;
255
+ const name = `wand-pty-${request.sessionId.replace(/[^a-zA-Z0-9-]/g, "-")}-${randomUUID().slice(0, 8)}`;
65
256
  const ipc = `/tmp/wand-pty-launch-${randomUUID()}.sock`;
66
257
  let accept;
67
258
  let reject;
@@ -94,15 +285,29 @@ export class PtyTmuxBackend {
94
285
  });
95
286
  await new Promise((resolve, fail) => { server.once("error", fail); server.listen(ipc, () => { chmodSync(ipc, 0o600); resolve(); }); });
96
287
  const timer = setTimeout(() => reject(new Error("tmux launch acknowledgement missing; input was not retried")), 10000);
97
- const record = { sessionId: request.sessionId, name, cols: request.cols, rows: request.rows,
288
+ const record = { sessionId: request.sessionId, name, socket: "default", cols: request.cols, rows: request.rows,
98
289
  launchMarkerToken: request.launchMarkerToken, pid: 0 };
99
290
  // Publish ownership before launch, never persist the command, environment or prompt.
100
291
  this.save(record);
101
292
  try {
102
- this.tmux("new-session", "-d", "-s", name, "-x", String(request.cols), "-y", String(request.rows), `${quote(process.execPath)} -e ${quote(bootstrap)} ${quote(ipc)}`, ";", "set-option", "-t", name, "remain-on-exit", "on", ";", "set-option", "-t", name, "status", "off");
293
+ record.pane = this.tmux(record, "new-session", "-d", "-P", "-F", "#{pane_id}", "-s", name, "-x", String(request.cols), "-y", String(request.rows), `${quote(process.execPath)} -e ${quote(bootstrap)} ${quote(ipc)}`);
294
+ if (!/^%\d+$/.test(record.pane))
295
+ throw new Error("tmux terminal pane identity is unavailable");
296
+ record.panePid = Number(this.tmux(record, "display-message", "-p", "-t", record.pane, "#{pane_pid}"));
297
+ if (!Number.isSafeInteger(record.panePid) || record.panePid <= 0)
298
+ throw new Error("Invalid tmux terminal pane PID");
299
+ this.tmux(record, "set-option", "-t", name, "@wand-pty-owner", record.sessionId);
300
+ this.tmux(record, "set-window-option", "-t", record.pane, "remain-on-exit", "on");
301
+ this.tmux(record, "set-option", "-t", name, "status", "off");
302
+ this.save(record);
103
303
  const log = path.join(this.directory, `${name}.output`);
104
304
  mkdirSync(log, { mode: 0o700 });
105
- this.tmux("pipe-pane", "-O", "-t", `${name}:0.0`, `exec ${quote(process.execPath)} -e ${quote(ptyTmuxLogWriter)} ${quote(log)}`);
305
+ const sockPath = streamSocketPath(name);
306
+ try {
307
+ unlinkSync(sockPath);
308
+ }
309
+ catch { }
310
+ this.tmux(record, "pipe-pane", "-O", "-t", record.pane, `exec ${quote(process.execPath)} -e ${quote(ptyTmuxLogWriter)} ${quote(log)} ${quote(sockPath)}`);
106
311
  ready();
107
312
  record.pid = await delivered;
108
313
  this.save(record);
@@ -147,8 +352,14 @@ export class PtyTmuxBackend {
147
352
  let segment = 0;
148
353
  let preservedMarker = "";
149
354
  const log = path.join(this.directory, `${record.name}.output`);
150
- const drain = () => {
151
- const segments = readdirSync(log).filter(file => /^\d+\.chunk$/.test(file)).map(file => Number(file.split(".")[0])).sort((a, b) => a - b);
355
+ const drain = (targetSegment, targetSize) => {
356
+ let segments = [];
357
+ try {
358
+ segments = readdirSync(log).filter(file => /^\d+\.chunk$/.test(file)).map(file => Number(file.split(".")[0])).sort((a, b) => a - b);
359
+ }
360
+ catch {
361
+ return false;
362
+ }
152
363
  if (!segments.length)
153
364
  return false;
154
365
  if (segment < segments[0]) {
@@ -165,37 +376,60 @@ export class PtyTmuxBackend {
165
376
  throw error;
166
377
  }
167
378
  }
168
- const file = path.join(log, `${segment}.chunk`);
169
- const size = statSync(file).size;
170
- if (offset >= size) {
171
- if (segment < segments[segments.length - 1]) {
172
- segment++;
173
- offset = 0;
174
- return false;
175
- }
176
- if (preservedMarker) {
177
- for (const listener of data)
178
- listener(preservedMarker);
179
- preservedMarker = "";
379
+ const maxSegment = typeof targetSegment === "number" ? Math.min(targetSegment, segments[segments.length - 1]) : segments[segments.length - 1];
380
+ while (segment <= maxSegment) {
381
+ const file = path.join(log, `${segment}.chunk`);
382
+ let size = 0;
383
+ try {
384
+ size = statSync(file).size;
385
+ }
386
+ catch {
387
+ break;
388
+ }
389
+ const limitSize = (typeof targetSegment === "number" && segment === targetSegment && typeof targetSize === "number")
390
+ ? Math.min(size, targetSize)
391
+ : size;
392
+ if (offset >= limitSize) {
393
+ if (segment < maxSegment) {
394
+ segment++;
395
+ offset = 0;
396
+ continue;
397
+ }
398
+ if (preservedMarker) {
399
+ for (const listener of data)
400
+ listener(preservedMarker);
401
+ preservedMarker = "";
402
+ }
403
+ return true;
404
+ }
405
+ const fd = openSync(file, "r");
406
+ try {
407
+ const buffer = Buffer.alloc(Math.min(limitSize - offset, 256 * 1024));
408
+ const count = readSync(fd, buffer, 0, buffer.length, offset);
409
+ offset += count;
410
+ const chunk = decoder.write(buffer.subarray(0, count));
411
+ if (chunk)
412
+ for (const listener of data)
413
+ listener(chunk);
414
+ }
415
+ finally {
416
+ closeSync(fd);
417
+ }
418
+ if (offset >= limitSize) {
419
+ if (segment < maxSegment) {
420
+ segment++;
421
+ offset = 0;
422
+ continue;
423
+ }
424
+ return true;
180
425
  }
181
- return true;
182
- }
183
- const fd = openSync(file, "r");
184
- try {
185
- const buffer = Buffer.alloc(Math.min(size - offset, 256 * 1024));
186
- const count = readSync(fd, buffer, 0, buffer.length, offset);
187
- offset += count;
188
- const chunk = decoder.write(buffer.subarray(0, count));
189
- if (chunk)
190
- for (const listener of data)
191
- listener(chunk);
192
- }
193
- finally {
194
- closeSync(fd);
195
426
  }
196
- return offset >= size && segment === segments[segments.length - 1] && !preservedMarker;
427
+ return false;
428
+ };
429
+ const inspect = () => {
430
+ this.assertOwner(record);
431
+ return this.tmux(record, "display-message", "-p", "-t", this.pane(record), "#{pane_dead}:#{pane_dead_status}:#{pane_pipe}");
197
432
  };
198
- const inspect = () => this.tmux("display-message", "-p", "-t", `${record.name}:0.0`, "#{pane_dead}:#{pane_dead_status}:#{pane_pipe}");
199
433
  const finish = (exitCode) => {
200
434
  if (exited)
201
435
  return;
@@ -203,46 +437,157 @@ export class PtyTmuxBackend {
203
437
  record.exitCode = exitCode;
204
438
  this.save(record);
205
439
  clearInterval(timer);
440
+ if (streamSocket) {
441
+ try {
442
+ streamSocket.destroy();
443
+ }
444
+ catch { }
445
+ streamSocket = null;
446
+ }
206
447
  transport?.kill();
207
448
  for (const listener of exits)
208
449
  listener(exited);
209
450
  };
210
451
  const connect = () => {
211
452
  ensureNodePtyHelperExecutable();
212
- transport = pty.spawn("tmux", ["-S", this.socket, "attach-session", "-t", record.name], {
453
+ transport = pty.spawn("tmux", [...this.connection(record), "attach-session", "-t", `=${record.name}`], {
213
454
  name: "xterm-256color", cols: record.cols, rows: record.rows,
214
- env: { PATH: process.env.PATH, HOME: process.env.HOME, TERM: "xterm-256color" },
455
+ env: { PATH: process.env.PATH, HOME: process.env.HOME, TERM: "xterm-256color", TMUX_TMPDIR: process.env.TMUX_TMPDIR },
215
456
  });
216
457
  transport.onData(() => { }); // Raw pipe-pane bytes, not screen repaint, are the PTY stream.
217
458
  transport.onExit(() => { transport = null; });
218
459
  };
460
+ const sockPath = streamSocketPath(record.name);
461
+ let streamSocket = null;
462
+ let handshaked = false;
463
+ let handshakeBuffer = "";
464
+ let connectAttempts = 0;
465
+ const checkExitAndFinish = () => {
466
+ if (exited)
467
+ return;
468
+ try {
469
+ const state = inspect();
470
+ if (state.startsWith("1:")) {
471
+ finish(Number(state.split(":")[1]) || 0);
472
+ }
473
+ else {
474
+ finish(0);
475
+ }
476
+ }
477
+ catch (error) {
478
+ if (this.isTmuxMissing(error))
479
+ finish(137);
480
+ else
481
+ finish(0);
482
+ }
483
+ };
484
+ const tryConnectStream = () => {
485
+ if (stopping || exited)
486
+ return;
487
+ const client = net.connect(sockPath);
488
+ streamSocket = client;
489
+ client.on("data", (chunk) => {
490
+ if (stopping || exited)
491
+ return;
492
+ if (!handshaked) {
493
+ handshakeBuffer += chunk.toString("utf8");
494
+ const newline = handshakeBuffer.indexOf("\n");
495
+ if (newline < 0)
496
+ return;
497
+ const line = handshakeBuffer.slice(0, newline);
498
+ const rest = handshakeBuffer.slice(newline + 1);
499
+ handshaked = true;
500
+ let targetSeg;
501
+ let targetSz;
502
+ try {
503
+ const meta = JSON.parse(line);
504
+ if (typeof meta.segment === "number")
505
+ targetSeg = meta.segment;
506
+ if (typeof meta.size === "number")
507
+ targetSz = meta.size;
508
+ }
509
+ catch { }
510
+ drain(targetSeg, targetSz);
511
+ if (rest.length > 0) {
512
+ for (const listener of data)
513
+ listener(rest);
514
+ }
515
+ return;
516
+ }
517
+ const text = decoder.write(chunk);
518
+ if (text) {
519
+ for (const listener of data)
520
+ listener(text);
521
+ }
522
+ });
523
+ client.on("end", () => {
524
+ if (stopping || exited)
525
+ return;
526
+ drain();
527
+ checkExitAndFinish();
528
+ });
529
+ client.on("error", () => {
530
+ if (streamSocket === client)
531
+ streamSocket = null;
532
+ if (stopping || exited || handshaked)
533
+ return;
534
+ if (connectAttempts++ < 30) {
535
+ setTimeout(tryConnectStream, 40).unref();
536
+ }
537
+ else {
538
+ drain();
539
+ if (record.exitCode !== undefined) {
540
+ finish(record.exitCode);
541
+ }
542
+ else {
543
+ checkExitAndFinish();
544
+ }
545
+ }
546
+ });
547
+ client.on("close", () => {
548
+ if (streamSocket === client)
549
+ streamSocket = null;
550
+ });
551
+ };
552
+ tryConnectStream();
219
553
  const timer = setInterval(() => {
554
+ if (stopping || exited || observationFailed) {
555
+ clearInterval(timer);
556
+ return;
557
+ }
220
558
  try {
221
559
  const state = record.exitCode === undefined ? inspect() : null;
222
- if (state?.startsWith("0:") && state.split(":")[2] !== "1")
560
+ if (state && !state.startsWith("0:") && !state.startsWith("1:")) {
223
561
  reportFailure();
224
- if (observationFailed)
562
+ clearInterval(timer);
225
563
  return;
226
- const drained = drain();
227
- if (record.exitCode !== undefined) {
228
- if (drained)
229
- finish(record.exitCode);
564
+ }
565
+ if (state?.startsWith("0:") && state.split(":")[2] !== "1") {
566
+ reportFailure();
567
+ clearInterval(timer);
230
568
  return;
231
569
  }
232
570
  if (state?.startsWith("1:")) {
233
- if (drained)
234
- finish(Number(state.split(":")[1]) || 0);
571
+ drain();
572
+ finish(Number(state.split(":")[1]) || 0);
573
+ clearInterval(timer);
574
+ return;
235
575
  }
236
- else if (!transport && !stopping)
576
+ if (!transport && !stopping && record.exitCode === undefined) {
237
577
  connect();
578
+ }
238
579
  }
239
580
  catch (error) {
240
- // A segment can disappear between listing and opening during ring rotation.
241
- // Other observation failures never acknowledge execution exit.
242
- if (error.code !== "ENOENT")
581
+ if (this.isTmuxMissing(error)) {
582
+ clearInterval(timer);
583
+ finish(137);
584
+ }
585
+ else if (error.code !== "ENOENT") {
243
586
  reportFailure();
587
+ clearInterval(timer);
588
+ }
244
589
  }
245
- }, 100);
590
+ }, 2000);
246
591
  timer.unref();
247
592
  if (record.exitCode === undefined)
248
593
  try {
@@ -252,31 +597,87 @@ export class PtyTmuxBackend {
252
597
  catch { /* Keep unknown published ownership visible and block new launches. */ }
253
598
  return {
254
599
  pid: record.pid,
600
+ detach() {
601
+ stopping = true;
602
+ clearInterval(timer);
603
+ if (streamSocket) {
604
+ try {
605
+ streamSocket.destroy();
606
+ }
607
+ catch { }
608
+ streamSocket = null;
609
+ }
610
+ transport?.kill();
611
+ transport = null;
612
+ data.clear();
613
+ exits.clear();
614
+ },
255
615
  write(chunk) { if (!transport || stopping || observationFailed)
256
616
  throw new Error("tmux terminal transport unavailable"); transport.write(chunk); },
257
- resize: (cols, rows) => { this.tmux("resize-window", "-t", record.name, "-x", String(cols), "-y", String(rows)); transport?.resize(cols, rows); },
617
+ resize: (cols, rows) => {
618
+ this.assertOwner(record);
619
+ this.tmux(record, "resize-window", "-t", this.pane(record), "-x", String(cols), "-y", String(rows));
620
+ transport?.resize(cols, rows);
621
+ },
258
622
  kill: () => {
259
623
  if (exited)
260
624
  return;
625
+ stopping = true;
626
+ clearInterval(timer);
627
+ if (streamSocket) {
628
+ try {
629
+ streamSocket.destroy();
630
+ }
631
+ catch { }
632
+ streamSocket = null;
633
+ }
261
634
  // Successful kill-session is the acknowledgement that the pane, not merely its client, was destroyed.
262
- const panePid = Number(this.tmux("display-message", "-p", "-t", `${record.name}:0.0`, "#{pane_pid}"));
263
- if (!Number.isSafeInteger(panePid) || panePid <= 0)
264
- throw new Error("Invalid tmux pane owner PID");
265
- const tree = execFileSync("ps", ["-axo", "pid=,ppid="], { encoding: "utf8" }).trim().split("\n")
266
- .map(line => line.trim().split(/\s+/).map(Number));
267
- const descendants = (pid) => tree.filter(row => row[1] === pid)
268
- .flatMap(row => [...descendants(row[0]), row[0]]);
269
- for (const pid of descendants(panePid)) {
635
+ let panePid = null;
636
+ try {
637
+ panePid = this.assertOwner(record);
638
+ }
639
+ catch (error) {
640
+ if (!this.isTmuxMissing(error))
641
+ throw error;
642
+ }
643
+ const killTree = (rootPid) => {
644
+ try {
645
+ const tree = execFileSync("ps", ["-axo", "pid=,ppid="], { encoding: "utf8" }).trim().split("\n")
646
+ .map(line => line.trim().split(/\s+/).map(Number));
647
+ const descendants = (pid) => tree.filter(row => row[1] === pid)
648
+ .flatMap(row => [...descendants(row[0]), row[0]]);
649
+ for (const pid of descendants(rootPid)) {
650
+ try {
651
+ process.kill(pid, "SIGKILL");
652
+ }
653
+ catch (error) {
654
+ if (error.code !== "ESRCH")
655
+ throw error;
656
+ }
657
+ }
658
+ }
659
+ catch { /* ps or process traversal unavailable */ }
660
+ };
661
+ if (panePid !== null) {
662
+ killTree(panePid);
663
+ }
664
+ else if (record.socket === undefined && record.pid > 0) {
665
+ killTree(record.pid);
270
666
  try {
271
- process.kill(pid, "SIGKILL");
667
+ process.kill(record.pid, "SIGKILL");
272
668
  }
273
669
  catch (error) {
274
670
  if (error.code !== "ESRCH")
275
671
  throw error;
276
672
  }
277
673
  }
278
- this.tmux("kill-session", "-t", record.name);
279
- stopping = true;
674
+ try {
675
+ this.tmux(record, "kill-session", "-t", `=${record.name}`);
676
+ }
677
+ catch (error) {
678
+ if (!this.isTmuxMissing(error))
679
+ throw error;
680
+ }
280
681
  finish(137);
281
682
  },
282
683
  onData(listener) { data.add(listener); return { dispose() { data.delete(listener); } }; },