@maudecode/talaria-web 0.0.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 (275) hide show
  1. package/_release.json +20 -0
  2. package/dist/api/chat-router.d.ts +120 -3
  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 +691 -40
  9. package/dist/api/sessions-router.js +2 -2
  10. package/dist/api/sessions-router.js.map +1 -1
  11. package/dist/api/settings-router.d.ts +8 -0
  12. package/dist/api/settings-router.js +13 -5
  13. package/dist/api/settings-router.js.map +1 -1
  14. package/dist/api/sse-routes.js +53 -9
  15. package/dist/api/sse-routes.js.map +1 -1
  16. package/dist/api/tools-router.d.ts +420 -0
  17. package/dist/api/tools-router.js +98 -5
  18. package/dist/api/tools-router.js.map +1 -1
  19. package/dist/app.d.ts +1247 -43
  20. package/dist/auth/oidc.js +2 -0
  21. package/dist/auth/oidc.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/bin/talaria-web.js +4 -1
  26. package/dist/bin/talaria-web.js.map +1 -1
  27. package/dist/cli/supervise.js +2 -1
  28. package/dist/cli/supervise.js.map +1 -1
  29. package/dist/fs/atomic.d.ts +8 -1
  30. package/dist/fs/atomic.js +52 -3
  31. package/dist/fs/atomic.js.map +1 -1
  32. package/dist/http/context.d.ts +2 -0
  33. package/dist/http/context.js.map +1 -1
  34. package/dist/redact.d.ts +15 -0
  35. package/dist/redact.js +1393 -9
  36. package/dist/redact.js.map +1 -1
  37. package/dist/release.js +3 -1
  38. package/dist/release.js.map +1 -1
  39. package/dist/runtime.js +28 -11
  40. package/dist/runtime.js.map +1 -1
  41. package/dist/server.d.ts +1 -1
  42. package/dist/server.js +11 -5
  43. package/dist/server.js.map +1 -1
  44. package/dist/sessions/anchor.d.ts +59 -5
  45. package/dist/sessions/anchor.js +373 -32
  46. package/dist/sessions/anchor.js.map +1 -1
  47. package/dist/sessions/file-changes.d.ts +15 -0
  48. package/dist/sessions/file-changes.js +58 -0
  49. package/dist/sessions/file-changes.js.map +1 -0
  50. package/dist/sessions/journal.d.ts +6 -1
  51. package/dist/sessions/journal.js +9 -15
  52. package/dist/sessions/journal.js.map +1 -1
  53. package/dist/sessions/list.d.ts +21 -1
  54. package/dist/sessions/list.js +81 -17
  55. package/dist/sessions/list.js.map +1 -1
  56. package/dist/sessions/merge.d.ts +72 -2
  57. package/dist/sessions/merge.js +326 -13
  58. package/dist/sessions/merge.js.map +1 -1
  59. package/dist/sessions/pending.d.ts +11 -2
  60. package/dist/sessions/pending.js +52 -3
  61. package/dist/sessions/pending.js.map +1 -1
  62. package/dist/sessions/service.d.ts +18 -3
  63. package/dist/sessions/service.js +115 -62
  64. package/dist/sessions/service.js.map +1 -1
  65. package/dist/sessions/session.js +1 -1
  66. package/dist/sessions/session.js.map +1 -1
  67. package/dist/sessions/state-db.d.ts +5 -0
  68. package/dist/sessions/state-db.js +34 -0
  69. package/dist/sessions/state-db.js.map +1 -1
  70. package/dist/sessions/streams.d.ts +4 -1
  71. package/dist/sessions/streams.js +3 -0
  72. package/dist/sessions/streams.js.map +1 -1
  73. package/dist/sessions/tool-display.d.ts +21 -0
  74. package/dist/sessions/tool-display.js +113 -0
  75. package/dist/sessions/tool-display.js.map +1 -0
  76. package/dist/sessions/turn.d.ts +39 -7
  77. package/dist/sessions/turn.js +288 -98
  78. package/dist/sessions/turn.js.map +1 -1
  79. package/dist/sessions/window.d.ts +1 -0
  80. package/dist/sessions/window.js +1 -1
  81. package/dist/sessions/window.js.map +1 -1
  82. package/dist/settings.js +5 -0
  83. package/dist/settings.js.map +1 -1
  84. package/dist/sidecar/client.d.ts +1 -1
  85. package/dist/sidecar/client.js +15 -4
  86. package/dist/sidecar/client.js.map +1 -1
  87. package/dist/sidecar/fake.js +3 -2
  88. package/dist/sidecar/fake.js.map +1 -1
  89. package/dist/spa.d.ts +2 -0
  90. package/dist/spa.js +9 -0
  91. package/dist/spa.js.map +1 -1
  92. package/dist/test/harness.d.ts +2 -0
  93. package/dist/test/harness.js +1 -1
  94. package/dist/test/harness.js.map +1 -1
  95. package/dist/tools/crons.js +55 -43
  96. package/dist/tools/crons.js.map +1 -1
  97. package/dist/tools/terminal.d.ts +6 -0
  98. package/dist/tools/terminal.js +24 -3
  99. package/dist/tools/terminal.js.map +1 -1
  100. package/dist/tools/update-notifications.d.ts +120 -0
  101. package/dist/tools/update-notifications.js +387 -0
  102. package/dist/tools/update-notifications.js.map +1 -0
  103. package/dist/tools/updates.d.ts +97 -13
  104. package/dist/tools/updates.js +638 -124
  105. package/dist/tools/updates.js.map +1 -1
  106. package/package.json +2 -2
  107. package/sidecar/agent_dependency.json +4 -3
  108. package/sidecar/scripts/replay_sidecar.py +17 -0
  109. package/sidecar/talaria_sidecar/__init__.py +1 -1
  110. package/sidecar/talaria_sidecar/home.py +51 -19
  111. package/sidecar/talaria_sidecar/methods/chat.py +113 -4
  112. package/sidecar/talaria_sidecar/methods/config.py +5 -3
  113. package/sidecar/talaria_sidecar/runtime.py +3 -9
  114. package/static/dist/FILES.txt +115 -116
  115. package/static/dist/assets/AppShell-m4w420jl.js +1 -0
  116. package/static/dist/assets/ChatPage-cuqcnisv.js +20 -0
  117. package/static/dist/assets/HubRoute-hqgqtm35.js +2 -0
  118. package/static/dist/assets/{Markdown-bt9bqd6a.js → Markdown-j2hlu30v.js} +7 -7
  119. package/static/dist/assets/RightPanel-oid5q557.js +1 -0
  120. package/static/dist/assets/Select-ot2zr0il.js +1 -0
  121. package/static/dist/assets/SessionListPanel-o3sgjnrq.js +1 -0
  122. package/static/dist/assets/SettingsLayout-b0idn649.js +1 -0
  123. package/static/dist/assets/States-htz1oee5.js +1 -0
  124. package/static/dist/assets/Toaster-h49pk1hy.js +1 -0
  125. package/static/dist/assets/_app-h7eaezs7.js +1 -0
  126. package/static/dist/assets/_app.ext._extensionId-h8oaqzsj.js +1 -0
  127. package/static/dist/assets/_app.index-xinao3xh.js +1 -0
  128. package/static/dist/assets/_app.insights-cxplvhkm.js +1 -0
  129. package/static/dist/assets/_app.kanban-hhli8h0h.js +1 -0
  130. package/static/dist/assets/_app.logs-f6srwjfl.js +1 -0
  131. package/static/dist/assets/_app.memory-lntcea85.js +1 -0
  132. package/static/dist/assets/_app.profiles-lftyp6of.js +1 -0
  133. package/static/dist/assets/{_app.session._sessionId-kevi4qd2.js → _app.session._sessionId-hhpcqih4.js} +1 -1
  134. package/static/dist/assets/_app.settings-rs93zpvw.js +1 -0
  135. package/static/dist/assets/_app.settings._section-nafqsbwm.js +1 -0
  136. package/static/dist/assets/_app.skills-jzoi1ivd.js +1 -0
  137. package/static/dist/assets/_app.tasks-ijphsw56.js +2 -0
  138. package/static/dist/assets/_app.todos-emyh8inv.js +1 -0
  139. package/static/dist/assets/_app.workspaces-fwr1cum8.js +1 -0
  140. package/static/dist/assets/{abnfDiagram-VCTEODGH-hvrr25lb.js → abnfDiagram-VCTEODGH-egykbz8h.js} +1 -1
  141. package/static/dist/assets/architecture-7GRP2DOG-l6y92mbn.js +1 -0
  142. package/static/dist/assets/{architectureDiagram-5GKGNRK7-b3r0z8sq.js → architectureDiagram-5GKGNRK7-b2e0wk3y.js} +1 -1
  143. package/static/dist/assets/{blockDiagram-I7D4REHJ-hd3gw0cj.js → blockDiagram-I7D4REHJ-i3cq67dn.js} +1 -1
  144. package/static/dist/assets/{c4Diagram-7LVT6UL2-jlmbjiji.js → c4Diagram-7LVT6UL2-lg1p1s7a.js} +1 -1
  145. package/static/dist/assets/channel-gq8cwu71.js +1 -0
  146. package/static/dist/assets/{chunk-4HAMMTFA-oydbpsin.js → chunk-4HAMMTFA-i0s1s7zi.js} +1 -1
  147. package/static/dist/assets/{chunk-75Z2AOVW-j633otj1.js → chunk-75Z2AOVW-kwinpapo.js} +1 -1
  148. package/static/dist/assets/{chunk-DU6HZSFF-ods2l027.js → chunk-DU6HZSFF-hb8qrbc3.js} +3 -3
  149. package/static/dist/assets/{chunk-F27PBJKO-ln4n2cwt.js → chunk-F27PBJKO-dy2tqtcs.js} +1 -1
  150. package/static/dist/assets/{chunk-GMAD6QVW-fpz133eh.js → chunk-GMAD6QVW-fqox5yvt.js} +1 -1
  151. package/static/dist/assets/{chunk-GVQU2GXP-abs9ixiy.js → chunk-GVQU2GXP-bo3x6ze4.js} +1 -1
  152. package/static/dist/assets/{chunk-IMKFNOWR-j9vukdy1.js → chunk-IMKFNOWR-nxs6rrcq.js} +1 -1
  153. package/static/dist/assets/{chunk-L3NEJ4N5-cfyw87r3.js → chunk-L3NEJ4N5-f9yeq1fi.js} +1 -1
  154. package/static/dist/assets/{chunk-OSK3NFVY-baja78wr.js → chunk-OSK3NFVY-c7df44qv.js} +1 -1
  155. package/static/dist/assets/{chunk-P2QGCYS3-g66xby8h.js → chunk-P2QGCYS3-eyjzf5o5.js} +1 -1
  156. package/static/dist/assets/{chunk-POPQ4Y6H-osqu864t.js → chunk-POPQ4Y6H-iy3hp9rc.js} +1 -1
  157. package/static/dist/assets/{chunk-PWAF6VOD-egejasv1.js → chunk-PWAF6VOD-co2yoefz.js} +1 -1
  158. package/static/dist/assets/{chunk-SHT3W25Y-l014h8zz.js → chunk-SHT3W25Y-obzqmn9k.js} +1 -1
  159. package/static/dist/assets/{chunk-SVP7TREG-n3z10bp3.js → chunk-SVP7TREG-hqwdo6vt.js} +1 -1
  160. package/static/dist/assets/{chunk-TICWLB2K-bzozkjc0.js → chunk-TICWLB2K-l8t1mdfd.js} +1 -1
  161. package/static/dist/assets/classDiagram-ZZMXUADV-fg9sc5r8.js +1 -0
  162. package/static/dist/assets/classDiagram-v2-VYDZK3BY-fg9sc5r8.js +1 -0
  163. package/static/dist/assets/{copied-cjsttec1.js → copied-hurn8vn4.js} +1 -1
  164. package/static/dist/assets/{cynefin-OW5HDTMX-elpt7h1j.js → cynefin-OW5HDTMX-ngx2ihay.js} +1 -1
  165. package/static/dist/assets/{cynefinDiagram-5FMLGOSQ-5mzl5pi2.js → cynefinDiagram-5FMLGOSQ-fiyooys3.js} +1 -1
  166. package/static/dist/assets/{dagre-GXQ25YYZ-ihwlhmfl.js → dagre-GXQ25YYZ-fctryel6.js} +1 -1
  167. package/static/dist/assets/{diagram-S7CK7UJ4-4z90agaz.js → diagram-S7CK7UJ4-m27jopok.js} +1 -1
  168. package/static/dist/assets/{diagram-UQ7AKVKN-eifw0lg7.js → diagram-UQ7AKVKN-ojgkbmp4.js} +1 -1
  169. package/static/dist/assets/{diagram-VSXAHHWV-ksh3sjqv.js → diagram-VSXAHHWV-gfafpgyj.js} +1 -1
  170. package/static/dist/assets/{diagram-VX7I27RA-ett72f71.js → diagram-VX7I27RA-daobicg4.js} +1 -1
  171. package/static/dist/assets/{diagram-Z3DM3KII-l8scp8nu.js → diagram-Z3DM3KII-mi475zvk.js} +1 -1
  172. package/static/dist/assets/{ebnfDiagram-PWID7BFC-f0pxcmit.js → ebnfDiagram-PWID7BFC-daf4zvsy.js} +1 -1
  173. package/static/dist/assets/endpoints-cyjhnrbn.js +64 -0
  174. package/static/dist/assets/{erDiagram-RLTQ6QDP-f47m5xh0.js → erDiagram-RLTQ6QDP-gxfhxvfp.js} +1 -1
  175. package/static/dist/assets/eventmodeling-NTZA5JFV-manga0bq.js +1 -0
  176. package/static/dist/assets/flowDiagram-HODETNUW-bpt13uny.js +1 -0
  177. package/static/dist/assets/{ganttDiagram-EL5Y4UJY-bmktjdi0.js → ganttDiagram-EL5Y4UJY-k3208e0p.js} +1 -1
  178. package/static/dist/assets/{gitGraph-4MIJSDKK-j2ueg258.js → gitGraph-4MIJSDKK-gjuhj4zk.js} +1 -1
  179. package/static/dist/assets/{gitGraphDiagram-WWUBYQGX-ms6586v9.js → gitGraphDiagram-WWUBYQGX-jglxvux1.js} +1 -1
  180. package/static/dist/assets/{highlighted-body-KPVGNVTW-ev52d2si.js → highlighted-body-KPVGNVTW-gh24xc9c.js} +1 -1
  181. package/static/dist/assets/index-gsg78cb4.css +2 -0
  182. package/static/dist/assets/{index-hywfjw88.js → index-jb47los5.js} +4 -4
  183. package/static/dist/assets/{info-A6RAGUB7-j717vyt7.js → info-A6RAGUB7-huf0xaoq.js} +1 -1
  184. package/static/dist/assets/{infoDiagram-27XIBGKW-dsbpib36.js → infoDiagram-27XIBGKW-jk3n96ix.js} +1 -1
  185. package/static/dist/assets/{ishikawaDiagram-5VMMS53U-n0sz3i2d.js → ishikawaDiagram-5VMMS53U-o95uo5dx.js} +1 -1
  186. package/static/dist/assets/{journeyDiagram-3NMN7TZE-mcxz1mme.js → journeyDiagram-3NMN7TZE-h5ye2l70.js} +1 -1
  187. package/static/dist/assets/{kanban-definition-UXKFOSKX-n6hq2lyc.js → kanban-definition-UXKFOSKX-lgrpk5yi.js} +1 -1
  188. package/static/dist/assets/{lazyRouteComponent-faquioun.js → lazyRouteComponent-owywngyt.js} +1 -1
  189. package/static/dist/assets/{line-h5xq20s4.js → line-cihmc0pn.js} +1 -1
  190. package/static/dist/assets/login-g063apx6.js +1 -0
  191. package/static/dist/assets/mermaid-HWGCJPDP-okd1mnvj.js +1 -0
  192. package/static/dist/assets/{mermaid-parser.core-n9cuu5ue.js → mermaid-parser.core-d1609aga.js} +3 -3
  193. package/static/dist/assets/{mindmap-definition-YA3MSWOX-gta3u7z4.js → mindmap-definition-YA3MSWOX-lbvtir31.js} +1 -1
  194. package/static/dist/assets/onboarding-ngw0x0pr.js +1 -0
  195. package/static/dist/assets/{packet-AYTQ26CC-bpzuq5n6.js → packet-AYTQ26CC-o4cp5z3k.js} +1 -1
  196. package/static/dist/assets/{pegDiagram-XKGWAZYB-juye13e5.js → pegDiagram-XKGWAZYB-e8xqu1qj.js} +1 -1
  197. package/static/dist/assets/{pie-WAS4IAKB-odhb8buq.js → pie-WAS4IAKB-hm9icuov.js} +1 -1
  198. package/static/dist/assets/{pieDiagram-E7YTZNPT-mgb14ksh.js → pieDiagram-E7YTZNPT-hj96x7hn.js} +1 -1
  199. package/static/dist/assets/{quadrantDiagram-AXDQQJYC-cg36naog.js → quadrantDiagram-AXDQQJYC-80utbxhm.js} +1 -1
  200. package/static/dist/assets/{radar-RG4KPBEZ-lgzfpnef.js → radar-RG4KPBEZ-k39b6100.js} +1 -1
  201. package/static/dist/assets/{railroad-74A4TZTK-gmglc3ua.js → railroad-74A4TZTK-ewyqz67j.js} +1 -1
  202. package/static/dist/assets/railroad-abnf-HS5TGJTU-dx2q0evz.js +1 -0
  203. package/static/dist/assets/railroad-ebnf-LZEXJU2U-l8xt49t5.js +1 -0
  204. package/static/dist/assets/railroad-peg-WCYAUIDC-kuxha9yz.js +1 -0
  205. package/static/dist/assets/{railroadDiagram-O6MQD6OU-cmxwjt73.js → railroadDiagram-O6MQD6OU-iwv9dby8.js} +1 -1
  206. package/static/dist/assets/{refresh-cw-mf192q3f.js → refresh-cw-hjzomwlo.js} +1 -1
  207. package/static/dist/assets/{requirementDiagram-BXWQKSXE-ir0s2mek.js → requirementDiagram-BXWQKSXE-cdgh5jha.js} +1 -1
  208. package/static/dist/assets/{sankeyDiagram-P5KCCOFB-jo7jcdq0.js → sankeyDiagram-P5KCCOFB-dbu6p97m.js} +1 -1
  209. package/static/dist/assets/{saved-g777hp1t.js → saved-feqhycco.js} +1 -1
  210. package/static/dist/assets/{search-i2kfogyx.js → search-n3jdc20p.js} +1 -1
  211. package/static/dist/assets/{sequenceDiagram-WJ2MYXX4-n0pf6xk5.js → sequenceDiagram-WJ2MYXX4-b0wchifg.js} +1 -1
  212. package/static/dist/assets/{settings_section_extensions_title-hgvh507l.js → settings_section_extensions_title-mtyviexd.js} +1 -1
  213. package/static/dist/assets/share._token-cwov8uqh.js +2 -0
  214. package/static/dist/assets/sliders-horizontal-i1lseyg0.js +1 -0
  215. package/static/dist/assets/{stateDiagram-D77RDMKH-olbrwyv9.js → stateDiagram-D77RDMKH-mb4b38xh.js} +1 -1
  216. package/static/dist/assets/stateDiagram-v2-MP3YSRHH-cn1fnqrg.js +1 -0
  217. package/static/dist/assets/{swimlanes-42K2YHIH-od14zrw6.js → swimlanes-42K2YHIH-ofrpfwj0.js} +1 -1
  218. package/static/dist/assets/swimlanesDiagram-VR7AAH4N-d652ummv.js +8 -0
  219. package/static/dist/assets/{timeline-definition-24CTP7MA-l0qokmnb.js → timeline-definition-24CTP7MA-hlw0jg6o.js} +1 -1
  220. package/static/dist/assets/todoStore-mfy9lpio.js +1 -0
  221. package/static/dist/assets/{treeView-Q6P3EWNA-hma3ip8m.js → treeView-Q6P3EWNA-d93wr19c.js} +1 -1
  222. package/static/dist/assets/{treemap-WGGIJYW6-bghjng5m.js → treemap-WGGIJYW6-g7ezdedz.js} +1 -1
  223. package/static/dist/assets/{untitled-nfaw0q7m.js → untitled-mqxhd1eh.js} +1 -1
  224. package/static/dist/assets/{useForm-k7fj2zsr.js → useForm-hh33yrwa.js} +1 -1
  225. package/static/dist/assets/useLocale-cr30vl7r.js +1 -0
  226. package/static/dist/assets/{vennDiagram-4TSXK5OY-bo97zf3z.js → vennDiagram-4TSXK5OY-m5vgeba5.js} +1 -1
  227. package/static/dist/assets/{wardley-WFR3VGLG-pbjl6jav.js → wardley-WFR3VGLG-gpao9bxv.js} +1 -1
  228. package/static/dist/assets/{wardleyDiagram-VM6X3IG4-bmy9i09l.js → wardleyDiagram-VM6X3IG4-g2yzwshm.js} +1 -1
  229. package/static/dist/assets/{xychartDiagram-S5SC5T6Z-eercu0mc.js → xychartDiagram-S5SC5T6Z-itakhpic.js} +1 -1
  230. package/static/dist/index.html +1 -1
  231. package/static/dist/sw.js +114 -152
  232. package/static/dist/assets/AppShell-gs0grlbs.js +0 -1
  233. package/static/dist/assets/ChatPage-gi1xlz52.js +0 -18
  234. package/static/dist/assets/HubRoute-qs9vzuh0.js +0 -2
  235. package/static/dist/assets/RightPanel-dn19xu42.js +0 -1
  236. package/static/dist/assets/Select-y7wtf85g.js +0 -1
  237. package/static/dist/assets/SessionListPanel-igwdj9xr.js +0 -1
  238. package/static/dist/assets/SettingsLayout-n6mu0ghg.js +0 -1
  239. package/static/dist/assets/States-oil2issf.js +0 -1
  240. package/static/dist/assets/Toaster-jtn59pjm.js +0 -1
  241. package/static/dist/assets/_app-k4jrkgfa.js +0 -1
  242. package/static/dist/assets/_app.ext._extensionId-mmf1mepm.js +0 -1
  243. package/static/dist/assets/_app.index-mb41nf00.js +0 -1
  244. package/static/dist/assets/_app.insights-mz5cep8m.js +0 -1
  245. package/static/dist/assets/_app.kanban-ocgw506i.js +0 -1
  246. package/static/dist/assets/_app.logs-lstmoju6.js +0 -1
  247. package/static/dist/assets/_app.memory-dishj9ra.js +0 -1
  248. package/static/dist/assets/_app.profiles-dynpvsio.js +0 -1
  249. package/static/dist/assets/_app.settings-c2czm9v4.js +0 -1
  250. package/static/dist/assets/_app.settings._section-exwflld7.js +0 -1
  251. package/static/dist/assets/_app.skills-j5jh2nug.js +0 -1
  252. package/static/dist/assets/_app.tasks-b99d98fo.js +0 -2
  253. package/static/dist/assets/_app.todos-ma8urvn3.js +0 -1
  254. package/static/dist/assets/_app.workspaces-l86cxeod.js +0 -1
  255. package/static/dist/assets/architecture-7GRP2DOG-mqft9w2p.js +0 -1
  256. package/static/dist/assets/channel-ix8cxdld.js +0 -1
  257. package/static/dist/assets/classDiagram-ZZMXUADV-c210y3il.js +0 -1
  258. package/static/dist/assets/classDiagram-v2-VYDZK3BY-c210y3il.js +0 -1
  259. package/static/dist/assets/endpoints-ipnv1hd4.js +0 -64
  260. package/static/dist/assets/eventmodeling-NTZA5JFV-d9b5a0ia.js +0 -1
  261. package/static/dist/assets/flowDiagram-HODETNUW-kfxxsyrk.js +0 -1
  262. package/static/dist/assets/index-bz7g7rhx.css +0 -2
  263. package/static/dist/assets/login-mvjys1ol.js +0 -1
  264. package/static/dist/assets/mermaid-HWGCJPDP-gmnkgf1n.js +0 -1
  265. package/static/dist/assets/onboarding-l6quy6gz.js +0 -1
  266. package/static/dist/assets/railroad-abnf-HS5TGJTU-5mjl5wpv.js +0 -1
  267. package/static/dist/assets/railroad-ebnf-LZEXJU2U-bfram8v8.js +0 -1
  268. package/static/dist/assets/railroad-peg-WCYAUIDC-nwfczn88.js +0 -1
  269. package/static/dist/assets/share._token-il3y6js2.js +0 -2
  270. package/static/dist/assets/sliders-horizontal-kd99ffvl.js +0 -1
  271. package/static/dist/assets/stateDiagram-v2-MP3YSRHH-dnzlmdpu.js +0 -1
  272. package/static/dist/assets/swimlanesDiagram-VR7AAH4N-ck0eqqdp.js +0 -8
  273. package/static/dist/assets/todoStore-la6uyqzj.js +0 -1
  274. package/static/dist/assets/trash-bxz1k9cw.js +0 -1
  275. package/static/dist/assets/useLocale-evhacmv2.js +0 -1
@@ -3,17 +3,20 @@
3
3
  *
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
- * from immutable git blobs. npm builds and unrecognized checkouts report
7
- * `manual_update`. Agent: the external checkout follows its own `v*` tags with a
6
+ * from immutable git blobs. Direct global npm installs can replace themselves
7
+ * with an exact completed Stable package or the verified GHCR Experimental artifact, switching
8
+ * channels in either direction; other packaged installs stay manual.
9
+ * Agent: the external checkout follows its own `v*` tags with a
8
10
  * stash/pull or force reset, then the gateway restarts through the sidecar.
9
11
  * Nothing here deletes git locks; `clearLock` only inventories them.
10
12
  */
11
13
  import { readCapped } from '../http/capped.js';
12
14
  import { execFile } from 'node:child_process';
13
15
  import { createHash } from 'node:crypto';
14
- import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, unlinkSync, writeFileSync } from 'node:fs';
15
- import { dirname, join, resolve } from 'node:path';
16
+ import { copyFileSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, unlinkSync, writeFileSync } from 'node:fs';
17
+ import { delimiter, dirname, isAbsolute, join, resolve } from 'node:path';
16
18
  import { dict } from '../config/agent-config.js';
19
+ import { validateReleaseInfo } from '../release.js';
17
20
  import { str } from '../util.js';
18
21
  export const REPOSITORY = 'MaudeCode/talaria';
19
22
  export const REPOSITORY_URL = `https://github.com/${REPOSITORY}`;
@@ -24,11 +27,13 @@ export const WEB_UPDATE_PATHS = [':(top)web/', ':(top)contracts/'];
24
27
  export const RELEASE_STAMP = 'web/_release.json';
25
28
  const RELEASE_BLOBS = ['sidecar/agent_dependency.json', 'contract_versions.json'];
26
29
  export const CACHE_TTL_S = 1800;
30
+ export const AUTO_UPDATE_INTERVAL_MS = 5 * 60_000;
27
31
  export const RESTART_MAX_WAIT_S = 300;
28
32
  export const DEFAULT_CHANNEL = 'stable';
29
33
  const SHA = /^[a-f0-9]{40}$/;
30
34
  const VERSION = '(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)';
31
35
  const AGENT_TAG_GLOB = 'v*';
36
+ const WEB_NPM_PACKAGE = '@maudecode/talaria-web';
32
37
  const GIT_LOCK_SIGNATURES = ["index.lock': file exists", ".lock': file exists", 'another git process seems to be running', 'unable to create .git/index.lock'];
33
38
  const NETWORK_FAILURES = ['could not resolve host', 'failed to connect', 'network is unreachable', 'no route to host', 'connection timed out', 'timed out after', 'connection reset by peer', 'remote end hung up unexpectedly', 'tls connection was non-properly terminated', 'ssl certificate problem'];
34
39
  export const normalizeChannel = (channel) => (channel === 'experimental' ? 'experimental' : 'stable');
@@ -62,10 +67,11 @@ export const WEB_BUILD_STEPS = [
62
67
  export const WEB_BUILD_TIMEOUT_MS = 10 * 60_000;
63
68
  /** The artifact the supervisor re-executes; a build that does not leave it behind did not succeed. */
64
69
  export const WEB_SERVER_ENTRY = 'packages/server/dist/bin/talaria-web.js';
65
- export const runNpm = (args, cwd, timeoutMs) => new Promise((done) => {
70
+ const runNpmCommand = (args, cwd, timeoutMs, env) => new Promise((done) => {
66
71
  // Prefer the npm beside the running node so a supervisor started with an absolute node path still finds it.
67
72
  const beside = join(dirname(process.execPath), 'npm');
68
- execFile(existsSync(beside) ? beside : 'npm', args, { cwd, encoding: 'utf8', timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024, env: { ...process.env, NODE_ENV: 'development' } }, (error, stdout, stderr) => {
73
+ const cli = resolve(dirname(process.execPath), '../lib/node_modules/npm/bin/npm-cli.js');
74
+ execFile(existsSync(cli) ? process.execPath : existsSync(beside) ? beside : 'npm', existsSync(cli) ? [cli, ...args] : args, { cwd, encoding: 'utf8', timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024, env: { ...env, PATH: `${dirname(process.execPath)}${delimiter}${env.PATH ?? ''}` } }, (error, stdout, stderr) => {
69
75
  if (!error) {
70
76
  done({ out: stdout.trim(), ok: true });
71
77
  return;
@@ -82,6 +88,8 @@ export const runNpm = (args, cwd, timeoutMs) => new Promise((done) => {
82
88
  done({ out: (stderr.trim() || stdout.trim() || `npm exited with status ${String(e.code)}`).slice(-4000), ok: false });
83
89
  });
84
90
  });
91
+ export const runNpm = (args, cwd, timeoutMs) => runNpmCommand(args, cwd, timeoutMs, { ...process.env, NODE_ENV: 'development' });
92
+ export const runPackageNpm = (args, cwd, timeoutMs) => runNpmCommand(args, cwd, timeoutMs, process.env);
85
93
  /** Install and build the checkout's Web packages; null on success, else the message for the caller. */
86
94
  async function buildWeb(root, build) {
87
95
  const cwd = join(root, 'web');
@@ -109,6 +117,15 @@ function fetchFailureMessage(out, network) {
109
117
  const lower = detail.toLowerCase();
110
118
  return NETWORK_FAILURES.some((s) => lower.includes(s)) ? network : `fetch failed: ${detail}`;
111
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
+ }
112
129
  function gitFailure(out, message) {
113
130
  if (isGitLockError(out))
114
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.' };
@@ -148,7 +165,13 @@ function sortKeys(value) {
148
165
  }
149
166
  const same = (a, b) => canon(a) === canon(b);
150
167
  // ── published release sets ───────────────────────────────────────────────────
168
+ /** `transient`: GitHub was unreachable, slow, or overloaded, so a retry can succeed; the rest describe published state. */
151
169
  export class ReleaseUnavailable extends Error {
170
+ transient;
171
+ constructor(message, transient = false) {
172
+ super(message);
173
+ this.transient = transient;
174
+ }
152
175
  }
153
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. */
154
177
  export function githubJson(fetchImpl, env) {
@@ -157,17 +180,25 @@ export function githubJson(fetchImpl, env) {
157
180
  const token = (env.TALARIA_RELEASE_TOKEN ?? '').trim();
158
181
  if (token)
159
182
  headers.Authorization = `Bearer ${token}`;
160
- 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) });
161
192
  if (res.status >= 300 && res.status < 400) {
162
193
  const target = new URL(res.headers.get('location') ?? '', API_ROOT + path);
163
194
  if (target.protocol !== 'https:' || target.hostname !== 'release-assets.githubusercontent.com')
164
195
  throw new ReleaseUnavailable('Unexpected release download redirect');
165
196
  const anonymous = { ...headers };
166
197
  delete anonymous.Authorization;
167
- res = await fetchImpl(target.toString(), { headers: anonymous, signal: AbortSignal.timeout(5000) });
198
+ res = await send(target.toString(), { headers: anonymous, signal: AbortSignal.timeout(5000) });
168
199
  }
169
200
  if (!res.ok)
170
- throw new ReleaseUnavailable(`GitHub answered ${String(res.status)}`);
201
+ throw new ReleaseUnavailable(`GitHub answered ${String(res.status)}`, res.status >= 500 || res.status === 429);
171
202
  const body = await readCapped(res, 2_000_000);
172
203
  if (!body)
173
204
  throw new ReleaseUnavailable('Release metadata exceeds the download limit');
@@ -180,7 +211,7 @@ export async function publishedWebRelease(channel, getJson, now = () => performa
180
211
  const deadline = now() + 15_000;
181
212
  const fetchJson = (path, asset = false) => {
182
213
  if (now() >= deadline)
183
- 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);
184
215
  return getJson(path, { asset });
185
216
  };
186
217
  const published = [];
@@ -227,6 +258,9 @@ export async function publishedWebRelease(channel, getJson, now = () => performa
227
258
  if (typeof source !== 'string' || !SHA.test(source) || component.releaseSet !== source || component.version !== componentTag.split('-v').pop() || typeof component.image !== 'string' || !/^ghcr\.io\/maudecode\/talaria-web@sha256:[a-f0-9]{64}$/.test(component.image)) {
228
259
  throw new ReleaseUnavailable('Web release references are mutable or inconsistent');
229
260
  }
261
+ const npm = component.npm;
262
+ if (npm !== undefined && npm !== `${WEB_NPM_PACKAGE}@${String(component.version)}`)
263
+ throw new ReleaseUnavailable('Web npm release identity is inconsistent');
230
264
  const contracts = dict(m.contracts);
231
265
  const supported = {};
232
266
  for (const name of ['appWeb', 'webRelay']) {
@@ -245,6 +279,7 @@ export async function publishedWebRelease(channel, getJson, now = () => performa
245
279
  sourceRevision: source,
246
280
  releaseSet: source,
247
281
  image: component.image,
282
+ npm: typeof npm === 'string' ? npm : null,
248
283
  manifestReleaseSet: str(m.releaseSet),
249
284
  runtime: { tag: componentTag, version: component.version, sourceRevision: source, releaseSet: source, contracts: supported, compatibleAgent: agent },
250
285
  release_url: `${REPOSITORY_URL}/releases/tag/${tag}`,
@@ -252,6 +287,81 @@ export async function publishedWebRelease(channel, getJson, now = () => performa
252
287
  }
253
288
  throw new ReleaseUnavailable('No completed Talaria Web release is available on this channel');
254
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
+ }
255
365
  // ── Web checkout ─────────────────────────────────────────────────────────────
256
366
  /** The git root when `webRoot` is `<root>/web` of a Talaria checkout with a GitHub `maudecode/talaria` origin; else null. */
257
367
  export async function checkoutRoot(webRoot, git) {
@@ -283,6 +393,72 @@ export async function checkoutRoot(webRoot, git) {
283
393
  return null;
284
394
  }
285
395
  }
396
+ /** A real global npm install of this package; local, npx, linked and container trees fail closed. */
397
+ export async function npmInstallInfo(webRoot, npm = runPackageNpm, container = existsSync('/.within_container') || existsSync('/.dockerenv') || existsSync('/run/.containerenv')) {
398
+ if (container || !webRoot || !existsSync(webRoot))
399
+ return null;
400
+ let metadata;
401
+ try {
402
+ metadata = dict(JSON.parse(readFileSync(join(webRoot, 'package.json'), 'utf8')));
403
+ }
404
+ catch {
405
+ return null;
406
+ }
407
+ if (metadata.name !== WEB_NPM_PACKAGE || typeof metadata.version !== 'string' || !(new RegExp(`^${VERSION}$`).test(metadata.version) || EXPERIMENTAL_VERSION.test(metadata.version)))
408
+ return null;
409
+ const global = await npm(['root', '--global'], webRoot, 10_000);
410
+ if (!global.ok || !isAbsolute(global.out))
411
+ return null;
412
+ const expected = join(resolve(global.out), '@maudecode', 'talaria-web');
413
+ try {
414
+ if (lstatSync(expected).isSymbolicLink() || realpathSync(expected) !== realpathSync(webRoot))
415
+ return null;
416
+ }
417
+ catch {
418
+ return null;
419
+ }
420
+ return { packageRoot: realpathSync(webRoot), version: metadata.version };
421
+ }
422
+ function compareVersions(installed, latest) {
423
+ const a = installed.split('.').map(Number);
424
+ const b = latest.split('.').map(Number);
425
+ for (let i = 0; i < 3; i += 1)
426
+ if (a[i] !== b[i])
427
+ return Math.sign((a[i] ?? 0) - (b[i] ?? 0));
428
+ return 0;
429
+ }
430
+ function diskRelease(webRoot) {
431
+ try {
432
+ return validateReleaseInfo(JSON.parse(readFileSync(join(webRoot, '_release.json'), 'utf8')), webRoot);
433
+ }
434
+ catch {
435
+ return null;
436
+ }
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
+ }
286
462
  /** Python `verify_release_source`: the stamp Web expects for `release`, computed from immutable blobs at its source revision. */
287
463
  export async function verifyReleaseSource(root, release, git) {
288
464
  const files = {};
@@ -337,9 +513,12 @@ function mainStamp(root, id) {
337
513
  return data;
338
514
  }
339
515
  async function mainRevision(root, git) {
340
- 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);
341
517
  if (!fetched.ok)
342
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);
343
522
  const source = await git(['rev-parse', 'refs/remotes/origin/main^{commit}'], root);
344
523
  return source.ok && SHA.test(source.out) ? [source.out, ''] : [null, ''];
345
524
  }
@@ -370,8 +549,10 @@ async function checkMainUpdate(root, result, git, id) {
370
549
  if (root === null)
371
550
  return { ...result, manual_update: true, message: 'Main updates require an authenticated Talaria source checkout with Web under web/.' };
372
551
  const [source, error] = await mainRevision(root, git);
373
- if (source === null)
374
- 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
+ }
375
556
  Object.assign(result, { latest_sha: source, latest_version: `main@${source.slice(0, 12)}` });
376
557
  const head = await git(['rev-parse', 'HEAD'], root);
377
558
  const status = await git(['status', '--porcelain', '--untracked-files=all'], root);
@@ -401,22 +582,41 @@ async function checkMainUpdate(root, result, git, id) {
401
582
  return result;
402
583
  }
403
584
  /** Python `check_web_update`. */
404
- export async function checkWebUpdate(webRoot, currentVersion, channel, git, getJson, id) {
585
+ export async function checkWebUpdate(webRoot, currentVersion, channel, git, getJson, id, npmRun = runPackageNpm, registry) {
405
586
  const root = await checkoutRoot(webRoot, git);
587
+ const npmInstall = root === null ? await npmInstallInfo(webRoot, npmRun) : null;
406
588
  const result = { name: 'webui', channel, repo_url: REPOSITORY_URL, current_version: currentVersion, behind: null, no_git: root === null };
407
589
  if (channel === 'experimental' && root !== null)
408
590
  return checkMainUpdate(root, result, git, id);
591
+ if (channel === 'experimental' && npmInstall && registry)
592
+ return checkNpmExperimental(result, npmInstall, id, registry);
409
593
  let release;
410
594
  try {
411
595
  release = await publishedWebRelease(channel, getJson);
412
596
  }
413
597
  catch (error) {
414
598
  if (error instanceof ReleaseUnavailable)
415
- return { ...result, manual_update: true, error: error.message };
599
+ return { ...result, manual_update: true, error: error.message, ...(error.transient ? { stale_check: true } : {}) };
416
600
  return { ...result, manual_update: true, error: 'Talaria release metadata is unavailable. Private repositories require TALARIA_RELEASE_TOKEN with Contents read access.' };
417
601
  }
418
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 });
419
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
+ }
608
+ if (npmInstall && channel === 'stable' && release.npm && !str(id.release().tag).startsWith('web-exp-v')) {
609
+ const comparison = compareVersions(npmInstall.version, release.version);
610
+ if (comparison > 0)
611
+ return { ...result, current_sha: id.release().sourceRevision ?? null, behind: 0, no_git: true, install_kind: 'npm', manual_update: true, message: 'This npm installation is ahead of the selected Stable release.' };
612
+ if (comparison < 0)
613
+ return { ...result, current_sha: id.release().sourceRevision ?? null, behind: 1, no_git: true, install_kind: 'npm', npm: release.npm, manual_update: false, message: `Stable npm update ${release.version} is available.` };
614
+ const installed = diskRelease(npmInstall.packageRoot);
615
+ if (!same(installed, release.runtime))
616
+ return { ...result, current_sha: id.release().sourceRevision ?? null, behind: null, no_git: true, install_kind: 'npm', manual_update: true, error: 'Installed npm release metadata does not match the completed release.' };
617
+ const metadataRepair = !same(id.release(), release.runtime);
618
+ return { ...result, current_sha: release.sourceRevision, behind: 0, no_git: true, install_kind: 'npm', npm: release.npm, manual_update: false, metadata_repair: metadataRepair, ...(metadataRepair ? { message: 'The npm package is current; finish the update to restart with that version.' } : {}) };
619
+ }
420
620
  const current = id.release().sourceRevision ?? null;
421
621
  const version = new RegExp(`^${channel === 'experimental' ? 'web-exp-v' : 'web-v'}${VERSION}$`).exec(currentVersion);
422
622
  let behind = version && current === release.sourceRevision ? 0 : null;
@@ -470,11 +670,158 @@ export async function checkWebUpdate(webRoot, currentVersion, channel, git, getJ
470
670
  Object.assign(result, { manual_update: true, message: 'Commit or remove local changes before updating; Web updates never discard them.' });
471
671
  return result;
472
672
  }
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) {
702
+ const installed = await npmInstallInfo(webRoot, npmRun);
703
+ if (!installed)
704
+ return { ok: false, manual_update: true, message: 'Automatic npm updates require a direct global @maudecode/talaria-web installation.' };
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
+ }
729
+ }
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 };
743
+ }
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}.` };
749
+ }
750
+ if (!canApply())
751
+ return { ok: false, message: 'Web update deferred because its settings or lifecycle changed.' };
752
+ // Global npm installs keep dependencies inside the package. Build the replacement beside the old package so
753
+ // install/verification failures cannot damage the running installation and the final rename stays on one volume.
754
+ const staging = mkdtempSync(join(dirname(installed.packageRoot), '.talaria-update-'));
755
+ const candidate = join(staging, 'lib/node_modules/@maudecode/talaria-web');
756
+ const backup = join(staging, 'previous');
757
+ let keepBackup = false;
758
+ let stores = null;
759
+ try {
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);
773
+ if (!update.ok)
774
+ return { ok: false, install_failed: true, target: 'webui', channel, message: `npm update failed: ${sanitizeGitDiagnostic(update.out, 1000) || 'unknown error'}` };
775
+ const metadata = dict(JSON.parse(readFileSync(join(candidate, 'package.json'), 'utf8')));
776
+ const bins = dict(metadata.bin);
777
+ if (metadata.name !== WEB_NPM_PACKAGE || metadata.version !== target.version || !target.matches(diskRelease(candidate))
778
+ || !['talaria-web', 'talaria-web-mcp'].every((name) => str(bins[name]).replace(/^\.\//, '') === `dist/bin/${name}.js` && existsSync(join(candidate, `dist/bin/${name}.js`)))) {
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.` };
780
+ }
781
+ const recheck = await npmInstallInfo(webRoot, npmRun);
782
+ if (!canApply() || recheck?.packageRoot !== installed.packageRoot || recheck.version !== installed.version)
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
+ }
794
+ renameSync(installed.packageRoot, backup);
795
+ try {
796
+ renameSync(candidate, installed.packageRoot);
797
+ }
798
+ catch (error) {
799
+ // Never delete the only surviving copy if a filesystem error also prevents restoration.
800
+ keepBackup = true;
801
+ renameSync(backup, installed.packageRoot);
802
+ keepBackup = false;
803
+ throw error;
804
+ }
805
+ }
806
+ catch (error) {
807
+ return { ok: false, install_failed: true, message: `npm update failed: ${sanitizeGitDiagnostic(error.message, 1000)}${keepBackup ? `; previous package retained at ${backup}` : ''}` };
808
+ }
809
+ finally {
810
+ if (!keepBackup) {
811
+ try {
812
+ rmSync(staging, { recursive: true, force: true });
813
+ }
814
+ catch { /* a leftover staging directory must not prevent restart after a verified switch */ }
815
+ }
816
+ }
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}.` : ''}` };
819
+ }
473
820
  /** Python `apply_web_update`: fast-forward a recognized clean checkout to main or a published Stable tag. */
474
- export async function applyWebUpdate(webRoot, channel, git, getJson, id, build = runNpm) {
821
+ export async function applyWebUpdate(webRoot, channel, git, getJson, id, build = runNpm, npmRun = runPackageNpm, canApply = () => true, packaged = {}) {
475
822
  const root = await checkoutRoot(webRoot, git);
476
823
  if (root === null)
477
- return { ok: false, manual_update: true, message: 'Automatic updates require a Talaria monorepo checkout with Web under web/. Migrate this installation manually.' };
824
+ return applyNpmWebUpdate(webRoot, channel, getJson, id, npmRun, canApply, packaged);
478
825
  const status = await git(['status', '--porcelain', '--untracked-files=all'], root);
479
826
  if (!status.ok || status.out)
480
827
  return { ok: false, dirty: true, message: 'Web update refused: the checkout must be clean, including untracked files.' };
@@ -552,6 +899,8 @@ export async function applyWebUpdate(webRoot, channel, git, getJson, id, build =
552
899
  const clean = await git(['status', '--porcelain', '--untracked-files=all'], root);
553
900
  if (!again.ok || again.out !== head || !clean.ok || clean.out)
554
901
  return { ok: false, message: 'The checkout changed during the update; retry after it is clean.' };
902
+ if (!canApply())
903
+ return { ok: false, message: 'Web update deferred because its settings or lifecycle changed.' };
555
904
  if (head !== source) {
556
905
  const merged = await git(['merge', '--ff-only', '--no-stat', '--no-overwrite-ignore', source], root, 30_000);
557
906
  const actual = await git(['rev-parse', 'HEAD'], root);
@@ -587,105 +936,91 @@ export async function applyWebUpdate(webRoot, channel, git, getJson, id, build =
587
936
  }
588
937
  return { ok: true, target: 'webui', channel, sourceRevision: source, message: `Updated Talaria Web to ${tag}.` };
589
938
  }
590
- // ── Agent checkout (external project, `v*` tags, stable only) ────────────────
939
+ // ── Agent checkout: independent Stable releases or Experimental default branch ──
591
940
  async function releaseTags(path, git) {
592
941
  const out = await git(['tag', '--list', AGENT_TAG_GLOB, '--sort=-v:refname'], path);
593
- return out.ok ? out.out.split('\n').map((l) => l.trim()).filter(Boolean) : [];
942
+ return out.ok ? out.out.split('\n').map((l) => l.trim()).filter((tag) => /^v\d+\.\d+\.\d+$/.test(tag)) : [];
594
943
  }
595
944
  async function currentReleaseTag(path, git) {
596
- const out = await git(['describe', '--tags', '--abbrev=0', '--match', AGENT_TAG_GLOB], path);
945
+ const out = await git(['describe', '--tags', '--exact-match', '--match', AGENT_TAG_GLOB, 'HEAD'], path);
597
946
  return out.ok && out.out ? out.out : null;
598
947
  }
599
- const releaseGap = (tags, current, latest) => (current === latest ? 0 : current !== null && tags.includes(current) ? tags.indexOf(current) : 1);
600
- async function headIsPastLatestTag(path, current, git) {
601
- if (!current)
602
- return false;
603
- const full = await git(['describe', '--tags', '--always', '--match', AGENT_TAG_GLOB], path);
604
- return full.ok && Boolean(full.out) && full.out !== current;
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) };
605
953
  }
606
954
  const headContainsRef = async (path, ref, git) => (await git(['merge-base', '--is-ancestor', ref, 'HEAD'], path)).ok;
607
955
  const canFastForwardTo = async (path, ref, git) => (await git(['merge-base', '--is-ancestor', 'HEAD', ref], path)).ok;
608
956
  async function detectDefaultBranch(path, git) {
957
+ const refreshed = await git(['remote', 'set-head', 'origin', '--auto'], path, 15_000);
958
+ if (!refreshed.ok)
959
+ throw new Error('Agent default branch could not be refreshed from origin');
609
960
  const out = await git(['symbolic-ref', 'refs/remotes/origin/HEAD'], path);
610
- if (out.ok && out.out)
611
- return out.out.split('/').pop() ?? 'master';
612
- for (const branch of ['master', 'main'])
613
- if ((await git(['rev-parse', '--verify', `origin/${branch}`], path)).ok)
614
- return branch;
615
- return 'master';
961
+ const prefix = 'refs/remotes/origin/';
962
+ if (out.ok && out.out.startsWith(prefix) && (await git(['rev-parse', '--verify', `${out.out}^{commit}`], path)).ok)
963
+ return out.out.slice(prefix.length);
964
+ throw new Error('Agent default branch is unavailable');
616
965
  }
617
- async function upstreamRef(path, git) {
618
- const upstream = await git(['rev-parse', '--abbrev-ref', '@{upstream}'], path);
619
- return upstream.ok && upstream.out ? upstream.out : `origin/${await detectDefaultBranch(path, git)}`;
966
+ async function agentTarget(path, git, channel) {
967
+ const ref = channel === 'experimental' ? `origin/${await detectDefaultBranch(path, git)}` : (await releaseTags(path, git))[0];
968
+ if (!ref)
969
+ throw new Error('No stable Agent release is available');
970
+ const current = await git(['rev-parse', 'HEAD'], path);
971
+ const latest = await git(['rev-parse', `${ref}^{commit}`], path);
972
+ if (!current.ok || !latest.ok || !SHA.test(current.out) || !SHA.test(latest.out))
973
+ throw new Error('Agent Git identity is unavailable');
974
+ const count = await git(['rev-list', '--count', `${current.out}..${latest.out}`], path);
975
+ const behind = count.ok && /^\d+$/.test(count.out) ? Number.parseInt(count.out, 10) : null;
976
+ const manual = current.out !== latest.out && !(await canFastForwardTo(path, latest.out, git));
977
+ const remote = normalizeRemoteUrl((await git(['remote', 'get-url', 'origin'], path)).out);
978
+ return {
979
+ name: 'agent', channel, branch: ref, current_sha: current.out, latest_sha: latest.out,
980
+ current_version: await currentReleaseTag(path, git) ?? current.out.slice(0, 12), latest_version: ref,
981
+ behind, release_based: channel === 'stable', repo_url: remote, compare_url: compareUrl(remote, current.out, latest.out),
982
+ ...(behind === null ? { error: 'Agent commit count is unavailable' } : {}),
983
+ ...(manual ? { manual_update: true, message: 'Agent checkout is ahead of or divergent from this channel; refusing to rewind it.' } : {}),
984
+ };
620
985
  }
621
- /** Python `_select_apply_compare_ref` for the Agent: the latest reachable `v*` tag, else the tracking branch. */
622
- async function selectAgentCompareRef(path, git) {
623
- const tags = await releaseTags(path, git);
624
- const latest = tags[0];
625
- if (latest) {
626
- const current = await currentReleaseTag(path, git);
627
- const behind = releaseGap(tags, current, latest);
628
- const fallthrough = (behind === 0 && (await headIsPastLatestTag(path, current, git))) || (behind > 0 && (await headContainsRef(path, latest, git))) || (behind > 0 && !(await canFastForwardTo(path, latest, git)));
629
- if (!fallthrough)
630
- return latest;
631
- }
632
- return upstreamRef(path, git);
986
+ function agentWarning(info, policy) {
987
+ return { candidate_revision: info.latest_sha, supported_revision: policy.supportedRevision, supported_version: policy.supportedVersion,
988
+ unsupported: info.latest_sha !== policy.supportedRevision };
633
989
  }
634
- async function checkAgentRelease(path, git) {
635
- const tags = await releaseTags(path, git);
636
- const latest = tags[0];
637
- if (!latest)
990
+ function confirmAgent(info, policy) {
991
+ // Ordinary apply handles its non-mutating no-op; force still cleans files at the same revision.
992
+ if (!policy)
638
993
  return null;
639
- const current = await currentReleaseTag(path, git);
640
- let behind = releaseGap(tags, current, latest);
641
- if (current === null) {
642
- const ahead = await git(['tag', '--list', AGENT_TAG_GLOB, '--contains', 'HEAD'], path);
643
- const count = ahead.ok ? ahead.out.split('\n').filter((l) => l.trim()).length : 0;
644
- if (count > 0)
645
- behind = count;
646
- }
647
- if (behind === 0 && (await headIsPastLatestTag(path, current, git)))
994
+ if (info.latest_sha === policy.supportedRevision)
995
+ return policy.confirmedRevision && policy.confirmedRevision !== info.latest_sha
996
+ ? { ok: false, message: 'The Agent update target changed. Check for updates and try again.' } : null;
997
+ if (policy.confirmedRevision === info.latest_sha)
648
998
  return null;
649
- if (behind > 0 && (await headContainsRef(path, latest, git)))
650
- return null;
651
- if (behind > 0 && !(await canFastForwardTo(path, latest, git)))
652
- return null;
653
- const remote = normalizeRemoteUrl((await git(['remote', 'get-url', 'origin'], path)).out);
654
- return { name: 'agent', behind, current_sha: current, latest_sha: latest, branch: latest, repo_url: remote, release_based: true, current_version: current, latest_version: latest, channel: DEFAULT_CHANNEL };
655
- }
656
- async function checkAgentBranch(path, git) {
657
- const ref = await upstreamRef(path, git);
658
- const count = await git(['rev-list', '--count', `HEAD..${ref}`], path);
659
- const behind = count.ok && /^\d+$/.test(count.out) ? Number.parseInt(count.out, 10) : 0;
660
- const mb = await git(['merge-base', 'HEAD', ref], path);
661
- let current = null;
662
- if (mb.ok && mb.out) {
663
- const short = await git(['rev-parse', '--short', mb.out], path);
664
- current = short.ok && short.out ? short.out : null;
665
- }
666
- const latest = (await git(['rev-parse', '--short', ref], path)).out;
667
- const remote = normalizeRemoteUrl((await git(['remote', 'get-url', 'origin'], path)).out);
668
- return { name: 'agent', behind, current_sha: current, latest_sha: latest, branch: ref, repo_url: remote, compare_url: compareUrl(remote, current, latest) };
999
+ return { ok: false, target: 'agent', agent_channel: info.channel, confirmation_required: true, ...agentWarning(info, policy),
1000
+ message: 'This Agent version is not officially supported by Talaria and may cause issues.' };
669
1001
  }
670
1002
  async function isDirty(path, git) {
671
1003
  const out = await git(['diff-index', '--quiet', 'HEAD', '--'], path, 1000);
672
1004
  return !out.ok && (out.out === 'git exited with status 1' || !out.out || out.out.startsWith('git exited with status '));
673
1005
  }
674
- /** Python `_check_repo` for the Agent checkout: fetch tags, prefer the release view, else the branch view. */
675
- export async function checkAgentUpdate(path, git) {
1006
+ /** Fetch and compare the independently selected Agent channel. */
1007
+ export async function checkAgentUpdate(path, git, channel = DEFAULT_CHANNEL) {
676
1008
  if (!path || !existsSync(join(path, '.git')))
677
1009
  return { name: 'agent', behind: null, no_git: true };
678
- const fetched = await git(['fetch', 'origin', '--tags', '--force'], path, 15_000);
1010
+ const fetched = await fetchWithRetry(git, ['fetch', 'origin', '--tags', '--force'], path, 15_000);
679
1011
  if (!fetched.ok) {
680
1012
  const message = fetched.out ? `fetch failed: ${sanitizeGitDiagnostic(fetched.out)}` : 'fetch failed';
681
- const info = (await checkAgentRelease(path, git)) ?? { name: 'agent', behind: null };
682
- return { ...info, error: message, stale_check: true, dirty: await isDirty(path, git) };
1013
+ return { name: 'agent', channel, behind: null, error: message, stale_check: true, dirty: await isDirty(path, git) };
1014
+ }
1015
+ try {
1016
+ return { ...await agentTarget(path, git, channel), dirty: await isDirty(path, git) };
1017
+ }
1018
+ catch (error) {
1019
+ return { name: 'agent', channel, behind: null, error: error.message };
683
1020
  }
684
- const info = (await checkAgentRelease(path, git)) ?? { ...(await checkAgentBranch(path, git)), channel: DEFAULT_CHANNEL };
685
- return { ...info, dirty: await isDirty(path, git) };
686
1021
  }
687
- /** Python `_apply_update_inner` (agent branch): fetch, stash, `pull --ff-only`, pop. */
688
- export async function applyAgentUpdate(path, git) {
1022
+ /** Fetch, confirm the immutable Agent target, stash, fast-forward, and pop. */
1023
+ export async function applyAgentUpdate(path, git, channel = DEFAULT_CHANNEL, policy) {
689
1024
  if (!path || !existsSync(join(path, '.git')))
690
1025
  return { ok: false, message: 'Not a git repository' };
691
1026
  const fetched = await git(['fetch', 'origin', '--quiet', '--tags', '--force'], path, 15_000);
@@ -694,7 +1029,26 @@ export async function applyAgentUpdate(path, git) {
694
1029
  return { ok: false, message: `Fetch failed due to a repository lock: ${fetched.out.trim()}`, lock_conflict: true };
695
1030
  return { ok: false, message: fetchFailureMessage(fetched.out, 'Could not reach the remote repository. Check your internet connection and try again.') };
696
1031
  }
697
- const ref = await selectAgentCompareRef(path, git);
1032
+ let info;
1033
+ try {
1034
+ info = await agentTarget(path, git, channel);
1035
+ }
1036
+ catch (error) {
1037
+ return { ok: false, message: error.message };
1038
+ }
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
+ }
1045
+ if (info.manual_update || info.error)
1046
+ return { ...info, ok: false, message: info.message ?? info.error };
1047
+ const warning = confirmAgent(info, policy);
1048
+ if (warning)
1049
+ return warning;
1050
+ const ref = str(info.branch);
1051
+ const revision = str(info.latest_sha);
698
1052
  const status = await git(['status', '--porcelain', '--untracked-files=no'], path);
699
1053
  if (!status.ok) {
700
1054
  if (isGitLockError(status.out))
@@ -710,9 +1064,8 @@ export async function applyAgentUpdate(path, git) {
710
1064
  return { ok: false, message: 'Failed to stash local changes' };
711
1065
  stashed = true;
712
1066
  }
713
- const slash = ref.indexOf('/');
714
- const pullArgs = slash > 0 ? ['pull', '--ff-only', ref.slice(0, slash), ref.slice(slash + 1)] : ['pull', '--ff-only', 'origin', ref];
715
- const pulled = await git(pullArgs, path, 30_000);
1067
+ // Merge the immutable commit that was checked/acknowledged, never re-fetch a moving ref here.
1068
+ const pulled = await git(['merge', '--ff-only', revision], path, 30_000);
716
1069
  if (!pulled.ok) {
717
1070
  let note = '';
718
1071
  if (stashed)
@@ -727,7 +1080,10 @@ export async function applyAgentUpdate(path, git) {
727
1080
  if (!popped.ok)
728
1081
  message += '. Local changes remain in `git stash list`; resolve them manually.';
729
1082
  }
730
- 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 };
731
1087
  }
732
1088
  async function restoreStash(path, git, pullOut) {
733
1089
  if ((await git(['stash', 'pop'], path)).ok)
@@ -739,23 +1095,37 @@ async function restoreStash(path, git, pullOut) {
739
1095
  return `Your local modifications could not be restored automatically (stash pop failed after pull error: ${pullOut.trim().slice(0, 200) || 'no detail'}). They remain safely in \`git stash list\`; run \`git -C ${path} stash pop\` once the lock is cleared.`;
740
1096
  }
741
1097
  /** Python `apply_force_update` (agent branch): fetch, refuse a pure-ancestor rewind, `checkout . && clean -fd && reset --hard`. */
742
- export async function forceAgentUpdate(path, git, log) {
1098
+ export async function forceAgentUpdate(path, git, log, channel = DEFAULT_CHANNEL, policy) {
743
1099
  if (!path || !existsSync(join(path, '.git')))
744
1100
  return { ok: false, message: 'Not a git repository' };
745
1101
  const fetched = await git(['fetch', 'origin', '--quiet', '--tags', '--force'], path, 15_000);
746
1102
  if (!fetched.ok)
747
1103
  return { ok: false, message: fetchFailureMessage(fetched.out, 'Could not reach the remote repository. Check your connection.') };
748
- const ref = await selectAgentCompareRef(path, git);
749
- if ((await headContainsRef(path, ref, git)) && !(await canFastForwardTo(path, ref, git))) {
750
- return { ok: false, message: `agent is already ahead of the stable channel (${ref}); refusing to rewind the checkout. Switching to a slower channel keeps your current version until that channel catches up.`, target: 'agent', channel: DEFAULT_CHANNEL, refused_rewind: true };
1104
+ let info;
1105
+ try {
1106
+ info = await agentTarget(path, git, channel);
1107
+ }
1108
+ catch (error) {
1109
+ return { ok: false, message: error.message };
1110
+ }
1111
+ const ref = str(info.branch);
1112
+ const revision = str(info.latest_sha);
1113
+ if ((await headContainsRef(path, revision, git)) && !(await canFastForwardTo(path, revision, git))) {
1114
+ return { ok: false, message: `agent is already ahead of the ${channel} channel (${ref}); refusing to rewind the checkout. Switching to a slower channel keeps your current version until that channel catches up.`, target: 'agent', channel, refused_rewind: true };
751
1115
  }
1116
+ const warning = confirmAgent(info, policy);
1117
+ if (warning)
1118
+ return warning;
752
1119
  await git(['checkout', '.'], path);
753
1120
  const cleaned = await git(['clean', '-fd'], path);
754
1121
  if (!cleaned.ok)
755
1122
  log(`[updates] force update: git clean -fd failed (continuing to reset --hard): ${cleaned.out}`);
756
- if (!(await git(['reset', '--hard', ref], path)).ok)
1123
+ if (!(await git(['reset', '--hard', revision], path)).ok)
757
1124
  return { ok: false, message: `Force reset to ${ref} failed` };
758
- 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 };
759
1129
  }
760
1130
  /** Python `_inventory_locks`: report `.git/**\/*.lock` without touching any of them. */
761
1131
  export function inventoryLocks(path) {
@@ -907,9 +1277,16 @@ function summaryPrompt(details) {
907
1277
  }
908
1278
  export class UpdateService {
909
1279
  deps;
910
- cache = { webui: null, agent: null, checked_at: 0, include_agent: true, channel: DEFAULT_CHANNEL };
1280
+ cache = { webui: null, agent: null, checked_at: 0, include_agent: true, channel: DEFAULT_CHANNEL, agent_channel: DEFAULT_CHANNEL };
911
1281
  checking = null;
1282
+ checkingKey = null;
1283
+ lastGood = new Map();
912
1284
  applying = false;
1285
+ autoTimer = null;
1286
+ autoStarted = false;
1287
+ lifecycle = 0;
1288
+ autoRunning = false;
1289
+ autoRestartScheduled = false;
913
1290
  summaries = new Map();
914
1291
  git;
915
1292
  now;
@@ -920,13 +1297,24 @@ export class UpdateService {
920
1297
  this.now = deps.now ?? (() => Date.now() / 1000);
921
1298
  this.sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
922
1299
  }
1300
+ agentChannel() { return this.deps.agentChannel?.() ?? DEFAULT_CHANNEL; }
1301
+ agentPolicy(confirmedRevision) {
1302
+ const supported = dict(this.deps.identity.release().compatibleAgent);
1303
+ return { supportedRevision: str(supported.sourceRevision), supportedVersion: str(supported.version), ...(confirmedRevision ? { confirmedRevision } : {}) };
1304
+ }
923
1305
  /** Python `cached_update_status`: no network, no git mutations. */
924
- cachedStatus(includeAgent = this.deps.includeAgent(), channel = this.deps.channel()) {
1306
+ cachedStatus(includeAgent = this.deps.includeAgent(), channel = this.deps.channel(), agentChannel = this.agentChannel()) {
925
1307
  const cached = { ...this.cache };
926
1308
  if (cached.channel !== channel) {
927
1309
  cached.channel = channel;
1310
+ cached.webui = null;
928
1311
  cached.stale_channel = true;
929
1312
  }
1313
+ if (cached.agent_channel !== agentChannel) {
1314
+ cached.agent_channel = agentChannel;
1315
+ cached.agent = null;
1316
+ cached.stale_agent_channel = true;
1317
+ }
930
1318
  if (cached.include_agent !== includeAgent) {
931
1319
  cached.include_agent = includeAgent;
932
1320
  if (!includeAgent)
@@ -935,27 +1323,129 @@ export class UpdateService {
935
1323
  cached.cached = true;
936
1324
  return cached;
937
1325
  }
938
- /** Python `check_for_updates`: 30 min cache keyed on channel + include_agent; one in-flight check per key. */
939
- async check(force = false, includeAgent = this.deps.includeAgent(), channel = this.deps.channel()) {
940
- const matches = this.cache.include_agent === includeAgent && this.cache.channel === channel;
1326
+ /** 30 min cache keyed on both channels + include_agent; one in-flight check per key. */
1327
+ async check(force = false, includeAgent = this.deps.includeAgent(), channel = this.deps.channel(), agentChannel = this.agentChannel()) {
1328
+ const key = `${channel}:${agentChannel}:${String(includeAgent)}`;
1329
+ if (this.checking) {
1330
+ if (this.checkingKey === key)
1331
+ return this.checking;
1332
+ await this.checking;
1333
+ return this.check(force, includeAgent, channel, agentChannel);
1334
+ }
1335
+ if (this.applying)
1336
+ return this.cachedStatus(includeAgent, channel, agentChannel);
1337
+ const matches = this.cache.include_agent === includeAgent && this.cache.channel === channel && this.cache.agent_channel === agentChannel;
941
1338
  if (!force && matches && this.now() - Number(this.cache.checked_at) < CACHE_TTL_S)
942
1339
  return { ...this.cache };
943
- if (this.checking && matches)
944
- return this.checking;
1340
+ this.checkingKey = key;
945
1341
  this.checking = (async () => {
946
1342
  try {
947
- const webui = await checkWebUpdate(this.deps.webRoot, this.deps.webuiVersion, channel, this.git, this.deps.getJson, this.deps.identity);
948
- // The channel is a Web concept; the Agent always follows its own stable tags.
949
- const agent = includeAgent ? await checkAgentUpdate(this.deps.agentDir(), this.git) : ignoredAgent();
950
- Object.assign(this.cache, { webui, agent, checked_at: this.now(), include_agent: includeAgent, channel });
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();
1348
+ if (agent.latest_sha)
1349
+ Object.assign(agent, agentWarning(agent, this.agentPolicy()));
1350
+ Object.assign(this.cache, { webui, agent, checked_at: this.now(), include_agent: includeAgent, channel, agent_channel: agentChannel });
951
1351
  return { ...this.cache };
952
1352
  }
953
1353
  finally {
954
1354
  this.checking = null;
1355
+ this.checkingKey = null;
955
1356
  }
956
1357
  })();
957
1358
  return this.checking;
958
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
+ }
1372
+ startAutoApply(intervalMs = AUTO_UPDATE_INTERVAL_MS) {
1373
+ if (this.autoStarted || this.autoRestartScheduled)
1374
+ return;
1375
+ this.autoStarted = true;
1376
+ const lifecycle = this.lifecycle;
1377
+ const schedule = (delay) => {
1378
+ this.autoTimer = setTimeout(() => {
1379
+ this.autoTimer = null;
1380
+ void this.autoApplyOnce().finally(() => { if (this.autoStarted && lifecycle === this.lifecycle && !this.autoRestartScheduled)
1381
+ schedule(intervalMs); });
1382
+ }, delay);
1383
+ this.autoTimer.unref();
1384
+ };
1385
+ schedule(0);
1386
+ }
1387
+ stopAutoApply() {
1388
+ this.autoStarted = false;
1389
+ this.lifecycle += 1;
1390
+ if (this.autoTimer)
1391
+ clearTimeout(this.autoTimer);
1392
+ this.autoTimer = null;
1393
+ }
1394
+ /** Shared turn admission stays closed from update start through the supervisor restart. */
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
+ }
1403
+ async autoApplyOnce() {
1404
+ if (this.autoRunning || this.applying || this.autoRestartScheduled || !(this.deps.checkEnabled?.() ?? this.deps.autoApply?.()))
1405
+ return null;
1406
+ this.autoRunning = true;
1407
+ const lifecycle = this.lifecycle;
1408
+ let notificationId = null;
1409
+ try {
1410
+ const channel = this.deps.channel();
1411
+ const stillEnabled = () => lifecycle === this.lifecycle && Boolean(this.deps.autoApply?.()) && (this.deps.checkEnabled?.() ?? true) && channel === this.deps.channel();
1412
+ const checked = await this.check(true, this.deps.includeAgent(), channel);
1413
+ const web = dict(checked.webui);
1414
+ if (!stillEnabled())
1415
+ return web;
1416
+ if (web.error || web.manual_update) {
1417
+ this.deps.log(`[updates] automatic Web update unavailable: ${str(web.error || web.message)}`);
1418
+ return web;
1419
+ }
1420
+ if (!(Number(web.behind) > 0 || web.metadata_repair === true))
1421
+ return web;
1422
+ notificationId = this.deps.autoNotification?.begin() ?? null;
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
+ }
1434
+ this.deps.log(`[updates] automatic Web update: ${str(result.message || (result.ok ? 'applied' : 'failed'))}`);
1435
+ if (!result.ok)
1436
+ this.cache.webui = { ...web, message: result.message, error: result.restart_blocked ? undefined : result.message };
1437
+ return result;
1438
+ }
1439
+ catch (error) {
1440
+ if (notificationId)
1441
+ this.deps.autoNotification?.transition(notificationId, 'failed');
1442
+ this.deps.log(`[updates] automatic Web update failed: ${error.message}`);
1443
+ return { ok: false, error: error.message };
1444
+ }
1445
+ finally {
1446
+ this.autoRunning = false;
1447
+ }
1448
+ }
959
1449
  blockedResponse(target) {
960
1450
  const b = this.deps.blockers();
961
1451
  if (!b.restart_blocked)
@@ -965,7 +1455,11 @@ export class UpdateService {
965
1455
  parts.push(`${String(b.active_streams)} active chat stream${b.active_streams === 1 ? '' : 's'}`);
966
1456
  if (b.active_runs)
967
1457
  parts.push(`${String(b.active_runs)} active agent run${b.active_runs === 1 ? '' : 's'}`);
968
- return { ok: false, message: `Cannot update ${target} while ${parts.join(' and ') || 'active chat work'} is running. Wait for the response to finish, then retry the update.`, target, restart_blocked: true, active_streams: b.active_streams, active_runs: b.active_runs, blocking_stream_ids: b.blocking_stream_ids, blocking_run_ids: b.blocking_run_ids };
1458
+ if (b.active_terminals)
1459
+ parts.push(`${String(b.active_terminals)} open terminal${b.active_terminals === 1 ? '' : 's'}`);
1460
+ if (b.active_cron_jobs)
1461
+ parts.push(`${String(b.active_cron_jobs)} active cron job${b.active_cron_jobs === 1 ? '' : 's'}`);
1462
+ return { ok: false, message: `Cannot update ${target} while ${parts.join(' and ') || 'active work'} is running. Wait for work to finish and close open terminals, then retry the update.`, target, ...b };
969
1463
  }
970
1464
  async locked(fn) {
971
1465
  if (this.applying)
@@ -979,17 +1473,29 @@ export class UpdateService {
979
1473
  }
980
1474
  }
981
1475
  /** Python `apply_update`. */
982
- apply(target, channel) {
1476
+ async apply(target, channel, canApply = () => true, agentOptions = {}) {
1477
+ if (this.checking)
1478
+ await this.checking;
1479
+ const pendingRestart = this.pendingRestartResponse(target);
1480
+ if (pendingRestart)
1481
+ return pendingRestart;
1482
+ if (!canApply())
1483
+ return { ok: false, message: 'Web update settings changed; update deferred.' };
983
1484
  const blocked = this.blockedResponse(target);
984
1485
  if (blocked)
985
- return Promise.resolve(blocked);
986
- return this.locked(() => this.applyInner(target, channel ?? this.deps.channel()));
1486
+ return blocked;
1487
+ const lifecycle = this.lifecycle;
1488
+ return this.locked(() => this.applyInner(target, channel ?? this.deps.channel(), () => lifecycle === this.lifecycle && canApply(), agentOptions));
987
1489
  }
988
- async applyInner(target, channel) {
1490
+ async applyInner(target, channel, canApply = () => true, agentOptions = {}) {
989
1491
  if (target === 'webui') {
990
- const result = await applyWebUpdate(this.deps.webRoot, channel, this.git, this.deps.getJson, this.deps.identity, this.deps.build ?? runNpm);
991
- if (result.ok && !result.up_to_date) {
1492
+ const lifecycle = this.lifecycle;
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 });
1494
+ // Settings can cancel before mutation, but a committed replacement must finish restarting unless shutting down.
1495
+ if (result.ok && !result.up_to_date && lifecycle === this.lifecycle) {
992
1496
  this.cache.checked_at = 0;
1497
+ this.autoRestartScheduled = true;
1498
+ this.stopAutoApply();
993
1499
  this.deps.scheduleRestart();
994
1500
  result.restart_scheduled = true;
995
1501
  }
@@ -997,8 +1503,8 @@ export class UpdateService {
997
1503
  }
998
1504
  if (target !== 'agent')
999
1505
  return { ok: false, message: `Unknown target: ${target}` };
1000
- const result = await applyAgentUpdate(this.deps.agentDir(), this.git);
1001
- if (!result.ok)
1506
+ const result = await applyAgentUpdate(this.deps.agentDir(), this.git, agentOptions.agentChannel ?? this.agentChannel(), this.agentPolicy(agentOptions.confirmedRevision));
1507
+ if (!result.ok || result.up_to_date)
1002
1508
  return result;
1003
1509
  return this.finishAgent(result);
1004
1510
  }
@@ -1035,23 +1541,31 @@ export class UpdateService {
1035
1541
  return [false, { ...annotated, message: `${str(first.message) || 'Restart failed'}; recovery retry did not complete: ${str(retry.message) || 'retry did not complete'}` }];
1036
1542
  }
1037
1543
  /** Python `apply_force_update`: Web keeps its clean-only policy; the Agent resets hard. */
1038
- force(target, channel) {
1544
+ async force(target, channel, agentOptions = {}) {
1545
+ if (this.checking)
1546
+ await this.checking;
1039
1547
  if (target === 'webui')
1040
1548
  return this.apply(target, channel);
1549
+ const pendingRestart = this.pendingRestartResponse(target);
1550
+ if (pendingRestart)
1551
+ return pendingRestart;
1041
1552
  const blocked = this.blockedResponse(target);
1042
1553
  if (blocked)
1043
1554
  return Promise.resolve(blocked);
1044
1555
  return this.locked(async () => {
1045
1556
  if (target !== 'agent')
1046
1557
  return { ok: false, message: `Unknown target: ${target}` };
1047
- const result = await forceAgentUpdate(this.deps.agentDir(), this.git, this.deps.log);
1558
+ const result = await forceAgentUpdate(this.deps.agentDir(), this.git, this.deps.log, agentOptions.agentChannel ?? this.agentChannel(), this.agentPolicy(agentOptions.confirmedRevision));
1048
1559
  return result.ok ? this.finishAgent(result) : result;
1049
1560
  });
1050
1561
  }
1051
1562
  /** Python `apply_clear_lock`: never removes a lock; Web retries the clean path, the Agent gets the manual command. */
1052
- clearLock(target) {
1563
+ clearLock(target, agentOptions = {}) {
1053
1564
  if (target === 'webui')
1054
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);
1055
1569
  const blocked = this.blockedResponse(target);
1056
1570
  if (blocked)
1057
1571
  return Promise.resolve(blocked);
@@ -1065,7 +1579,7 @@ export class UpdateService {
1065
1579
  const manual = `rm -f ${str(inv.well_known_lock_path)}`;
1066
1580
  if (!inv.well_known_lock_present) {
1067
1581
  this.cache.checked_at = 0;
1068
- const retry = await this.applyInner(target, this.deps.channel());
1582
+ const retry = await this.applyInner(target, this.deps.channel(), () => true, agentOptions);
1069
1583
  return { ...retry, lock_recovery: { action: 'no-lock-found', manual_command: manual, other_locks: inv.other_locks } };
1070
1584
  }
1071
1585
  return { ok: false, message: `A git lock file (.git/index.lock) is present. The server does not delete locks automatically -- git uses O_CREAT|O_EXCL locking, which cannot be detected with advisory probes. To recover: confirm no other git process is running against this checkout, then run: ${manual} Click "Retry update" once you have removed it.`, lock_held: true, target, manual_command: manual, well_known_lock_path: inv.well_known_lock_path, other_locks: inv.other_locks };