@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
@@ -3,10 +3,9 @@ import { resolveAutoAssign } from "./model-auto-assign.js";
3
3
  import { withModelGroups } from "./model-group-runner.js";
4
4
  import { classifyStructuredFailure, hasStructuredExecutionProgress } from "./structured-failure.js";
5
5
  import { withAutomaticPiResources } from "./pi-auto-resources.js";
6
- import { randomUUID } from "node:crypto";
6
+ import { createHash, randomUUID } from "node:crypto";
7
7
  import { waitForSessionProcessExit } from "./session-artifact-cleanup.js";
8
8
  import { prepareSessionWorktree } from "./git-worktree.js";
9
- import { BoundedKeySet } from "./structured-bounded-keys.js";
10
9
  import { buildChildEnv } from "./env-utils.js";
11
10
  import { getErrorMessage } from "./error-utils.js";
12
11
  import { fallbackProviderModel } from "./session-model.js";
@@ -36,6 +35,7 @@ import { QoderRunner } from "./structured-qoder-adapter.js";
36
35
  import { PiRunner, isMissingPiSession } from "./structured-pi-adapter.js";
37
36
  import { PiOutputFile } from "./pi-output-file.js";
38
37
  import { PiProtocolReader } from "./pi-protocol-reader.js";
38
+ import { upsertPiAssistantSegment } from "./structured-session-manager-replay.js";
39
39
  import { structuredRunId } from "./structured-exec-host.js";
40
40
  import { defaultStructuredRunner, defaultStructuredState, isStructuredRunnerForProvider, normalizeThinkingEffort, resolveStructuredRunner } from "./structured-provider-common.js";
41
41
  import { TurnQuietHeartbeat } from "./turn-heartbeat.js";
@@ -79,19 +79,18 @@ const EMPLOYEE_CANDIDATE_STARTUP_TIMEOUT_MS = 180_000;
79
79
  // second on the event loop.
80
80
  const STREAM_SAVE_THROTTLE_MS = 1_000;
81
81
  const DETACHED_RECOVERY_RETRY_MS = 500;
82
- /** Upper bound on remembered (session, idempotency key) pairs; oldest half is dropped past it. */
83
- const IDEMPOTENCY_KEY_LIMIT = 1024;
84
82
  /** The turn recorded for one user prompt; an open AskUserQuestion is answered by it as a tool_result. */
85
- function buildUserTurn(prompt, pendingAsk, createdAt, requestId) {
83
+ function buildUserTurn(prompt, pendingAsk, createdAt, requestId, messageId) {
84
+ const identity = { ...(requestId ? { requestId } : {}), ...(messageId ? { messageId } : {}) };
86
85
  if (pendingAsk) {
87
86
  return {
88
87
  role: "user",
89
- ...(requestId ? { requestId } : {}),
88
+ ...identity,
90
89
  createdAt,
91
90
  content: [{ type: "tool_result", tool_use_id: pendingAsk.id, content: prompt, is_error: false }],
92
91
  };
93
92
  }
94
- return { role: "user", ...(requestId ? { requestId } : {}), createdAt, content: [{ type: "text", text: prompt }] };
93
+ return { role: "user", ...identity, createdAt, content: [{ type: "text", text: prompt }] };
95
94
  }
96
95
  export class StructuredSessionManager {
97
96
  storage;
@@ -122,19 +121,12 @@ export class StructuredSessionManager {
122
121
  interruptedImmediateStart = new Set();
123
122
  employeeInputAdmission = null;
124
123
  inputSequence = 0;
124
+ dispatchingAcceptedInputs = new Set();
125
125
  /** Last wall-clock time (ms) a streaming checkpoint reached SQLite. */
126
126
  lastStreamSaveAt = new Map();
127
127
  lastPiCheckpointAt = new Map();
128
128
  streamCheckpointTimers = new Map();
129
129
  streamCheckpointDirty = new Map();
130
- /**
131
- * Idempotency keys we've already accepted, mapped to their wall-clock timestamp.
132
- * Android WebView 在进程恢复时偶尔会重发上一个未收到响应的 POST(HTTP/2 stream
133
- * reset 等场景),客户端 JS 没有重试逻辑也拦不住。这里用 (sessionId, key) 永
134
- * 久去重,重复就抛错让前端弹 toast 提示,**不**做任何处理。timestamp 仅用于
135
- * map 大小溢出时按时间裁剪。
136
- */
137
- seenIdempotencyKeys = new BoundedKeySet(IDEMPOTENCY_KEY_LIMIT);
138
130
  relayHandlers = new Map();
139
131
  systemCallDrainReleases = new Set();
140
132
  emitEvent = null;
@@ -163,6 +155,7 @@ export class StructuredSessionManager {
163
155
  unsupportedStructuredProviders = new Set();
164
156
  /** Structured CLI runs that were mid-flight when the previous web process died. */
165
157
  pendingRecoveryIds = [];
158
+ detachedInventoryReconciled = false;
166
159
  detachedRecoveryPromise = null;
167
160
  detachedRecoveryRetryTimer = null;
168
161
  detachedRecoveryFailureLogged = false;
@@ -309,8 +302,9 @@ export class StructuredSessionManager {
309
302
  && !(this.coreRestartDrains > 0 && this.usesCoreEngine(session)))
310
303
  .flatMap(session => {
311
304
  const metadata = queuedInputScheduling(session);
312
- const userIndex = metadata.findIndex(item => item.source === "user");
313
- const index = userIndex >= 0 ? userIndex : 0;
305
+ const available = (item) => !item.acceptanceId || !this.dispatchingAcceptedInputs.has(item.acceptanceId);
306
+ const userIndex = metadata.findIndex(item => available(item) && item.source === "user");
307
+ const index = userIndex >= 0 ? userIndex : metadata.findIndex(available);
314
308
  // 同一会话只暴露最高来源档的队首,保留用户显式 reorder 的意义。
315
309
  return metadata[index] ? [{ session, index, scheduling: metadata[index] }] : [];
316
310
  });
@@ -449,7 +443,7 @@ export class StructuredSessionManager {
449
443
  // ---------------------------------------------------------------------------
450
444
  /** Called once after startup wiring; transient daemon failures retry automatically. */
451
445
  recoverDetachedRuns() {
452
- if (this.disposed || this.execHost?.persistent !== true || this.pendingRecoveryIds.length === 0) {
446
+ if (this.disposed || this.execHost?.persistent !== true || (this.detachedInventoryReconciled && this.pendingRecoveryIds.length === 0)) {
453
447
  return Promise.resolve();
454
448
  }
455
449
  if (this.detachedRecoveryPromise)
@@ -485,6 +479,23 @@ export class StructuredSessionManager {
485
479
  }
486
480
  if (this.disposed)
487
481
  return;
482
+ // The durable UI status may have failed before the actual process exited.
483
+ // Inventory is authoritative: recover those live owners too, never spawn a
484
+ // replacement merely because a previous observer wrote idle/failed.
485
+ for (const run of runs) {
486
+ if (!run.runId.startsWith("structured:"))
487
+ continue;
488
+ const id = run.runId.slice("structured:".length);
489
+ const session = this.sessions.get(id);
490
+ if (!session || this.usesCoreEngine(session) || this.unsupportedStructuredProviders.has(id)
491
+ || this.pendingRecoveryIds.includes(id) || this.pendingRunnerExecutions.has(id))
492
+ continue;
493
+ if (run.status === "running")
494
+ this.pendingRecoveryIds.push(id);
495
+ else
496
+ this.execHost.forgetRun(run.runId);
497
+ }
498
+ this.detachedInventoryReconciled = true;
488
499
  const ids = [...this.pendingRecoveryIds];
489
500
  const byRunId = new Map(runs.map((run) => [run.runId, run]));
490
501
  for (const sessionId of ids) {
@@ -495,10 +506,13 @@ export class StructuredSessionManager {
495
506
  }
496
507
  const state = byRunId.get(structuredRunId(sessionId));
497
508
  if (!state) {
509
+ if (session.structuredState?.activeRequestId)
510
+ this.settleNativeInputs(sessionId, session.structuredState.activeRequestId, { blocks: [], result: "", sessionId: session.claudeSessionId }, true);
511
+ const reconciled = this.requireSession(sessionId);
498
512
  // Inventory was read successfully, so this is a genuinely lost run,
499
513
  // not a temporary control-plane outage. Commit the interrupted view.
500
514
  const interrupted = {
501
- ...session,
515
+ ...reconciled,
502
516
  status: "idle",
503
517
  exitCode: null,
504
518
  endedAt: null,
@@ -539,7 +553,7 @@ export class StructuredSessionManager {
539
553
  this.scheduleDetachedRecovery();
540
554
  }
541
555
  scheduleDetachedRecovery() {
542
- if (this.disposed || this.pendingRecoveryIds.length === 0 || this.detachedRecoveryRetryTimer)
556
+ if (this.disposed || (this.detachedInventoryReconciled && this.pendingRecoveryIds.length === 0) || this.detachedRecoveryRetryTimer)
543
557
  return;
544
558
  this.detachedRecoveryRetryTimer = setTimeout(() => {
545
559
  this.detachedRecoveryRetryTimer = null;
@@ -654,7 +668,9 @@ export class StructuredSessionManager {
654
668
  content: this.compactContentBlocks([...turnState.blocks], turnState.result),
655
669
  usage: turnState.usage,
656
670
  };
657
- const messages = upsertAssistantMessage(current.messages, turn, false, !replayingHistory);
671
+ const messages = turnState.inputMessageId
672
+ ? upsertPiAssistantSegment(current.messages, turn, turnState.inputMessageId)
673
+ : upsertAssistantMessage(current.messages, turn, false, !replayingHistory);
658
674
  const patched = {
659
675
  ...current,
660
676
  claudeSessionId: turnState.sessionId ?? current.claudeSessionId,
@@ -690,6 +706,10 @@ export class StructuredSessionManager {
690
706
  let lastStdoutSeq = initialState.stdoutSeq;
691
707
  let lastStderrSeq = initialState.stderrSeq;
692
708
  const feedLine = (line) => {
709
+ if (resumed.provider === "pi" && line.trim().startsWith("{")) {
710
+ const event = JSON.parse(line);
711
+ this.handleNativeInputEvent(sessionId, requestId, event, processor.state);
712
+ }
693
713
  onApplied(processor.feed(line, replayingHistory ? undefined : new Date().toISOString()));
694
714
  };
695
715
  const piReader = resumed.provider === "pi" && resumed.structuredState?.engine !== "core"
@@ -715,10 +735,11 @@ export class StructuredSessionManager {
715
735
  return;
716
736
  fileFailed = true;
717
737
  stdoutFile?.dispose();
718
- processor.primaryError = `Pi 实时事件文件读写失败:${error.message};送达结果未知,不自动重试。`;
738
+ processor.primaryError = `CLI 输出观察失败:${error.message};送达结果未知,不自动重试。`;
719
739
  this.finalizeRecoveredRun(sessionId, requestId, processor, replayView, {
720
740
  exitCode: 1, signal: null, stderr: processor.stderr, stdoutTruncated: true,
721
741
  });
742
+ runningHandle?.interrupt();
722
743
  flushEmit();
723
744
  };
724
745
  const captureStdout = (text) => {
@@ -880,6 +901,10 @@ export class StructuredSessionManager {
880
901
  processor.stderr += event.data;
881
902
  });
882
903
  runningHandle.onExit((event) => {
904
+ if (event.processExited === false) {
905
+ fileFailure(new Error("CLI 输出连接中断,正在等待原进程停止;新消息会排队。"));
906
+ return;
907
+ }
883
908
  if (fileFailed)
884
909
  return;
885
910
  try {
@@ -911,6 +936,7 @@ export class StructuredSessionManager {
911
936
  this.releasePendingRunnerExecution(sessionId, execution);
912
937
  else
913
938
  this.execHost?.forgetRun(structuredRunId(sessionId));
939
+ this.settleNativeInputs(sessionId, requestId, processor.state);
914
940
  const current = this.sessions.get(sessionId);
915
941
  if (!current)
916
942
  return;
@@ -1095,8 +1121,6 @@ export class StructuredSessionManager {
1095
1121
  return;
1096
1122
  this.unacceptedTeamStarts.delete(sessionId);
1097
1123
  this.recoveredLogicalRequests.delete(sessionId);
1098
- const keyPrefix = `${sessionId}:`;
1099
- this.seenIdempotencyKeys.deleteMatching((key) => key.startsWith(keyPrefix));
1100
1124
  }
1101
1125
  /**
1102
1126
  * Arms the per-session "no protocol output yet" watchdog. It is unref'd so it can
@@ -1136,8 +1160,10 @@ export class StructuredSessionManager {
1136
1160
  this.clearStreamingCheckpoint(snapshot.id);
1137
1161
  }
1138
1162
  checkpointSessionMessages(snapshot, includeOutput = false) {
1139
- this.storage.updateSessionRuntimeMetadata(snapshot);
1140
- this.storage.checkpointSessionMessages(snapshot.id, snapshot.messages ?? [], snapshot.structuredState, includeOutput ? snapshot.output : undefined);
1163
+ this.storage.transaction(() => {
1164
+ this.storage.updateSessionRuntimeMetadata(snapshot);
1165
+ this.storage.checkpointSessionMessages(snapshot.id, snapshot.messages ?? [], snapshot.structuredState, includeOutput ? snapshot.output : undefined);
1166
+ });
1141
1167
  }
1142
1168
  list() {
1143
1169
  return Array.from(this.sessions.values())
@@ -1504,6 +1530,138 @@ export class StructuredSessionManager {
1504
1530
  if (this.disposed)
1505
1531
  throw new Error("StructuredSessionManager has been disposed.");
1506
1532
  let session = this.requireSession(id);
1533
+ // Scheduler/interrupt continuation consumes an already accepted item, never
1534
+ // creates a second acceptance or replays an uncertain external execution.
1535
+ if (opts?.scheduling?.acceptanceId)
1536
+ return this.sendAcceptedMessage(id, input, opts);
1537
+ const key = opts?.idempotencyKey;
1538
+ if (key !== undefined && (!key || Buffer.byteLength(key) > 256)) {
1539
+ throw Object.assign(new Error("幂等键必须为 1 至 256 字节。"), { code: "invalid_idempotency_key" });
1540
+ }
1541
+ const signature = createHash("sha256").update(JSON.stringify(opts?.promotion
1542
+ ? { v: 1, action: "promote", index: opts.promotion.index, expectedText: opts.promotion.expectedText ?? null }
1543
+ : { v: 1, action: "message", input: input.trim(), interrupt: !!opts?.interrupt,
1544
+ preserveQueue: !!opts?.interrupt && !!opts?.preserveQueue, source: opts?.source ?? "task" })).digest("hex");
1545
+ const previous = key ? this.storage.getStructuredInputReceipt(id, key, signature) : null;
1546
+ if (previous) {
1547
+ opts?.onAccepted?.(previous);
1548
+ return session;
1549
+ }
1550
+ let prompt = input.trim();
1551
+ if (opts?.promotion) {
1552
+ this.requireRecoveryControl(id);
1553
+ this.assertNativeInputMutable(session);
1554
+ if (this.coreRestartDrains > 0 && this.usesCoreEngine(session)) {
1555
+ throw new Error("服务正在排空 Wand Agent,排队消息已保留,请稍后重试立即发送。");
1556
+ }
1557
+ const { index, expectedText } = opts.promotion;
1558
+ const queue = session.queuedMessages ?? [];
1559
+ if (!Number.isInteger(index) || index < 0 || index >= queue.length)
1560
+ throw new Error("队列中没有该条消息(可能已被处理)。");
1561
+ if (expectedText !== undefined && queue[index] !== expectedText)
1562
+ throw new Error("排队消息已变化,请按最新顺序重试。");
1563
+ prompt = queue[index];
1564
+ session = { ...session, queuedMessages: queue.filter((_, at) => at !== index),
1565
+ queuedInputScheduling: queuedInputScheduling(session).filter((_, at) => at !== index) };
1566
+ }
1567
+ if (!prompt) {
1568
+ opts?.onAccepted?.();
1569
+ return session;
1570
+ }
1571
+ if (this.unsupportedStructuredProviders.has(id))
1572
+ throw new Error("会话使用的 provider 已不受支持,不能继续发送。");
1573
+ const acceptanceId = randomUUID();
1574
+ const acceptedAt = isoNow();
1575
+ const receipt = key
1576
+ ? { accepted: true, acceptanceId, sessionId: id, idempotencyKey: key, acceptedAt } : undefined;
1577
+ const scheduling = { ...(opts?.scheduling
1578
+ ?? { source: opts?.source ?? "task", enqueuedAt: acceptedAt, sequence: ++this.inputSequence,
1579
+ teamRequestStarted: opts?.teamRequestStarted }), acceptanceId };
1580
+ const relay = this.relayHandlerFor(session);
1581
+ const childActive = this.pendingRunnerExecutions.has(id) || this.pendingRecoveryIds.includes(id) || this.nativeProcessExits.has(id);
1582
+ const interrupt = !!opts?.interrupt && !!session.structuredState?.inFlight && childActive;
1583
+ if (interrupt) {
1584
+ this.requireRecoveryControl(id);
1585
+ this.assertNativeInputMutable(session);
1586
+ }
1587
+ const mustQueue = !relay && ((this.coreRestartDrains > 0 && this.usesCoreEngine(session))
1588
+ || this.autoAssignPending.has(id) || (childActive && !interrupt)
1589
+ || (!!session.employeeId && !!this.employeeInputAdmission
1590
+ && !this.employeeInputAdmission.canStart(session, scheduling, opts?.immediateStart)));
1591
+ if (mustQueue && !opts?.promotion) {
1592
+ if (isDuplicateStructuredQueueInput(session, prompt)) {
1593
+ throw Object.assign(new Error("与上一条消息相同,已忽略,不会加入排队。"), { code: "duplicate_queued_message" });
1594
+ }
1595
+ if ((session.queuedMessages?.length ?? 0) >= 10)
1596
+ throw new Error("排队消息已满(最多 10 条),请等待当前消息处理完成。");
1597
+ }
1598
+ let queue = session.queuedMessages ?? [];
1599
+ let metadata = queuedInputScheduling(session);
1600
+ if (interrupt && !opts?.preserveQueue) {
1601
+ queue = [];
1602
+ metadata = [];
1603
+ }
1604
+ if (interrupt && opts?.preserveQueue && !opts.queueAlreadyRemoved && !opts.promotion) {
1605
+ const at = queue.indexOf(prompt);
1606
+ if (at >= 0) {
1607
+ queue = queue.filter((_, index) => index !== at);
1608
+ metadata = metadata.filter((_, index) => index !== at);
1609
+ }
1610
+ }
1611
+ const accepted = relay
1612
+ ? { ...session, messages: [...(session.messages ?? []), buildUserTurn(prompt, undefined, acceptedAt)] }
1613
+ : { ...session, queuedMessages: interrupt ? [prompt, ...queue] : [...queue, prompt],
1614
+ queuedInputScheduling: interrupt ? [scheduling, ...metadata] : [...metadata, scheduling] };
1615
+ // Persist before publishing state, generating a topic, interrupting or starting a runner.
1616
+ const inserted = receipt ? this.storage.acceptStructuredInput(accepted, signature, receipt, !!relay)
1617
+ : this.storage.transaction(() => {
1618
+ if (relay)
1619
+ this.checkpointSessionMessages(accepted);
1620
+ else
1621
+ this.storage.updateSessionRuntimeMetadata(accepted);
1622
+ return true;
1623
+ });
1624
+ if (!inserted) {
1625
+ opts?.onAccepted?.(this.storage.getStructuredInputReceipt(id, key, signature));
1626
+ return this.requireSession(id);
1627
+ }
1628
+ this.sessions.set(id, accepted);
1629
+ opts?.onAccepted?.(receipt);
1630
+ if (relay)
1631
+ return this.relayUserMessage(id, prompt, relay, true);
1632
+ if (mustQueue && !interrupt) {
1633
+ this.emitStructuredSnapshot(accepted);
1634
+ if (accepted.employeeId)
1635
+ this.employeeInputAdmission?.schedule(accepted.employeeId);
1636
+ return accepted;
1637
+ }
1638
+ return this.sendAcceptedMessage(id, prompt, { ...opts, scheduling, queueAlreadyRemoved: true,
1639
+ // Accepted replacement is now itself a durable queue item; the original
1640
+ // clear/preserve intent was applied in the acceptance transaction above.
1641
+ preserveQueue: interrupt ? true : opts?.preserveQueue });
1642
+ }
1643
+ withoutAcceptedQueueItem(session, scheduling) {
1644
+ if (!scheduling?.acceptanceId)
1645
+ return session;
1646
+ const at = queuedInputScheduling(session).findIndex(item => item.acceptanceId === scheduling.acceptanceId);
1647
+ if (at < 0)
1648
+ return session;
1649
+ return { ...session, queuedMessages: session.queuedMessages?.filter((_, index) => index !== at),
1650
+ queuedInputScheduling: queuedInputScheduling(session).filter((_, index) => index !== at) };
1651
+ }
1652
+ sendAcceptedMessage(id, input, opts) {
1653
+ const acceptanceId = opts?.scheduling?.acceptanceId;
1654
+ if (acceptanceId)
1655
+ this.dispatchingAcceptedInputs.add(acceptanceId);
1656
+ return this.runAcceptedMessage(id, input, opts).finally(() => {
1657
+ if (acceptanceId)
1658
+ this.dispatchingAcceptedInputs.delete(acceptanceId);
1659
+ });
1660
+ }
1661
+ async runAcceptedMessage(id, input, opts) {
1662
+ if (this.disposed)
1663
+ throw new Error("StructuredSessionManager has been disposed.");
1664
+ let session = this.withoutAcceptedQueueItem(this.requireSession(id), opts?.scheduling);
1507
1665
  const prompt = input.trim();
1508
1666
  if (!prompt)
1509
1667
  return session;
@@ -1515,8 +1673,6 @@ export class StructuredSessionManager {
1515
1673
  const relay = this.relayHandlerFor(session);
1516
1674
  if (relay)
1517
1675
  return await this.relayUserMessage(id, prompt, relay);
1518
- if (opts?.idempotencyKey)
1519
- this.claimIdempotencyKey(id, opts.idempotencyKey);
1520
1676
  // The drain owns Core admission, not CLI admission. Check before any
1521
1677
  // asynchronous selection/topic work; accepted Core inputs remain durable.
1522
1678
  if (this.coreRestartDrains > 0 && this.usesCoreEngine(session))
@@ -1525,6 +1681,8 @@ export class StructuredSessionManager {
1525
1681
  // 「智能分配」结算窗口:还没有 runner 归属,新输入先排队,不能另起一轮。
1526
1682
  if (this.autoAssignPending.has(id))
1527
1683
  return this.enqueueInput(session, prompt, false, scheduling);
1684
+ if (!session.structuredState?.inFlight)
1685
+ this.assertNativeInputMutable(session);
1528
1686
  const inFlight = this.resolveInFlightSend(id, session, prompt, opts);
1529
1687
  if ("result" in inFlight)
1530
1688
  return inFlight.result;
@@ -1533,10 +1691,10 @@ export class StructuredSessionManager {
1533
1691
  return this.enqueueInput(session, prompt, false, scheduling);
1534
1692
  }
1535
1693
  if (isAutoAssignSelector(session.selectedModel)) {
1536
- const assigned = await this.settleAutoAssignForSend(id, session, prompt);
1694
+ const assigned = await this.settleAutoAssignForSend(id, session, prompt, scheduling.acceptanceId);
1537
1695
  if ("result" in assigned)
1538
1696
  return assigned.result;
1539
- session = this.requireSession(id);
1697
+ session = this.withoutAcceptedQueueItem(this.requireSession(id), scheduling);
1540
1698
  // Selection yielded: a drain or another input may now own admission.
1541
1699
  // This request preceded inputs accepted during its selection window.
1542
1700
  if (this.coreRestartDrains > 0 && this.usesCoreEngine(session))
@@ -1563,7 +1721,7 @@ export class StructuredSessionManager {
1563
1721
  // a durable transcript identity. Team startup correlation IDs are private
1564
1722
  // lifecycle tokens and must never be copied into persisted/public messages.
1565
1723
  const transcriptRequestId = session.automationId?.startsWith("wand-task-comment:") ? requestId : undefined;
1566
- const userTurn = buildUserTurn(prompt, pendingAsk, userTurnAt, transcriptRequestId);
1724
+ const userTurn = buildUserTurn(prompt, pendingAsk, userTurnAt, transcriptRequestId, session.provider === "pi" && session.structuredState?.engine !== "core" ? randomUUID() : undefined);
1567
1725
  this.unacceptedTeamStarts.delete(id);
1568
1726
  if (this.unsupportedStructuredProviders.has(id)) {
1569
1727
  throw new Error(`会话使用的 provider 已不受支持,不能继续发送。`);
@@ -1584,10 +1742,10 @@ export class StructuredSessionManager {
1584
1742
  lastError: null,
1585
1743
  }),
1586
1744
  };
1745
+ this.checkpointSessionMessages(updated);
1587
1746
  this.sessions.set(id, updated);
1588
1747
  if (updated.automationId?.startsWith("ai-team:"))
1589
1748
  (opts?.teamRequestStarted ?? scheduling.teamRequestStarted)?.(id, requestId);
1590
- this.checkpointSessionMessages(updated);
1591
1749
  this.emitStructuredSnapshot(updated);
1592
1750
  if (transcriptRequestId)
1593
1751
  this.storage.bindTaskCommentResultStart(id, transcriptRequestId, updated.messages.length - 1);
@@ -1616,8 +1774,11 @@ export class StructuredSessionManager {
1616
1774
  }
1617
1775
  }
1618
1776
  /** 群聊不起 CLI:先落下用户这句话,再交给转发方;转发失败以提示行告诉用户。 */
1619
- async relayUserMessage(id, prompt, relay) {
1620
- const posted = this.appendRelayTurns(id, [{ role: "user", content: [{ type: "text", text: prompt }] }]);
1777
+ async relayUserMessage(id, prompt, relay, accepted = false) {
1778
+ const posted = accepted ? this.requireSession(id)
1779
+ : this.appendRelayTurns(id, [{ role: "user", content: [{ type: "text", text: prompt }] }]);
1780
+ if (accepted)
1781
+ this.emitStructuredSnapshot(posted);
1621
1782
  try {
1622
1783
  await relay(id, prompt);
1623
1784
  }
@@ -1628,29 +1789,18 @@ export class StructuredSessionManager {
1628
1789
  }
1629
1790
  return this.sessions.get(id) ?? posted;
1630
1791
  }
1631
- /** Rejects a repeated (session, key) pair; otherwise remembers it (bounded). */
1632
- claimIdempotencyKey(id, idempotencyKey) {
1633
- const mapKey = `${id}:${idempotencyKey}`;
1634
- if (this.seenIdempotencyKeys.has(mapKey)) {
1635
- const err = new Error("检测到重复发送,已拦截。");
1636
- err.code = "duplicate_idempotency_key";
1637
- throw err;
1638
- }
1639
- // Bounded: BoundedKeySet drops the oldest half past IDEMPOTENCY_KEY_LIMIT.
1640
- this.seenIdempotencyKeys.add(mapKey);
1641
- }
1642
1792
  /**
1643
1793
  * While a turn is in flight, decide what this send does: recover a turn whose
1644
1794
  * runner is gone, interrupt the current turn, or queue the input. Returns the
1645
1795
  * session to continue with, or the value sendMessage should return now.
1646
1796
  */
1647
1797
  resolveInFlightSend(id, session, prompt, opts) {
1648
- if (!session.structuredState?.inFlight)
1798
+ if (!session.structuredState?.inFlight && !this.nativeProcessExits.has(id))
1649
1799
  return { session };
1650
1800
  const runnerExecution = this.pendingRunnerExecutions.get(id);
1651
1801
  // interrupt() only requests cancellation; completion can settle later.
1652
1802
  // Treat runner-map ownership as the authoritative in-flight state.
1653
- const childActive = Boolean(runnerExecution) || this.pendingRecoveryIds.includes(id);
1803
+ const childActive = Boolean(runnerExecution) || this.pendingRecoveryIds.includes(id) || this.nativeProcessExits.has(id);
1654
1804
  if (!childActive) {
1655
1805
  const recovered = {
1656
1806
  ...session,
@@ -1662,8 +1812,8 @@ export class StructuredSessionManager {
1662
1812
  activeRequestId: null,
1663
1813
  },
1664
1814
  };
1665
- this.sessions.set(id, recovered);
1666
- this.storage.updateSessionRuntimeMetadata(recovered);
1815
+ // The next start/queue checkpoint persists this transition together with
1816
+ // the accepted item. Do not remove its durable queue entry in a prior write.
1667
1817
  return { session: recovered };
1668
1818
  }
1669
1819
  if (!opts?.interrupt)
@@ -1705,10 +1855,10 @@ export class StructuredSessionManager {
1705
1855
  * 「智能分配」:只有真拿到这一轮提示词才能结算成本次使用的分组。结算后钉在会话上,
1706
1856
  * 后续轮次用同一个分组(不在会话中途换模型);客户端选择器同步显示实际分组。
1707
1857
  */
1708
- async settleAutoAssignForSend(id, session, prompt) {
1858
+ async settleAutoAssignForSend(id, session, prompt, acceptanceId) {
1709
1859
  if (!isAutoAssignSelector(session.selectedModel))
1710
1860
  return { session };
1711
- const token = { cancelled: false };
1861
+ const token = { cancelled: false, acceptanceId };
1712
1862
  this.autoAssignPending.set(id, token);
1713
1863
  let assigned;
1714
1864
  try {
@@ -1854,6 +2004,9 @@ export class StructuredSessionManager {
1854
2004
  // stop() or a newer turn may have invalidated this execution while its
1855
2005
  // runner was unwinding. A stale rejection must never fail the new turn.
1856
2006
  if (!this.isCurrentRequest(id, requestId)) {
2007
+ if (!this.pendingRunnerExecutions.has(id) && !this.nativeProcessExits.has(id)
2008
+ && !current.structuredState?.inFlight)
2009
+ setImmediate(() => { void this.flushNextQueuedMessage(id); });
1857
2010
  return current;
1858
2011
  }
1859
2012
  const retry = this.retryEmployeeCandidate(id, session, prompt, error, requestId);
@@ -1904,6 +2057,7 @@ export class StructuredSessionManager {
1904
2057
  */
1905
2058
  reorderQueuedMessages(sessionId, order, expectedQueue) {
1906
2059
  const session = this.requireSession(sessionId);
2060
+ this.assertNativeInputMutable(session);
1907
2061
  const queue = session.queuedMessages ?? [];
1908
2062
  this.assertExpectedQueue(queue, expectedQueue);
1909
2063
  if (!Array.isArray(order) || order.length !== queue.length) {
@@ -1930,6 +2084,7 @@ export class StructuredSessionManager {
1930
2084
  /** Edit only the pending item still identified by both index and original text. */
1931
2085
  editQueuedMessage(sessionId, index, expectedText, text) {
1932
2086
  const session = this.requireSession(sessionId);
2087
+ this.assertNativeInputMutable(session);
1933
2088
  const queue = session.queuedMessages ?? [];
1934
2089
  if (!Number.isInteger(index) || index < 0 || index >= queue.length || queue[index] !== expectedText) {
1935
2090
  throw new Error("排队消息已变化,请刷新后重试。");
@@ -1948,6 +2103,7 @@ export class StructuredSessionManager {
1948
2103
  /** Remove a single queued message only while index and text still identify the same item. */
1949
2104
  deleteQueuedMessage(sessionId, index, expectedText) {
1950
2105
  const session = this.requireSession(sessionId);
2106
+ this.assertNativeInputMutable(session);
1951
2107
  const queue = session.queuedMessages ?? [];
1952
2108
  if (!Number.isInteger(index) || index < 0 || index >= queue.length) {
1953
2109
  throw new Error("队列中没有该条消息(可能已被处理)。");
@@ -1971,48 +2127,12 @@ export class StructuredSessionManager {
1971
2127
  * on the server prevents clients from re-sending the text while the original
1972
2128
  * queue entry remains available for the automatic flush path.
1973
2129
  */
1974
- async promoteQueuedMessage(sessionId, index, expectedText, idempotencyKey) {
1975
- const session = this.requireSession(sessionId);
1976
- this.requireRecoveryControl(sessionId);
1977
- if (this.coreRestartDrains > 0 && this.usesCoreEngine(session)) {
1978
- throw new Error("服务正在排空 Wand Agent,排队消息已保留,请稍后重试立即发送。");
1979
- }
1980
- if (idempotencyKey && this.seenIdempotencyKeys.has(`${sessionId}:${idempotencyKey}`)) {
1981
- return session;
1982
- }
1983
- const queue = session.queuedMessages ?? [];
1984
- if (!Number.isInteger(index) || index < 0 || index >= queue.length) {
1985
- throw new Error("队列中没有该条消息(可能已被处理)。");
1986
- }
1987
- if (expectedText !== undefined && queue[index] !== expectedText) {
1988
- throw new Error("排队消息已变化,请按最新顺序重试。");
1989
- }
1990
- const prompt = queue[index];
1991
- const remaining = queue.slice(0, index).concat(queue.slice(index + 1));
1992
- const inFlight = session.status === "running" && session.structuredState?.inFlight === true;
1993
- const updated = {
1994
- ...session,
1995
- queuedMessages: remaining,
1996
- queuedInputScheduling: queuedInputScheduling(session).filter((_, at) => at !== index),
1997
- };
1998
- this.sessions.set(sessionId, updated);
1999
- this.storage.updateSessionRuntimeMetadata(updated);
2000
- this.emitStructuredSnapshot(updated);
2001
- try {
2002
- return await this.sendMessage(sessionId, prompt, {
2003
- interrupt: inFlight,
2004
- preserveQueue: inFlight,
2005
- queueAlreadyRemoved: true,
2006
- immediateStart: true,
2007
- idempotencyKey,
2008
- source: "user",
2009
- });
2010
- }
2011
- catch {
2012
- // Once the item has been promoted it must not return to the queue: the
2013
- // send path may have already persisted its user turn before a runner error.
2014
- return this.requireSession(sessionId);
2015
- }
2130
+ async promoteQueuedMessage(sessionId, index, expectedText, idempotencyKey, onAccepted) {
2131
+ return this.sendMessage(sessionId, "", {
2132
+ onAccepted,
2133
+ interrupt: true, preserveQueue: true, queueAlreadyRemoved: true,
2134
+ immediateStart: true, idempotencyKey, source: "user", promotion: { index, expectedText },
2135
+ });
2016
2136
  }
2017
2137
  assertExpectedQueue(queue, expected) {
2018
2138
  if (expected !== undefined && (!Array.isArray(expected) || expected.length !== queue.length
@@ -2023,6 +2143,7 @@ export class StructuredSessionManager {
2023
2143
  /** Clear all queued messages. No-op when queue is already empty. */
2024
2144
  clearQueuedMessages(sessionId, expectedQueue) {
2025
2145
  const session = this.requireSession(sessionId);
2146
+ this.assertNativeInputMutable(session);
2026
2147
  this.assertExpectedQueue(session.queuedMessages ?? [], expectedQueue);
2027
2148
  if (!session.queuedMessages || session.queuedMessages.length === 0) {
2028
2149
  return session;
@@ -2236,10 +2357,12 @@ export class StructuredSessionManager {
2236
2357
  const updated = { ...session, piSettings: updatedSettings };
2237
2358
  // Do not publish a setting that did not reach durable storage.
2238
2359
  this.storage.updateSessionRuntimeMetadata(updated);
2239
- // 记住这次设置:新建 Pi CLI 会话以它起步。只合并本次真正改动的字段——
2360
+ // 只记住交互会话的选择,自动化/启动会话的临时工具限制不能污染用户默认。
2361
+ // automationId 也用于用户单聊关联,不能用它判断会话来源。
2362
+ // 新建 Pi CLI 会话以默认起步。只合并本次真正改动的字段——
2240
2363
  // 从旧默认起步的会话不能用自己的 stale 值(例如 autoResources=false)覆盖后来在别处开启的默认。
2241
2364
  // SDK 会话表达不出资源/CodeMode,不写回默认。
2242
- if (resolution.engine === "cli") {
2365
+ if (resolution.engine === "cli" && (session.sessionSource ?? "interactive") === "interactive") {
2243
2366
  const defaults = this.storage.getPiSessionDefaults() ?? defaultPiCliSessionSettings(this.config.harness?.compaction?.enabled ?? true);
2244
2367
  this.storage.setPiSessionDefaults(mergePiSessionDefaults(defaults, patch));
2245
2368
  }
@@ -2397,8 +2520,13 @@ export class StructuredSessionManager {
2397
2520
  }
2398
2521
  stop(id) {
2399
2522
  this.unacceptedTeamStarts.delete(id);
2400
- const session = this.requireSession(id);
2523
+ let session = this.requireSession(id);
2401
2524
  this.requireRecoveryControl(id);
2525
+ if (session.structuredState?.activeRequestId) {
2526
+ this.settleNativeInputs(id, session.structuredState.activeRequestId, { blocks: [], result: "", sessionId: session.claudeSessionId }, true);
2527
+ session = this.requireSession(id);
2528
+ }
2529
+ session = this.withoutAcceptedQueueItem(session, this.autoAssignPending.get(id));
2402
2530
  // 结算中的智能分配还没有 runner:置位取消,结算回来后不得再启动。
2403
2531
  this.disposeSession(id, "run");
2404
2532
  // Clearing activeRequestId is the generation barrier: late data/close callbacks
@@ -2506,7 +2634,26 @@ export class StructuredSessionManager {
2506
2634
  releasePendingRunnerExecution(sessionId, execution, forgetRun = true) {
2507
2635
  if (this.pendingRunnerExecutions.get(sessionId) !== execution)
2508
2636
  return false;
2637
+ const exits = this.nativeProcessExits.get(sessionId);
2638
+ if (forgetRun && exits?.size) {
2639
+ // A protocol/observation result is not an OS exit. Keep the handle usable
2640
+ // for stop and prevent a new input from colliding with the live tmux owner.
2641
+ void Promise.all([...exits]).then(() => {
2642
+ if (!this.releasePendingRunnerExecution(sessionId, execution, forgetRun))
2643
+ return;
2644
+ const current = this.sessions.get(sessionId);
2645
+ if (!this.disposed && (this.execHost?.persistent || current?.status === "idle")
2646
+ && current?.status !== "running" && !current?.structuredState?.inFlight) {
2647
+ void this.flushNextQueuedMessage(sessionId);
2648
+ }
2649
+ }, () => { });
2650
+ return false;
2651
+ }
2509
2652
  this.pendingRunnerExecutions.delete(sessionId);
2653
+ const current = this.sessions.get(sessionId);
2654
+ if (forgetRun && !this.disposed && current?.status === "idle" && !current.structuredState?.inFlight) {
2655
+ setImmediate(() => { void this.flushNextQueuedMessage(sessionId); });
2656
+ }
2510
2657
  // A failed recovery attach must retain the daemon record for the next retry.
2511
2658
  if (forgetRun) {
2512
2659
  const exits = this.nativeProcessExits.get(sessionId);
@@ -2532,7 +2679,7 @@ export class StructuredSessionManager {
2532
2679
  beginTurn(sessionId, state) {
2533
2680
  const at = isoNow();
2534
2681
  this.turnHeartbeat.observe(sessionId);
2535
- return { ...state, turnStartedAt: at, lastActivityAt: at };
2682
+ return { ...state, inputMode: state.engine === "core" ? "steer" : "turn-end", turnStartedAt: at, lastActivityAt: at };
2536
2683
  }
2537
2684
  /**
2538
2685
  * 领养 daemon 里仍在飞的回合:起点是重启前那一轮的真实时刻,不能改写成重连时刻;
@@ -2607,14 +2754,126 @@ export class StructuredSessionManager {
2607
2754
  data,
2608
2755
  });
2609
2756
  }
2757
+ /** Native Pi claims one durable queue item; the reservation survives until its transcript commit. */
2758
+ reserveNativeInput(sessionId, requestId, identity) {
2759
+ const current = this.currentSessionForRequest(sessionId, requestId);
2760
+ if (!current || this.disposed || this.interruptedWith.has(sessionId))
2761
+ return null;
2762
+ const scheduling = queuedInputScheduling(current);
2763
+ const index = scheduling.findIndex(item => item.source === "user");
2764
+ const text = index >= 0 ? current.queuedMessages?.[index] : undefined;
2765
+ if (text === undefined || scheduling.some(item => item.handoff))
2766
+ return null;
2767
+ const reserved = scheduling.map((item, at) => at === index ? { ...item, handoff: identity } : item);
2768
+ const updated = { ...current, queuedInputScheduling: reserved };
2769
+ this.storage.updateSessionRuntimeMetadata(updated);
2770
+ this.sessions.set(sessionId, updated);
2771
+ this.emitStructuredSnapshot(updated);
2772
+ return text;
2773
+ }
2774
+ /** A claim is the native transcript commit: persist the boundary and remove only that reservation. */
2775
+ commitNativeInput(sessionId, requestId, offer, state) {
2776
+ const current = this.currentSessionForRequest(sessionId, requestId);
2777
+ if (!current)
2778
+ return;
2779
+ const scheduling = queuedInputScheduling(current);
2780
+ const index = scheduling.findIndex(item => item.handoff?.id === offer.id);
2781
+ const alreadyCommitted = current.messages?.some(turn => turn.role === "user" && turn.messageId === offer.id);
2782
+ if (state.inputMessageId === offer.id || (index < 0 && !alreadyCommitted)
2783
+ || (index >= 0 && current.queuedMessages?.[index] !== offer.text))
2784
+ return;
2785
+ const preceding = state.blocks.length || state.result
2786
+ ? this.buildCompletedAssistantMessages(current, state) : [...(current.messages ?? [])];
2787
+ const committed = {
2788
+ ...current,
2789
+ queuedMessages: current.queuedMessages?.filter((_, at) => at !== index),
2790
+ queuedInputScheduling: scheduling.filter((_, at) => at !== index),
2791
+ messages: preceding.some(turn => turn.messageId === offer.id) ? preceding
2792
+ : [...preceding, buildUserTurn(offer.text, undefined, isoNow(), undefined, offer.id)],
2793
+ };
2794
+ this.saveAuthoritativeSession(committed);
2795
+ state.blocks = [];
2796
+ state.result = "";
2797
+ state.usage = undefined;
2798
+ state.inputMessageId = offer.id;
2799
+ this.sessions.set(sessionId, committed);
2800
+ this.emitStructuredSnapshot(committed);
2801
+ }
2802
+ handleNativeInputEvent(sessionId, requestId, event, state) {
2803
+ if (event.type === "wand_input_boundary" && typeof event.id === "string") {
2804
+ this.execHost?.offerPiInput?.(structuredRunId(sessionId), event.id, identity => this.reserveNativeInput(sessionId, requestId, identity));
2805
+ }
2806
+ if (event.type === "wand_input_ready" && typeof event.id === "string")
2807
+ state.inputMessageId = event.id;
2808
+ if (event.type === "wand_input_boundary") {
2809
+ const current = this.currentSessionForRequest(sessionId, requestId);
2810
+ if (current && current.structuredState?.inputMode !== "steer") {
2811
+ const updated = { ...current, structuredState: { ...current.structuredState, inputMode: "steer" } };
2812
+ this.sessions.set(sessionId, updated);
2813
+ this.storage.updateSessionRuntimeMetadata(updated);
2814
+ this.emitStructuredSnapshot(updated);
2815
+ }
2816
+ }
2817
+ if (event.type !== "wand_input_committed")
2818
+ return;
2819
+ const offer = event.message;
2820
+ if (!offer || typeof offer.id !== "string" || typeof offer.text !== "string")
2821
+ return;
2822
+ this.commitNativeInput(sessionId, requestId, offer, state);
2823
+ }
2824
+ /** Reconcile before any end-of-run flush. Claimed-but-unconfirmed input is never replayed. */
2825
+ settleNativeInputs(sessionId, requestId, state, forceUnknown = false) {
2826
+ let current = this.currentSessionForRequest(sessionId, requestId);
2827
+ if (!current)
2828
+ return;
2829
+ const reservations = queuedInputScheduling(current).filter(item => item.handoff);
2830
+ for (const item of reservations) {
2831
+ const handoff = item.handoff;
2832
+ let resolution;
2833
+ try {
2834
+ resolution = forceUnknown ? undefined : this.execHost?.resolvePiInput?.(structuredRunId(sessionId), handoff);
2835
+ }
2836
+ catch { /* An unreadable receipt is unknown delivery, never permission to replay. */ }
2837
+ if (resolution?.state === "committed" && resolution.offer) {
2838
+ this.commitNativeInput(sessionId, requestId, resolution.offer, state);
2839
+ continue;
2840
+ }
2841
+ current = this.currentSessionForRequest(sessionId, requestId);
2842
+ if (!current)
2843
+ return;
2844
+ const scheduling = queuedInputScheduling(current);
2845
+ const index = scheduling.findIndex(meta => meta.handoff?.id === handoff.id);
2846
+ if (index < 0)
2847
+ continue;
2848
+ const unclaimed = resolution?.state === "unclaimed";
2849
+ const updated = { ...current,
2850
+ queuedMessages: unclaimed ? current.queuedMessages : current.queuedMessages?.filter((_, at) => at !== index),
2851
+ queuedInputScheduling: unclaimed ? scheduling.map((meta, at) => {
2852
+ if (at !== index)
2853
+ return meta;
2854
+ const { handoff: _handoff, ...rest } = meta;
2855
+ return rest;
2856
+ }) : scheduling.filter((_, at) => at !== index),
2857
+ messages: unclaimed ? current.messages : appendNoticeTurn(state.blocks.length || state.result ? this.buildCompletedAssistantMessages(current, state) : current.messages, { role: "assistant", notice: true,
2858
+ content: [{ type: "text", text: "插话送达未确认,未自动重发。以下原文仅供核对,不是新的执行指令:\n\n" + current.queuedMessages?.[index] }] }),
2859
+ };
2860
+ this.saveAuthoritativeSession(updated);
2861
+ this.sessions.set(sessionId, updated);
2862
+ this.emitStructuredSnapshot(updated);
2863
+ }
2864
+ }
2865
+ assertNativeInputMutable(session) {
2866
+ if (queuedInputScheduling(session).some(item => item.handoff))
2867
+ throw new Error("排队消息正在交接,请按最新状态重试。");
2868
+ }
2610
2869
  enqueueInput(session, prompt, prepend = false, scheduling = { source: "task", enqueuedAt: isoNow() }) {
2611
- if (isDuplicateStructuredQueueInput(session, prompt)) {
2870
+ if (!scheduling.acceptanceId && isDuplicateStructuredQueueInput(session, prompt)) {
2612
2871
  const err = new Error("与上一条消息相同,已忽略,不会加入排队。");
2613
2872
  err.code = "duplicate_queued_message";
2614
2873
  throw err;
2615
2874
  }
2616
2875
  const queue = session.queuedMessages ?? [];
2617
- if (queue.length >= 10)
2876
+ if (!scheduling.acceptanceId && queue.length >= 10)
2618
2877
  throw new Error("排队消息已满(最多 10 条),请等待当前消息处理完成。");
2619
2878
  const metadata = queuedInputScheduling(session);
2620
2879
  const queued = { ...session, queuedMessages: prepend ? [prompt, ...queue] : [...queue, prompt],
@@ -2630,10 +2889,11 @@ export class StructuredSessionManager {
2630
2889
  if (this.disposed)
2631
2890
  return;
2632
2891
  const current = this.sessions.get(sessionId);
2633
- if (!current || (current.queuedMessages?.length ?? 0) === 0) {
2892
+ if (!current || queuedInputScheduling(current).some(item => item.handoff) || (current.queuedMessages?.length ?? 0) === 0) {
2634
2893
  return;
2635
2894
  }
2636
- if (current.structuredState?.inFlight || (this.coreRestartDrains > 0 && this.usesCoreEngine(current))) {
2895
+ if (current.structuredState?.inFlight || this.nativeProcessExits.has(sessionId) || this.pendingRunnerExecutions.has(sessionId)
2896
+ || (this.coreRestartDrains > 0 && this.usesCoreEngine(current))) {
2637
2897
  return;
2638
2898
  }
2639
2899
  if (current.employeeId && this.employeeInputAdmission && !fromEmployeeScheduler) {
@@ -2641,28 +2901,25 @@ export class StructuredSessionManager {
2641
2901
  return;
2642
2902
  }
2643
2903
  const nextInput = current.queuedMessages?.[index];
2644
- const scheduling = queuedInputScheduling(current)[index];
2645
- const restQueue = current.queuedMessages.filter((_, at) => at !== index);
2646
- if (!nextInput) {
2904
+ const scheduling = { ...queuedInputScheduling(current)[index],
2905
+ acceptanceId: queuedInputScheduling(current)[index]?.acceptanceId ?? randomUUID() };
2906
+ if (!nextInput)
2647
2907
  return;
2648
- }
2649
- const nextSession = {
2650
- ...current,
2651
- queuedMessages: restQueue,
2652
- queuedInputScheduling: queuedInputScheduling(current).filter((_, at) => at !== index),
2653
- };
2654
- this.sessions.set(sessionId, nextSession);
2655
- this.storage.updateSessionRuntimeMetadata(nextSession);
2656
- this.emitStructuredSnapshot(nextSession);
2908
+ // Legacy queues acquire an identity before dispatch too. The queue item stays
2909
+ // durable until its user turn and removal commit together.
2910
+ const identified = { ...current, queuedInputScheduling: queuedInputScheduling(current).map((item, at) => at === index ? scheduling : item) };
2911
+ this.storage.updateSessionRuntimeMetadata(identified);
2912
+ this.sessions.set(sessionId, identified);
2657
2913
  try {
2658
- await this.sendMessage(sessionId, nextInput, { scheduling });
2914
+ await this.sendAcceptedMessage(sessionId, nextInput, { scheduling });
2659
2915
  }
2660
2916
  catch (error) {
2661
2917
  console.error("[WAND] flushNextQueuedMessage failed:", error);
2662
2918
  // Only an explicit pre-execution rejection permits replay. A persisted
2663
2919
  // runner failure or arbitrary exception can mean delivery is unknown.
2664
2920
  const afterFail = this.sessions.get(sessionId);
2665
- if (afterFail && error instanceof UnacceptedStructuredInputError) {
2921
+ const stillQueued = afterFail && queuedInputScheduling(afterFail).some(item => item.acceptanceId === scheduling.acceptanceId);
2922
+ if (afterFail && error instanceof UnacceptedStructuredInputError && !stillQueued) {
2666
2923
  const rescued = {
2667
2924
  ...afterFail,
2668
2925
  queuedMessages: [nextInput, ...(afterFail.queuedMessages ?? [])],
@@ -2672,7 +2929,7 @@ export class StructuredSessionManager {
2672
2929
  this.storage.updateSessionRuntimeMetadata(rescued);
2673
2930
  this.emitStructuredSnapshot(rescued);
2674
2931
  }
2675
- else if (afterFail && (afterFail.messages?.length ?? 0) <= (current.messages?.length ?? 0)) {
2932
+ else if (afterFail && !stillQueued && (afterFail.messages?.length ?? 0) <= (current.messages?.length ?? 0)) {
2676
2933
  // If dispatch failed before a user turn was checkpointed, retain the
2677
2934
  // original visibly for reconciliation, never as another sendable item.
2678
2935
  const retained = { ...afterFail, messages: appendNoticeTurn(afterFail.messages, {
@@ -2721,7 +2978,9 @@ export class StructuredSessionManager {
2721
2978
  usage: turnState.usage,
2722
2979
  ...(turnState.resourceSelection ? { resourceSelection: turnState.resourceSelection } : {}),
2723
2980
  };
2724
- messages = upsertAssistantMessage(current.messages, turn);
2981
+ messages = turnState.inputMessageId
2982
+ ? upsertPiAssistantSegment(current.messages, turn, turnState.inputMessageId)
2983
+ : upsertAssistantMessage(current.messages, turn);
2725
2984
  }
2726
2985
  const patched = {
2727
2986
  ...current,
@@ -2767,6 +3026,9 @@ export class StructuredSessionManager {
2767
3026
  session,
2768
3027
  prompt,
2769
3028
  env: buildChildEnv(this.config.inheritEnv !== false),
3029
+ ...(provider === "pi" && session.structuredState?.engine !== "core" ? {
3030
+ onPiInputEvent: (event, state) => this.handleNativeInputEvent(sessionId, requestId, event, state),
3031
+ } : {}),
2770
3032
  ...(provider === "pi" && session.structuredState?.engine === "core" ? {
2771
3033
  takeSteering: (turnState) => {
2772
3034
  const current = this.currentSessionForRequest(sessionId, requestId);
@@ -2834,6 +3096,10 @@ export class StructuredSessionManager {
2834
3096
  let result;
2835
3097
  try {
2836
3098
  result = await execution.completion;
3099
+ // Wrappers may resolve their exit promise a microtask after completion.
3100
+ // Only an explicit observation-only result keeps ownership after return.
3101
+ if (result.processExited !== false && execution.exited)
3102
+ await execution.exited;
2837
3103
  }
2838
3104
  finally {
2839
3105
  turnCompleted = true;
@@ -2871,6 +3137,8 @@ export class StructuredSessionManager {
2871
3137
  primaryError: result.primaryError,
2872
3138
  ...(provider === "codex" ? { codexErrors: result.errors, codexTurnFailed: result.primaryError } : {}),
2873
3139
  });
3140
+ if (provider === "pi")
3141
+ this.settleNativeInputs(sessionId, requestId, result.state);
2874
3142
  const current = this.currentSessionForRequest(sessionId, requestId);
2875
3143
  if (!current)
2876
3144
  return;
@@ -3018,7 +3286,16 @@ export class StructuredSessionManager {
3018
3286
  usage: turnState.usage,
3019
3287
  ...(turnState.resourceSelection ? { resourceSelection: turnState.resourceSelection } : {}),
3020
3288
  };
3021
- return upsertAssistantMessage(current.messages, assistantTurn, true, stampNewTools);
3289
+ if (turnState.inputMessageId && !turnState.blocks.length && !turnState.result && current.messages?.at(-1)?.role === "user")
3290
+ return [...current.messages];
3291
+ if (turnState.inputMessageId)
3292
+ return upsertPiAssistantSegment(current.messages, assistantTurn, turnState.inputMessageId, true);
3293
+ // A delivery notice is independent of the preceding assistant segment.
3294
+ const messages = [...(current.messages ?? [])];
3295
+ const notices = [];
3296
+ while (messages.at(-1)?.notice)
3297
+ notices.unshift(messages.pop());
3298
+ return [...upsertAssistantMessage(messages, assistantTurn, true, stampNewTools), ...notices];
3022
3299
  }
3023
3300
  /**
3024
3301
  * Rebuild the trailing assistant turn when the replayed log was truncated.