@maudecode/talaria-web 1.20.0 → 1.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. package/_release.json +8 -8
  2. package/dist/api/automation-router.d.ts +313 -8
  3. package/dist/api/automation-router.js +21 -1
  4. package/dist/api/automation-router.js.map +1 -1
  5. package/dist/api/chat-router.d.ts +412 -15
  6. package/dist/api/chat-router.js +22 -170
  7. package/dist/api/chat-router.js.map +1 -1
  8. package/dist/api/git-router.d.ts +50 -0
  9. package/dist/api/git-router.js +17 -4
  10. package/dist/api/git-router.js.map +1 -1
  11. package/dist/api/router.d.ts +1 -1
  12. package/dist/api/sessions-router.d.ts +1138 -29
  13. package/dist/api/sessions-router.js +58 -41
  14. package/dist/api/sessions-router.js.map +1 -1
  15. package/dist/api/sse-routes.js +4 -2
  16. package/dist/api/sse-routes.js.map +1 -1
  17. package/dist/api/tools-router.d.ts +481 -29
  18. package/dist/api/tools-router.js +34 -9
  19. package/dist/api/tools-router.js.map +1 -1
  20. package/dist/http/context.d.ts +2 -0
  21. package/dist/http/context.js.map +1 -1
  22. package/dist/redact.d.ts +2 -0
  23. package/dist/redact.js +17 -2
  24. package/dist/redact.js.map +1 -1
  25. package/dist/runtime.js +36 -8
  26. package/dist/runtime.js.map +1 -1
  27. package/dist/server.js +1 -0
  28. package/dist/server.js.map +1 -1
  29. package/dist/sessions/admission.d.ts +21 -0
  30. package/dist/sessions/admission.js +176 -0
  31. package/dist/sessions/admission.js.map +1 -0
  32. package/dist/sessions/anchor.d.ts +2 -0
  33. package/dist/sessions/anchor.js +4 -4
  34. package/dist/sessions/anchor.js.map +1 -1
  35. package/dist/sessions/background-tasks.d.ts +19 -1
  36. package/dist/sessions/background-tasks.js +36 -6
  37. package/dist/sessions/background-tasks.js.map +1 -1
  38. package/dist/sessions/events.d.ts +3 -0
  39. package/dist/sessions/events.js +4 -0
  40. package/dist/sessions/events.js.map +1 -1
  41. package/dist/sessions/gateway-sessions.d.ts +50 -2
  42. package/dist/sessions/gateway-sessions.js +144 -11
  43. package/dist/sessions/gateway-sessions.js.map +1 -1
  44. package/dist/sessions/merge.d.ts +1 -0
  45. package/dist/sessions/merge.js +7 -2
  46. package/dist/sessions/merge.js.map +1 -1
  47. package/dist/sessions/prefix-index.d.ts +8 -0
  48. package/dist/sessions/prefix-index.js +14 -3
  49. package/dist/sessions/prefix-index.js.map +1 -1
  50. package/dist/sessions/prompt-queues.d.ts +36 -0
  51. package/dist/sessions/prompt-queues.js +318 -0
  52. package/dist/sessions/prompt-queues.js.map +1 -0
  53. package/dist/sessions/relay.d.ts +12 -1
  54. package/dist/sessions/relay.js +75 -17
  55. package/dist/sessions/relay.js.map +1 -1
  56. package/dist/sessions/service.d.ts +26 -1
  57. package/dist/sessions/service.js +200 -6
  58. package/dist/sessions/service.js.map +1 -1
  59. package/dist/sessions/session.js +1 -0
  60. package/dist/sessions/session.js.map +1 -1
  61. package/dist/sessions/storage/json-documents.js +1 -0
  62. package/dist/sessions/storage/json-documents.js.map +1 -1
  63. package/dist/sessions/store.d.ts +1 -0
  64. package/dist/sessions/store.js +1 -0
  65. package/dist/sessions/store.js.map +1 -1
  66. package/dist/sessions/streams.d.ts +2 -0
  67. package/dist/sessions/streams.js.map +1 -1
  68. package/dist/sessions/tool-display.d.ts +8 -1
  69. package/dist/sessions/tool-display.js +39 -4
  70. package/dist/sessions/tool-display.js.map +1 -1
  71. package/dist/sessions/turn.d.ts +4 -1
  72. package/dist/sessions/turn.js +20 -13
  73. package/dist/sessions/turn.js.map +1 -1
  74. package/dist/tools/gateway-restart.d.ts +1 -1
  75. package/dist/tools/kanban.d.ts +10 -1
  76. package/dist/tools/kanban.js +73 -1
  77. package/dist/tools/kanban.js.map +1 -1
  78. package/dist/tools/mcp.d.ts +3 -1
  79. package/dist/tools/mcp.js +73 -0
  80. package/dist/tools/mcp.js.map +1 -1
  81. package/dist/tools/memory.d.ts +28 -0
  82. package/dist/tools/memory.js +120 -14
  83. package/dist/tools/memory.js.map +1 -1
  84. package/dist/tools/update-notifications.d.ts +12 -10
  85. package/dist/tools/update-notifications.js +124 -14
  86. package/dist/tools/update-notifications.js.map +1 -1
  87. package/dist/tools/updates.d.ts +22 -3
  88. package/dist/tools/updates.js +118 -20
  89. package/dist/tools/updates.js.map +1 -1
  90. package/dist/workspace/managed-buffers.d.ts +58 -0
  91. package/dist/workspace/managed-buffers.js +76 -0
  92. package/dist/workspace/managed-buffers.js.map +1 -0
  93. package/dist/workspace/rollback.js +1 -1
  94. package/dist/workspace/rollback.js.map +1 -1
  95. package/package.json +2 -2
  96. package/sidecar/agent_dependency.json +4 -4
  97. package/sidecar/agent_support.json +20 -8
  98. package/sidecar/talaria_sidecar/__init__.py +1 -1
  99. package/sidecar/talaria_sidecar/agent_mutation.py +87 -9
  100. package/sidecar/talaria_sidecar/agent_services.py +83 -23
  101. package/sidecar/talaria_sidecar/methods/__init__.py +2 -2
  102. package/sidecar/talaria_sidecar/methods/dashboard_plugins.py +4 -4
  103. package/sidecar/talaria_sidecar/methods/gateway_sessions.py +109 -19
  104. package/sidecar/talaria_sidecar/methods/kanban.py +48 -0
  105. package/sidecar/talaria_sidecar/methods/managed_buffers.py +110 -0
  106. package/sidecar/talaria_sidecar/methods/mcp.py +55 -0
  107. package/sidecar/talaria_sidecar/methods/outbound_drafts.py +165 -0
  108. package/static/dist/FILES.txt +127 -128
  109. package/static/dist/assets/AppShell-c5989995.js +8 -0
  110. package/static/dist/assets/Button-nuaikybd.js +1 -0
  111. package/static/dist/assets/ChatPage-bfg4khph.js +6 -0
  112. package/static/dist/assets/ConnectTargets-k5wo9yo8.js +1 -0
  113. package/static/dist/assets/{Dialog-hta5utr3.js → Dialog-o8vo2aws.js} +1 -1
  114. package/static/dist/assets/{ExtensionRoute-zrhjn0wm.js → ExtensionRoute-l1yjrso4.js} +1 -1
  115. package/static/dist/assets/Field-j7o8cfps.js +1 -0
  116. package/static/dist/assets/HubRoute-efnwe4xz.js +2 -0
  117. package/static/dist/assets/Markdown-clqf5m22.js +2 -0
  118. package/static/dist/assets/{MarkdownRenderer-fbswsgld.js → MarkdownRenderer-ccgqrza3.js} +18 -18
  119. package/static/dist/assets/OAuthSignIn-h7bjaqp6.js +1 -0
  120. package/static/dist/assets/{ResourceSwitch-odi6dy2o.js → ResourceSwitch-oai2zqaj.js} +1 -1
  121. package/static/dist/assets/RightPanel-ho161a45.js +1 -0
  122. package/static/dist/assets/{Select-kr07928w.js → Select-d1g5a8bm.js} +1 -1
  123. package/static/dist/assets/SessionListPanel-i7kaokhi.js +1 -0
  124. package/static/dist/assets/SettingsLayout-lnecm87w.js +1 -0
  125. package/static/dist/assets/{States-icab19yg.js → States-easntmry.js} +1 -1
  126. package/static/dist/assets/TerminalPanel-lxhwd4jl.js +16 -0
  127. package/static/dist/assets/Toaster-l4sciy8t.js +1 -0
  128. package/static/dist/assets/_app-mqexqbg8.js +1 -0
  129. package/static/dist/assets/{_app.ext._extensionId-na62087t.js → _app.ext._extensionId-n797jp72.js} +1 -1
  130. package/static/dist/assets/{_app.index-jypgdqoi.js → _app.index-ne43mdw2.js} +1 -1
  131. package/static/dist/assets/{_app.insights-kabejdtm.js → _app.insights-fqonovb4.js} +1 -1
  132. package/static/dist/assets/{_app.kanban-m19kfcmx.js → _app.kanban-kysliv19.js} +1 -1
  133. package/static/dist/assets/{_app.logs-iw4m96sz.js → _app.logs-febut82r.js} +1 -1
  134. package/static/dist/assets/{_app.memory-jq5frwcg.js → _app.memory-ha86oj1c.js} +1 -1
  135. package/static/dist/assets/{_app.profiles-kwa3m7rp.js → _app.profiles-cnu3gxxq.js} +1 -1
  136. package/static/dist/assets/{_app.session._sessionId-o5lyeipj.js → _app.session._sessionId-ifzjpcgn.js} +1 -1
  137. package/static/dist/assets/{_app.settings-ckl8hdll.js → _app.settings-nmakdiaf.js} +1 -1
  138. package/static/dist/assets/_app.settings._section-djglnjmi.js +3 -0
  139. package/static/dist/assets/{_app.skills-n016rihd.js → _app.skills-f3wad8yo.js} +1 -1
  140. package/static/dist/assets/_app.tasks-l10py1c0.js +1 -0
  141. package/static/dist/assets/{_app.todos-btew5v2s.js → _app.todos-bml1m3r1.js} +1 -1
  142. package/static/dist/assets/{_app.workspaces-lnfk95ch.js → _app.workspaces-f3d066lx.js} +1 -1
  143. package/static/dist/assets/{abnfDiagram-VCTEODGH-dttheegb.js → abnfDiagram-VCTEODGH-i53jc4yp.js} +1 -1
  144. package/static/dist/assets/{architecture-7GRP2DOG-ic435919.js → architecture-7GRP2DOG-e3bv5m6f.js} +1 -1
  145. package/static/dist/assets/{architectureDiagram-5GKGNRK7-8r3glgiq.js → architectureDiagram-5GKGNRK7-pakekxu2.js} +1 -1
  146. package/static/dist/assets/{archive-il7c6053.js → archive-ihqi7z2x.js} +1 -1
  147. package/static/dist/assets/{blockDiagram-I7D4REHJ-jenyos6c.js → blockDiagram-I7D4REHJ-bc6bo1n6.js} +1 -1
  148. package/static/dist/assets/{c4Diagram-7LVT6UL2-nzlx7yc4.js → c4Diagram-7LVT6UL2-btvwk2qx.js} +1 -1
  149. package/static/dist/assets/channel-jn4f8443.js +1 -0
  150. package/static/dist/assets/{chunk-4HAMMTFA-g0cru63b.js → chunk-4HAMMTFA-dlwab11c.js} +1 -1
  151. package/static/dist/assets/{chunk-75Z2AOVW-7rkswkjw.js → chunk-75Z2AOVW-dp5gh7i8.js} +1 -1
  152. package/static/dist/assets/{chunk-DU6HZSFF-fn73jzdo.js → chunk-DU6HZSFF-bi06zl1c.js} +1 -1
  153. package/static/dist/assets/{chunk-F27PBJKO-oey8e72m.js → chunk-F27PBJKO-kvv1rnlh.js} +1 -1
  154. package/static/dist/assets/{chunk-GMAD6QVW-l2a32wiy.js → chunk-GMAD6QVW-ji0rx9by.js} +1 -1
  155. package/static/dist/assets/{chunk-GVQU2GXP-o9dwzh8h.js → chunk-GVQU2GXP-nmz0j82p.js} +1 -1
  156. package/static/dist/assets/{chunk-IMKFNOWR-eblwj20b.js → chunk-IMKFNOWR-kmi1qx88.js} +1 -1
  157. package/static/dist/assets/{chunk-L3NEJ4N5-o2mufspb.js → chunk-L3NEJ4N5-xhb7mebj.js} +1 -1
  158. package/static/dist/assets/{chunk-OSK3NFVY-ijp9pw29.js → chunk-OSK3NFVY-5a9sgscq.js} +1 -1
  159. package/static/dist/assets/{chunk-P2QGCYS3-hs9hqwl4.js → chunk-P2QGCYS3-dfezp4vu.js} +1 -1
  160. package/static/dist/assets/{chunk-POPQ4Y6H-bp9rsqez.js → chunk-POPQ4Y6H-gd2vq3c0.js} +1 -1
  161. package/static/dist/assets/{chunk-PWAF6VOD-brkprov6.js → chunk-PWAF6VOD-wvw2nksf.js} +1 -1
  162. package/static/dist/assets/{chunk-SHT3W25Y-gmpv0pxz.js → chunk-SHT3W25Y-f58q97px.js} +1 -1
  163. package/static/dist/assets/{chunk-SVP7TREG-oldm08fu.js → chunk-SVP7TREG-abgwpyz9.js} +1 -1
  164. package/static/dist/assets/{chunk-TICWLB2K-ooqilfv7.js → chunk-TICWLB2K-h4sbs8ds.js} +1 -1
  165. package/static/dist/assets/classDiagram-ZZMXUADV-dzrw66se.js +1 -0
  166. package/static/dist/assets/classDiagram-v2-VYDZK3BY-dzrw66se.js +1 -0
  167. package/static/dist/assets/completionAudio-ce86msbu.js +1 -0
  168. package/static/dist/assets/{copy-jo006coq.js → copy-m9h56qa8.js} +1 -1
  169. package/static/dist/assets/{cynefin-OW5HDTMX-d47op7qs.js → cynefin-OW5HDTMX-g081vffs.js} +1 -1
  170. package/static/dist/assets/{cynefinDiagram-5FMLGOSQ-doa7594y.js → cynefinDiagram-5FMLGOSQ-odxvjesk.js} +1 -1
  171. package/static/dist/assets/{dagre-GXQ25YYZ-os768dp4.js → dagre-GXQ25YYZ-cpdwzjs4.js} +1 -1
  172. package/static/dist/assets/{diagram-S7CK7UJ4-fyi81nxd.js → diagram-S7CK7UJ4-clv5mxvu.js} +1 -1
  173. package/static/dist/assets/{diagram-UQ7AKVKN-j3exggaq.js → diagram-UQ7AKVKN-m2ia1mbf.js} +1 -1
  174. package/static/dist/assets/{diagram-VSXAHHWV-omo1en2l.js → diagram-VSXAHHWV-ea6zv636.js} +1 -1
  175. package/static/dist/assets/{diagram-VX7I27RA-oro2cyu0.js → diagram-VX7I27RA-oqlef685.js} +1 -1
  176. package/static/dist/assets/{diagram-Z3DM3KII-byt9vdb7.js → diagram-Z3DM3KII-kjubcbwu.js} +1 -1
  177. package/static/dist/assets/{dist-etpoyscq.js → dist-carq1azj.js} +4 -4
  178. package/static/dist/assets/{ebnfDiagram-PWID7BFC-ee3haugh.js → ebnfDiagram-PWID7BFC-fg0zri3e.js} +1 -1
  179. package/static/dist/assets/{erDiagram-RLTQ6QDP-d1e93yff.js → erDiagram-RLTQ6QDP-eb7pmk3x.js} +1 -1
  180. package/static/dist/assets/{eventmodeling-NTZA5JFV-i1ihb40c.js → eventmodeling-NTZA5JFV-gvi1fxhn.js} +1 -1
  181. package/static/dist/assets/flowDiagram-HODETNUW-abekab15.js +1 -0
  182. package/static/dist/assets/folder-plus-ksaons88.js +1 -0
  183. package/static/dist/assets/{ganttDiagram-EL5Y4UJY-m7g0jud4.js → ganttDiagram-EL5Y4UJY-jmisowrc.js} +1 -1
  184. package/static/dist/assets/{gitGraph-4MIJSDKK-hzdj07yy.js → gitGraph-4MIJSDKK-m7kt3u6e.js} +1 -1
  185. package/static/dist/assets/{gitGraphDiagram-WWUBYQGX-dm9agbna.js → gitGraphDiagram-WWUBYQGX-nq7vfzh2.js} +1 -1
  186. package/static/dist/assets/{highlighted-body-KPVGNVTW-mmgz2czu.js → highlighted-body-KPVGNVTW-gcu017ay.js} +1 -1
  187. package/static/dist/assets/{host-fr6lni1p.js → host-ipp4e84c.js} +1 -1
  188. package/static/dist/assets/{index-73649w7o.js → index-d6vbxlra.js} +7 -4
  189. package/static/dist/assets/index-ey6qo0zx.css +2 -0
  190. package/static/dist/assets/{info-A6RAGUB7-lll1dqmm.js → info-A6RAGUB7-idazwfqe.js} +1 -1
  191. package/static/dist/assets/{infoDiagram-27XIBGKW-k7esc639.js → infoDiagram-27XIBGKW-bb9jh8gl.js} +1 -1
  192. package/static/dist/assets/{ishikawaDiagram-5VMMS53U-lgr6k9wo.js → ishikawaDiagram-5VMMS53U-nqc132rv.js} +1 -1
  193. package/static/dist/assets/{journeyDiagram-3NMN7TZE-ndmbxuqk.js → journeyDiagram-3NMN7TZE-fpvnhy44.js} +1 -1
  194. package/static/dist/assets/{kanban-definition-UXKFOSKX-mjxym0jp.js → kanban-definition-UXKFOSKX-ndzqm6fu.js} +1 -1
  195. package/static/dist/assets/{lazyRouteComponent-cwb6l7xh.js → lazyRouteComponent-javud235.js} +1 -1
  196. package/static/dist/assets/{line-mtr5k6il.js → line-g625e7cj.js} +1 -1
  197. package/static/dist/assets/{link-iadw4ghw.js → link-gfdaho08.js} +1 -1
  198. package/static/dist/assets/{login-lkhtzfcl.js → login-iv6yq4xf.js} +1 -1
  199. package/static/dist/assets/mermaid-HWGCJPDP-cwj5cl6k.js +1 -0
  200. package/static/dist/assets/{mermaid-parser.core-k91vdnvi.js → mermaid-parser.core-plzkuzov.js} +3 -3
  201. package/static/dist/assets/{mindmap-definition-YA3MSWOX-ehztfea4.js → mindmap-definition-YA3MSWOX-ojrgs569.js} +1 -1
  202. package/static/dist/assets/onboarding-ff4emacu.js +1 -0
  203. package/static/dist/assets/{packet-AYTQ26CC-ok2xrblz.js → packet-AYTQ26CC-gyaf9s8h.js} +1 -1
  204. package/static/dist/assets/{pegDiagram-XKGWAZYB-ec3rpj0m.js → pegDiagram-XKGWAZYB-bf6xj8mq.js} +1 -1
  205. package/static/dist/assets/{pie-WAS4IAKB-iwkir23u.js → pie-WAS4IAKB-f5ymzelo.js} +1 -1
  206. package/static/dist/assets/{pieDiagram-E7YTZNPT-fcpqpekh.js → pieDiagram-E7YTZNPT-d0qjl3zi.js} +1 -1
  207. package/static/dist/assets/{quadrantDiagram-AXDQQJYC-lqsync43.js → quadrantDiagram-AXDQQJYC-oqy1mras.js} +1 -1
  208. package/static/dist/assets/queryKeys-x8fjxhut.js +9 -0
  209. package/static/dist/assets/{radar-RG4KPBEZ-hje4jbk6.js → radar-RG4KPBEZ-b6jcjvda.js} +1 -1
  210. package/static/dist/assets/{railroad-74A4TZTK-gll9n6qi.js → railroad-74A4TZTK-e3a6lk5k.js} +1 -1
  211. package/static/dist/assets/{railroad-abnf-HS5TGJTU-o6h6lpn4.js → railroad-abnf-HS5TGJTU-ff5hocrx.js} +1 -1
  212. package/static/dist/assets/{railroad-ebnf-LZEXJU2U-juaefj6o.js → railroad-ebnf-LZEXJU2U-exkhyw4n.js} +1 -1
  213. package/static/dist/assets/{railroad-peg-WCYAUIDC-cgfhvpah.js → railroad-peg-WCYAUIDC-jefk9b7c.js} +1 -1
  214. package/static/dist/assets/{railroadDiagram-O6MQD6OU-lp3w7awf.js → railroadDiagram-O6MQD6OU-e4amks1i.js} +1 -1
  215. package/static/dist/assets/{refresh-cw-iccm7a4e.js → refresh-cw-lfp4u1oh.js} +1 -1
  216. package/static/dist/assets/{requirementDiagram-BXWQKSXE-ha8eibcf.js → requirementDiagram-BXWQKSXE-c77k1e2y.js} +1 -1
  217. package/static/dist/assets/{sankeyDiagram-P5KCCOFB-oxf31pr9.js → sankeyDiagram-P5KCCOFB-oa1cc52s.js} +1 -1
  218. package/static/dist/assets/{sequenceDiagram-WJ2MYXX4-jiq8lhyo.js → sequenceDiagram-WJ2MYXX4-j20gqt22.js} +1 -1
  219. package/static/dist/assets/{share._token-o76ki0ly.js → share._token-kmfoflw6.js} +2 -2
  220. package/static/dist/assets/{sliders-horizontal-ddcs88av.js → sliders-horizontal-g5jkzd9k.js} +1 -1
  221. package/static/dist/assets/{stateDiagram-D77RDMKH-f3fslnon.js → stateDiagram-D77RDMKH-co2ghkdp.js} +1 -1
  222. package/static/dist/assets/stateDiagram-v2-MP3YSRHH-faw1opgh.js +1 -0
  223. package/static/dist/assets/{swimlanes-42K2YHIH-k1hmk7jf.js → swimlanes-42K2YHIH-m30cktqe.js} +1 -1
  224. package/static/dist/assets/swimlanesDiagram-VR7AAH4N-hfil433g.js +8 -0
  225. package/static/dist/assets/{timeline-definition-24CTP7MA-n3ncfy9r.js → timeline-definition-24CTP7MA-liuep6fl.js} +1 -1
  226. package/static/dist/assets/toast-zmb35u3r.js +56 -0
  227. package/static/dist/assets/{treeView-Q6P3EWNA-l3y2pejt.js → treeView-Q6P3EWNA-np5d2jv2.js} +1 -1
  228. package/static/dist/assets/{treemap-WGGIJYW6-jown9axh.js → treemap-WGGIJYW6-cmpltxh1.js} +1 -1
  229. package/static/dist/assets/{useForm-o4uu74mx.js → useForm-f1udzsjf.js} +1 -1
  230. package/static/dist/assets/{useParams-ftsveu84.js → useParams-ierevvts.js} +1 -1
  231. package/static/dist/assets/{useSearch-crqueqga.js → useSearch-i7nr9ypl.js} +1 -1
  232. package/static/dist/assets/{vennDiagram-4TSXK5OY-dh59uq1c.js → vennDiagram-4TSXK5OY-b2g42qrg.js} +1 -1
  233. package/static/dist/assets/{wardley-WFR3VGLG-e6d74b0j.js → wardley-WFR3VGLG-lb0eqbff.js} +1 -1
  234. package/static/dist/assets/{wardleyDiagram-VM6X3IG4-hnofh78f.js → wardleyDiagram-VM6X3IG4-ermwaqha.js} +1 -1
  235. package/static/dist/assets/{xychartDiagram-S5SC5T6Z-bpymjelb.js → xychartDiagram-S5SC5T6Z-dll0jldk.js} +1 -1
  236. package/static/dist/index.html +1 -1
  237. package/static/dist/sw.js +1 -1
  238. package/static/dist/assets/AppShell-3isgvoro.js +0 -8
  239. package/static/dist/assets/Button-f4n6dapf.js +0 -1
  240. package/static/dist/assets/ChatPage-k7b6xfax.js +0 -4
  241. package/static/dist/assets/ConnectTargets-juqvt5lm.js +0 -1
  242. package/static/dist/assets/Field-b6ez48oz.js +0 -1
  243. package/static/dist/assets/HubRoute-iwvso2tz.js +0 -2
  244. package/static/dist/assets/Markdown-jdeth7zq.js +0 -2
  245. package/static/dist/assets/OAuthSignIn-bpd8hom3.js +0 -1
  246. package/static/dist/assets/RightPanel-eu6enlo6.js +0 -1
  247. package/static/dist/assets/SessionListPanel-etb2i54l.js +0 -1
  248. package/static/dist/assets/SettingsLayout-eu3xxp23.js +0 -1
  249. package/static/dist/assets/TerminalPanel-hngmsr8b.js +0 -16
  250. package/static/dist/assets/Toaster-nb2uzjw1.js +0 -1
  251. package/static/dist/assets/_app-ga1moszl.js +0 -1
  252. package/static/dist/assets/_app.settings._section-dtdjk98a.js +0 -3
  253. package/static/dist/assets/_app.tasks-n8zuqsqe.js +0 -1
  254. package/static/dist/assets/cancel-bsnfo36u.js +0 -1
  255. package/static/dist/assets/channel-dsbtk01y.js +0 -1
  256. package/static/dist/assets/classDiagram-ZZMXUADV-fvgcv24g.js +0 -1
  257. package/static/dist/assets/classDiagram-v2-VYDZK3BY-fvgcv24g.js +0 -1
  258. package/static/dist/assets/flowDiagram-HODETNUW-jahfpiod.js +0 -1
  259. package/static/dist/assets/folder-plus-ivoglq98.js +0 -1
  260. package/static/dist/assets/index-lhf1fsdf.css +0 -2
  261. package/static/dist/assets/mermaid-HWGCJPDP-jx0q5dm2.js +0 -1
  262. package/static/dist/assets/onboarding-f67z0a2r.js +0 -1
  263. package/static/dist/assets/queryKeys-ed4n2egc.js +0 -9
  264. package/static/dist/assets/refresh-i9ya9dvl.js +0 -1
  265. package/static/dist/assets/stateDiagram-v2-MP3YSRHH-hlt6yphw.js +0 -1
  266. package/static/dist/assets/swimlanesDiagram-VR7AAH4N-hd5t9tnt.js +0 -8
  267. package/static/dist/assets/toast-liparz2j.js +0 -56
@@ -164,12 +164,15 @@ def snapshot(entry, *, records=None, expected=None):
164
164
  from gateway.status import get_process_start_time, _pid_exists
165
165
  service, pid = entry.get('service_pid', entry['pid']), entry['pid']
166
166
  anchors = {service, pid}
167
- observed = dict(expected or {})
167
+ observed = {**entry.get('departed_processes', {}), **(expected or {})}
168
168
  seen_parents = {int(p): proof['parent'] for p, proof in observed.items() if 'parent' in proof}
169
- departed = set()
169
+ departed = set(map(int, entry.get('departed_processes', {})))
170
170
 
171
171
  def exited(p):
172
- if _pid_exists(p) is False:
172
+ live = _pid_exists(p)
173
+ if live is None:
174
+ raise RuntimeError(f'{_name(entry)} process {p} liveness is unreadable; source was not changed.')
175
+ if live is False:
173
176
  if p in anchors:
174
177
  raise RuntimeError(f'{_name(entry)} anchor process {p} disappeared during discovery; source was not changed.')
175
178
  departed.add(p)
@@ -179,7 +182,7 @@ def snapshot(entry, *, records=None, expected=None):
179
182
  def inventory():
180
183
  parents = _process_table()
181
184
  if service not in _ancestors(pid, parents):
182
- raise RuntimeError(f'{_name(entry)} does not own backend/gateway PID {pid}.')
185
+ raise RuntimeError(f'{_name(entry)} identity/ancestry changed: does not own backend/gateway PID {pid}.')
183
186
  if service in _ancestors(os.getpid(), parents):
184
187
  raise RuntimeError(f'{_name(entry)} owns the updater or an ancestor; source was not changed.')
185
188
  for p, parent in seen_parents.items():
@@ -188,6 +191,10 @@ def snapshot(entry, *, records=None, expected=None):
188
191
  continue
189
192
  raise RuntimeError(f'{_name(entry)} process {p} identity/ancestry changed during discovery; source was not changed.')
190
193
  descendants = _descendants(service, parents)
194
+ # Successor retries cannot discard a live writer from a previously observed tree.
195
+ for p in seen_parents.keys() - descendants - departed:
196
+ if not exited(p):
197
+ raise RuntimeError(f'{_name(entry)} process {p} remains live outside the attested tree; recovery remains owed.')
191
198
  seen_parents.update({p: parents[p] for p in descendants})
192
199
  # Zombies may remain in ps. Recheck departed PIDs at every inventory so
193
200
  # neither a recycled PID nor unknown liveness can be silently excluded.
@@ -205,6 +212,8 @@ def snapshot(entry, *, records=None, expected=None):
205
212
  if exited(p):
206
213
  return None
207
214
  raise RuntimeError(f'{_name(entry)} process {p} birth identity is live or unreadable; source was not changed.')
215
+ if exited(p):
216
+ return None
208
217
  if any(proof.get(key) != value for key, value in observed.get(str(p), {}).items()):
209
218
  raise RuntimeError(f'{_name(entry)} process {p} identity/ancestry changed during discovery; source was not changed.')
210
219
  observed[str(p)] = proof
@@ -219,6 +228,8 @@ def snapshot(entry, *, records=None, expected=None):
219
228
  _, final = inventory()
220
229
  if live & departed or remaining != live or final != live:
221
230
  continue
231
+ if departed:
232
+ entry['departed_processes'] = {str(p): observed.get(str(p), {'parent': seen_parents[p]}) for p in departed}
222
233
  return proofs
223
234
  raise RuntimeError(f'{_name(entry)} process inventory kept changing after {SNAPSHOT_ATTEMPTS} samples; retry when descendants settle. Source was not changed and recovery remains owed.')
224
235
 
@@ -403,8 +414,23 @@ def _service_pid(entry):
403
414
 
404
415
  def verify_processes(entry, records, *, stopping=False, fenced=False):
405
416
  from gateway.status import get_process_start_time, _pid_exists
417
+ if not stopping:
418
+ # Reuse discovery's bounded proof without granting permission to lose an anchor.
419
+ candidate = dict(entry)
420
+ for boundary in range(2):
421
+ _, service_pid = _service_pid(entry)
422
+ if service_pid != entry['service_pid']:
423
+ raise RuntimeError(f'{_name(entry)} supervisor respawned during quiescence; source was not changed.')
424
+ if 'config_stamp' in entry and _config_stamp(entry) != entry['config_stamp']:
425
+ raise RuntimeError(f'{_name(entry)} service configuration changed; recovery remains owed.')
426
+ if boundary == 0:
427
+ candidate['processes'] = snapshot(candidate, records=records, expected=entry['processes'])
428
+ entry.update(processes=candidate['processes'])
429
+ if candidate.get('departed_processes'):
430
+ entry['departed_processes'] = candidate['departed_processes']
431
+ return True
406
432
  parents = _process_table()
407
- departed = set()
433
+ departed = set(map(int, entry.get('departed_processes', {})))
408
434
  for pid, expected in entry['processes'].items():
409
435
  pid = int(pid)
410
436
  if _pid_exists(pid) is False:
@@ -442,7 +468,6 @@ def verify_processes(entry, records, *, stopping=False, fenced=False):
442
468
 
443
469
  def bind_processes(entry, records):
444
470
  if entry.get('processes'):
445
- entry['processes'] = snapshot(entry, records=records, expected=entry['processes'])
446
471
  verify_processes(entry, records)
447
472
 
448
473
 
@@ -713,8 +738,20 @@ class ServiceRecovery:
713
738
  or ('runtime_revision' in transaction and not re.fullmatch(r'[a-f0-9]{40}', str(transaction['runtime_revision'])))):
714
739
  raise RuntimeError('Service recovery transaction has an unknown identity or format; its evidence is retained.')
715
740
 
741
+ operation = self.state.get('update_operation')
742
+ if operation is not None and (
743
+ not isinstance(operation, dict)
744
+ or not re.fullmatch(r'[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}', str(operation.get('operation_id', '')))
745
+ or any(not re.fullmatch(r'[a-f0-9]{40}', str(operation.get(key, ''))) for key in ('baseline_revision', 'target_revision'))
746
+ or ('attempt_error' in operation and (not isinstance(operation['attempt_error'], str) or len(operation['attempt_error']) > 2000))
747
+ or ('source_moved' in operation and not isinstance(operation['source_moved'], bool))
748
+ or ('failed_stage' in operation and operation['failed_stage'] not in {'install', 'gateway_restart', 'agent_ready'})
749
+ or operation.get('stage') not in {'install', 'gateway_restart', 'agent_ready'}
750
+ or ('recovered_revision' in operation and not re.fullmatch(r'[a-f0-9]{40}', str(operation['recovered_revision'])))):
751
+ raise RuntimeError('Update operation correlation is malformed; its evidence is retained.')
752
+
716
753
  def save(self, token=None):
717
- if not self.state['entries'] and not self.state.get('sync_failed') and not self.state.get('transaction'):
754
+ if not self.state['entries'] and not self.state.get('sync_failed') and not self.state.get('transaction') and not self.state.get('update_operation'):
718
755
  if self.path.exists():
719
756
  self.path.unlink()
720
757
  self._sync_directory()
@@ -734,9 +771,9 @@ class ServiceRecovery:
734
771
  with contextlib.suppress(FileNotFoundError):
735
772
  os.unlink(temporary)
736
773
 
737
- def _sync_directory(self):
774
+ def _sync_directory(self, directory=None):
738
775
  if os.name != 'nt':
739
- fd = os.open(self.path.parent, os.O_RDONLY)
776
+ fd = os.open(directory if directory is not None else self.path.parent, os.O_RDONLY)
740
777
  try:
741
778
  os.fsync(fd)
742
779
  finally:
@@ -767,10 +804,17 @@ class ServiceRecovery:
767
804
  self.records.write(token)
768
805
  self.save(token)
769
806
 
807
+ def verify_processes(self, entry, **permissions):
808
+ owed = next(e for e in self.state['entries'] if _service_key(e) == _service_key(entry))
809
+ result = verify_processes(owed, self.records, **permissions)
810
+ self.save() # Updated custody must precede the next lifecycle effect.
811
+ return result
812
+
770
813
  def stop(self, entry, stopped):
771
814
  owed = next(e for e in self.state['entries'] if _service_key(e) == _service_key(entry))
772
- verify_processes(owed, self.records)
815
+ self.verify_processes(owed)
773
816
  backend_request(owed)
817
+ self.verify_processes(owed)
774
818
  permit = backend_request(owed, 'prepare')
775
819
  if permit.get('ok') is not True or permit.get('idle') is not True or not isinstance(permit.get('token'), str):
776
820
  if owed['phase'] == 'recorded':
@@ -783,7 +827,7 @@ class ServiceRecovery:
783
827
  except BaseException:
784
828
  backend_request(owed, 'cancel', permit['token'])
785
829
  raise
786
- verify_processes(owed, self.records)
830
+ self.verify_processes(owed)
787
831
  if backend_request(owed, 'commit', permit['token']).get('ok') is not True:
788
832
  raise RuntimeError(f'{_name(owed)} did not commit its backend retirement; recovery remains owed.')
789
833
  owed['backend_committed'] = True
@@ -794,14 +838,14 @@ class ServiceRecovery:
794
838
  # Disarm through the original manager before its KeepAlive can admit a successor.
795
839
  owed['backend_stop_requested'] = True
796
840
  self.save()
797
- verify_processes(owed, self.records, stopping=stopping)
841
+ self.verify_processes(owed, stopping=stopping)
798
842
  if _config_stamp(owed) != owed['config_stamp']:
799
843
  raise RuntimeError(f'{_name(owed)} service configuration changed before backend stop; recovery remains owed.')
800
844
  if stopped.is_set():
801
845
  raise RuntimeError(f'{_name(owed)} backend stop was cancelled; recovery remains owed.')
802
846
  self.pause._disarm(service_entry(owed))
803
847
  wait_stopped(owed, self.records, stopped)
804
- verify_processes(owed, self.records, stopping=True)
848
+ self.verify_processes(owed, stopping=True)
805
849
  owed['phase'] = 'stopped'
806
850
  self.save()
807
851
 
@@ -811,15 +855,15 @@ class ServiceRecovery:
811
855
  raise RuntimeError(f'{_name(entry)} legacy shutdown remains owed; no signal was replayed.')
812
856
  if _config_stamp(owed) != owed['config_stamp']:
813
857
  raise RuntimeError(f'{_name(entry)} configuration changed before legacy refresh.')
814
- verify_processes(owed, self.records)
858
+ self.verify_processes(owed)
815
859
  verify_listener(owed)
816
860
  owed.update(legacy_refresh=True, phase='refreshing')
817
861
  self.save() # Older readers reject this phase and retain debt; no reader replays its signal.
818
- verify_processes(owed, self.records)
862
+ self.verify_processes(owed)
819
863
  verify_listener(owed)
820
864
  if _config_stamp(owed) != owed['config_stamp']:
821
865
  raise RuntimeError(f'{_name(owed)} configuration changed before legacy refresh.')
822
- verify_processes(owed, self.records)
866
+ self.verify_processes(owed)
823
867
  try:
824
868
  if stopped.is_set():
825
869
  raise InterruptedError('Legacy refresh was cancelled before interruption.')
@@ -883,6 +927,8 @@ class ServiceRecovery:
883
927
  if (_config_stamp(candidate) != entry['config_stamp']
884
928
  or candidate.get('profile') != entry.get('profile')):
885
929
  raise RuntimeError(f'{_name(entry)} successor configuration/profile changed; recovery remains owed.')
930
+ if entry.get('departed_processes'):
931
+ candidate['departed_processes'] = entry['departed_processes']
886
932
  bind_processes(candidate, self.records)
887
933
  parents = {int(pid): p['parent'] for pid, p in candidate['processes'].items()}
888
934
  chain, pid = set(), candidate['pid']
@@ -894,7 +940,8 @@ class ServiceRecovery:
894
940
  if chain | _descendants(candidate['pid'], parents) != set(parents):
895
941
  raise RuntimeError(f'{_name(entry)} successor has unrelated descendants; recovery remains owed.')
896
942
  for pid, process in candidate['processes'].items():
897
- old = entry['processes'].get(pid) or entry.get('retired_processes', {}).get(pid)
943
+ old = (entry['processes'].get(pid) or entry.get('departed_processes', {}).get(pid)
944
+ or entry.get('retired_processes', {}).get(pid))
898
945
  if old is not None and old != process:
899
946
  raise RuntimeError(f'{_name(entry)} successor reused a recorded process identity; recovery remains owed.')
900
947
  return candidate
@@ -966,7 +1013,7 @@ class ServiceRecovery:
966
1013
  if pending:
967
1014
  stopped.wait(.05)
968
1015
  continue
969
- if not verify_processes(entry, self.records, stopping=True, fenced=True):
1016
+ if not self.verify_processes(entry, stopping=True, fenced=True):
970
1017
  stopped.wait(.05)
971
1018
  continue
972
1019
  if self._gateway_writers_live(entry) or _config_stamp(entry) != entry['config_stamp']:
@@ -989,7 +1036,7 @@ class ServiceRecovery:
989
1036
  entry.update(candidate, retired_processes=retired, phase='retiring',
990
1037
  drain_acknowledged=False, drain_acknowledged_at=None, settle_attempts=attempts)
991
1038
  self.save() # Successor custody precedes its cooperative lifecycle request.
992
- verify_processes(entry, self.records)
1039
+ self.verify_processes(entry)
993
1040
  if _config_stamp(entry) != entry['config_stamp']:
994
1041
  raise RuntimeError(f'{_name(entry)} service configuration changed; recovery remains owed.')
995
1042
  if not self.pause._ask_to_drain(entry):
@@ -1036,7 +1083,7 @@ class ServiceRecovery:
1036
1083
  raise RuntimeError(f'{_name(entry)} uncommitted retirement could not yet be cancelled; recovery remains owed.')
1037
1084
  self.stop(entry, threading.Event())
1038
1085
  elif any(self.records.identity_is_live({'pid': int(pid), 'ct': p['ct']}) for pid, p in entry['processes'].items()):
1039
- verify_processes(entry, self.records, stopping=True)
1086
+ self.verify_processes(entry, stopping=True)
1040
1087
  if any(self.records.identity_is_live({'pid': int(pid), 'ct': p['ct']}) for pid, p in entry['processes'].items() if int(pid) != entry['service_pid']):
1041
1088
  raise RuntimeError(f'{_name(entry)} still has live writer descendants; recovery remains owed.')
1042
1089
  self._stop_backend_supervisor(entry, threading.Event(), stopping=True)
@@ -1062,6 +1109,18 @@ class ServiceRecovery:
1062
1109
  raise RuntimeError(f'{_name(entry)} readiness is unverified at its original endpoint; recovery remains owed.')
1063
1110
  time.sleep(.1)
1064
1111
 
1112
+ def _verify_candidate(self, candidate, entry):
1113
+ # Readiness retries must retain both live observations and proved departures.
1114
+ candidate['config_stamp'] = entry['config_stamp']
1115
+ candidate['processes'] = {**candidate['processes'], **entry.get('readiness_processes', {})}
1116
+ candidate['departed_processes'] = {**entry.get('departed_processes', {}),
1117
+ **candidate.get('departed_processes', {})}
1118
+ bind_processes(candidate, self.records)
1119
+ entry['readiness_processes'] = candidate['processes']
1120
+ if candidate['departed_processes']:
1121
+ entry['departed_processes'] = candidate['departed_processes']
1122
+ self.save()
1123
+
1065
1124
  def _ready(self, entry, ledger):
1066
1125
  if _config_stamp(entry) != entry['config_stamp']:
1067
1126
  raise RuntimeError(f'{_name(entry)} service configuration changed; backend recovery remains owed.')
@@ -1075,8 +1134,9 @@ class ServiceRecovery:
1075
1134
  candidate = self._gateway_candidate(entry)
1076
1135
  if candidate is None:
1077
1136
  return False
1137
+ self._verify_candidate(candidate, entry)
1078
1138
  ready = _gateway_home_matches(entry['home'], (candidate['pid'], candidate['ct']), self.pause)
1079
- verify_processes(candidate, self.records)
1139
+ self._verify_candidate(candidate, entry)
1080
1140
  return ready
1081
1141
  if entry.get('legacy_refresh') and any(self.records.identity_is_live({'pid': int(pid), 'ct': p['ct']})
1082
1142
  for pid, p in entry['processes'].items()
@@ -1095,7 +1155,7 @@ class ServiceRecovery:
1095
1155
  if (candidate['profile'] != entry['profile'] or _service_key(candidate) != _service_key(entry)
1096
1156
  or _config_stamp(candidate) != entry['config_stamp']):
1097
1157
  return False
1098
- bind_processes(candidate, self.records)
1158
+ self._verify_candidate(candidate, entry)
1099
1159
  try:
1100
1160
  if entry.get('legacy_refresh'):
1101
1161
  verify_listener(candidate)
@@ -1106,7 +1166,7 @@ class ServiceRecovery:
1106
1166
  return False
1107
1167
  if backend_request(candidate, 'cancel', permit['token']).get('ok') is not True:
1108
1168
  return False
1109
- verify_processes(candidate, self.records)
1169
+ self._verify_candidate(candidate, entry)
1110
1170
  if self.identity._pid_alive_matches(row['pid'], row['create_time'], strict=True) is not True:
1111
1171
  return False
1112
1172
  except BackendLifecycleUnsupported:
@@ -29,9 +29,9 @@ class Registry:
29
29
 
30
30
  def build_methods(runtime: AgentRuntime) -> dict[str, Handler]:
31
31
  registry = Registry(runtime)
32
- from . import aux, chat, checkpoints, commands, config, connectors, cron, dashboard_plugins, gateway, goals, kanban, mcp, memory, oauth, plugin_oauth, process, profiles, providers, skills, state_db, stt, text, toolsets, usage, worktree, write_approval # noqa: WPS433
32
+ from . import aux, chat, checkpoints, commands, config, connectors, cron, dashboard_plugins, gateway, goals, kanban, mcp, managed_buffers, memory, oauth, outbound_drafts, plugin_oauth, process, profiles, providers, skills, state_db, stt, text, toolsets, usage, worktree, write_approval # noqa: WPS433
33
33
  from . import runtime as runtime_methods
34
34
 
35
- for module in (runtime_methods, goals, commands, kanban, state_db, profiles, skills, mcp, stt, cron, providers, oauth, plugin_oauth, aux, text, process, usage, gateway, worktree, checkpoints, memory, chat, config, toolsets, write_approval, connectors, dashboard_plugins):
35
+ for module in (runtime_methods, goals, commands, kanban, state_db, profiles, skills, mcp, stt, cron, providers, oauth, plugin_oauth, aux, text, process, usage, gateway, worktree, checkpoints, memory, chat, config, toolsets, write_approval, connectors, dashboard_plugins, outbound_drafts, managed_buffers):
36
36
  module.register(registry)
37
37
  return registry.methods
@@ -190,15 +190,15 @@ def _plugins(home):
190
190
  return out
191
191
 
192
192
 
193
- def _named(home, name):
194
- return next((p for p in _plugins(home) if p['name'] == name and p['enabled'] and p['panel']), None)
193
+ def _named(home, name, *, require_panel=True):
194
+ return next((p for p in _plugins(home) if p['name'] == name and p['enabled'] and (p['panel'] if require_panel else p.get('_api_file'))), None)
195
195
 
196
196
 
197
197
  def _reply(status=404, body=b'', content_type='application/octet-stream'):
198
198
  return {'status': status, 'body': base64.b64encode(body).decode('ascii'), 'content_type': content_type}
199
199
 
200
200
 
201
- def _request(ctx, plugin, params):
201
+ def _request(ctx, plugin, params, *, require_panel=True):
202
202
  ctx.check_cancelled()
203
203
  if params.get('authenticated') is not True:
204
204
  return _reply()
@@ -240,7 +240,7 @@ def _request(ctx, plugin, params):
240
240
  content_type = next((v for k, v in result.get('headers', []) if k.lower() == 'content-type'), 'application/octet-stream')
241
241
  if 'text/event-stream' in content_type.lower():
242
242
  return _reply(501, b'Agent plugin host supports buffered HTTP only', 'text/plain; charset=utf-8')
243
- if _named(homes.profile_home_param(params), plugin['name']) is None:
243
+ if _named(homes.profile_home_param(params), plugin['name'], require_panel=require_panel) is None:
244
244
  return _reply()
245
245
  raw = result.get('body', b'')
246
246
  if not isinstance(raw, bytes) or len(raw) > _MAX_API:
@@ -17,8 +17,9 @@ from ..errors import InvalidParams
17
17
  from ..home import profile_home_param, scoped_home
18
18
 
19
19
  _ACTIONS = {"snapshot", "watch", "context_detail", "submit", "receipt", "cancel", "steer_queued", "replace_queued", "interrupt", "approval", "clarify",
20
- "clarify_lock", "discard", "mutate", "export"}
21
- _EXPORT_MAX_BYTES = 4 * 1024 * 1024
20
+ "clarify_lock", "discard", "mutate", "export", "checkpoint_diff", "terminal_info", "shell_exec", "react"}
21
+ _OWNER_READ_MAX_BYTES = 4 * 1024 * 1024
22
+ _REACTION_EMOJIS = {"👍", "👎", "❤️", "😂", "😮", "🎉"}
22
23
  # A restart's after-turn wait keeps the owner reachable only to finish its parked turn (the owner's
23
24
  # RESTART_WAIT_WS_METHODS); new work waits for the next owner.
24
25
  _RESTART_ACTIONS = {"snapshot", "watch", "receipt", "approval", "clarify", "clarify_lock", "interrupt"}
@@ -75,8 +76,15 @@ def _snapshot(value: dict) -> dict:
75
76
  message_keys = {"role", "content", "tool_calls", "tool_call_id", "name", "timestamp", "message_id", "row_id",
76
77
  "reasoning", "reasoning_content", "display_kind", "finish_reason"}
77
78
  info_keys = {"title", "model", "cwd", "source", "provider", "config_warning"}
78
- return {**value, "messages": [{k: v for k, v in row.items() if k in message_keys} for row in value["messages"]],
79
- "info": {k: v for k, v in value["info"].items() if k in info_keys}}
79
+ rows = []
80
+ for row in value["messages"]:
81
+ metadata = row.get("display_metadata")
82
+ reactions = metadata.get("reactions", []) if isinstance(metadata, dict) else []
83
+ rows.append({**{k: v for k, v in row.items() if k in message_keys}, "reactions": [
84
+ {"author": reaction["author"], "emoji": reaction["emoji"]} for reaction in reactions
85
+ if isinstance(reaction, dict) and reaction.get("author") in ("user", "agent")
86
+ and isinstance(reaction.get("emoji"), str) and reaction["emoji"] in _REACTION_EMOJIS] if isinstance(reactions, list) else []})
87
+ return {**value, "messages": rows, "info": {k: v for k, v in value["info"].items() if k in info_keys}}
80
88
 
81
89
 
82
90
  def _refused(state: str, reason: str) -> dict:
@@ -120,16 +128,20 @@ def _served(home: Path, endpoint, *, allow_verified: bool = False) -> bool:
120
128
  return False
121
129
  if not allow_verified:
122
130
  return True
131
+ return not _verified_contract(endpoint, "served-local-policy-v1")
132
+
133
+
134
+ def _verified_contract(endpoint, name: str) -> bool:
123
135
  # The private control socket reports the owner's boot revision, not the sidecar's
124
136
  # checkout or a version label. The authenticated describe must still match below.
125
137
  try:
126
138
  record = json.loads((Path(__file__).resolve().parents[2] / "agent_support.json").read_text())
127
- revisions = record["nativeContracts"]["served-local-policy-v1"]
128
- return (not isinstance(revisions, list)
129
- or not all(isinstance(sha, str) and re.fullmatch(r"[0-9a-f]{40}", sha) for sha in revisions)
130
- or getattr(endpoint, "code_sha", None) not in revisions)
139
+ revisions = record["nativeContracts"][name]
140
+ return (isinstance(revisions, list)
141
+ and all(isinstance(sha, str) and re.fullmatch(r"[0-9a-f]{40}", sha) for sha in revisions)
142
+ and getattr(endpoint, "code_sha", None) in revisions)
131
143
  except (OSError, ValueError, KeyError, TypeError):
132
- return True
144
+ return False
133
145
 
134
146
 
135
147
  def _owner_refusal(described: dict, endpoint) -> dict | None:
@@ -145,12 +157,41 @@ def _owner_refusal(described: dict, endpoint) -> dict | None:
145
157
  return None
146
158
 
147
159
 
160
+ def _checkpoint_read(value, *, no_baseline=False):
161
+ comparison = {"provenance": "native_owner", "baseline": "earliest_retained_project_checkpoint",
162
+ "available": False, "reason": None, "truncated": False}
163
+ result = {"diff": "", "stat": "", "comparison": comparison}
164
+ reason = None
165
+ if not isinstance(value, dict) or type(value.get("enabled")) is not bool:
166
+ reason = "invalid_payload"
167
+ elif not value["enabled"]:
168
+ reason = "checkpoints_disabled"
169
+ elif value.get("success") is not True:
170
+ reason = "checkpoint_unavailable"
171
+ elif (not isinstance(value.get("diff"), str) or not isinstance(value.get("stat"), str)
172
+ or type(value.get("diff_lines")) is not int or value["diff_lines"] < 0
173
+ or type(value.get("empty")) is not bool):
174
+ reason = "invalid_payload"
175
+ elif sum(len(json.dumps(value[key], ensure_ascii=False).encode("utf-8", "surrogatepass")) - 2
176
+ for key in ("stat", "diff")) > _OWNER_READ_MAX_BYTES:
177
+ reason = "payload_too_large"
178
+ elif no_baseline:
179
+ reason = "no_baseline"
180
+ else:
181
+ result.update(diff=value["diff"], stat=value["stat"])
182
+ comparison.update(available=True, truncated=value["diff_lines"] > len(value["diff"].splitlines()))
183
+ comparison["reason"] = reason
184
+ return result
185
+
186
+
148
187
  async def _run(ctx, params: dict, home: Path, discovery) -> dict:
149
188
  from hermes_cli.gateway_client import GatewayRPCError
150
189
 
151
190
  endpoint = discovery.endpoint
152
191
  if _served(home, endpoint, allow_verified=True):
153
192
  return _refused("incompatible", "served_profile_sessions_unavailable")
193
+ if params["action"] in {"terminal_info", "shell_exec"} and not _verified_contract(endpoint, "captured-command-v1"):
194
+ return _refused("incompatible", "owner_shell_unsupported")
154
195
  restarting = discovery.state != "ready"
155
196
  if restarting and params["action"] not in _RESTART_ACTIONS:
156
197
  return _refused("refused", "gateway_restarting")
@@ -170,10 +211,22 @@ async def _run(ctx, params: dict, home: Path, discovery) -> dict:
170
211
  subscription = value["subscription_id"]
171
212
  if reason := await asyncio.to_thread(_control_refusal, home, sid):
172
213
  raise GatewayRPCError(reason)
173
- return _snapshot(value)
214
+ return {**_snapshot(value), "reactions_supported": _verified_contract(endpoint, "message-reactions-v1")}
174
215
 
175
216
  receipt = lock = None
176
217
  try:
218
+ if params["action"] == "checkpoint_diff":
219
+ # The owner resolves the physical target and frozen policy. Never resume or read host files.
220
+ value = await asyncio.wait_for(client.rpc("rollback.session_diff", session_id=sid, _timeout=25), timeout=30)
221
+ read = _checkpoint_read(value)
222
+ if read["comparison"]["available"] and value["empty"] and not value["diff"] and not value["stat"]:
223
+ listed = await asyncio.wait_for(client.rpc("rollback.list", session_id=sid, _timeout=3), timeout=3)
224
+ if (not isinstance(listed, dict) or listed.get("enabled") is not True
225
+ or not isinstance(listed.get("checkpoints"), list)):
226
+ read = _checkpoint_read(None)
227
+ elif not listed["checkpoints"]:
228
+ read = _checkpoint_read(value, no_baseline=True)
229
+ return {"state": "attached", "reason": None, "snapshot": None, "receipt": None, "checkpoint_diff": read}
177
230
  if params["action"] == "export":
178
231
  # No resume/subscription or mutation: the owner selects its current physical target.
179
232
  fmt = "md" if params["format"] == "markdown" else params["format"]
@@ -183,14 +236,34 @@ async def _run(ctx, params: dict, home: Path, discovery) -> dict:
183
236
  or not isinstance(value.get("filename"), str) or not 0 < len(value["filename"]) <= 255
184
237
  or not isinstance(value.get("content"), str)):
185
238
  return _refused("refused", "owner_export_invalid")
186
- if len(value["content"]) > _EXPORT_MAX_BYTES or len(value["content"].encode("utf-8")) > _EXPORT_MAX_BYTES:
239
+ if len(value["content"]) > _OWNER_READ_MAX_BYTES or len(value["content"].encode("utf-8")) > _OWNER_READ_MAX_BYTES:
187
240
  return _refused("refused", "export_too_large")
188
241
  return {"state": "attached", "reason": None, "snapshot": None, "receipt": None,
189
242
  "export": {"format": params["format"], "filename": value["filename"], "content": value["content"]}}
190
243
  snap = await snapshot()
191
244
  action = params["action"]
192
245
  common = {"session_id": sid}
193
- if action == "context_detail":
246
+ if action == "terminal_info":
247
+ return {"state": "attached", "reason": None, "snapshot": snap, "receipt": None}
248
+ if action == "shell_exec":
249
+ if (snap.get("session_id") != params["session_id"] or snap.get("authority_epoch") != params["authority_epoch"]
250
+ or snap.get("replay_epoch") != params["replay_epoch"]):
251
+ return _refused("refused", "owner_changed")
252
+ # The native owner alone authorizes the frozen backend/cwd and redacts before bounding.
253
+ # No second observation may discard a received result; no retry after an unknown ACK.
254
+ value = await client.rpc("shell.exec", **common, command=params["command"], _timeout=35)
255
+ if (not isinstance(value, dict) or not isinstance(value.get("stdout"), str)
256
+ or not isinstance(value.get("stderr"), str) or type(value.get("code")) is not int
257
+ or len(value["stdout"]) > 4000 or len(value["stderr"]) > 2000):
258
+ return _refused("refused", "owner_shell_invalid")
259
+ return {"state": "attached", "reason": None, "snapshot": None, "receipt": None, "shell": value}
260
+ if action == "react":
261
+ if not snap["reactions_supported"]:
262
+ return _refused("refused", "owner_reactions_unsupported")
263
+ if not any(row.get("row_id") == params["row_id"] and row.get("role") in {"user", "assistant"} for row in snap["messages"]):
264
+ return _refused("refused", "reaction_row_unavailable")
265
+ await client.rpc("message.react", **common, row_id=params["row_id"], emoji=params["emoji"], author="user")
266
+ elif action == "context_detail":
194
267
  from ..context_detail import project, unavailable
195
268
  # The authorized native read owns lookup and prompt interpretation. No local Agent is built.
196
269
  try:
@@ -264,16 +337,22 @@ async def _run(ctx, params: dict, home: Path, discovery) -> dict:
264
337
  ctx.emit("replay_complete", {})
265
338
  checked = asyncio.get_running_loop().time()
266
339
  while not ctx.cancelled:
340
+ now = asyncio.get_running_loop().time()
341
+ if not restarting and now - checked >= 2:
342
+ checked = now
343
+ if restarting := await asyncio.to_thread(_restart_wait, home):
344
+ result["restarting"] = True
345
+ ctx.emit("restarting", {"restarting": True})
346
+ if not restarting and snap.get("reactions_supported"):
347
+ # The owner emits no reaction event. Read through this viewer's existing subscription,
348
+ # including while tokens keep the event queue busy.
349
+ current = await snapshot()
350
+ if [(r.get("row_id"), r.get("reactions")) for r in current["messages"] if r.get("reactions")] != [(r.get("row_id"), r.get("reactions")) for r in snap["messages"] if r.get("reactions")]:
351
+ ctx.emit("snapshot", {**result, "snapshot": current, "reaction_only": True})
352
+ snap = current
267
353
  try:
268
354
  frame = await asyncio.wait_for(events.get(), .25)
269
355
  except TimeoutError:
270
- now = asyncio.get_running_loop().time()
271
- if not restarting and now - checked >= 2:
272
- checked = now
273
- # One-way for this owner: its next state is exit, which ends the watch.
274
- if restarting := await asyncio.to_thread(_restart_wait, home):
275
- result["restarting"] = True
276
- ctx.emit("restarting", {"restarting": True})
277
356
  continue
278
357
  if isinstance(frame, Exception):
279
358
  raise frame
@@ -310,6 +389,8 @@ async def _run(ctx, params: dict, home: Path, discovery) -> dict:
310
389
  ctx.emit("event", event)
311
390
  return result
312
391
  except GatewayRPCError as exc:
392
+ if params["action"] == "react" and str(exc) not in {"invalid_params", "not_found", "permission_denied", "profile_mismatch"}:
393
+ return _refused("unavailable", "reaction_outcome_unknown")
313
394
  if params["action"] == "export":
314
395
  return _refused("refused", "export_too_large" if str(exc) == "export_too_large" else "owner_export_unsupported")
315
396
  return {**_refused("incompatible" if str(exc) in _CONTROL_REFUSALS else "refused", str(exc)), "receipt": receipt}
@@ -330,9 +411,18 @@ def session(ctx, params: dict) -> dict:
330
411
  if action not in _ACTIONS or not isinstance(sid, str) or not sid.strip() or sid != sid.strip():
331
412
  raise InvalidParams("session_id and supported action are required")
332
413
  params = {**params, "action": action}
414
+ if action == "react" and (set(params) - {"profile_home", "session_id", "action", "row_id", "emoji"}
415
+ or type(params.get("row_id")) is not int or params["row_id"] <= 0
416
+ or "emoji" not in params or (params["emoji"] is not None and (not isinstance(params["emoji"], str) or params["emoji"] not in _REACTION_EMOJIS))):
417
+ raise InvalidParams("react requires a visible row and an allowed emoji")
333
418
  if action == "export" and (params.get("format") not in ("json", "markdown", "html")
334
419
  or not isinstance(params.get("redact"), bool)):
335
420
  raise InvalidParams("export requires an allowed format and redaction policy")
421
+ if action == "shell_exec" and (not isinstance(params.get("command"), str)
422
+ or not params["command"].strip() or len(params["command"]) > 8192
423
+ or type(params.get("authority_epoch")) is not int or params["authority_epoch"] < 0
424
+ or not isinstance(params.get("replay_epoch"), str) or not params["replay_epoch"]):
425
+ raise InvalidParams("captured command requires command and owner identity")
336
426
  required = {
337
427
  "submit": ("request_id", "text"), "receipt": ("admission_id",), "cancel": ("admission_id",),
338
428
  "steer_queued": ("admission_id",), "replace_queued": ("admission_id", "submission_id", "text"),
@@ -10,8 +10,11 @@ from __future__ import annotations
10
10
  import functools
11
11
  import json
12
12
  import secrets
13
+ import sqlite3
13
14
  import time
15
+ from contextlib import closing
14
16
  from dataclasses import asdict, is_dataclass
17
+ from pathlib import Path
15
18
  from typing import Any, NamedTuple
16
19
 
17
20
  from ..errors import InvalidParams, RpcError
@@ -463,6 +466,50 @@ def _bool(params: dict, key: str, default: bool = False) -> bool:
463
466
 
464
467
  # ── payload builders ──────────────────────────────────────────────────────
465
468
 
469
+ def inspection_payload(params: dict) -> dict:
470
+ """Read only the selected profile's live boards, with shared global row/edge budgets."""
471
+ kb = _kb()
472
+ selected = _board(params)
473
+ boards = sorted((b for b in kb.list_boards(include_archived=False) if not b.get("archived")), key=lambda b: b["slug"])
474
+ if selected is not None:
475
+ boards = [b for b in boards if b["slug"] == selected]
476
+ if not boards:
477
+ raise NotFound("board not found")
478
+ nodes, edges = [], []
479
+ truncated = len(boards) > 32
480
+ for meta in boards[:32]:
481
+ slug = _normalize_existing_slug(meta["slug"])
482
+ if len(nodes) >= 500:
483
+ truncated = True
484
+ break
485
+ # An inspection must never initialize a missing database or run migrations.
486
+ uri = Path(kb.kanban_db_path(slug)).resolve().as_uri() + "?mode=ro"
487
+ with closing(sqlite3.connect(uri, uri=True)) as conn:
488
+ conn.row_factory = sqlite3.Row
489
+ conn.execute("BEGIN")
490
+ where = "" if _bool(params, "include_archived") else "WHERE t.status != 'archived'"
491
+ rows = conn.execute(
492
+ f"SELECT t.*, r.started_at AS current_run_started_at FROM tasks t LEFT JOIN task_runs r ON r.id = t.current_run_id {where} ORDER BY t.id LIMIT ?",
493
+ (501 - len(nodes),),
494
+ ).fetchall()
495
+ remaining = 500 - len(nodes)
496
+ truncated = truncated or len(rows) > remaining
497
+ rows = rows[:remaining]
498
+ ids = [row["id"] for row in rows]
499
+ for row in rows:
500
+ task = _task_dict(dict(row), conn)
501
+ nodes.append({"key": f"{slug}/{row['id']}", "board": slug, "board_name": meta.get("name") or slug, "task": task})
502
+ if ids:
503
+ placeholders = ",".join("?" for _ in ids)
504
+ links = conn.execute(
505
+ f"SELECT parent_id, child_id FROM task_links WHERE parent_id IN ({placeholders}) AND child_id IN ({placeholders}) ORDER BY parent_id, child_id LIMIT ?",
506
+ [*ids, *ids, 2001 - len(edges)],
507
+ ).fetchall()
508
+ remaining_edges = 2000 - len(edges)
509
+ truncated = truncated or len(links) > remaining_edges
510
+ edges.extend({"source": f"{slug}/{r['parent_id']}", "target": f"{slug}/{r['child_id']}"} for r in links[:remaining_edges])
511
+ return {"nodes": nodes, "edges": edges, "generated_at": time.time(), "truncated": truncated, "read_only": False}
512
+
466
513
  def board_payload(params: dict) -> dict:
467
514
  board = _board(params)
468
515
  kb = _kb()
@@ -852,6 +899,7 @@ def normalize_board_payload(params: dict) -> dict:
852
899
 
853
900
 
854
901
  METHODS = {
902
+ "kanban.inspection": inspection_payload,
855
903
  "kanban.normalize_board": normalize_board_payload,
856
904
  "kanban.board": board_payload,
857
905
  "kanban.boards": list_boards_payload,