@maudecode/talaria-web 1.11.0 → 1.13.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 (263) hide show
  1. package/LICENSE +22 -0
  2. package/_release.json +4 -4
  3. package/dist/api/chat-router.d.ts +68 -4
  4. package/dist/api/chat-router.js +11 -8
  5. package/dist/api/chat-router.js.map +1 -1
  6. package/dist/api/router.d.ts +10 -0
  7. package/dist/api/router.js +2 -1
  8. package/dist/api/router.js.map +1 -1
  9. package/dist/api/sessions-router.d.ts +407 -51
  10. package/dist/api/sessions-router.js +2 -2
  11. package/dist/api/sessions-router.js.map +1 -1
  12. package/dist/api/sse-routes.js +52 -8
  13. package/dist/api/sse-routes.js.map +1 -1
  14. package/dist/api/tools-router.d.ts +443 -0
  15. package/dist/api/tools-router.js +99 -4
  16. package/dist/api/tools-router.js.map +1 -1
  17. package/dist/app.d.ts +926 -55
  18. package/dist/auth/oidc.js +2 -0
  19. package/dist/auth/oidc.js.map +1 -1
  20. package/dist/auth/passkeys.js +1 -1
  21. package/dist/auth/passkeys.js.map +1 -1
  22. package/dist/auth/store.d.ts +17 -0
  23. package/dist/auth/store.js +77 -19
  24. package/dist/auth/store.js.map +1 -1
  25. package/dist/cli/ctl.d.ts +6 -3
  26. package/dist/cli/ctl.js +25 -16
  27. package/dist/cli/ctl.js.map +1 -1
  28. package/dist/cli/launcher.js +2 -2
  29. package/dist/cli/launcher.js.map +1 -1
  30. package/dist/fs/atomic.d.ts +8 -1
  31. package/dist/fs/atomic.js +52 -3
  32. package/dist/fs/atomic.js.map +1 -1
  33. package/dist/http/context.d.ts +2 -0
  34. package/dist/http/context.js.map +1 -1
  35. package/dist/mcp/server.js +1 -1
  36. package/dist/mcp/server.js.map +1 -1
  37. package/dist/redact.d.ts +15 -0
  38. package/dist/redact.js +1393 -9
  39. package/dist/redact.js.map +1 -1
  40. package/dist/release.js +3 -1
  41. package/dist/release.js.map +1 -1
  42. package/dist/runtime.js +20 -9
  43. package/dist/runtime.js.map +1 -1
  44. package/dist/server.d.ts +1 -1
  45. package/dist/server.js +12 -6
  46. package/dist/server.js.map +1 -1
  47. package/dist/sessions/anchor.d.ts +2 -0
  48. package/dist/sessions/anchor.js +22 -17
  49. package/dist/sessions/anchor.js.map +1 -1
  50. package/dist/sessions/export.js +1 -1
  51. package/dist/sessions/export.js.map +1 -1
  52. package/dist/sessions/file-changes.d.ts +15 -0
  53. package/dist/sessions/file-changes.js +58 -0
  54. package/dist/sessions/file-changes.js.map +1 -0
  55. package/dist/sessions/journal.d.ts +6 -1
  56. package/dist/sessions/journal.js +9 -15
  57. package/dist/sessions/journal.js.map +1 -1
  58. package/dist/sessions/list.d.ts +21 -1
  59. package/dist/sessions/list.js +81 -17
  60. package/dist/sessions/list.js.map +1 -1
  61. package/dist/sessions/merge.d.ts +66 -2
  62. package/dist/sessions/merge.js +291 -9
  63. package/dist/sessions/merge.js.map +1 -1
  64. package/dist/sessions/pending.d.ts +11 -2
  65. package/dist/sessions/pending.js +52 -3
  66. package/dist/sessions/pending.js.map +1 -1
  67. package/dist/sessions/relay.js +1 -1
  68. package/dist/sessions/relay.js.map +1 -1
  69. package/dist/sessions/service.d.ts +16 -3
  70. package/dist/sessions/service.js +92 -54
  71. package/dist/sessions/service.js.map +1 -1
  72. package/dist/sessions/state-db.d.ts +5 -0
  73. package/dist/sessions/state-db.js +34 -0
  74. package/dist/sessions/state-db.js.map +1 -1
  75. package/dist/sessions/streams.d.ts +4 -1
  76. package/dist/sessions/streams.js +3 -0
  77. package/dist/sessions/streams.js.map +1 -1
  78. package/dist/sessions/tool-display.d.ts +21 -0
  79. package/dist/sessions/tool-display.js +113 -0
  80. package/dist/sessions/tool-display.js.map +1 -0
  81. package/dist/sessions/turn.d.ts +25 -7
  82. package/dist/sessions/turn.js +153 -90
  83. package/dist/sessions/turn.js.map +1 -1
  84. package/dist/spa.d.ts +2 -0
  85. package/dist/spa.js +9 -0
  86. package/dist/spa.js.map +1 -1
  87. package/dist/test/harness.d.ts +2 -0
  88. package/dist/test/harness.js +1 -1
  89. package/dist/test/harness.js.map +1 -1
  90. package/dist/tools/terminal.d.ts +6 -0
  91. package/dist/tools/terminal.js +24 -3
  92. package/dist/tools/terminal.js.map +1 -1
  93. package/dist/tools/update-notifications.d.ts +133 -0
  94. package/dist/tools/update-notifications.js +407 -0
  95. package/dist/tools/update-notifications.js.map +1 -0
  96. package/dist/tools/updates.d.ts +53 -16
  97. package/dist/tools/updates.js +314 -351
  98. package/dist/tools/updates.js.map +1 -1
  99. package/package.json +3 -2
  100. package/sidecar/scripts/replay_sidecar.py +17 -0
  101. package/sidecar/talaria_sidecar/__init__.py +1 -1
  102. package/sidecar/talaria_sidecar/home.py +39 -16
  103. package/sidecar/talaria_sidecar/methods/chat.py +113 -4
  104. package/static/dist/FILES.txt +116 -116
  105. package/static/dist/THIRD_PARTY_LICENSES.md +5170 -0
  106. package/static/dist/assets/AppShell-irdqom6e.js +1 -0
  107. package/static/dist/assets/ChatPage-ldhqevjn.js +20 -0
  108. package/static/dist/assets/HubRoute-hq24vhj8.js +2 -0
  109. package/static/dist/assets/{Markdown-ftyfzds0.js → Markdown-bx4n7qs4.js} +7 -7
  110. package/static/dist/assets/{RightPanel-febk3dkd.js → RightPanel-ds4majj3.js} +1 -1
  111. package/static/dist/assets/Select-gpqmt6x1.js +1 -0
  112. package/static/dist/assets/SessionListPanel-hkvipik4.js +1 -0
  113. package/static/dist/assets/{SettingsLayout-pdpmhy1o.js → SettingsLayout-oigbg9rl.js} +1 -1
  114. package/static/dist/assets/States-fn4o2wdf.js +1 -0
  115. package/static/dist/assets/Toaster-cwnut53k.js +1 -0
  116. package/static/dist/assets/_app-h6ago6y6.js +1 -0
  117. package/static/dist/assets/_app.ext._extensionId-erqnfff5.js +1 -0
  118. package/static/dist/assets/_app.index-cq2fxmda.js +1 -0
  119. package/static/dist/assets/_app.insights-nzhld9gu.js +1 -0
  120. package/static/dist/assets/_app.kanban-m5uggety.js +1 -0
  121. package/static/dist/assets/_app.logs-d2kk9lza.js +1 -0
  122. package/static/dist/assets/_app.memory-gcmop1e7.js +1 -0
  123. package/static/dist/assets/_app.profiles-e9b2my7x.js +1 -0
  124. package/static/dist/assets/{_app.session._sessionId-n1ggcf5e.js → _app.session._sessionId-ciix6itb.js} +1 -1
  125. package/static/dist/assets/_app.settings-kcjum3c1.js +1 -0
  126. package/static/dist/assets/_app.settings._section-bi4jjex0.js +1 -0
  127. package/static/dist/assets/_app.skills-e4e61l23.js +1 -0
  128. package/static/dist/assets/_app.tasks-f1evdhbl.js +2 -0
  129. package/static/dist/assets/_app.todos-o7ijtk42.js +1 -0
  130. package/static/dist/assets/_app.workspaces-o18voh0w.js +1 -0
  131. package/static/dist/assets/{abnfDiagram-VCTEODGH-i4bgzu83.js → abnfDiagram-VCTEODGH-krt0yaru.js} +1 -1
  132. package/static/dist/assets/architecture-7GRP2DOG-notdhmpv.js +1 -0
  133. package/static/dist/assets/{architectureDiagram-5GKGNRK7-mx21lwqa.js → architectureDiagram-5GKGNRK7-i54t9ywl.js} +1 -1
  134. package/static/dist/assets/{blockDiagram-I7D4REHJ-o1hhchxv.js → blockDiagram-I7D4REHJ-fevbz4mz.js} +1 -1
  135. package/static/dist/assets/{c4Diagram-7LVT6UL2-jygqtvd8.js → c4Diagram-7LVT6UL2-np8eclmc.js} +1 -1
  136. package/static/dist/assets/channel-jpk1nas2.js +1 -0
  137. package/static/dist/assets/{chunk-4HAMMTFA-hjcpubgk.js → chunk-4HAMMTFA-owoi036m.js} +1 -1
  138. package/static/dist/assets/{chunk-75Z2AOVW-eibnex6m.js → chunk-75Z2AOVW-md44zf1n.js} +1 -1
  139. package/static/dist/assets/{chunk-DU6HZSFF-k1tp3qnp.js → chunk-DU6HZSFF-ka3rw8d6.js} +3 -3
  140. package/static/dist/assets/{chunk-F27PBJKO-lvcgn16f.js → chunk-F27PBJKO-iasnjsky.js} +1 -1
  141. package/static/dist/assets/{chunk-GMAD6QVW-ldlzruv1.js → chunk-GMAD6QVW-ex1up6tz.js} +1 -1
  142. package/static/dist/assets/{chunk-GVQU2GXP-hs1mva9d.js → chunk-GVQU2GXP-d4kmmg9p.js} +1 -1
  143. package/static/dist/assets/{chunk-IMKFNOWR-o7eqx3yb.js → chunk-IMKFNOWR-mwy42t85.js} +1 -1
  144. package/static/dist/assets/{chunk-L3NEJ4N5-bp1ofl70.js → chunk-L3NEJ4N5-fv7ry213.js} +1 -1
  145. package/static/dist/assets/{chunk-OSK3NFVY-o8m6mdz5.js → chunk-OSK3NFVY-jg9ir8ac.js} +1 -1
  146. package/static/dist/assets/{chunk-P2QGCYS3-kx83fti2.js → chunk-P2QGCYS3-pe9ms3ua.js} +1 -1
  147. package/static/dist/assets/{chunk-POPQ4Y6H-mwl99e9y.js → chunk-POPQ4Y6H-dimsjrn1.js} +1 -1
  148. package/static/dist/assets/{chunk-PWAF6VOD-oriuokxy.js → chunk-PWAF6VOD-m4hv98dy.js} +1 -1
  149. package/static/dist/assets/{chunk-SHT3W25Y-ab5cf00v.js → chunk-SHT3W25Y-gkjgl6qe.js} +1 -1
  150. package/static/dist/assets/{chunk-SVP7TREG-kqdrpr17.js → chunk-SVP7TREG-bc8qss5l.js} +1 -1
  151. package/static/dist/assets/{chunk-TICWLB2K-kfbnjabk.js → chunk-TICWLB2K-k9wzw38w.js} +1 -1
  152. package/static/dist/assets/classDiagram-ZZMXUADV-hld301gc.js +1 -0
  153. package/static/dist/assets/classDiagram-v2-VYDZK3BY-hld301gc.js +1 -0
  154. package/static/dist/assets/{copied-dp33szhq.js → copied-d5joolw7.js} +1 -1
  155. package/static/dist/assets/{cynefin-OW5HDTMX-mm5sj9s7.js → cynefin-OW5HDTMX-hueklikk.js} +1 -1
  156. package/static/dist/assets/{cynefinDiagram-5FMLGOSQ-nfon5q38.js → cynefinDiagram-5FMLGOSQ-e83o1wr1.js} +1 -1
  157. package/static/dist/assets/{dagre-GXQ25YYZ-dzdp9ypb.js → dagre-GXQ25YYZ-ir1tj2ar.js} +1 -1
  158. package/static/dist/assets/{diagram-S7CK7UJ4-ckxzqo01.js → diagram-S7CK7UJ4-o5218p2q.js} +1 -1
  159. package/static/dist/assets/{diagram-UQ7AKVKN-hw16h1um.js → diagram-UQ7AKVKN-ciad4o1i.js} +1 -1
  160. package/static/dist/assets/{diagram-VSXAHHWV-mu7wu2zg.js → diagram-VSXAHHWV-oharqjbt.js} +1 -1
  161. package/static/dist/assets/{diagram-VX7I27RA-k9uprwtj.js → diagram-VX7I27RA-wkwy62iv.js} +1 -1
  162. package/static/dist/assets/{diagram-Z3DM3KII-csdjzrg9.js → diagram-Z3DM3KII-bl252i7r.js} +1 -1
  163. package/static/dist/assets/{ebnfDiagram-PWID7BFC-n8mbw2jj.js → ebnfDiagram-PWID7BFC-nm0rcly7.js} +1 -1
  164. package/static/dist/assets/endpoints-d8n28yf3.js +64 -0
  165. package/static/dist/assets/{erDiagram-RLTQ6QDP-fsfe1uj0.js → erDiagram-RLTQ6QDP-m1w0vta2.js} +1 -1
  166. package/static/dist/assets/eventmodeling-NTZA5JFV-eaqs19mj.js +1 -0
  167. package/static/dist/assets/flowDiagram-HODETNUW-eli5764n.js +1 -0
  168. package/static/dist/assets/{ganttDiagram-EL5Y4UJY-gsba3iq9.js → ganttDiagram-EL5Y4UJY-czxo6iew.js} +1 -1
  169. package/static/dist/assets/{gitGraph-4MIJSDKK-na86q1pu.js → gitGraph-4MIJSDKK-ip9j1rph.js} +1 -1
  170. package/static/dist/assets/{gitGraphDiagram-WWUBYQGX-g72ew3xz.js → gitGraphDiagram-WWUBYQGX-kalxdole.js} +1 -1
  171. package/static/dist/assets/{highlighted-body-KPVGNVTW-okn18cp1.js → highlighted-body-KPVGNVTW-lgsj6fsk.js} +1 -1
  172. package/static/dist/assets/index-l08181vr.js +11 -0
  173. package/static/dist/assets/index-pdtcl20t.css +2 -0
  174. package/static/dist/assets/{info-A6RAGUB7-i01jeltk.js → info-A6RAGUB7-fv4fbc5m.js} +1 -1
  175. package/static/dist/assets/{infoDiagram-27XIBGKW-cayiv6fa.js → infoDiagram-27XIBGKW-kinx5jsl.js} +1 -1
  176. package/static/dist/assets/{ishikawaDiagram-5VMMS53U-g4rf14bv.js → ishikawaDiagram-5VMMS53U-kozvlqdv.js} +1 -1
  177. package/static/dist/assets/{journeyDiagram-3NMN7TZE-fusb2424.js → journeyDiagram-3NMN7TZE-lgpouxoe.js} +1 -1
  178. package/static/dist/assets/{kanban-definition-UXKFOSKX-g831pwlr.js → kanban-definition-UXKFOSKX-b320wo7b.js} +1 -1
  179. package/static/dist/assets/{lazyRouteComponent-ol965zwk.js → lazyRouteComponent-hy8doeye.js} +1 -1
  180. package/static/dist/assets/{line-md6em6gc.js → line-gxfwyhjq.js} +1 -1
  181. package/static/dist/assets/{login-kfclsqd2.js → login-llplsuey.js} +1 -1
  182. package/static/dist/assets/mermaid-HWGCJPDP-f3scrlds.js +1 -0
  183. package/static/dist/assets/{mermaid-parser.core-mq3uns9m.js → mermaid-parser.core-loxeqsu3.js} +3 -3
  184. package/static/dist/assets/{mindmap-definition-YA3MSWOX-oxafrned.js → mindmap-definition-YA3MSWOX-edi534gc.js} +1 -1
  185. package/static/dist/assets/{onboarding-fo6hnkyv.js → onboarding-n1xhlgym.js} +1 -1
  186. package/static/dist/assets/{packet-AYTQ26CC-ggxyvbym.js → packet-AYTQ26CC-gmb3yy71.js} +1 -1
  187. package/static/dist/assets/{pegDiagram-XKGWAZYB-nk7xcqgp.js → pegDiagram-XKGWAZYB-fxy5j8cx.js} +1 -1
  188. package/static/dist/assets/{pie-WAS4IAKB-ito2razh.js → pie-WAS4IAKB-do2pm4s6.js} +1 -1
  189. package/static/dist/assets/{pieDiagram-E7YTZNPT-g02xexar.js → pieDiagram-E7YTZNPT-mgjvb365.js} +1 -1
  190. package/static/dist/assets/{quadrantDiagram-AXDQQJYC-d4jn4xq5.js → quadrantDiagram-AXDQQJYC-dhok6m1j.js} +1 -1
  191. package/static/dist/assets/{radar-RG4KPBEZ-nebvfdzt.js → radar-RG4KPBEZ-n5giw3vb.js} +1 -1
  192. package/static/dist/assets/{railroad-74A4TZTK-ny3gebse.js → railroad-74A4TZTK-h6nfs9bj.js} +1 -1
  193. package/static/dist/assets/railroad-abnf-HS5TGJTU-d5gaaud9.js +1 -0
  194. package/static/dist/assets/railroad-ebnf-LZEXJU2U-ihudnbec.js +1 -0
  195. package/static/dist/assets/railroad-peg-WCYAUIDC-lgpvhel7.js +1 -0
  196. package/static/dist/assets/{railroadDiagram-O6MQD6OU-m09mmu67.js → railroadDiagram-O6MQD6OU-lfwg92r4.js} +1 -1
  197. package/static/dist/assets/{refresh-cw-eitd7yva.js → refresh-cw-bg6oc499.js} +1 -1
  198. package/static/dist/assets/{requirementDiagram-BXWQKSXE-evggpcbk.js → requirementDiagram-BXWQKSXE-c5k5pg07.js} +1 -1
  199. package/static/dist/assets/{sankeyDiagram-P5KCCOFB-bun89039.js → sankeyDiagram-P5KCCOFB-o9auio4y.js} +1 -1
  200. package/static/dist/assets/{saved-culx14ii.js → saved-j5wyhbik.js} +1 -1
  201. package/static/dist/assets/{search-nu6kzq55.js → search-ipa17zlr.js} +1 -1
  202. package/static/dist/assets/{sequenceDiagram-WJ2MYXX4-hhs9f09o.js → sequenceDiagram-WJ2MYXX4-pfgodnmk.js} +1 -1
  203. package/static/dist/assets/{settings_section_extensions_title-mj0gcozh.js → settings_section_extensions_title-fcfjf6l8.js} +1 -1
  204. package/static/dist/assets/share._token-i339gqj4.js +2 -0
  205. package/static/dist/assets/{sliders-horizontal-b9ljga1o.js → sliders-horizontal-bfrh4rch.js} +1 -1
  206. package/static/dist/assets/{stateDiagram-D77RDMKH-gi7t1tro.js → stateDiagram-D77RDMKH-m1y2cr3q.js} +1 -1
  207. package/static/dist/assets/stateDiagram-v2-MP3YSRHH-hehvfv55.js +1 -0
  208. package/static/dist/assets/{swimlanes-42K2YHIH-ea5urvph.js → swimlanes-42K2YHIH-hzai6fwo.js} +1 -1
  209. package/static/dist/assets/swimlanesDiagram-VR7AAH4N-nfk9mtst.js +8 -0
  210. package/static/dist/assets/{timeline-definition-24CTP7MA-mq64rm3b.js → timeline-definition-24CTP7MA-gwefy9gi.js} +1 -1
  211. package/static/dist/assets/todoStore-fhvdsen0.js +1 -0
  212. package/static/dist/assets/{treeView-Q6P3EWNA-cimbtxlz.js → treeView-Q6P3EWNA-7q91p0dn.js} +1 -1
  213. package/static/dist/assets/{treemap-WGGIJYW6-wig5gi1y.js → treemap-WGGIJYW6-occom39a.js} +1 -1
  214. package/static/dist/assets/{untitled-mbwemvxf.js → untitled-cgw2enra.js} +1 -1
  215. package/static/dist/assets/{useForm-li4bljlv.js → useForm-jwbfla68.js} +1 -1
  216. package/static/dist/assets/useLocale-gieo1fn7.js +1 -0
  217. package/static/dist/assets/{vennDiagram-4TSXK5OY-ez63kh7n.js → vennDiagram-4TSXK5OY-ew4bhldi.js} +1 -1
  218. package/static/dist/assets/{wardley-WFR3VGLG-r7rftz31.js → wardley-WFR3VGLG-njdacy0u.js} +1 -1
  219. package/static/dist/assets/{wardleyDiagram-VM6X3IG4-mvqg9eqc.js → wardleyDiagram-VM6X3IG4-mkvxx7u9.js} +1 -1
  220. package/static/dist/assets/{xychartDiagram-S5SC5T6Z-eqxdfc91.js → xychartDiagram-S5SC5T6Z-8uriotdw.js} +1 -1
  221. package/static/dist/index.html +1 -1
  222. package/static/dist/manifest.webmanifest +1 -1
  223. package/static/dist/sw.js +114 -152
  224. package/static/dist/assets/AppShell-hikzkcql.js +0 -1
  225. package/static/dist/assets/ChatPage-ofqn89h9.js +0 -21
  226. package/static/dist/assets/HubRoute-f2zrs9si.js +0 -2
  227. package/static/dist/assets/Select-lvx8gihl.js +0 -1
  228. package/static/dist/assets/SessionListPanel-o70ljmik.js +0 -1
  229. package/static/dist/assets/States-i46tz8an.js +0 -1
  230. package/static/dist/assets/Toaster-fklyeeyh.js +0 -1
  231. package/static/dist/assets/_app-j3rors25.js +0 -1
  232. package/static/dist/assets/_app.ext._extensionId-c0ey5ec9.js +0 -1
  233. package/static/dist/assets/_app.index-coy9jrto.js +0 -1
  234. package/static/dist/assets/_app.insights-ebd2k0i5.js +0 -1
  235. package/static/dist/assets/_app.kanban-e2p7ypi5.js +0 -1
  236. package/static/dist/assets/_app.logs-ml21mfyz.js +0 -1
  237. package/static/dist/assets/_app.memory-lhu5mm47.js +0 -1
  238. package/static/dist/assets/_app.profiles-egx0fgjo.js +0 -1
  239. package/static/dist/assets/_app.settings-d0vqtyid.js +0 -1
  240. package/static/dist/assets/_app.settings._section-cnzj6pli.js +0 -1
  241. package/static/dist/assets/_app.skills-kkug8obu.js +0 -1
  242. package/static/dist/assets/_app.tasks-nf285d2o.js +0 -2
  243. package/static/dist/assets/_app.todos-dxalqp1e.js +0 -1
  244. package/static/dist/assets/_app.workspaces-bqtduujs.js +0 -1
  245. package/static/dist/assets/architecture-7GRP2DOG-o8zqevhv.js +0 -1
  246. package/static/dist/assets/channel-mw8l793e.js +0 -1
  247. package/static/dist/assets/classDiagram-ZZMXUADV-egajr1wp.js +0 -1
  248. package/static/dist/assets/classDiagram-v2-VYDZK3BY-egajr1wp.js +0 -1
  249. package/static/dist/assets/endpoints-f6ngsyu5.js +0 -64
  250. package/static/dist/assets/eventmodeling-NTZA5JFV-k3zhqcyl.js +0 -1
  251. package/static/dist/assets/flowDiagram-HODETNUW-bdxes51f.js +0 -1
  252. package/static/dist/assets/index-ab0wj9ik.css +0 -2
  253. package/static/dist/assets/index-fd27xooo.js +0 -11
  254. package/static/dist/assets/mermaid-HWGCJPDP-ejk0d88t.js +0 -1
  255. package/static/dist/assets/railroad-abnf-HS5TGJTU-gtd38edw.js +0 -1
  256. package/static/dist/assets/railroad-ebnf-LZEXJU2U-o238oax2.js +0 -1
  257. package/static/dist/assets/railroad-peg-WCYAUIDC-c4acqp2j.js +0 -1
  258. package/static/dist/assets/share._token-k4pcnxqu.js +0 -2
  259. package/static/dist/assets/stateDiagram-v2-MP3YSRHH-jtadbol7.js +0 -1
  260. package/static/dist/assets/swimlanesDiagram-VR7AAH4N-ki7weepw.js +0 -8
  261. package/static/dist/assets/todoStore-azvfuqbv.js +0 -1
  262. package/static/dist/assets/trash-osyo895g.js +0 -1
  263. package/static/dist/assets/useLocale-kdew6auf.js +0 -1
@@ -4,7 +4,8 @@
4
4
  * Web: a recognized clean Talaria checkout fast-forwards to the newest completed
5
5
  * release set (stable) or `origin/main` (experimental) and stamps `web/_release.json`
6
6
  * from immutable git blobs. Direct global npm installs can replace themselves
7
- * with an exact completed Stable package; other packaged installs stay manual.
7
+ * with an exact completed Stable package or the verified GHCR Experimental artifact, switching
8
+ * channels in either direction; other packaged installs stay manual.
8
9
  * Agent: the external checkout follows its own `v*` tags with a
9
10
  * stash/pull or force reset, then the gateway restarts through the sidecar.
10
11
  * Nothing here deletes git locks; `clearLock` only inventories them.
@@ -12,7 +13,7 @@
12
13
  import { readCapped } from '../http/capped.js';
13
14
  import { execFile } from 'node:child_process';
14
15
  import { createHash } from 'node:crypto';
15
- import { existsSync, lstatSync, mkdtempSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, unlinkSync, writeFileSync } from 'node:fs';
16
+ import { copyFileSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from 'node:fs';
16
17
  import { delimiter, dirname, isAbsolute, join, resolve } from 'node:path';
17
18
  import { dict } from '../config/agent-config.js';
18
19
  import { validateReleaseInfo } from '../release.js';
@@ -22,9 +23,6 @@ export const REPOSITORY_URL = `https://github.com/${REPOSITORY}`;
22
23
  export const API_ROOT = `https://api.github.com/repos/${REPOSITORY}`;
23
24
  /** Anchored paths work from both the git root and Web's nested working directory. */
24
25
  export const WEB_UPDATE_PATHS = [':(top)web/', ':(top)contracts/'];
25
- /** Relative to the git root: the release stamp and the blobs it is verified against. */
26
- export const RELEASE_STAMP = 'web/_release.json';
27
- const RELEASE_BLOBS = ['sidecar/agent_dependency.json', 'contract_versions.json'];
28
26
  export const CACHE_TTL_S = 1800;
29
27
  export const AUTO_UPDATE_INTERVAL_MS = 5 * 60_000;
30
28
  export const RESTART_MAX_WAIT_S = 300;
@@ -57,15 +55,9 @@ export const runGit = (args, cwd, timeoutMs = 10_000) => new Promise((done) => {
57
55
  done({ out: err || out || `git exited with status ${String(e.code)}`, ok: false });
58
56
  });
59
57
  });
60
- /** The steps a source checkout needs after its files change (README "From a source checkout"), run from `<root>/web`. */
61
- export const WEB_BUILD_STEPS = [
62
- ['ci', '--workspace', 'packages/contracts', '--workspace', 'packages/server', '--include=dev'],
63
- ['run', 'build', '--workspace', 'packages/contracts'],
64
- ['run', 'build', '--workspace', 'packages/server'],
65
- ];
66
58
  export const WEB_BUILD_TIMEOUT_MS = 10 * 60_000;
67
- /** The artifact the supervisor re-executes; a build that does not leave it behind did not succeed. */
68
- export const WEB_SERVER_ENTRY = 'packages/server/dist/bin/talaria-web.js';
59
+ /** Source checkouts are contributor-only: they update with git, and the updater never mutates them (TAL-379). */
60
+ export const CONTRIBUTOR_CHECKOUT_MESSAGE = 'This is a contributor source checkout. Update it with git and rebuild Web (see web/README.md); only npm installs update automatically.';
69
61
  const runNpmCommand = (args, cwd, timeoutMs, env) => new Promise((done) => {
70
62
  // Prefer the npm beside the running node so a supervisor started with an absolute node path still finds it.
71
63
  const beside = join(dirname(process.execPath), 'npm');
@@ -87,20 +79,7 @@ const runNpmCommand = (args, cwd, timeoutMs, env) => new Promise((done) => {
87
79
  done({ out: (stderr.trim() || stdout.trim() || `npm exited with status ${String(e.code)}`).slice(-4000), ok: false });
88
80
  });
89
81
  });
90
- export const runNpm = (args, cwd, timeoutMs) => runNpmCommand(args, cwd, timeoutMs, { ...process.env, NODE_ENV: 'development' });
91
82
  export const runPackageNpm = (args, cwd, timeoutMs) => runNpmCommand(args, cwd, timeoutMs, process.env);
92
- /** Install and build the checkout's Web packages; null on success, else the message for the caller. */
93
- async function buildWeb(root, build) {
94
- const cwd = join(root, 'web');
95
- for (const step of WEB_BUILD_STEPS) {
96
- const result = await build(step, cwd, WEB_BUILD_TIMEOUT_MS);
97
- if (!result.ok)
98
- return `\`npm ${step.join(' ')}\` failed: ${result.out || 'unknown error'}`;
99
- }
100
- if (!existsSync(join(cwd, WEB_SERVER_ENTRY)))
101
- return `build finished without producing ${WEB_SERVER_ENTRY}`;
102
- return null;
103
- }
104
83
  export const isGitLockError = (output) => { const l = output.toLowerCase(); return GIT_LOCK_SIGNATURES.some((s) => l.includes(s)); };
105
84
  /** Python `_sanitize_git_diagnostic`: strip URL userinfo, GitHub token shapes, and secret query values. */
106
85
  export function sanitizeGitDiagnostic(output, limit = 300) {
@@ -116,10 +95,14 @@ function fetchFailureMessage(out, network) {
116
95
  const lower = detail.toLowerCase();
117
96
  return NETWORK_FAILURES.some((s) => lower.includes(s)) ? network : `fetch failed: ${detail}`;
118
97
  }
119
- function gitFailure(out, message) {
120
- if (isGitLockError(out))
121
- return { ok: false, lock_conflict: true, message: 'Web update is blocked by a repository lock. Wait for the other Git operation or inspect the checkout manually.' };
122
- return { ok: false, message };
98
+ /** One delayed retry absorbs a transient network blip; a lock conflict is not transient. */
99
+ export const FETCH_RETRY_MS = 1000;
100
+ async function fetchWithRetry(git, args, cwd, timeoutMs) {
101
+ const first = await git(args, cwd, timeoutMs);
102
+ if (first.ok || isGitLockError(first.out))
103
+ return first;
104
+ await new Promise((r) => setTimeout(r, FETCH_RETRY_MS));
105
+ return git(args, cwd, timeoutMs);
123
106
  }
124
107
  function normalizeRemoteUrl(remote) {
125
108
  let url = remote.trim();
@@ -155,7 +138,13 @@ function sortKeys(value) {
155
138
  }
156
139
  const same = (a, b) => canon(a) === canon(b);
157
140
  // ── published release sets ───────────────────────────────────────────────────
141
+ /** `transient`: GitHub was unreachable, slow, or overloaded, so a retry can succeed; the rest describe published state. */
158
142
  export class ReleaseUnavailable extends Error {
143
+ transient;
144
+ constructor(message, transient = false) {
145
+ super(message);
146
+ this.transient = transient;
147
+ }
159
148
  }
160
149
  /** GitHub API client: opt-in `TALARIA_RELEASE_TOKEN`, 2 MB cap, and asset redirects only to GitHub's release host with the token stripped. */
161
150
  export function githubJson(fetchImpl, env) {
@@ -164,17 +153,25 @@ export function githubJson(fetchImpl, env) {
164
153
  const token = (env.TALARIA_RELEASE_TOKEN ?? '').trim();
165
154
  if (token)
166
155
  headers.Authorization = `Bearer ${token}`;
167
- let res = await fetchImpl(API_ROOT + path, { headers, redirect: 'manual', signal: AbortSignal.timeout(5000) });
156
+ const send = async (url, init) => {
157
+ try {
158
+ return await fetchImpl(url, init);
159
+ }
160
+ catch (error) {
161
+ throw new ReleaseUnavailable(`GitHub is unreachable: ${error.message}`, true);
162
+ }
163
+ };
164
+ let res = await send(API_ROOT + path, { headers, redirect: 'manual', signal: AbortSignal.timeout(5000) });
168
165
  if (res.status >= 300 && res.status < 400) {
169
166
  const target = new URL(res.headers.get('location') ?? '', API_ROOT + path);
170
167
  if (target.protocol !== 'https:' || target.hostname !== 'release-assets.githubusercontent.com')
171
168
  throw new ReleaseUnavailable('Unexpected release download redirect');
172
169
  const anonymous = { ...headers };
173
170
  delete anonymous.Authorization;
174
- res = await fetchImpl(target.toString(), { headers: anonymous, signal: AbortSignal.timeout(5000) });
171
+ res = await send(target.toString(), { headers: anonymous, signal: AbortSignal.timeout(5000) });
175
172
  }
176
173
  if (!res.ok)
177
- throw new ReleaseUnavailable(`GitHub answered ${String(res.status)}`);
174
+ throw new ReleaseUnavailable(`GitHub answered ${String(res.status)}`, res.status >= 500 || res.status === 429);
178
175
  const body = await readCapped(res, 2_000_000);
179
176
  if (!body)
180
177
  throw new ReleaseUnavailable('Release metadata exceeds the download limit');
@@ -187,7 +184,7 @@ export async function publishedWebRelease(channel, getJson, now = () => performa
187
184
  const deadline = now() + 15_000;
188
185
  const fetchJson = (path, asset = false) => {
189
186
  if (now() >= deadline)
190
- throw new ReleaseUnavailable('Release lookup exceeded its deadline; retry or update manually');
187
+ throw new ReleaseUnavailable('Release lookup exceeded its deadline; retry or update manually', true);
191
188
  return getJson(path, { asset });
192
189
  };
193
190
  const published = [];
@@ -263,6 +260,81 @@ export async function publishedWebRelease(channel, getJson, now = () => performa
263
260
  }
264
261
  throw new ReleaseUnavailable('No completed Talaria Web release is available on this channel');
265
262
  }
263
+ // ── Experimental artifacts (TAL-343) ─────────────────────────────────────────
264
+ const GHCR = 'https://ghcr.io';
265
+ const EXPERIMENTAL_REPOSITORY = 'maudecode/talaria-web-experimental';
266
+ const EXPERIMENTAL_ARTIFACT_TYPE = 'application/vnd.maudecode.talaria-web.experimental.v1';
267
+ const EXPERIMENTAL_LAYER_TYPE = 'application/vnd.maudecode.talaria-web.npm.tgz';
268
+ /** GHCR serves blobs by redirecting here; the pull token never follows. */
269
+ const EXPERIMENTAL_BLOB_HOST = 'pkg-containers.githubusercontent.com';
270
+ export const EXPERIMENTAL_TARBALL_CAP = 200_000_000;
271
+ const EXPERIMENTAL_VERSION = new RegExp(`^${VERSION}-exp\\.([a-f0-9]{12})$`);
272
+ /** Anonymous GHCR client with `githubJson`'s timeouts, caps, and transient classification. */
273
+ export function ghcrExperimental(fetchImpl) {
274
+ const send = async (url, init) => {
275
+ try {
276
+ return await fetchImpl(url, { redirect: 'manual', ...init });
277
+ }
278
+ catch (error) {
279
+ throw new ReleaseUnavailable(`GHCR is unreachable: ${error.message}`, true);
280
+ }
281
+ };
282
+ const check = (res, what) => {
283
+ if (!res.ok)
284
+ throw new ReleaseUnavailable(`GHCR answered ${String(res.status)} for the Experimental ${what}`, res.status >= 500 || res.status === 429);
285
+ };
286
+ const json = async (res, what) => {
287
+ check(res, what);
288
+ const body = await readCapped(res, 2_000_000);
289
+ if (!body)
290
+ throw new ReleaseUnavailable(`Experimental ${what} exceeds the download limit`);
291
+ try {
292
+ return dict(JSON.parse(body.toString('utf8')));
293
+ }
294
+ catch {
295
+ throw new ReleaseUnavailable(`Invalid Experimental ${what}`);
296
+ }
297
+ };
298
+ const token = async () => {
299
+ const body = await json(await send(`${GHCR}/token?scope=repository:${EXPERIMENTAL_REPOSITORY}:pull`, { signal: AbortSignal.timeout(5000) }), 'pull token');
300
+ if (typeof body.token !== 'string' || !body.token)
301
+ throw new ReleaseUnavailable('GHCR did not issue an anonymous pull token');
302
+ return body.token;
303
+ };
304
+ return {
305
+ release: async () => {
306
+ const auth = { Authorization: `Bearer ${await token()}` };
307
+ const m = await json(await send(`${GHCR}/v2/${EXPERIMENTAL_REPOSITORY}/manifests/experimental`, { headers: { ...auth, Accept: 'application/vnd.oci.image.manifest.v1+json' }, signal: AbortSignal.timeout(5000) }), 'manifest');
308
+ const layers = Array.isArray(m.layers) ? m.layers.map(dict) : [];
309
+ const layer = layers[0];
310
+ if (m.artifactType !== EXPERIMENTAL_ARTIFACT_TYPE || layers.length !== 1 || layer?.mediaType !== EXPERIMENTAL_LAYER_TYPE)
311
+ throw new ReleaseUnavailable('The Experimental artifact has an unexpected format');
312
+ const annotations = dict(m.annotations);
313
+ const sourceRevision = annotations['org.opencontainers.image.revision'];
314
+ const version = annotations['org.opencontainers.image.version'];
315
+ const size = layer.size;
316
+ if (typeof sourceRevision !== 'string' || !SHA.test(sourceRevision) || typeof version !== 'string' || EXPERIMENTAL_VERSION.exec(version)?.[4] !== sourceRevision.slice(0, 12)
317
+ || typeof layer.digest !== 'string' || !/^sha256:[a-f0-9]{64}$/.test(layer.digest) || typeof size !== 'number' || !Number.isInteger(size) || size < 1 || size > EXPERIMENTAL_TARBALL_CAP) {
318
+ throw new ReleaseUnavailable('The Experimental artifact has malformed provenance');
319
+ }
320
+ return { tag: `web-exp-v${version}`, version, sourceRevision, digest: layer.digest, size };
321
+ },
322
+ download: async (release) => {
323
+ let res = await send(`${GHCR}/v2/${EXPERIMENTAL_REPOSITORY}/blobs/${release.digest}`, { headers: { Authorization: `Bearer ${await token()}` }, signal: AbortSignal.timeout(120_000) });
324
+ if (res.status >= 300 && res.status < 400) {
325
+ const target = new URL(res.headers.get('location') ?? '', GHCR);
326
+ if (target.protocol !== 'https:' || target.hostname !== EXPERIMENTAL_BLOB_HOST)
327
+ throw new ReleaseUnavailable('Unexpected Experimental download redirect');
328
+ res = await send(target.toString(), { redirect: 'error', signal: AbortSignal.timeout(120_000) });
329
+ }
330
+ check(res, 'download');
331
+ const body = await readCapped(res, release.size);
332
+ if (body?.length !== release.size || `sha256:${createHash('sha256').update(body).digest('hex')}` !== release.digest)
333
+ throw new ReleaseUnavailable('The Experimental download does not match its digest');
334
+ return body;
335
+ },
336
+ };
337
+ }
266
338
  // ── Web checkout ─────────────────────────────────────────────────────────────
267
339
  /** The git root when `webRoot` is `<root>/web` of a Talaria checkout with a GitHub `maudecode/talaria` origin; else null. */
268
340
  export async function checkoutRoot(webRoot, git) {
@@ -305,7 +377,7 @@ export async function npmInstallInfo(webRoot, npm = runPackageNpm, container = e
305
377
  catch {
306
378
  return null;
307
379
  }
308
- if (metadata.name !== WEB_NPM_PACKAGE || typeof metadata.version !== 'string' || !new RegExp(`^${VERSION}$`).test(metadata.version))
380
+ if (metadata.name !== WEB_NPM_PACKAGE || typeof metadata.version !== 'string' || !(new RegExp(`^${VERSION}$`).test(metadata.version) || EXPERIMENTAL_VERSION.test(metadata.version)))
309
381
  return null;
310
382
  const global = await npm(['root', '--global'], webRoot, 10_000);
311
383
  if (!global.ok || !isAbsolute(global.out))
@@ -336,141 +408,54 @@ function diskRelease(webRoot) {
336
408
  return null;
337
409
  }
338
410
  }
339
- /** Python `verify_release_source`: the stamp Web expects for `release`, computed from immutable blobs at its source revision. */
340
- export async function verifyReleaseSource(root, release, git) {
341
- const files = {};
342
- for (const name of RELEASE_BLOBS) {
343
- const shown = await git(['show', `${release.sourceRevision}:web/${name}`], root);
344
- if (!shown.ok)
345
- throw new Error('missing release metadata');
346
- files[name] = JSON.parse(shown.out);
347
- }
348
- const pin = dict(files['sidecar/agent_dependency.json']);
349
- const versions = dict(files['contract_versions.json']);
350
- const expected = {
351
- tag: release.tag,
352
- version: release.version,
353
- sourceRevision: release.sourceRevision,
354
- releaseSet: release.sourceRevision,
355
- contracts: { appWeb: [dict(versions.appWeb).fixtureVersion], webRelay: [dict(versions.webRelay).protocolVersion] },
356
- compatibleAgent: { ...dict(pin['x-talaria']), image: dict(dict(pin.services)['hermes-agent']).image },
357
- };
358
- if (!same(expected, release.runtime))
359
- throw new Error('release metadata differs from source');
360
- return expected;
361
- }
362
- const stampPath = (root) => join(root, RELEASE_STAMP);
363
- const readStamp = (path) => {
364
- if (!existsSync(path) && !isSymlink(path))
365
- return null;
366
- if (isSymlink(path))
367
- throw new Error('local release stamp is a symbolic link');
368
- return readFileSync(path, 'utf8');
369
- };
370
- const isSymlink = (path) => { try {
371
- return lstatSync(path).isSymbolicLink();
372
- }
373
- catch {
374
- return false;
375
- } };
376
- /** Python `_verified_release_stamp`: `[expected, installed]`; throws on a modified stamp. */
377
- async function verifiedReleaseStamp(root, release, git, id) {
378
- const expected = await verifyReleaseSource(root, release, git);
379
- const raw = readStamp(stampPath(root));
380
- const installed = raw === null ? null : JSON.parse(raw);
381
- if (installed !== null && ![id.release(), id.stamped(), expected].some((c) => same(c, installed)))
382
- throw new Error('local release stamp was modified');
383
- return [expected, installed];
384
- }
385
- /** Python `_main_stamp`: the unchanged generated stamp bytes (to discard when leaving a release), else null. */
386
- function mainStamp(root, id) {
387
- const data = readStamp(stampPath(root));
388
- if (data !== null && (!id.stamped().tag || !same(JSON.parse(data), id.stamped())))
389
- throw new Error('local release stamp was modified');
390
- return data;
391
- }
392
- async function mainRevision(root, git) {
393
- const fetched = await git(['fetch', '--no-tags', 'origin', 'refs/heads/main:refs/remotes/origin/main'], root, 30_000);
394
- if (!fetched.ok)
395
- return [null, fetched.out];
396
- const source = await git(['rev-parse', 'refs/remotes/origin/main^{commit}'], root);
397
- return source.ok && SHA.test(source.out) ? [source.out, ''] : [null, ''];
398
- }
399
- async function mainPathsDiffer(root, before, after, git) {
400
- const files = await git(['diff', '--no-renames', '--name-only', before, after, '--', ...WEB_UPDATE_PATHS], root);
401
- return files.ok ? Boolean(files.out) : null;
402
- }
403
- async function mainChangeCount(root, before, after, git) {
404
- const changed = await mainPathsDiffer(root, before, after, git);
405
- if (changed === null)
406
- return null;
407
- if (!changed)
408
- return 0;
409
- const count = await git(['rev-list', '--count', '--full-history', `${before}..${after}`, '--', ...WEB_UPDATE_PATHS], root);
410
- const n = Number.parseInt(count.out, 10);
411
- return count.ok && /^\d+$/.test(count.out) && n > 0 ? n : null;
412
- }
413
- async function mainRestartPending(root, head, git, id) {
414
- const running = id.runningSourceRevision();
415
- if (running === head)
416
- return false;
417
- if (!running)
418
- return true;
419
- return (await mainPathsDiffer(root, running, head, git)) !== false;
411
+ /** The channel an npm install came from: its release tag prefix (`web-exp-v` or `web-v`). */
412
+ function npmInstallChannel(installed, id) {
413
+ return str(diskRelease(installed.packageRoot)?.tag ?? id.release().tag).startsWith('web-exp-v') ? 'experimental' : 'stable';
420
414
  }
421
- async function checkMainUpdate(root, result, git, id) {
422
- Object.assign(result, { branch: 'origin/main', release_based: false });
423
- if (root === null)
424
- return { ...result, manual_update: true, message: 'Main updates require an authenticated Talaria source checkout with Web under web/.' };
425
- const [source, error] = await mainRevision(root, git);
426
- if (source === null)
427
- return { ...result, error: gitFailure(error, 'Could not fetch origin/main; check Git read access.').message };
428
- Object.assign(result, { latest_sha: source, latest_version: `main@${source.slice(0, 12)}` });
429
- const head = await git(['rev-parse', 'HEAD'], root);
430
- const status = await git(['status', '--porcelain', '--untracked-files=all'], root);
431
- if (!head.ok || !SHA.test(head.out) || !status.ok)
432
- return { ...result, manual_update: true, error: 'Could not verify the source checkout' };
433
- Object.assign(result, { installed_sha: head.out, dirty: Boolean(status.out) });
434
- const base = await git(['merge-base', head.out, source], root);
435
- result.current_sha = base.ok && SHA.test(base.out) ? base.out : null;
436
- if (result.current_sha)
437
- result.compare_url = `${REPOSITORY_URL}/compare/${base.out}...${source}`;
438
- if (!base.ok || base.out !== head.out)
439
- return { ...result, manual_update: true, message: 'This checkout is ahead of or diverged from origin/main; reconcile it manually.' };
440
- const count = await mainChangeCount(root, head.out, source, git);
441
- if (count === null)
442
- return { ...result, error: 'Could not compare the source checkout with origin/main' };
415
+ /** An npm install on Experimental: behind when its source differs from the `experimental` tag, or when switching channels. */
416
+ async function checkNpmExperimental(result, installed, id, registry) {
417
+ let release;
443
418
  try {
444
- mainStamp(root, id);
419
+ release = await registry.release();
445
420
  }
446
- catch {
447
- return { ...result, manual_update: true, error: 'Inspect the modified release stamp before updating.' };
448
- }
449
- Object.assign(result, { behind: count, metadata_repair: count === 0 && (await mainRestartPending(root, head.out, git, id)) });
450
- if (status.out)
451
- Object.assign(result, { manual_update: true, message: 'Commit or remove local changes before updating; Web updates never discard them.' });
452
- else if (result.metadata_repair)
453
- result.message = 'Source is current; finish the update to restart with that revision.';
454
- return result;
421
+ catch (error) {
422
+ const unavailable = error instanceof ReleaseUnavailable ? error : new ReleaseUnavailable('The Experimental artifact is unavailable');
423
+ return { ...result, install_kind: 'npm', manual_update: true, error: unavailable.message, ...(unavailable.transient ? { stale_check: true } : {}) };
424
+ }
425
+ Object.assign(result, { latest_version: release.tag, latest_sha: release.sourceRevision, branch: 'experimental', release_based: true, no_git: true, install_kind: 'npm', manual_update: false });
426
+ const disk = diskRelease(installed.packageRoot);
427
+ const current = str(disk?.sourceRevision ?? id.release().sourceRevision) || null;
428
+ if (npmInstallChannel(installed, id) !== 'experimental')
429
+ return { ...result, current_sha: current, behind: 1, channel_switch: true, message: `Switching to Experimental installs ${release.version}.` };
430
+ if (disk?.sourceRevision !== release.sourceRevision)
431
+ return { ...result, current_sha: current, behind: 1, message: `Experimental update ${release.version} is available.` };
432
+ const metadataRepair = !same(id.release(), disk);
433
+ return { ...result, current_sha: release.sourceRevision, behind: 0, metadata_repair: metadataRepair, ...(metadataRepair ? { message: 'The npm package is current; finish the update to restart with that version.' } : {}) };
455
434
  }
456
- /** Python `check_web_update`. */
457
- export async function checkWebUpdate(webRoot, currentVersion, channel, git, getJson, id, npmRun = runPackageNpm) {
435
+ /** Python `check_web_update`: npm and packaged installs; a source checkout is contributor-only (TAL-379). */
436
+ export async function checkWebUpdate(webRoot, currentVersion, channel, git, getJson, id, npmRun = runPackageNpm, registry) {
458
437
  const root = await checkoutRoot(webRoot, git);
459
438
  const npmInstall = root === null ? await npmInstallInfo(webRoot, npmRun) : null;
460
439
  const result = { name: 'webui', channel, repo_url: REPOSITORY_URL, current_version: currentVersion, behind: null, no_git: root === null };
461
- if (channel === 'experimental' && root !== null)
462
- return checkMainUpdate(root, result, git, id);
440
+ if (root !== null)
441
+ return { ...result, manual_update: true, message: CONTRIBUTOR_CHECKOUT_MESSAGE };
442
+ if (channel === 'experimental' && npmInstall && registry)
443
+ return checkNpmExperimental(result, npmInstall, id, registry);
463
444
  let release;
464
445
  try {
465
446
  release = await publishedWebRelease(channel, getJson);
466
447
  }
467
448
  catch (error) {
468
449
  if (error instanceof ReleaseUnavailable)
469
- return { ...result, manual_update: true, error: error.message };
450
+ return { ...result, manual_update: true, error: error.message, ...(error.transient ? { stale_check: true } : {}) };
470
451
  return { ...result, manual_update: true, error: 'Talaria release metadata is unavailable. Private repositories require TALARIA_RELEASE_TOKEN with Contents read access.' };
471
452
  }
472
453
  Object.assign(result, { latest_version: release.tag, latest_sha: release.sourceRevision, branch: release.tag, release_based: true, release_url: release.release_url, image: release.image });
473
- if (root === null) {
454
+ {
455
+ if (npmInstall && channel === 'stable' && release.npm && npmInstallChannel(npmInstall, id) === 'experimental') {
456
+ // A channel switch installs the newest Stable package regardless of version order (TAL-343).
457
+ return { ...result, current_sha: id.release().sourceRevision ?? null, behind: 1, no_git: true, install_kind: 'npm', npm: release.npm, manual_update: false, channel_switch: true, message: `Switching to Stable installs npm release ${release.version}.` };
458
+ }
474
459
  if (npmInstall && channel === 'stable' && release.npm && !str(id.release().tag).startsWith('web-exp-v')) {
475
460
  const comparison = compareVersions(npmInstall.version, release.version);
476
461
  if (comparison > 0)
@@ -495,75 +480,83 @@ export async function checkWebUpdate(webRoot, currentVersion, channel, git, getJ
495
480
  }
496
481
  return { ...result, current_sha: current, behind, no_git: true, manual_update: true, message: 'Use the published Talaria Web image or authenticated monorepo installation; legacy checkouts require migration.' };
497
482
  }
498
- const head = await git(['rev-parse', 'HEAD'], root);
499
- const status = await git(['status', '--porcelain', '--untracked-files=all'], root);
500
- if (!head.ok || !SHA.test(head.out) || !status.ok)
501
- return { ...result, manual_update: true, error: 'Could not verify the source checkout' };
502
- const current = head.out;
503
- Object.assign(result, { installed_sha: current, dirty: Boolean(status.out) });
504
- let base;
505
- let knownBase;
506
- if (current === release.sourceRevision) {
507
- try {
508
- const [expected, installed] = await verifiedReleaseStamp(root, release, git, id);
509
- result.behind = 0;
510
- result.metadata_repair = !same(installed, expected) || !same(id.release(), expected);
511
- if (result.metadata_repair)
512
- result.message = 'Apply the selected release again to repair its metadata or restart with its recorded identity.';
513
- }
514
- catch {
515
- Object.assign(result, { behind: null, manual_update: true, error: 'Could not verify local release provenance; inspect the release stamp before updating.' });
516
- }
517
- base = current;
518
- knownBase = true;
483
+ }
484
+ export const CHANNEL_SWITCH_BACKUPS = 5;
485
+ /** Web's persisted stores (`settings.json`, the project store, the workspace store) as seen from the state dir. */
486
+ const PERSISTED_STORES = ['settings.json', 'projects.json', 'workspaces.json', 'last_workspace.txt'];
487
+ /** Copy the persisted stores to `<stateDir>/backups/channel-switch-<UTC timestamp>/`, keeping the newest few; throws on failure. */
488
+ export function backupPersistedStores(stateDir, now = new Date(), keep = CHANNEL_SWITCH_BACKUPS) {
489
+ const root = join(stateDir, 'backups');
490
+ mkdirSync(root, { recursive: true });
491
+ const name = `channel-switch-${now.toISOString().replace(/[:.]/g, '-')}`;
492
+ const target = join(root, name);
493
+ if (existsSync(target))
494
+ throw new Error(`${name} already exists`);
495
+ // Only a complete copy is published under the name that counts toward the retained backups.
496
+ const partial = mkdtempSync(join(root, `.${name}-`));
497
+ try {
498
+ for (const store of PERSISTED_STORES)
499
+ if (existsSync(join(stateDir, store)))
500
+ copyFileSync(join(stateDir, store), join(partial, store));
501
+ renameSync(partial, target);
519
502
  }
520
- else {
521
- const contains = await git(['merge-base', '--is-ancestor', release.sourceRevision, current], root);
522
- if (contains.ok)
523
- return { ...result, behind: null, manual_update: true, current_sha: null, message: 'This checkout is ahead of the selected release. Manage it manually or check out the published release and restart Web.' };
524
- result.behind = 1;
525
- const mb = await git(['merge-base', current, release.sourceRevision], root);
526
- base = mb.out;
527
- knownBase = mb.ok;
528
- if (knownBase && base !== current)
529
- Object.assign(result, { manual_update: true, message: 'Reconcile divergent source history before updating Web.' });
530
- }
531
- // Local-only commits cannot appear in a GitHub comparison; omit unresolvable links.
532
- result.current_sha = knownBase && SHA.test(base) ? base : null;
533
- if (result.current_sha)
534
- result.compare_url = `${REPOSITORY_URL}/compare/${base}...${release.sourceRevision}`;
535
- if (status.out)
536
- Object.assign(result, { manual_update: true, message: 'Commit or remove local changes before updating; Web updates never discard them.' });
537
- return result;
503
+ catch (error) {
504
+ rmSync(partial, { recursive: true, force: true });
505
+ throw error;
506
+ }
507
+ const backups = readdirSync(root).filter((name) => name.startsWith('channel-switch-')).sort();
508
+ for (const old of backups.slice(0, Math.max(0, backups.length - keep)))
509
+ rmSync(join(root, old), { recursive: true, force: true });
510
+ return target;
538
511
  }
539
- async function applyNpmWebUpdate(webRoot, channel, getJson, id, npmRun, canApply) {
540
- if (channel !== 'stable')
541
- return { ok: false, manual_update: true, message: 'Packaged Experimental updates require manual artifact replacement.' };
542
- if (str(id.release().tag).startsWith('web-exp-v'))
543
- return { ok: false, manual_update: true, message: 'Switch packaged update channels manually before enabling Stable updates.' };
512
+ async function applyNpmWebUpdate(webRoot, channel, getJson, id, npmRun, canApply, packaged) {
544
513
  const installed = await npmInstallInfo(webRoot, npmRun);
545
514
  if (!installed)
546
515
  return { ok: false, manual_update: true, message: 'Automatic npm updates require a direct global @maudecode/talaria-web installation.' };
547
- let release;
548
- try {
549
- release = await publishedWebRelease('stable', getJson);
516
+ // Switching channels installs the selected channel's newest artifact regardless of version order (TAL-343).
517
+ const switching = npmInstallChannel(installed, id) !== channel;
518
+ const onDisk = diskRelease(installed.packageRoot);
519
+ let target;
520
+ if (channel === 'stable') {
521
+ let release;
522
+ try {
523
+ release = await publishedWebRelease('stable', getJson);
524
+ }
525
+ catch {
526
+ return { ok: false, message: 'Cannot resolve a completed Talaria release. Check private-repository read access.' };
527
+ }
528
+ if (!release.npm)
529
+ return { ok: false, manual_update: true, message: 'The completed Stable release does not include an npm package identity.' };
530
+ target = { version: release.version, sourceRevision: release.sourceRevision, label: `npm release ${release.version}`, matches: (r) => same(r, release.runtime), npm: release.npm };
531
+ if (!switching) {
532
+ if (!canApply())
533
+ return { ok: false, message: 'Web update deferred because its settings or lifecycle changed.' };
534
+ const comparison = compareVersions(installed.version, release.version);
535
+ if (comparison > 0)
536
+ return { ok: false, manual_update: true, message: 'This npm installation is ahead of the selected Stable release.' };
537
+ if (comparison === 0 && !target.matches(onDisk))
538
+ return { ok: false, manual_update: true, message: 'Installed npm release metadata does not match the completed release.' };
539
+ }
550
540
  }
551
- catch {
552
- return { ok: false, message: 'Cannot resolve a completed Talaria release. Check private-repository read access.' };
541
+ else {
542
+ if (!packaged.registry)
543
+ return { ok: false, manual_update: true, message: 'Packaged Experimental updates are unavailable in this installation.' };
544
+ let release;
545
+ try {
546
+ release = await packaged.registry.release();
547
+ }
548
+ catch (error) {
549
+ return { ok: false, message: error instanceof ReleaseUnavailable ? error.message : 'The Experimental artifact is unavailable.' };
550
+ }
551
+ const identity = { tag: release.tag, version: release.version, sourceRevision: release.sourceRevision, releaseSet: release.sourceRevision };
552
+ // validateReleaseInfo (in diskRelease) already applies Stable's contract and Agent-pin checks to the packaged files.
553
+ target = { version: release.version, sourceRevision: release.sourceRevision, label: `Experimental ${release.version}`, matches: (r) => r !== null && same({ tag: r.tag, version: r.version, sourceRevision: r.sourceRevision, releaseSet: r.releaseSet }, identity), experimental: release };
553
554
  }
554
- if (!release.npm)
555
- return { ok: false, manual_update: true, message: 'The completed Stable release does not include an npm package identity.' };
556
- if (!canApply())
557
- return { ok: false, message: 'Web update deferred because its settings or lifecycle changed.' };
558
- const comparison = compareVersions(installed.version, release.version);
559
- if (comparison > 0)
560
- return { ok: false, manual_update: true, message: 'This npm installation is ahead of the selected Stable release.' };
561
- if (comparison === 0) {
562
- if (!same(diskRelease(installed.packageRoot), release.runtime))
563
- return { ok: false, manual_update: true, message: 'Installed npm release metadata does not match the completed release.' };
564
- if (same(id.release(), release.runtime))
565
- return { ok: true, up_to_date: true, target: 'webui', channel, message: 'Talaria Web already contains the selected npm release.' };
566
- return { ok: true, target: 'webui', channel, sourceRevision: release.sourceRevision, npm: release.npm, message: `Restarting Talaria Web with npm release ${release.version}.` };
555
+ const current = !switching && target.matches(onDisk);
556
+ if (current) {
557
+ if (same(id.release(), onDisk))
558
+ return { ok: true, up_to_date: true, target: 'webui', channel, message: channel === 'stable' ? 'Talaria Web already contains the selected npm release.' : `Talaria Web already contains ${target.label}.` };
559
+ return { ok: true, target: 'webui', channel, sourceRevision: target.sourceRevision, ...(target.npm ? { npm: target.npm } : {}), message: `Restarting Talaria Web with ${target.label}.` };
567
560
  }
568
561
  if (!canApply())
569
562
  return { ok: false, message: 'Web update deferred because its settings or lifecycle changed.' };
@@ -573,19 +566,42 @@ async function applyNpmWebUpdate(webRoot, channel, getJson, id, npmRun, canApply
573
566
  const candidate = join(staging, 'lib/node_modules/@maudecode/talaria-web');
574
567
  const backup = join(staging, 'previous');
575
568
  let keepBackup = false;
569
+ let stores = null;
576
570
  try {
577
- const update = await npmRun(['install', '--global', '--prefix', staging, '--no-audit', '--no-fund', release.npm], staging, WEB_BUILD_TIMEOUT_MS);
571
+ let spec = target.npm ?? '';
572
+ if (target.experimental) {
573
+ let tarball;
574
+ try {
575
+ tarball = await packaged.registry.download(target.experimental);
576
+ }
577
+ catch (error) {
578
+ return { ok: false, verification_failed: true, target: 'webui', channel, message: `${error instanceof ReleaseUnavailable ? error.message : 'The Experimental download failed'}. The installed package was preserved.` };
579
+ }
580
+ spec = join(staging, 'talaria-web-experimental.tgz');
581
+ writeFileSync(spec, tarball);
582
+ }
583
+ const update = await npmRun(['install', '--global', '--prefix', staging, '--no-audit', '--no-fund', spec], staging, WEB_BUILD_TIMEOUT_MS);
578
584
  if (!update.ok)
579
585
  return { ok: false, install_failed: true, target: 'webui', channel, message: `npm update failed: ${sanitizeGitDiagnostic(update.out, 1000) || 'unknown error'}` };
580
586
  const metadata = dict(JSON.parse(readFileSync(join(candidate, 'package.json'), 'utf8')));
581
587
  const bins = dict(metadata.bin);
582
- if (metadata.name !== WEB_NPM_PACKAGE || metadata.version !== release.version || !same(diskRelease(candidate), release.runtime)
588
+ if (metadata.name !== WEB_NPM_PACKAGE || metadata.version !== target.version || !target.matches(diskRelease(candidate))
583
589
  || !['talaria-web', 'talaria-web-mcp'].every((name) => str(bins[name]).replace(/^\.\//, '') === `dist/bin/${name}.js` && existsSync(join(candidate, `dist/bin/${name}.js`)))) {
584
- return { ok: false, verification_failed: true, target: 'webui', channel, message: 'The downloaded npm package does not match the completed release. The installed package was preserved.' };
590
+ return { ok: false, verification_failed: true, target: 'webui', channel, message: `The downloaded npm package does not match ${channel === 'stable' ? 'the completed release' : 'the Experimental artifact'}. The installed package was preserved.` };
585
591
  }
586
- const current = await npmInstallInfo(webRoot, npmRun);
587
- if (!canApply() || current?.packageRoot !== installed.packageRoot || current.version !== installed.version)
592
+ const recheck = await npmInstallInfo(webRoot, npmRun);
593
+ if (!canApply() || recheck?.packageRoot !== installed.packageRoot || recheck.version !== installed.version)
588
594
  return { ok: false, message: 'The npm installation or update settings changed; retry the update.' };
595
+ if (switching) {
596
+ try {
597
+ if (!packaged.stateDir)
598
+ throw new Error('the Web state directory is unknown');
599
+ stores = backupPersistedStores(packaged.stateDir);
600
+ }
601
+ catch (error) {
602
+ return { ok: false, backup_failed: true, target: 'webui', channel, message: `Channel switch aborted: Web settings could not be backed up (${sanitizeGitDiagnostic(error.message, 300)}). The installed package was preserved.` };
603
+ }
604
+ }
589
605
  renameSync(installed.packageRoot, backup);
590
606
  try {
591
607
  renameSync(candidate, installed.packageRoot);
@@ -609,128 +625,15 @@ async function applyNpmWebUpdate(webRoot, channel, getJson, id, npmRun, canApply
609
625
  catch { /* a leftover staging directory must not prevent restart after a verified switch */ }
610
626
  }
611
627
  }
612
- return { ok: true, target: 'webui', channel, sourceRevision: release.sourceRevision, npm: release.npm, message: `Updated Talaria Web to npm release ${release.version}.` };
628
+ return { ok: true, target: 'webui', channel, sourceRevision: target.sourceRevision, ...(target.npm ? { npm: target.npm } : {}),
629
+ ...(switching ? { channel_switch: true, backup_dir: stores } : {}), message: `Updated Talaria Web to ${target.label}.${stores ? ` Settings were backed up to ${stores}.` : ''}` };
613
630
  }
614
- /** Python `apply_web_update`: fast-forward a recognized clean checkout to main or a published Stable tag. */
615
- export async function applyWebUpdate(webRoot, channel, git, getJson, id, build = runNpm, npmRun = runPackageNpm, canApply = () => true) {
616
- const root = await checkoutRoot(webRoot, git);
617
- if (root === null)
618
- return applyNpmWebUpdate(webRoot, channel, getJson, id, npmRun, canApply);
619
- const status = await git(['status', '--porcelain', '--untracked-files=all'], root);
620
- if (!status.ok || status.out)
621
- return { ok: false, dirty: true, message: 'Web update refused: the checkout must be clean, including untracked files.' };
622
- for (const marker of ['MERGE_HEAD', 'CHERRY_PICK_HEAD', 'REVERT_HEAD', 'rebase-merge', 'rebase-apply', 'BISECT_LOG']) {
623
- const path = await git(['rev-parse', '--git-path', marker], root);
624
- if (!path.ok || existsSync(resolve(root, path.out)))
625
- return { ok: false, message: 'Finish or abort the repository operation before updating Web.' };
626
- }
627
- const headRes = await git(['rev-parse', 'HEAD'], root);
628
- if (!headRes.ok || !SHA.test(headRes.out))
629
- return { ok: false, message: 'Could not verify the current source revision' };
630
- const head = headRes.out;
631
- const main = channel === 'experimental';
632
- let source;
633
- let tag;
634
- let release = null;
635
- if (main) {
636
- const [revision, error] = await mainRevision(root, git);
637
- if (revision === null)
638
- return gitFailure(error, 'Could not fetch origin/main; check Git read access.');
639
- source = revision;
640
- tag = 'main';
641
- }
642
- else {
643
- try {
644
- release = await publishedWebRelease(channel, getJson);
645
- }
646
- catch {
647
- return { ok: false, message: 'Cannot resolve a completed Talaria release. Check private-repository read access.' };
648
- }
649
- source = release.sourceRevision;
650
- tag = release.tag;
651
- }
652
- if (head !== source && !main) {
653
- // Fetch only this immutable tag; never force-replace a local tag or pull an unrecorded tip.
654
- const fetched = await git(['fetch', '--no-tags', 'origin', `refs/tags/${tag}:refs/tags/${tag}`], root, 30_000);
655
- if (!fetched.ok)
656
- return gitFailure(fetched.out, 'Could not fetch the published Web tag. Check Git credentials or a conflicting local tag.');
657
- const resolved = await git(['rev-parse', `refs/tags/${tag}^{commit}`], root);
658
- if (!resolved.ok || resolved.out !== source)
659
- return { ok: false, message: 'Published Web tag does not match the immutable release manifest' };
660
- }
661
- if (head !== source) {
662
- const forward = await git(['merge-base', '--is-ancestor', head, source], root);
663
- if (!forward.ok) {
664
- const contains = await git(['merge-base', '--is-ancestor', source, head], root);
665
- if (contains.ok)
666
- return { ok: false, manual_update: true, target: 'webui', channel, message: 'This checkout is ahead of the selected source. Manage it manually; updates never rewind local work.' };
667
- return { ok: false, message: 'Web update refused: source histories diverge; reconcile the checkout manually.' };
668
- }
669
- }
670
- if (main) {
671
- const count = await mainChangeCount(root, head, source, git);
672
- if (count === null)
673
- return { ok: false, message: 'Could not compare Web and contract changes against origin/main.' };
674
- if (count === 0)
675
- source = head; // No checkout mutation for App/Relay-only changes.
676
- }
677
- // Compare provenance with the immutable incoming files before modifying the checkout.
678
- let expected = null;
679
- let installed;
680
- try {
681
- if (main)
682
- installed = mainStamp(root, id);
683
- else
684
- [expected, installed] = await verifiedReleaseStamp(root, release, git, id);
685
- }
686
- catch {
687
- return { ok: false, message: 'Web update refused: source or local provenance does not match the release manifest.' };
688
- }
689
- const runtimeCurrent = main ? !(await mainRestartPending(root, head, git, id)) : same(installed, expected) && same(id.release(), expected);
690
- if (head === source && runtimeCurrent)
691
- return { ok: true, up_to_date: true, target: 'webui', channel, message: main ? 'Web and shared contracts are current on main.' : 'Talaria Web already contains the selected release.' };
692
- const again = await git(['rev-parse', 'HEAD'], root);
693
- const clean = await git(['status', '--porcelain', '--untracked-files=all'], root);
694
- if (!again.ok || again.out !== head || !clean.ok || clean.out)
695
- return { ok: false, message: 'The checkout changed during the update; retry after it is clean.' };
696
- if (!canApply())
697
- return { ok: false, message: 'Web update deferred because its settings or lifecycle changed.' };
698
- if (head !== source) {
699
- const merged = await git(['merge', '--ff-only', '--no-stat', '--no-overwrite-ignore', source], root, 30_000);
700
- const actual = await git(['rev-parse', 'HEAD'], root);
701
- if (!merged.ok || !actual.ok || actual.out !== source)
702
- return gitFailure(merged.out, 'Web fast-forward failed; no local changes were discarded.');
703
- }
704
- // The supervisor re-executes the built artifact, so the checkout is installed and rebuilt before the stamp claims
705
- // the new release; a failed build leaves the old stamp (and the running server) truthful.
706
- const buildError = await buildWeb(root, build);
707
- if (buildError !== null)
708
- return { ok: false, build_failed: true, target: 'webui', channel, sourceRevision: source, message: `Source advanced to ${tag}, but the Web build failed: ${buildError}. Run \`npm ci\` and \`npm run build\` in web/ (see README), then restart Web.` };
709
- const stamp = stampPath(root);
710
- if (main && installed !== null) {
711
- try {
712
- if (isSymlink(stamp) || readFileSync(stamp, 'utf8') !== installed)
713
- throw new Error('release stamp changed during update');
714
- unlinkSync(stamp);
715
- }
716
- catch {
717
- return { ok: false, message: 'Source advanced but its release stamp could not be cleared; inspect it before restarting Web.' };
718
- }
719
- }
720
- else if (!main && !same(installed, expected)) {
721
- const temporary = join(dirname(stamp), `.release-${String(process.pid)}-${String(Date.now())}`);
722
- try {
723
- writeFileSync(temporary, `${JSON.stringify(expected, null, 2)}\n`);
724
- renameSync(temporary, stamp);
725
- }
726
- catch {
727
- rmSync(temporary, { force: true });
728
- return { ok: false, message: 'Source advanced, but release metadata could not be written. Repair file permissions before restarting Web.' };
729
- }
730
- }
731
- return { ok: true, target: 'webui', channel, sourceRevision: source, message: `Updated Talaria Web to ${tag}.` };
631
+ /** Python `apply_web_update`: installs npm releases; a source checkout is never mutated (TAL-379). */
632
+ export async function applyWebUpdate(webRoot, channel, git, getJson, id, npmRun = runPackageNpm, canApply = () => true, packaged = {}) {
633
+ if (await checkoutRoot(webRoot, git) !== null)
634
+ return { ok: false, manual_update: true, target: 'webui', channel, message: CONTRIBUTOR_CHECKOUT_MESSAGE };
635
+ return applyNpmWebUpdate(webRoot, channel, getJson, id, npmRun, canApply, packaged);
732
636
  }
733
- // ── Agent checkout: independent Stable releases or Experimental default branch ──
734
637
  async function releaseTags(path, git) {
735
638
  const out = await git(['tag', '--list', AGENT_TAG_GLOB, '--sort=-v:refname'], path);
736
639
  return out.ok ? out.out.split('\n').map((l) => l.trim()).filter((tag) => /^v\d+\.\d+\.\d+$/.test(tag)) : [];
@@ -739,6 +642,12 @@ async function currentReleaseTag(path, git) {
739
642
  const out = await git(['describe', '--tags', '--exact-match', '--match', AGENT_TAG_GLOB, 'HEAD'], path);
740
643
  return out.ok && out.out ? out.out : null;
741
644
  }
645
+ async function verifiedAgentIdentity(path, expectedRevision, git) {
646
+ const head = await git(['rev-parse', 'HEAD'], path);
647
+ if (!head.ok || head.out !== expectedRevision || !SHA.test(head.out))
648
+ return null;
649
+ return { verified_revision: head.out, verified_version: await currentReleaseTag(path, git) };
650
+ }
742
651
  const headContainsRef = async (path, ref, git) => (await git(['merge-base', '--is-ancestor', ref, 'HEAD'], path)).ok;
743
652
  const canFastForwardTo = async (path, ref, git) => (await git(['merge-base', '--is-ancestor', 'HEAD', ref], path)).ok;
744
653
  async function detectDefaultBranch(path, git) {
@@ -795,7 +704,7 @@ async function isDirty(path, git) {
795
704
  export async function checkAgentUpdate(path, git, channel = DEFAULT_CHANNEL) {
796
705
  if (!path || !existsSync(join(path, '.git')))
797
706
  return { name: 'agent', behind: null, no_git: true };
798
- const fetched = await git(['fetch', 'origin', '--tags', '--force'], path, 15_000);
707
+ const fetched = await fetchWithRetry(git, ['fetch', 'origin', '--tags', '--force'], path, 15_000);
799
708
  if (!fetched.ok) {
800
709
  const message = fetched.out ? `fetch failed: ${sanitizeGitDiagnostic(fetched.out)}` : 'fetch failed';
801
710
  return { name: 'agent', channel, behind: null, error: message, stale_check: true, dirty: await isDirty(path, git) };
@@ -824,8 +733,12 @@ export async function applyAgentUpdate(path, git, channel = DEFAULT_CHANNEL, pol
824
733
  catch (error) {
825
734
  return { ok: false, message: error.message };
826
735
  }
827
- if (info.current_sha === info.latest_sha)
828
- return { ok: true, up_to_date: true, target: 'agent', message: 'Agent is up to date.' };
736
+ if (info.current_sha === info.latest_sha) {
737
+ const verified = await verifiedAgentIdentity(path, str(info.latest_sha), git);
738
+ if (!verified)
739
+ return { ok: false, message: 'The installed Agent revision could not be verified.' };
740
+ return { ok: true, up_to_date: true, target: 'agent', message: 'Agent is up to date.', ...verified };
741
+ }
829
742
  if (info.manual_update || info.error)
830
743
  return { ...info, ok: false, message: info.message ?? info.error };
831
744
  const warning = confirmAgent(info, policy);
@@ -864,7 +777,10 @@ export async function applyAgentUpdate(path, git, channel = DEFAULT_CHANNEL, pol
864
777
  if (!popped.ok)
865
778
  message += '. Local changes remain in `git stash list`; resolve them manually.';
866
779
  }
867
- return { ok: true, message, target: 'agent', ref };
780
+ const verified = await verifiedAgentIdentity(path, revision, git);
781
+ if (!verified)
782
+ return { ok: false, message: 'The Agent update completed, but the installed revision could not be verified.', target: 'agent' };
783
+ return { ok: true, message, target: 'agent', ref, ...verified };
868
784
  }
869
785
  async function restoreStash(path, git, pullOut) {
870
786
  if ((await git(['stash', 'pop'], path)).ok)
@@ -903,7 +819,10 @@ export async function forceAgentUpdate(path, git, log, channel = DEFAULT_CHANNEL
903
819
  log(`[updates] force update: git clean -fd failed (continuing to reset --hard): ${cleaned.out}`);
904
820
  if (!(await git(['reset', '--hard', revision], path)).ok)
905
821
  return { ok: false, message: `Force reset to ${ref} failed` };
906
- return { ok: true, message: `agent force-updated to ${ref}`, target: 'agent', ref };
822
+ const verified = await verifiedAgentIdentity(path, revision, git);
823
+ if (!verified)
824
+ return { ok: false, message: 'The Agent force update completed, but the installed revision could not be verified.', target: 'agent' };
825
+ return { ok: true, message: `agent force-updated to ${ref}`, target: 'agent', ref, ...verified };
907
826
  }
908
827
  /** Python `_inventory_locks`: report `.git/**\/*.lock` without touching any of them. */
909
828
  export function inventoryLocks(path) {
@@ -1058,6 +977,7 @@ export class UpdateService {
1058
977
  cache = { webui: null, agent: null, checked_at: 0, include_agent: true, channel: DEFAULT_CHANNEL, agent_channel: DEFAULT_CHANNEL };
1059
978
  checking = null;
1060
979
  checkingKey = null;
980
+ lastGood = new Map();
1061
981
  applying = false;
1062
982
  autoTimer = null;
1063
983
  autoStarted = false;
@@ -1117,8 +1037,11 @@ export class UpdateService {
1117
1037
  this.checkingKey = key;
1118
1038
  this.checking = (async () => {
1119
1039
  try {
1120
- const webui = await checkWebUpdate(this.deps.webRoot, this.deps.webuiVersion, channel, this.git, this.deps.getJson, this.deps.identity, this.deps.npm ?? runPackageNpm);
1121
- const agent = includeAgent ? await checkAgentUpdate(this.deps.agentDir(), this.git, agentChannel) : ignoredAgent();
1040
+ const webui = this.keepLastGood('webui', channel, await checkWebUpdate(this.deps.webRoot, this.deps.webuiVersion, channel, this.git, this.deps.getJson, this.deps.identity, this.deps.npm ?? runPackageNpm, this.deps.experimental));
1041
+ // The check may have fetched release tags the startup `git describe` never saw.
1042
+ if (this.deps.refreshWebuiVersion)
1043
+ webui.current_version = this.deps.refreshWebuiVersion();
1044
+ const agent = includeAgent ? this.keepLastGood('agent', agentChannel, await checkAgentUpdate(this.deps.agentDir(), this.git, agentChannel)) : ignoredAgent();
1122
1045
  if (agent.latest_sha)
1123
1046
  Object.assign(agent, agentWarning(agent, this.agentPolicy()));
1124
1047
  Object.assign(this.cache, { webui, agent, checked_at: this.now(), include_agent: includeAgent, channel, agent_channel: agentChannel });
@@ -1131,6 +1054,18 @@ export class UpdateService {
1131
1054
  })();
1132
1055
  return this.checking;
1133
1056
  }
1057
+ /** A failed fetch (`stale_check`) keeps the channel's last good result until the cache TTL, so a network blip never flips the status; persistent failures surface. */
1058
+ keepLastGood(target, channel, result) {
1059
+ const key = `${target}:${channel}`;
1060
+ if (!result.stale_check) {
1061
+ if (!result.error)
1062
+ this.lastGood.set(key, { at: this.now(), result });
1063
+ return result;
1064
+ }
1065
+ this.deps.log(`[updates] ${target} fetch failed: ${str(result.error)}`);
1066
+ const prior = this.lastGood.get(key);
1067
+ return prior && this.now() - prior.at < CACHE_TTL_S ? { ...prior.result, stale_check: true } : result;
1068
+ }
1134
1069
  startAutoApply(intervalMs = AUTO_UPDATE_INTERVAL_MS) {
1135
1070
  if (this.autoStarted || this.autoRestartScheduled)
1136
1071
  return;
@@ -1155,11 +1090,19 @@ export class UpdateService {
1155
1090
  }
1156
1091
  /** Shared turn admission stays closed from update start through the supervisor restart. */
1157
1092
  blocksNewWork() { return this.applying || this.autoRestartScheduled; }
1093
+ pendingRestartResponse(target) {
1094
+ if (!this.autoRestartScheduled)
1095
+ return null;
1096
+ if (target === 'webui')
1097
+ return { ok: true, target, restart_scheduled: true, message: 'A Web restart is already scheduled.' };
1098
+ return { ok: false, status: 'already_in_progress', target, message: 'A Talaria Web restart is already scheduled. Wait for the server to restart before updating Hermes Agent.' };
1099
+ }
1158
1100
  async autoApplyOnce() {
1159
1101
  if (this.autoRunning || this.applying || this.autoRestartScheduled || !(this.deps.checkEnabled?.() ?? this.deps.autoApply?.()))
1160
1102
  return null;
1161
1103
  this.autoRunning = true;
1162
1104
  const lifecycle = this.lifecycle;
1105
+ let notificationId = null;
1163
1106
  try {
1164
1107
  const channel = this.deps.channel();
1165
1108
  const stillEnabled = () => lifecycle === this.lifecycle && Boolean(this.deps.autoApply?.()) && (this.deps.checkEnabled?.() ?? true) && channel === this.deps.channel();
@@ -1173,13 +1116,26 @@ export class UpdateService {
1173
1116
  }
1174
1117
  if (!(Number(web.behind) > 0 || web.metadata_repair === true))
1175
1118
  return web;
1119
+ notificationId = this.deps.autoNotification?.begin() ?? null;
1176
1120
  const result = await this.apply('webui', channel, stillEnabled);
1121
+ if (notificationId) {
1122
+ if (result.restart_blocked === true)
1123
+ this.deps.autoNotification?.transition(notificationId, 'blocked', null, undefined, result.message);
1124
+ else if (result.ok !== true)
1125
+ this.deps.autoNotification?.transition(notificationId, 'failed', null, undefined, result.message || result.error);
1126
+ else if (result.restart_scheduled === true)
1127
+ this.deps.autoNotification?.transition(notificationId, 'restarting', str(result.sourceRevision || result.candidate_revision));
1128
+ else
1129
+ this.deps.autoNotification?.transition(notificationId, 'succeeded');
1130
+ }
1177
1131
  this.deps.log(`[updates] automatic Web update: ${str(result.message || (result.ok ? 'applied' : 'failed'))}`);
1178
1132
  if (!result.ok)
1179
1133
  this.cache.webui = { ...web, message: result.message, error: result.restart_blocked ? undefined : result.message };
1180
1134
  return result;
1181
1135
  }
1182
1136
  catch (error) {
1137
+ if (notificationId)
1138
+ this.deps.autoNotification?.transition(notificationId, 'failed', null, undefined, error.message);
1183
1139
  this.deps.log(`[updates] automatic Web update failed: ${error.message}`);
1184
1140
  return { ok: false, error: error.message };
1185
1141
  }
@@ -1217,8 +1173,9 @@ export class UpdateService {
1217
1173
  async apply(target, channel, canApply = () => true, agentOptions = {}) {
1218
1174
  if (this.checking)
1219
1175
  await this.checking;
1220
- if (this.autoRestartScheduled)
1221
- return { ok: true, restart_scheduled: true, message: 'A Web restart is already scheduled.' };
1176
+ const pendingRestart = this.pendingRestartResponse(target);
1177
+ if (pendingRestart)
1178
+ return pendingRestart;
1222
1179
  if (!canApply())
1223
1180
  return { ok: false, message: 'Web update settings changed; update deferred.' };
1224
1181
  const blocked = this.blockedResponse(target);
@@ -1230,7 +1187,7 @@ export class UpdateService {
1230
1187
  async applyInner(target, channel, canApply = () => true, agentOptions = {}) {
1231
1188
  if (target === 'webui') {
1232
1189
  const lifecycle = this.lifecycle;
1233
- const result = await applyWebUpdate(this.deps.webRoot, channel, this.git, this.deps.getJson, this.deps.identity, this.deps.build ?? runNpm, this.deps.npm ?? runPackageNpm, canApply);
1190
+ const result = await applyWebUpdate(this.deps.webRoot, channel, this.git, this.deps.getJson, this.deps.identity, this.deps.npm ?? runPackageNpm, canApply, { registry: this.deps.experimental, stateDir: this.deps.stateDir });
1234
1191
  // Settings can cancel before mutation, but a committed replacement must finish restarting unless shutting down.
1235
1192
  if (result.ok && !result.up_to_date && lifecycle === this.lifecycle) {
1236
1193
  this.cache.checked_at = 0;
@@ -1286,6 +1243,9 @@ export class UpdateService {
1286
1243
  await this.checking;
1287
1244
  if (target === 'webui')
1288
1245
  return this.apply(target, channel);
1246
+ const pendingRestart = this.pendingRestartResponse(target);
1247
+ if (pendingRestart)
1248
+ return pendingRestart;
1289
1249
  const blocked = this.blockedResponse(target);
1290
1250
  if (blocked)
1291
1251
  return Promise.resolve(blocked);
@@ -1300,6 +1260,9 @@ export class UpdateService {
1300
1260
  clearLock(target, agentOptions = {}) {
1301
1261
  if (target === 'webui')
1302
1262
  return this.apply(target).then((r) => ({ ...r, lock_recovery: { action: 'retry-only' } }));
1263
+ const pendingRestart = this.pendingRestartResponse(target);
1264
+ if (pendingRestart)
1265
+ return Promise.resolve(pendingRestart);
1303
1266
  const blocked = this.blockedResponse(target);
1304
1267
  if (blocked)
1305
1268
  return Promise.resolve(blocked);