@maudecode/talaria-web 1.11.0 → 1.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (247) hide show
  1. package/_release.json +4 -4
  2. package/dist/api/chat-router.d.ts +68 -4
  3. package/dist/api/chat-router.js +11 -8
  4. package/dist/api/chat-router.js.map +1 -1
  5. package/dist/api/router.d.ts +10 -0
  6. package/dist/api/router.js +2 -1
  7. package/dist/api/router.js.map +1 -1
  8. package/dist/api/sessions-router.d.ts +407 -51
  9. package/dist/api/sessions-router.js +2 -2
  10. package/dist/api/sessions-router.js.map +1 -1
  11. package/dist/api/sse-routes.js +52 -8
  12. package/dist/api/sse-routes.js.map +1 -1
  13. package/dist/api/tools-router.d.ts +329 -0
  14. package/dist/api/tools-router.js +97 -4
  15. package/dist/api/tools-router.js.map +1 -1
  16. package/dist/app.d.ts +812 -55
  17. package/dist/auth/oidc.js +2 -0
  18. package/dist/auth/oidc.js.map +1 -1
  19. package/dist/auth/store.d.ts +17 -0
  20. package/dist/auth/store.js +77 -19
  21. package/dist/auth/store.js.map +1 -1
  22. package/dist/fs/atomic.d.ts +8 -1
  23. package/dist/fs/atomic.js +52 -3
  24. package/dist/fs/atomic.js.map +1 -1
  25. package/dist/http/context.d.ts +2 -0
  26. package/dist/http/context.js.map +1 -1
  27. package/dist/redact.d.ts +15 -0
  28. package/dist/redact.js +1393 -9
  29. package/dist/redact.js.map +1 -1
  30. package/dist/release.js +3 -1
  31. package/dist/release.js.map +1 -1
  32. package/dist/runtime.js +20 -9
  33. package/dist/runtime.js.map +1 -1
  34. package/dist/server.d.ts +1 -1
  35. package/dist/server.js +11 -5
  36. package/dist/server.js.map +1 -1
  37. package/dist/sessions/anchor.d.ts +2 -0
  38. package/dist/sessions/anchor.js +22 -17
  39. package/dist/sessions/anchor.js.map +1 -1
  40. package/dist/sessions/file-changes.d.ts +15 -0
  41. package/dist/sessions/file-changes.js +58 -0
  42. package/dist/sessions/file-changes.js.map +1 -0
  43. package/dist/sessions/journal.d.ts +6 -1
  44. package/dist/sessions/journal.js +9 -15
  45. package/dist/sessions/journal.js.map +1 -1
  46. package/dist/sessions/list.d.ts +21 -1
  47. package/dist/sessions/list.js +81 -17
  48. package/dist/sessions/list.js.map +1 -1
  49. package/dist/sessions/merge.d.ts +66 -2
  50. package/dist/sessions/merge.js +291 -9
  51. package/dist/sessions/merge.js.map +1 -1
  52. package/dist/sessions/pending.d.ts +11 -2
  53. package/dist/sessions/pending.js +52 -3
  54. package/dist/sessions/pending.js.map +1 -1
  55. package/dist/sessions/service.d.ts +16 -3
  56. package/dist/sessions/service.js +92 -54
  57. package/dist/sessions/service.js.map +1 -1
  58. package/dist/sessions/state-db.d.ts +5 -0
  59. package/dist/sessions/state-db.js +34 -0
  60. package/dist/sessions/state-db.js.map +1 -1
  61. package/dist/sessions/streams.d.ts +4 -1
  62. package/dist/sessions/streams.js +3 -0
  63. package/dist/sessions/streams.js.map +1 -1
  64. package/dist/sessions/tool-display.d.ts +21 -0
  65. package/dist/sessions/tool-display.js +113 -0
  66. package/dist/sessions/tool-display.js.map +1 -0
  67. package/dist/sessions/turn.d.ts +25 -7
  68. package/dist/sessions/turn.js +153 -90
  69. package/dist/sessions/turn.js.map +1 -1
  70. package/dist/spa.d.ts +2 -0
  71. package/dist/spa.js +9 -0
  72. package/dist/spa.js.map +1 -1
  73. package/dist/test/harness.d.ts +2 -0
  74. package/dist/test/harness.js +1 -1
  75. package/dist/test/harness.js.map +1 -1
  76. package/dist/tools/terminal.d.ts +6 -0
  77. package/dist/tools/terminal.js +24 -3
  78. package/dist/tools/terminal.js.map +1 -1
  79. package/dist/tools/update-notifications.d.ts +120 -0
  80. package/dist/tools/update-notifications.js +387 -0
  81. package/dist/tools/update-notifications.js.map +1 -0
  82. package/dist/tools/updates.d.ts +49 -3
  83. package/dist/tools/updates.js +319 -53
  84. package/dist/tools/updates.js.map +1 -1
  85. package/package.json +2 -2
  86. package/sidecar/scripts/replay_sidecar.py +17 -0
  87. package/sidecar/talaria_sidecar/__init__.py +1 -1
  88. package/sidecar/talaria_sidecar/home.py +39 -16
  89. package/sidecar/talaria_sidecar/methods/chat.py +113 -4
  90. package/static/dist/FILES.txt +115 -116
  91. package/static/dist/assets/AppShell-m4w420jl.js +1 -0
  92. package/static/dist/assets/ChatPage-cuqcnisv.js +20 -0
  93. package/static/dist/assets/HubRoute-hqgqtm35.js +2 -0
  94. package/static/dist/assets/{Markdown-ftyfzds0.js → Markdown-j2hlu30v.js} +7 -7
  95. package/static/dist/assets/RightPanel-oid5q557.js +1 -0
  96. package/static/dist/assets/Select-ot2zr0il.js +1 -0
  97. package/static/dist/assets/SessionListPanel-o3sgjnrq.js +1 -0
  98. package/static/dist/assets/{SettingsLayout-pdpmhy1o.js → SettingsLayout-b0idn649.js} +1 -1
  99. package/static/dist/assets/States-htz1oee5.js +1 -0
  100. package/static/dist/assets/Toaster-h49pk1hy.js +1 -0
  101. package/static/dist/assets/_app-h7eaezs7.js +1 -0
  102. package/static/dist/assets/_app.ext._extensionId-h8oaqzsj.js +1 -0
  103. package/static/dist/assets/_app.index-xinao3xh.js +1 -0
  104. package/static/dist/assets/_app.insights-cxplvhkm.js +1 -0
  105. package/static/dist/assets/_app.kanban-hhli8h0h.js +1 -0
  106. package/static/dist/assets/_app.logs-f6srwjfl.js +1 -0
  107. package/static/dist/assets/_app.memory-lntcea85.js +1 -0
  108. package/static/dist/assets/_app.profiles-lftyp6of.js +1 -0
  109. package/static/dist/assets/{_app.session._sessionId-n1ggcf5e.js → _app.session._sessionId-hhpcqih4.js} +1 -1
  110. package/static/dist/assets/_app.settings-rs93zpvw.js +1 -0
  111. package/static/dist/assets/_app.settings._section-nafqsbwm.js +1 -0
  112. package/static/dist/assets/_app.skills-jzoi1ivd.js +1 -0
  113. package/static/dist/assets/_app.tasks-ijphsw56.js +2 -0
  114. package/static/dist/assets/_app.todos-emyh8inv.js +1 -0
  115. package/static/dist/assets/_app.workspaces-fwr1cum8.js +1 -0
  116. package/static/dist/assets/{abnfDiagram-VCTEODGH-i4bgzu83.js → abnfDiagram-VCTEODGH-egykbz8h.js} +1 -1
  117. package/static/dist/assets/architecture-7GRP2DOG-l6y92mbn.js +1 -0
  118. package/static/dist/assets/{architectureDiagram-5GKGNRK7-mx21lwqa.js → architectureDiagram-5GKGNRK7-b2e0wk3y.js} +1 -1
  119. package/static/dist/assets/{blockDiagram-I7D4REHJ-o1hhchxv.js → blockDiagram-I7D4REHJ-i3cq67dn.js} +1 -1
  120. package/static/dist/assets/{c4Diagram-7LVT6UL2-jygqtvd8.js → c4Diagram-7LVT6UL2-lg1p1s7a.js} +1 -1
  121. package/static/dist/assets/channel-gq8cwu71.js +1 -0
  122. package/static/dist/assets/{chunk-4HAMMTFA-hjcpubgk.js → chunk-4HAMMTFA-i0s1s7zi.js} +1 -1
  123. package/static/dist/assets/{chunk-75Z2AOVW-eibnex6m.js → chunk-75Z2AOVW-kwinpapo.js} +1 -1
  124. package/static/dist/assets/{chunk-DU6HZSFF-k1tp3qnp.js → chunk-DU6HZSFF-hb8qrbc3.js} +3 -3
  125. package/static/dist/assets/{chunk-F27PBJKO-lvcgn16f.js → chunk-F27PBJKO-dy2tqtcs.js} +1 -1
  126. package/static/dist/assets/{chunk-GMAD6QVW-ldlzruv1.js → chunk-GMAD6QVW-fqox5yvt.js} +1 -1
  127. package/static/dist/assets/{chunk-GVQU2GXP-hs1mva9d.js → chunk-GVQU2GXP-bo3x6ze4.js} +1 -1
  128. package/static/dist/assets/{chunk-IMKFNOWR-o7eqx3yb.js → chunk-IMKFNOWR-nxs6rrcq.js} +1 -1
  129. package/static/dist/assets/{chunk-L3NEJ4N5-bp1ofl70.js → chunk-L3NEJ4N5-f9yeq1fi.js} +1 -1
  130. package/static/dist/assets/{chunk-OSK3NFVY-o8m6mdz5.js → chunk-OSK3NFVY-c7df44qv.js} +1 -1
  131. package/static/dist/assets/{chunk-P2QGCYS3-kx83fti2.js → chunk-P2QGCYS3-eyjzf5o5.js} +1 -1
  132. package/static/dist/assets/{chunk-POPQ4Y6H-mwl99e9y.js → chunk-POPQ4Y6H-iy3hp9rc.js} +1 -1
  133. package/static/dist/assets/{chunk-PWAF6VOD-oriuokxy.js → chunk-PWAF6VOD-co2yoefz.js} +1 -1
  134. package/static/dist/assets/{chunk-SHT3W25Y-ab5cf00v.js → chunk-SHT3W25Y-obzqmn9k.js} +1 -1
  135. package/static/dist/assets/{chunk-SVP7TREG-kqdrpr17.js → chunk-SVP7TREG-hqwdo6vt.js} +1 -1
  136. package/static/dist/assets/{chunk-TICWLB2K-kfbnjabk.js → chunk-TICWLB2K-l8t1mdfd.js} +1 -1
  137. package/static/dist/assets/classDiagram-ZZMXUADV-fg9sc5r8.js +1 -0
  138. package/static/dist/assets/classDiagram-v2-VYDZK3BY-fg9sc5r8.js +1 -0
  139. package/static/dist/assets/{copied-dp33szhq.js → copied-hurn8vn4.js} +1 -1
  140. package/static/dist/assets/{cynefin-OW5HDTMX-mm5sj9s7.js → cynefin-OW5HDTMX-ngx2ihay.js} +1 -1
  141. package/static/dist/assets/{cynefinDiagram-5FMLGOSQ-nfon5q38.js → cynefinDiagram-5FMLGOSQ-fiyooys3.js} +1 -1
  142. package/static/dist/assets/{dagre-GXQ25YYZ-dzdp9ypb.js → dagre-GXQ25YYZ-fctryel6.js} +1 -1
  143. package/static/dist/assets/{diagram-S7CK7UJ4-ckxzqo01.js → diagram-S7CK7UJ4-m27jopok.js} +1 -1
  144. package/static/dist/assets/{diagram-UQ7AKVKN-hw16h1um.js → diagram-UQ7AKVKN-ojgkbmp4.js} +1 -1
  145. package/static/dist/assets/{diagram-VSXAHHWV-mu7wu2zg.js → diagram-VSXAHHWV-gfafpgyj.js} +1 -1
  146. package/static/dist/assets/{diagram-VX7I27RA-k9uprwtj.js → diagram-VX7I27RA-daobicg4.js} +1 -1
  147. package/static/dist/assets/{diagram-Z3DM3KII-csdjzrg9.js → diagram-Z3DM3KII-mi475zvk.js} +1 -1
  148. package/static/dist/assets/{ebnfDiagram-PWID7BFC-n8mbw2jj.js → ebnfDiagram-PWID7BFC-daf4zvsy.js} +1 -1
  149. package/static/dist/assets/endpoints-cyjhnrbn.js +64 -0
  150. package/static/dist/assets/{erDiagram-RLTQ6QDP-fsfe1uj0.js → erDiagram-RLTQ6QDP-gxfhxvfp.js} +1 -1
  151. package/static/dist/assets/eventmodeling-NTZA5JFV-manga0bq.js +1 -0
  152. package/static/dist/assets/flowDiagram-HODETNUW-bpt13uny.js +1 -0
  153. package/static/dist/assets/{ganttDiagram-EL5Y4UJY-gsba3iq9.js → ganttDiagram-EL5Y4UJY-k3208e0p.js} +1 -1
  154. package/static/dist/assets/{gitGraph-4MIJSDKK-na86q1pu.js → gitGraph-4MIJSDKK-gjuhj4zk.js} +1 -1
  155. package/static/dist/assets/{gitGraphDiagram-WWUBYQGX-g72ew3xz.js → gitGraphDiagram-WWUBYQGX-jglxvux1.js} +1 -1
  156. package/static/dist/assets/{highlighted-body-KPVGNVTW-okn18cp1.js → highlighted-body-KPVGNVTW-gh24xc9c.js} +1 -1
  157. package/static/dist/assets/index-gsg78cb4.css +2 -0
  158. package/static/dist/assets/index-jb47los5.js +11 -0
  159. package/static/dist/assets/{info-A6RAGUB7-i01jeltk.js → info-A6RAGUB7-huf0xaoq.js} +1 -1
  160. package/static/dist/assets/{infoDiagram-27XIBGKW-cayiv6fa.js → infoDiagram-27XIBGKW-jk3n96ix.js} +1 -1
  161. package/static/dist/assets/{ishikawaDiagram-5VMMS53U-g4rf14bv.js → ishikawaDiagram-5VMMS53U-o95uo5dx.js} +1 -1
  162. package/static/dist/assets/{journeyDiagram-3NMN7TZE-fusb2424.js → journeyDiagram-3NMN7TZE-h5ye2l70.js} +1 -1
  163. package/static/dist/assets/{kanban-definition-UXKFOSKX-g831pwlr.js → kanban-definition-UXKFOSKX-lgrpk5yi.js} +1 -1
  164. package/static/dist/assets/{lazyRouteComponent-ol965zwk.js → lazyRouteComponent-owywngyt.js} +1 -1
  165. package/static/dist/assets/{line-md6em6gc.js → line-cihmc0pn.js} +1 -1
  166. package/static/dist/assets/{login-kfclsqd2.js → login-g063apx6.js} +1 -1
  167. package/static/dist/assets/mermaid-HWGCJPDP-okd1mnvj.js +1 -0
  168. package/static/dist/assets/{mermaid-parser.core-mq3uns9m.js → mermaid-parser.core-d1609aga.js} +3 -3
  169. package/static/dist/assets/{mindmap-definition-YA3MSWOX-oxafrned.js → mindmap-definition-YA3MSWOX-lbvtir31.js} +1 -1
  170. package/static/dist/assets/{onboarding-fo6hnkyv.js → onboarding-ngw0x0pr.js} +1 -1
  171. package/static/dist/assets/{packet-AYTQ26CC-ggxyvbym.js → packet-AYTQ26CC-o4cp5z3k.js} +1 -1
  172. package/static/dist/assets/{pegDiagram-XKGWAZYB-nk7xcqgp.js → pegDiagram-XKGWAZYB-e8xqu1qj.js} +1 -1
  173. package/static/dist/assets/{pie-WAS4IAKB-ito2razh.js → pie-WAS4IAKB-hm9icuov.js} +1 -1
  174. package/static/dist/assets/{pieDiagram-E7YTZNPT-g02xexar.js → pieDiagram-E7YTZNPT-hj96x7hn.js} +1 -1
  175. package/static/dist/assets/{quadrantDiagram-AXDQQJYC-d4jn4xq5.js → quadrantDiagram-AXDQQJYC-80utbxhm.js} +1 -1
  176. package/static/dist/assets/{radar-RG4KPBEZ-nebvfdzt.js → radar-RG4KPBEZ-k39b6100.js} +1 -1
  177. package/static/dist/assets/{railroad-74A4TZTK-ny3gebse.js → railroad-74A4TZTK-ewyqz67j.js} +1 -1
  178. package/static/dist/assets/railroad-abnf-HS5TGJTU-dx2q0evz.js +1 -0
  179. package/static/dist/assets/railroad-ebnf-LZEXJU2U-l8xt49t5.js +1 -0
  180. package/static/dist/assets/railroad-peg-WCYAUIDC-kuxha9yz.js +1 -0
  181. package/static/dist/assets/{railroadDiagram-O6MQD6OU-m09mmu67.js → railroadDiagram-O6MQD6OU-iwv9dby8.js} +1 -1
  182. package/static/dist/assets/{refresh-cw-eitd7yva.js → refresh-cw-hjzomwlo.js} +1 -1
  183. package/static/dist/assets/{requirementDiagram-BXWQKSXE-evggpcbk.js → requirementDiagram-BXWQKSXE-cdgh5jha.js} +1 -1
  184. package/static/dist/assets/{sankeyDiagram-P5KCCOFB-bun89039.js → sankeyDiagram-P5KCCOFB-dbu6p97m.js} +1 -1
  185. package/static/dist/assets/{saved-culx14ii.js → saved-feqhycco.js} +1 -1
  186. package/static/dist/assets/{search-nu6kzq55.js → search-n3jdc20p.js} +1 -1
  187. package/static/dist/assets/{sequenceDiagram-WJ2MYXX4-hhs9f09o.js → sequenceDiagram-WJ2MYXX4-b0wchifg.js} +1 -1
  188. package/static/dist/assets/{settings_section_extensions_title-mj0gcozh.js → settings_section_extensions_title-mtyviexd.js} +1 -1
  189. package/static/dist/assets/{share._token-k4pcnxqu.js → share._token-cwov8uqh.js} +2 -2
  190. package/static/dist/assets/{sliders-horizontal-b9ljga1o.js → sliders-horizontal-i1lseyg0.js} +1 -1
  191. package/static/dist/assets/{stateDiagram-D77RDMKH-gi7t1tro.js → stateDiagram-D77RDMKH-mb4b38xh.js} +1 -1
  192. package/static/dist/assets/stateDiagram-v2-MP3YSRHH-cn1fnqrg.js +1 -0
  193. package/static/dist/assets/{swimlanes-42K2YHIH-ea5urvph.js → swimlanes-42K2YHIH-ofrpfwj0.js} +1 -1
  194. package/static/dist/assets/swimlanesDiagram-VR7AAH4N-d652ummv.js +8 -0
  195. package/static/dist/assets/{timeline-definition-24CTP7MA-mq64rm3b.js → timeline-definition-24CTP7MA-hlw0jg6o.js} +1 -1
  196. package/static/dist/assets/todoStore-mfy9lpio.js +1 -0
  197. package/static/dist/assets/{treeView-Q6P3EWNA-cimbtxlz.js → treeView-Q6P3EWNA-d93wr19c.js} +1 -1
  198. package/static/dist/assets/{treemap-WGGIJYW6-wig5gi1y.js → treemap-WGGIJYW6-g7ezdedz.js} +1 -1
  199. package/static/dist/assets/{untitled-mbwemvxf.js → untitled-mqxhd1eh.js} +1 -1
  200. package/static/dist/assets/{useForm-li4bljlv.js → useForm-hh33yrwa.js} +1 -1
  201. package/static/dist/assets/useLocale-cr30vl7r.js +1 -0
  202. package/static/dist/assets/{vennDiagram-4TSXK5OY-ez63kh7n.js → vennDiagram-4TSXK5OY-m5vgeba5.js} +1 -1
  203. package/static/dist/assets/{wardley-WFR3VGLG-r7rftz31.js → wardley-WFR3VGLG-gpao9bxv.js} +1 -1
  204. package/static/dist/assets/{wardleyDiagram-VM6X3IG4-mvqg9eqc.js → wardleyDiagram-VM6X3IG4-g2yzwshm.js} +1 -1
  205. package/static/dist/assets/{xychartDiagram-S5SC5T6Z-eqxdfc91.js → xychartDiagram-S5SC5T6Z-itakhpic.js} +1 -1
  206. package/static/dist/index.html +1 -1
  207. package/static/dist/sw.js +114 -152
  208. package/static/dist/assets/AppShell-hikzkcql.js +0 -1
  209. package/static/dist/assets/ChatPage-ofqn89h9.js +0 -21
  210. package/static/dist/assets/HubRoute-f2zrs9si.js +0 -2
  211. package/static/dist/assets/RightPanel-febk3dkd.js +0 -1
  212. package/static/dist/assets/Select-lvx8gihl.js +0 -1
  213. package/static/dist/assets/SessionListPanel-o70ljmik.js +0 -1
  214. package/static/dist/assets/States-i46tz8an.js +0 -1
  215. package/static/dist/assets/Toaster-fklyeeyh.js +0 -1
  216. package/static/dist/assets/_app-j3rors25.js +0 -1
  217. package/static/dist/assets/_app.ext._extensionId-c0ey5ec9.js +0 -1
  218. package/static/dist/assets/_app.index-coy9jrto.js +0 -1
  219. package/static/dist/assets/_app.insights-ebd2k0i5.js +0 -1
  220. package/static/dist/assets/_app.kanban-e2p7ypi5.js +0 -1
  221. package/static/dist/assets/_app.logs-ml21mfyz.js +0 -1
  222. package/static/dist/assets/_app.memory-lhu5mm47.js +0 -1
  223. package/static/dist/assets/_app.profiles-egx0fgjo.js +0 -1
  224. package/static/dist/assets/_app.settings-d0vqtyid.js +0 -1
  225. package/static/dist/assets/_app.settings._section-cnzj6pli.js +0 -1
  226. package/static/dist/assets/_app.skills-kkug8obu.js +0 -1
  227. package/static/dist/assets/_app.tasks-nf285d2o.js +0 -2
  228. package/static/dist/assets/_app.todos-dxalqp1e.js +0 -1
  229. package/static/dist/assets/_app.workspaces-bqtduujs.js +0 -1
  230. package/static/dist/assets/architecture-7GRP2DOG-o8zqevhv.js +0 -1
  231. package/static/dist/assets/channel-mw8l793e.js +0 -1
  232. package/static/dist/assets/classDiagram-ZZMXUADV-egajr1wp.js +0 -1
  233. package/static/dist/assets/classDiagram-v2-VYDZK3BY-egajr1wp.js +0 -1
  234. package/static/dist/assets/endpoints-f6ngsyu5.js +0 -64
  235. package/static/dist/assets/eventmodeling-NTZA5JFV-k3zhqcyl.js +0 -1
  236. package/static/dist/assets/flowDiagram-HODETNUW-bdxes51f.js +0 -1
  237. package/static/dist/assets/index-ab0wj9ik.css +0 -2
  238. package/static/dist/assets/index-fd27xooo.js +0 -11
  239. package/static/dist/assets/mermaid-HWGCJPDP-ejk0d88t.js +0 -1
  240. package/static/dist/assets/railroad-abnf-HS5TGJTU-gtd38edw.js +0 -1
  241. package/static/dist/assets/railroad-ebnf-LZEXJU2U-o238oax2.js +0 -1
  242. package/static/dist/assets/railroad-peg-WCYAUIDC-c4acqp2j.js +0 -1
  243. package/static/dist/assets/stateDiagram-v2-MP3YSRHH-jtadbol7.js +0 -1
  244. package/static/dist/assets/swimlanesDiagram-VR7AAH4N-ki7weepw.js +0 -8
  245. package/static/dist/assets/todoStore-azvfuqbv.js +0 -1
  246. package/static/dist/assets/trash-osyo895g.js +0 -1
  247. 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, unlinkSync, 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';
@@ -116,6 +117,15 @@ function fetchFailureMessage(out, network) {
116
117
  const lower = detail.toLowerCase();
117
118
  return NETWORK_FAILURES.some((s) => lower.includes(s)) ? network : `fetch failed: ${detail}`;
118
119
  }
120
+ /** One delayed retry absorbs a transient network blip; a lock conflict is not transient. */
121
+ export const FETCH_RETRY_MS = 1000;
122
+ async function fetchWithRetry(git, args, cwd, timeoutMs) {
123
+ const first = await git(args, cwd, timeoutMs);
124
+ if (first.ok || isGitLockError(first.out))
125
+ return first;
126
+ await new Promise((r) => setTimeout(r, FETCH_RETRY_MS));
127
+ return git(args, cwd, timeoutMs);
128
+ }
119
129
  function gitFailure(out, message) {
120
130
  if (isGitLockError(out))
121
131
  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.' };
@@ -155,7 +165,13 @@ function sortKeys(value) {
155
165
  }
156
166
  const same = (a, b) => canon(a) === canon(b);
157
167
  // ── published release sets ───────────────────────────────────────────────────
168
+ /** `transient`: GitHub was unreachable, slow, or overloaded, so a retry can succeed; the rest describe published state. */
158
169
  export class ReleaseUnavailable extends Error {
170
+ transient;
171
+ constructor(message, transient = false) {
172
+ super(message);
173
+ this.transient = transient;
174
+ }
159
175
  }
160
176
  /** 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
177
  export function githubJson(fetchImpl, env) {
@@ -164,17 +180,25 @@ export function githubJson(fetchImpl, env) {
164
180
  const token = (env.TALARIA_RELEASE_TOKEN ?? '').trim();
165
181
  if (token)
166
182
  headers.Authorization = `Bearer ${token}`;
167
- let res = await fetchImpl(API_ROOT + path, { headers, redirect: 'manual', signal: AbortSignal.timeout(5000) });
183
+ const send = async (url, init) => {
184
+ try {
185
+ return await fetchImpl(url, init);
186
+ }
187
+ catch (error) {
188
+ throw new ReleaseUnavailable(`GitHub is unreachable: ${error.message}`, true);
189
+ }
190
+ };
191
+ let res = await send(API_ROOT + path, { headers, redirect: 'manual', signal: AbortSignal.timeout(5000) });
168
192
  if (res.status >= 300 && res.status < 400) {
169
193
  const target = new URL(res.headers.get('location') ?? '', API_ROOT + path);
170
194
  if (target.protocol !== 'https:' || target.hostname !== 'release-assets.githubusercontent.com')
171
195
  throw new ReleaseUnavailable('Unexpected release download redirect');
172
196
  const anonymous = { ...headers };
173
197
  delete anonymous.Authorization;
174
- res = await fetchImpl(target.toString(), { headers: anonymous, signal: AbortSignal.timeout(5000) });
198
+ res = await send(target.toString(), { headers: anonymous, signal: AbortSignal.timeout(5000) });
175
199
  }
176
200
  if (!res.ok)
177
- throw new ReleaseUnavailable(`GitHub answered ${String(res.status)}`);
201
+ throw new ReleaseUnavailable(`GitHub answered ${String(res.status)}`, res.status >= 500 || res.status === 429);
178
202
  const body = await readCapped(res, 2_000_000);
179
203
  if (!body)
180
204
  throw new ReleaseUnavailable('Release metadata exceeds the download limit');
@@ -187,7 +211,7 @@ export async function publishedWebRelease(channel, getJson, now = () => performa
187
211
  const deadline = now() + 15_000;
188
212
  const fetchJson = (path, asset = false) => {
189
213
  if (now() >= deadline)
190
- throw new ReleaseUnavailable('Release lookup exceeded its deadline; retry or update manually');
214
+ throw new ReleaseUnavailable('Release lookup exceeded its deadline; retry or update manually', true);
191
215
  return getJson(path, { asset });
192
216
  };
193
217
  const published = [];
@@ -263,6 +287,81 @@ export async function publishedWebRelease(channel, getJson, now = () => performa
263
287
  }
264
288
  throw new ReleaseUnavailable('No completed Talaria Web release is available on this channel');
265
289
  }
290
+ // ── Experimental artifacts (TAL-343) ─────────────────────────────────────────
291
+ const GHCR = 'https://ghcr.io';
292
+ const EXPERIMENTAL_REPOSITORY = 'maudecode/talaria-web-experimental';
293
+ const EXPERIMENTAL_ARTIFACT_TYPE = 'application/vnd.maudecode.talaria-web.experimental.v1';
294
+ const EXPERIMENTAL_LAYER_TYPE = 'application/vnd.maudecode.talaria-web.npm.tgz';
295
+ /** GHCR serves blobs by redirecting here; the pull token never follows. */
296
+ const EXPERIMENTAL_BLOB_HOST = 'pkg-containers.githubusercontent.com';
297
+ export const EXPERIMENTAL_TARBALL_CAP = 200_000_000;
298
+ const EXPERIMENTAL_VERSION = new RegExp(`^${VERSION}-exp\\.([a-f0-9]{12})$`);
299
+ /** Anonymous GHCR client with `githubJson`'s timeouts, caps, and transient classification. */
300
+ export function ghcrExperimental(fetchImpl) {
301
+ const send = async (url, init) => {
302
+ try {
303
+ return await fetchImpl(url, { redirect: 'manual', ...init });
304
+ }
305
+ catch (error) {
306
+ throw new ReleaseUnavailable(`GHCR is unreachable: ${error.message}`, true);
307
+ }
308
+ };
309
+ const check = (res, what) => {
310
+ if (!res.ok)
311
+ throw new ReleaseUnavailable(`GHCR answered ${String(res.status)} for the Experimental ${what}`, res.status >= 500 || res.status === 429);
312
+ };
313
+ const json = async (res, what) => {
314
+ check(res, what);
315
+ const body = await readCapped(res, 2_000_000);
316
+ if (!body)
317
+ throw new ReleaseUnavailable(`Experimental ${what} exceeds the download limit`);
318
+ try {
319
+ return dict(JSON.parse(body.toString('utf8')));
320
+ }
321
+ catch {
322
+ throw new ReleaseUnavailable(`Invalid Experimental ${what}`);
323
+ }
324
+ };
325
+ const token = async () => {
326
+ const body = await json(await send(`${GHCR}/token?scope=repository:${EXPERIMENTAL_REPOSITORY}:pull`, { signal: AbortSignal.timeout(5000) }), 'pull token');
327
+ if (typeof body.token !== 'string' || !body.token)
328
+ throw new ReleaseUnavailable('GHCR did not issue an anonymous pull token');
329
+ return body.token;
330
+ };
331
+ return {
332
+ release: async () => {
333
+ const auth = { Authorization: `Bearer ${await token()}` };
334
+ 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');
335
+ const layers = Array.isArray(m.layers) ? m.layers.map(dict) : [];
336
+ const layer = layers[0];
337
+ if (m.artifactType !== EXPERIMENTAL_ARTIFACT_TYPE || layers.length !== 1 || layer?.mediaType !== EXPERIMENTAL_LAYER_TYPE)
338
+ throw new ReleaseUnavailable('The Experimental artifact has an unexpected format');
339
+ const annotations = dict(m.annotations);
340
+ const sourceRevision = annotations['org.opencontainers.image.revision'];
341
+ const version = annotations['org.opencontainers.image.version'];
342
+ const size = layer.size;
343
+ if (typeof sourceRevision !== 'string' || !SHA.test(sourceRevision) || typeof version !== 'string' || EXPERIMENTAL_VERSION.exec(version)?.[4] !== sourceRevision.slice(0, 12)
344
+ || typeof layer.digest !== 'string' || !/^sha256:[a-f0-9]{64}$/.test(layer.digest) || typeof size !== 'number' || !Number.isInteger(size) || size < 1 || size > EXPERIMENTAL_TARBALL_CAP) {
345
+ throw new ReleaseUnavailable('The Experimental artifact has malformed provenance');
346
+ }
347
+ return { tag: `web-exp-v${version}`, version, sourceRevision, digest: layer.digest, size };
348
+ },
349
+ download: async (release) => {
350
+ let res = await send(`${GHCR}/v2/${EXPERIMENTAL_REPOSITORY}/blobs/${release.digest}`, { headers: { Authorization: `Bearer ${await token()}` }, signal: AbortSignal.timeout(120_000) });
351
+ if (res.status >= 300 && res.status < 400) {
352
+ const target = new URL(res.headers.get('location') ?? '', GHCR);
353
+ if (target.protocol !== 'https:' || target.hostname !== EXPERIMENTAL_BLOB_HOST)
354
+ throw new ReleaseUnavailable('Unexpected Experimental download redirect');
355
+ res = await send(target.toString(), { redirect: 'error', signal: AbortSignal.timeout(120_000) });
356
+ }
357
+ check(res, 'download');
358
+ const body = await readCapped(res, release.size);
359
+ if (body?.length !== release.size || `sha256:${createHash('sha256').update(body).digest('hex')}` !== release.digest)
360
+ throw new ReleaseUnavailable('The Experimental download does not match its digest');
361
+ return body;
362
+ },
363
+ };
364
+ }
266
365
  // ── Web checkout ─────────────────────────────────────────────────────────────
267
366
  /** The git root when `webRoot` is `<root>/web` of a Talaria checkout with a GitHub `maudecode/talaria` origin; else null. */
268
367
  export async function checkoutRoot(webRoot, git) {
@@ -305,7 +404,7 @@ export async function npmInstallInfo(webRoot, npm = runPackageNpm, container = e
305
404
  catch {
306
405
  return null;
307
406
  }
308
- if (metadata.name !== WEB_NPM_PACKAGE || typeof metadata.version !== 'string' || !new RegExp(`^${VERSION}$`).test(metadata.version))
407
+ if (metadata.name !== WEB_NPM_PACKAGE || typeof metadata.version !== 'string' || !(new RegExp(`^${VERSION}$`).test(metadata.version) || EXPERIMENTAL_VERSION.test(metadata.version)))
309
408
  return null;
310
409
  const global = await npm(['root', '--global'], webRoot, 10_000);
311
410
  if (!global.ok || !isAbsolute(global.out))
@@ -336,6 +435,30 @@ function diskRelease(webRoot) {
336
435
  return null;
337
436
  }
338
437
  }
438
+ /** The channel an npm install came from: its release tag prefix (`web-exp-v` or `web-v`). */
439
+ function npmInstallChannel(installed, id) {
440
+ return str(diskRelease(installed.packageRoot)?.tag ?? id.release().tag).startsWith('web-exp-v') ? 'experimental' : 'stable';
441
+ }
442
+ /** An npm install on Experimental: behind when its source differs from the `experimental` tag, or when switching channels. */
443
+ async function checkNpmExperimental(result, installed, id, registry) {
444
+ let release;
445
+ try {
446
+ release = await registry.release();
447
+ }
448
+ catch (error) {
449
+ const unavailable = error instanceof ReleaseUnavailable ? error : new ReleaseUnavailable('The Experimental artifact is unavailable');
450
+ return { ...result, install_kind: 'npm', manual_update: true, error: unavailable.message, ...(unavailable.transient ? { stale_check: true } : {}) };
451
+ }
452
+ 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 });
453
+ const disk = diskRelease(installed.packageRoot);
454
+ const current = str(disk?.sourceRevision ?? id.release().sourceRevision) || null;
455
+ if (npmInstallChannel(installed, id) !== 'experimental')
456
+ return { ...result, current_sha: current, behind: 1, channel_switch: true, message: `Switching to Experimental installs ${release.version}.` };
457
+ if (disk?.sourceRevision !== release.sourceRevision)
458
+ return { ...result, current_sha: current, behind: 1, message: `Experimental update ${release.version} is available.` };
459
+ const metadataRepair = !same(id.release(), disk);
460
+ 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.' } : {}) };
461
+ }
339
462
  /** Python `verify_release_source`: the stamp Web expects for `release`, computed from immutable blobs at its source revision. */
340
463
  export async function verifyReleaseSource(root, release, git) {
341
464
  const files = {};
@@ -390,9 +513,12 @@ function mainStamp(root, id) {
390
513
  return data;
391
514
  }
392
515
  async function mainRevision(root, git) {
393
- const fetched = await git(['fetch', '--no-tags', 'origin', 'refs/heads/main:refs/remotes/origin/main'], root, 30_000);
516
+ const fetched = await fetchWithRetry(git, ['fetch', '--no-tags', 'origin', 'refs/heads/main:refs/remotes/origin/main'], root, 30_000);
394
517
  if (!fetched.ok)
395
518
  return [null, fetched.out];
519
+ // Single-branch clones never receive tags, so `git describe` would name a stale Web release.
520
+ // Best effort and unforced: a conflicting local tag is kept and never fails the main fetch.
521
+ await git(['fetch', '--no-tags', 'origin', 'refs/tags/web-v*:refs/tags/web-v*', 'refs/tags/web-exp-v*:refs/tags/web-exp-v*'], root, 30_000);
396
522
  const source = await git(['rev-parse', 'refs/remotes/origin/main^{commit}'], root);
397
523
  return source.ok && SHA.test(source.out) ? [source.out, ''] : [null, ''];
398
524
  }
@@ -423,8 +549,10 @@ async function checkMainUpdate(root, result, git, id) {
423
549
  if (root === null)
424
550
  return { ...result, manual_update: true, message: 'Main updates require an authenticated Talaria source checkout with Web under web/.' };
425
551
  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 };
552
+ if (source === null) {
553
+ const detail = sanitizeGitDiagnostic(error);
554
+ return { ...result, stale_check: true, error: gitFailure(error, detail ? `Could not fetch origin/main: ${detail}` : 'Could not fetch origin/main; check Git read access.').message };
555
+ }
428
556
  Object.assign(result, { latest_sha: source, latest_version: `main@${source.slice(0, 12)}` });
429
557
  const head = await git(['rev-parse', 'HEAD'], root);
430
558
  const status = await git(['status', '--porcelain', '--untracked-files=all'], root);
@@ -454,23 +582,29 @@ async function checkMainUpdate(root, result, git, id) {
454
582
  return result;
455
583
  }
456
584
  /** Python `check_web_update`. */
457
- export async function checkWebUpdate(webRoot, currentVersion, channel, git, getJson, id, npmRun = runPackageNpm) {
585
+ export async function checkWebUpdate(webRoot, currentVersion, channel, git, getJson, id, npmRun = runPackageNpm, registry) {
458
586
  const root = await checkoutRoot(webRoot, git);
459
587
  const npmInstall = root === null ? await npmInstallInfo(webRoot, npmRun) : null;
460
588
  const result = { name: 'webui', channel, repo_url: REPOSITORY_URL, current_version: currentVersion, behind: null, no_git: root === null };
461
589
  if (channel === 'experimental' && root !== null)
462
590
  return checkMainUpdate(root, result, git, id);
591
+ if (channel === 'experimental' && npmInstall && registry)
592
+ return checkNpmExperimental(result, npmInstall, id, registry);
463
593
  let release;
464
594
  try {
465
595
  release = await publishedWebRelease(channel, getJson);
466
596
  }
467
597
  catch (error) {
468
598
  if (error instanceof ReleaseUnavailable)
469
- return { ...result, manual_update: true, error: error.message };
599
+ return { ...result, manual_update: true, error: error.message, ...(error.transient ? { stale_check: true } : {}) };
470
600
  return { ...result, manual_update: true, error: 'Talaria release metadata is unavailable. Private repositories require TALARIA_RELEASE_TOKEN with Contents read access.' };
471
601
  }
472
602
  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
603
  if (root === null) {
604
+ if (npmInstall && channel === 'stable' && release.npm && npmInstallChannel(npmInstall, id) === 'experimental') {
605
+ // A channel switch installs the newest Stable package regardless of version order (TAL-343).
606
+ 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}.` };
607
+ }
474
608
  if (npmInstall && channel === 'stable' && release.npm && !str(id.release().tag).startsWith('web-exp-v')) {
475
609
  const comparison = compareVersions(npmInstall.version, release.version);
476
610
  if (comparison > 0)
@@ -536,34 +670,82 @@ export async function checkWebUpdate(webRoot, currentVersion, channel, git, getJ
536
670
  Object.assign(result, { manual_update: true, message: 'Commit or remove local changes before updating; Web updates never discard them.' });
537
671
  return result;
538
672
  }
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.' };
673
+ export const CHANNEL_SWITCH_BACKUPS = 5;
674
+ /** Web's persisted stores (`settings.json`, the project store, the workspace store) as seen from the state dir. */
675
+ const PERSISTED_STORES = ['settings.json', 'projects.json', 'workspaces.json', 'last_workspace.txt'];
676
+ /** Copy the persisted stores to `<stateDir>/backups/channel-switch-<UTC timestamp>/`, keeping the newest few; throws on failure. */
677
+ export function backupPersistedStores(stateDir, now = new Date(), keep = CHANNEL_SWITCH_BACKUPS) {
678
+ const root = join(stateDir, 'backups');
679
+ mkdirSync(root, { recursive: true });
680
+ const name = `channel-switch-${now.toISOString().replace(/[:.]/g, '-')}`;
681
+ const target = join(root, name);
682
+ if (existsSync(target))
683
+ throw new Error(`${name} already exists`);
684
+ // Only a complete copy is published under the name that counts toward the retained backups.
685
+ const partial = mkdtempSync(join(root, `.${name}-`));
686
+ try {
687
+ for (const store of PERSISTED_STORES)
688
+ if (existsSync(join(stateDir, store)))
689
+ copyFileSync(join(stateDir, store), join(partial, store));
690
+ renameSync(partial, target);
691
+ }
692
+ catch (error) {
693
+ rmSync(partial, { recursive: true, force: true });
694
+ throw error;
695
+ }
696
+ const backups = readdirSync(root).filter((name) => name.startsWith('channel-switch-')).sort();
697
+ for (const old of backups.slice(0, Math.max(0, backups.length - keep)))
698
+ rmSync(join(root, old), { recursive: true, force: true });
699
+ return target;
700
+ }
701
+ async function applyNpmWebUpdate(webRoot, channel, getJson, id, npmRun, canApply, packaged) {
544
702
  const installed = await npmInstallInfo(webRoot, npmRun);
545
703
  if (!installed)
546
704
  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);
705
+ // Switching channels installs the selected channel's newest artifact regardless of version order (TAL-343).
706
+ const switching = npmInstallChannel(installed, id) !== channel;
707
+ const onDisk = diskRelease(installed.packageRoot);
708
+ let target;
709
+ if (channel === 'stable') {
710
+ let release;
711
+ try {
712
+ release = await publishedWebRelease('stable', getJson);
713
+ }
714
+ catch {
715
+ return { ok: false, message: 'Cannot resolve a completed Talaria release. Check private-repository read access.' };
716
+ }
717
+ if (!release.npm)
718
+ return { ok: false, manual_update: true, message: 'The completed Stable release does not include an npm package identity.' };
719
+ target = { version: release.version, sourceRevision: release.sourceRevision, label: `npm release ${release.version}`, matches: (r) => same(r, release.runtime), npm: release.npm };
720
+ if (!switching) {
721
+ if (!canApply())
722
+ return { ok: false, message: 'Web update deferred because its settings or lifecycle changed.' };
723
+ const comparison = compareVersions(installed.version, release.version);
724
+ if (comparison > 0)
725
+ return { ok: false, manual_update: true, message: 'This npm installation is ahead of the selected Stable release.' };
726
+ if (comparison === 0 && !target.matches(onDisk))
727
+ return { ok: false, manual_update: true, message: 'Installed npm release metadata does not match the completed release.' };
728
+ }
550
729
  }
551
- catch {
552
- return { ok: false, message: 'Cannot resolve a completed Talaria release. Check private-repository read access.' };
730
+ else {
731
+ if (!packaged.registry)
732
+ return { ok: false, manual_update: true, message: 'Packaged Experimental updates are unavailable in this installation.' };
733
+ let release;
734
+ try {
735
+ release = await packaged.registry.release();
736
+ }
737
+ catch (error) {
738
+ return { ok: false, message: error instanceof ReleaseUnavailable ? error.message : 'The Experimental artifact is unavailable.' };
739
+ }
740
+ const identity = { tag: release.tag, version: release.version, sourceRevision: release.sourceRevision, releaseSet: release.sourceRevision };
741
+ // validateReleaseInfo (in diskRelease) already applies Stable's contract and Agent-pin checks to the packaged files.
742
+ 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
743
  }
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}.` };
744
+ const current = !switching && target.matches(onDisk);
745
+ if (current) {
746
+ if (same(id.release(), onDisk))
747
+ 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}.` };
748
+ return { ok: true, target: 'webui', channel, sourceRevision: target.sourceRevision, ...(target.npm ? { npm: target.npm } : {}), message: `Restarting Talaria Web with ${target.label}.` };
567
749
  }
568
750
  if (!canApply())
569
751
  return { ok: false, message: 'Web update deferred because its settings or lifecycle changed.' };
@@ -573,19 +755,42 @@ async function applyNpmWebUpdate(webRoot, channel, getJson, id, npmRun, canApply
573
755
  const candidate = join(staging, 'lib/node_modules/@maudecode/talaria-web');
574
756
  const backup = join(staging, 'previous');
575
757
  let keepBackup = false;
758
+ let stores = null;
576
759
  try {
577
- const update = await npmRun(['install', '--global', '--prefix', staging, '--no-audit', '--no-fund', release.npm], staging, WEB_BUILD_TIMEOUT_MS);
760
+ let spec = target.npm ?? '';
761
+ if (target.experimental) {
762
+ let tarball;
763
+ try {
764
+ tarball = await packaged.registry.download(target.experimental);
765
+ }
766
+ catch (error) {
767
+ return { ok: false, verification_failed: true, target: 'webui', channel, message: `${error instanceof ReleaseUnavailable ? error.message : 'The Experimental download failed'}. The installed package was preserved.` };
768
+ }
769
+ spec = join(staging, 'talaria-web-experimental.tgz');
770
+ writeFileSync(spec, tarball);
771
+ }
772
+ const update = await npmRun(['install', '--global', '--prefix', staging, '--no-audit', '--no-fund', spec], staging, WEB_BUILD_TIMEOUT_MS);
578
773
  if (!update.ok)
579
774
  return { ok: false, install_failed: true, target: 'webui', channel, message: `npm update failed: ${sanitizeGitDiagnostic(update.out, 1000) || 'unknown error'}` };
580
775
  const metadata = dict(JSON.parse(readFileSync(join(candidate, 'package.json'), 'utf8')));
581
776
  const bins = dict(metadata.bin);
582
- if (metadata.name !== WEB_NPM_PACKAGE || metadata.version !== release.version || !same(diskRelease(candidate), release.runtime)
777
+ if (metadata.name !== WEB_NPM_PACKAGE || metadata.version !== target.version || !target.matches(diskRelease(candidate))
583
778
  || !['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.' };
779
+ 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
780
  }
586
- const current = await npmInstallInfo(webRoot, npmRun);
587
- if (!canApply() || current?.packageRoot !== installed.packageRoot || current.version !== installed.version)
781
+ const recheck = await npmInstallInfo(webRoot, npmRun);
782
+ if (!canApply() || recheck?.packageRoot !== installed.packageRoot || recheck.version !== installed.version)
588
783
  return { ok: false, message: 'The npm installation or update settings changed; retry the update.' };
784
+ if (switching) {
785
+ try {
786
+ if (!packaged.stateDir)
787
+ throw new Error('the Web state directory is unknown');
788
+ stores = backupPersistedStores(packaged.stateDir);
789
+ }
790
+ catch (error) {
791
+ 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.` };
792
+ }
793
+ }
589
794
  renameSync(installed.packageRoot, backup);
590
795
  try {
591
796
  renameSync(candidate, installed.packageRoot);
@@ -609,13 +814,14 @@ async function applyNpmWebUpdate(webRoot, channel, getJson, id, npmRun, canApply
609
814
  catch { /* a leftover staging directory must not prevent restart after a verified switch */ }
610
815
  }
611
816
  }
612
- return { ok: true, target: 'webui', channel, sourceRevision: release.sourceRevision, npm: release.npm, message: `Updated Talaria Web to npm release ${release.version}.` };
817
+ return { ok: true, target: 'webui', channel, sourceRevision: target.sourceRevision, ...(target.npm ? { npm: target.npm } : {}),
818
+ ...(switching ? { channel_switch: true, backup_dir: stores } : {}), message: `Updated Talaria Web to ${target.label}.${stores ? ` Settings were backed up to ${stores}.` : ''}` };
613
819
  }
614
820
  /** 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) {
821
+ export async function applyWebUpdate(webRoot, channel, git, getJson, id, build = runNpm, npmRun = runPackageNpm, canApply = () => true, packaged = {}) {
616
822
  const root = await checkoutRoot(webRoot, git);
617
823
  if (root === null)
618
- return applyNpmWebUpdate(webRoot, channel, getJson, id, npmRun, canApply);
824
+ return applyNpmWebUpdate(webRoot, channel, getJson, id, npmRun, canApply, packaged);
619
825
  const status = await git(['status', '--porcelain', '--untracked-files=all'], root);
620
826
  if (!status.ok || status.out)
621
827
  return { ok: false, dirty: true, message: 'Web update refused: the checkout must be clean, including untracked files.' };
@@ -739,6 +945,12 @@ async function currentReleaseTag(path, git) {
739
945
  const out = await git(['describe', '--tags', '--exact-match', '--match', AGENT_TAG_GLOB, 'HEAD'], path);
740
946
  return out.ok && out.out ? out.out : null;
741
947
  }
948
+ async function verifiedAgentIdentity(path, expectedRevision, git) {
949
+ const head = await git(['rev-parse', 'HEAD'], path);
950
+ if (!head.ok || head.out !== expectedRevision || !SHA.test(head.out))
951
+ return null;
952
+ return { verified_revision: head.out, verified_version: await currentReleaseTag(path, git) };
953
+ }
742
954
  const headContainsRef = async (path, ref, git) => (await git(['merge-base', '--is-ancestor', ref, 'HEAD'], path)).ok;
743
955
  const canFastForwardTo = async (path, ref, git) => (await git(['merge-base', '--is-ancestor', 'HEAD', ref], path)).ok;
744
956
  async function detectDefaultBranch(path, git) {
@@ -795,7 +1007,7 @@ async function isDirty(path, git) {
795
1007
  export async function checkAgentUpdate(path, git, channel = DEFAULT_CHANNEL) {
796
1008
  if (!path || !existsSync(join(path, '.git')))
797
1009
  return { name: 'agent', behind: null, no_git: true };
798
- const fetched = await git(['fetch', 'origin', '--tags', '--force'], path, 15_000);
1010
+ const fetched = await fetchWithRetry(git, ['fetch', 'origin', '--tags', '--force'], path, 15_000);
799
1011
  if (!fetched.ok) {
800
1012
  const message = fetched.out ? `fetch failed: ${sanitizeGitDiagnostic(fetched.out)}` : 'fetch failed';
801
1013
  return { name: 'agent', channel, behind: null, error: message, stale_check: true, dirty: await isDirty(path, git) };
@@ -824,8 +1036,12 @@ export async function applyAgentUpdate(path, git, channel = DEFAULT_CHANNEL, pol
824
1036
  catch (error) {
825
1037
  return { ok: false, message: error.message };
826
1038
  }
827
- if (info.current_sha === info.latest_sha)
828
- return { ok: true, up_to_date: true, target: 'agent', message: 'Agent is up to date.' };
1039
+ if (info.current_sha === info.latest_sha) {
1040
+ const verified = await verifiedAgentIdentity(path, str(info.latest_sha), git);
1041
+ if (!verified)
1042
+ return { ok: false, message: 'The installed Agent revision could not be verified.' };
1043
+ return { ok: true, up_to_date: true, target: 'agent', message: 'Agent is up to date.', ...verified };
1044
+ }
829
1045
  if (info.manual_update || info.error)
830
1046
  return { ...info, ok: false, message: info.message ?? info.error };
831
1047
  const warning = confirmAgent(info, policy);
@@ -864,7 +1080,10 @@ export async function applyAgentUpdate(path, git, channel = DEFAULT_CHANNEL, pol
864
1080
  if (!popped.ok)
865
1081
  message += '. Local changes remain in `git stash list`; resolve them manually.';
866
1082
  }
867
- return { ok: true, message, target: 'agent', ref };
1083
+ const verified = await verifiedAgentIdentity(path, revision, git);
1084
+ if (!verified)
1085
+ return { ok: false, message: 'The Agent update completed, but the installed revision could not be verified.', target: 'agent' };
1086
+ return { ok: true, message, target: 'agent', ref, ...verified };
868
1087
  }
869
1088
  async function restoreStash(path, git, pullOut) {
870
1089
  if ((await git(['stash', 'pop'], path)).ok)
@@ -903,7 +1122,10 @@ export async function forceAgentUpdate(path, git, log, channel = DEFAULT_CHANNEL
903
1122
  log(`[updates] force update: git clean -fd failed (continuing to reset --hard): ${cleaned.out}`);
904
1123
  if (!(await git(['reset', '--hard', revision], path)).ok)
905
1124
  return { ok: false, message: `Force reset to ${ref} failed` };
906
- return { ok: true, message: `agent force-updated to ${ref}`, target: 'agent', ref };
1125
+ const verified = await verifiedAgentIdentity(path, revision, git);
1126
+ if (!verified)
1127
+ return { ok: false, message: 'The Agent force update completed, but the installed revision could not be verified.', target: 'agent' };
1128
+ return { ok: true, message: `agent force-updated to ${ref}`, target: 'agent', ref, ...verified };
907
1129
  }
908
1130
  /** Python `_inventory_locks`: report `.git/**\/*.lock` without touching any of them. */
909
1131
  export function inventoryLocks(path) {
@@ -1058,6 +1280,7 @@ export class UpdateService {
1058
1280
  cache = { webui: null, agent: null, checked_at: 0, include_agent: true, channel: DEFAULT_CHANNEL, agent_channel: DEFAULT_CHANNEL };
1059
1281
  checking = null;
1060
1282
  checkingKey = null;
1283
+ lastGood = new Map();
1061
1284
  applying = false;
1062
1285
  autoTimer = null;
1063
1286
  autoStarted = false;
@@ -1117,8 +1340,11 @@ export class UpdateService {
1117
1340
  this.checkingKey = key;
1118
1341
  this.checking = (async () => {
1119
1342
  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();
1343
+ 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));
1344
+ // The check may have fetched release tags the startup `git describe` never saw.
1345
+ if (this.deps.refreshWebuiVersion)
1346
+ webui.current_version = this.deps.refreshWebuiVersion();
1347
+ const agent = includeAgent ? this.keepLastGood('agent', agentChannel, await checkAgentUpdate(this.deps.agentDir(), this.git, agentChannel)) : ignoredAgent();
1122
1348
  if (agent.latest_sha)
1123
1349
  Object.assign(agent, agentWarning(agent, this.agentPolicy()));
1124
1350
  Object.assign(this.cache, { webui, agent, checked_at: this.now(), include_agent: includeAgent, channel, agent_channel: agentChannel });
@@ -1131,6 +1357,18 @@ export class UpdateService {
1131
1357
  })();
1132
1358
  return this.checking;
1133
1359
  }
1360
+ /** 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. */
1361
+ keepLastGood(target, channel, result) {
1362
+ const key = `${target}:${channel}`;
1363
+ if (!result.stale_check) {
1364
+ if (!result.error)
1365
+ this.lastGood.set(key, { at: this.now(), result });
1366
+ return result;
1367
+ }
1368
+ this.deps.log(`[updates] ${target} fetch failed: ${str(result.error)}`);
1369
+ const prior = this.lastGood.get(key);
1370
+ return prior && this.now() - prior.at < CACHE_TTL_S ? { ...prior.result, stale_check: true } : result;
1371
+ }
1134
1372
  startAutoApply(intervalMs = AUTO_UPDATE_INTERVAL_MS) {
1135
1373
  if (this.autoStarted || this.autoRestartScheduled)
1136
1374
  return;
@@ -1155,11 +1393,19 @@ export class UpdateService {
1155
1393
  }
1156
1394
  /** Shared turn admission stays closed from update start through the supervisor restart. */
1157
1395
  blocksNewWork() { return this.applying || this.autoRestartScheduled; }
1396
+ pendingRestartResponse(target) {
1397
+ if (!this.autoRestartScheduled)
1398
+ return null;
1399
+ if (target === 'webui')
1400
+ return { ok: true, target, restart_scheduled: true, message: 'A Web restart is already scheduled.' };
1401
+ 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.' };
1402
+ }
1158
1403
  async autoApplyOnce() {
1159
1404
  if (this.autoRunning || this.applying || this.autoRestartScheduled || !(this.deps.checkEnabled?.() ?? this.deps.autoApply?.()))
1160
1405
  return null;
1161
1406
  this.autoRunning = true;
1162
1407
  const lifecycle = this.lifecycle;
1408
+ let notificationId = null;
1163
1409
  try {
1164
1410
  const channel = this.deps.channel();
1165
1411
  const stillEnabled = () => lifecycle === this.lifecycle && Boolean(this.deps.autoApply?.()) && (this.deps.checkEnabled?.() ?? true) && channel === this.deps.channel();
@@ -1173,13 +1419,26 @@ export class UpdateService {
1173
1419
  }
1174
1420
  if (!(Number(web.behind) > 0 || web.metadata_repair === true))
1175
1421
  return web;
1422
+ notificationId = this.deps.autoNotification?.begin() ?? null;
1176
1423
  const result = await this.apply('webui', channel, stillEnabled);
1424
+ if (notificationId) {
1425
+ if (result.restart_blocked === true)
1426
+ this.deps.autoNotification?.transition(notificationId, 'blocked');
1427
+ else if (result.ok !== true)
1428
+ this.deps.autoNotification?.transition(notificationId, 'failed');
1429
+ else if (result.restart_scheduled === true)
1430
+ this.deps.autoNotification?.transition(notificationId, 'restarting', str(result.sourceRevision || result.candidate_revision));
1431
+ else
1432
+ this.deps.autoNotification?.transition(notificationId, 'succeeded');
1433
+ }
1177
1434
  this.deps.log(`[updates] automatic Web update: ${str(result.message || (result.ok ? 'applied' : 'failed'))}`);
1178
1435
  if (!result.ok)
1179
1436
  this.cache.webui = { ...web, message: result.message, error: result.restart_blocked ? undefined : result.message };
1180
1437
  return result;
1181
1438
  }
1182
1439
  catch (error) {
1440
+ if (notificationId)
1441
+ this.deps.autoNotification?.transition(notificationId, 'failed');
1183
1442
  this.deps.log(`[updates] automatic Web update failed: ${error.message}`);
1184
1443
  return { ok: false, error: error.message };
1185
1444
  }
@@ -1217,8 +1476,9 @@ export class UpdateService {
1217
1476
  async apply(target, channel, canApply = () => true, agentOptions = {}) {
1218
1477
  if (this.checking)
1219
1478
  await this.checking;
1220
- if (this.autoRestartScheduled)
1221
- return { ok: true, restart_scheduled: true, message: 'A Web restart is already scheduled.' };
1479
+ const pendingRestart = this.pendingRestartResponse(target);
1480
+ if (pendingRestart)
1481
+ return pendingRestart;
1222
1482
  if (!canApply())
1223
1483
  return { ok: false, message: 'Web update settings changed; update deferred.' };
1224
1484
  const blocked = this.blockedResponse(target);
@@ -1230,7 +1490,7 @@ export class UpdateService {
1230
1490
  async applyInner(target, channel, canApply = () => true, agentOptions = {}) {
1231
1491
  if (target === 'webui') {
1232
1492
  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);
1493
+ 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, { registry: this.deps.experimental, stateDir: this.deps.stateDir });
1234
1494
  // Settings can cancel before mutation, but a committed replacement must finish restarting unless shutting down.
1235
1495
  if (result.ok && !result.up_to_date && lifecycle === this.lifecycle) {
1236
1496
  this.cache.checked_at = 0;
@@ -1286,6 +1546,9 @@ export class UpdateService {
1286
1546
  await this.checking;
1287
1547
  if (target === 'webui')
1288
1548
  return this.apply(target, channel);
1549
+ const pendingRestart = this.pendingRestartResponse(target);
1550
+ if (pendingRestart)
1551
+ return pendingRestart;
1289
1552
  const blocked = this.blockedResponse(target);
1290
1553
  if (blocked)
1291
1554
  return Promise.resolve(blocked);
@@ -1300,6 +1563,9 @@ export class UpdateService {
1300
1563
  clearLock(target, agentOptions = {}) {
1301
1564
  if (target === 'webui')
1302
1565
  return this.apply(target).then((r) => ({ ...r, lock_recovery: { action: 'retry-only' } }));
1566
+ const pendingRestart = this.pendingRestartResponse(target);
1567
+ if (pendingRestart)
1568
+ return Promise.resolve(pendingRestart);
1303
1569
  const blocked = this.blockedResponse(target);
1304
1570
  if (blocked)
1305
1571
  return Promise.resolve(blocked);