@maudecode/talaria-web 0.0.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (211) hide show
  1. package/_release.json +20 -0
  2. package/dist/api/chat-router.d.ts +54 -1
  3. package/dist/api/sessions-router.d.ts +296 -1
  4. package/dist/api/settings-router.d.ts +8 -0
  5. package/dist/api/settings-router.js +13 -5
  6. package/dist/api/settings-router.js.map +1 -1
  7. package/dist/api/sse-routes.js +1 -1
  8. package/dist/api/sse-routes.js.map +1 -1
  9. package/dist/api/tools-router.d.ts +91 -0
  10. package/dist/api/tools-router.js +4 -4
  11. package/dist/api/tools-router.js.map +1 -1
  12. package/dist/app.d.ts +449 -2
  13. package/dist/bin/talaria-web.js +4 -1
  14. package/dist/bin/talaria-web.js.map +1 -1
  15. package/dist/cli/supervise.js +2 -1
  16. package/dist/cli/supervise.js.map +1 -1
  17. package/dist/runtime.js +8 -2
  18. package/dist/runtime.js.map +1 -1
  19. package/dist/sessions/anchor.d.ts +57 -5
  20. package/dist/sessions/anchor.js +368 -32
  21. package/dist/sessions/anchor.js.map +1 -1
  22. package/dist/sessions/merge.d.ts +6 -0
  23. package/dist/sessions/merge.js +35 -4
  24. package/dist/sessions/merge.js.map +1 -1
  25. package/dist/sessions/service.d.ts +2 -0
  26. package/dist/sessions/service.js +31 -16
  27. package/dist/sessions/service.js.map +1 -1
  28. package/dist/sessions/session.js +1 -1
  29. package/dist/sessions/session.js.map +1 -1
  30. package/dist/sessions/turn.d.ts +14 -0
  31. package/dist/sessions/turn.js +147 -20
  32. package/dist/sessions/turn.js.map +1 -1
  33. package/dist/sessions/window.d.ts +1 -0
  34. package/dist/sessions/window.js +1 -1
  35. package/dist/sessions/window.js.map +1 -1
  36. package/dist/settings.js +5 -0
  37. package/dist/settings.js.map +1 -1
  38. package/dist/sidecar/client.d.ts +1 -1
  39. package/dist/sidecar/client.js +15 -4
  40. package/dist/sidecar/client.js.map +1 -1
  41. package/dist/sidecar/fake.js +3 -2
  42. package/dist/sidecar/fake.js.map +1 -1
  43. package/dist/tools/crons.js +55 -43
  44. package/dist/tools/crons.js.map +1 -1
  45. package/dist/tools/updates.d.ts +51 -13
  46. package/dist/tools/updates.js +362 -114
  47. package/dist/tools/updates.js.map +1 -1
  48. package/package.json +2 -2
  49. package/sidecar/agent_dependency.json +4 -3
  50. package/sidecar/talaria_sidecar/home.py +29 -20
  51. package/sidecar/talaria_sidecar/methods/config.py +5 -3
  52. package/sidecar/talaria_sidecar/runtime.py +3 -9
  53. package/static/dist/FILES.txt +116 -116
  54. package/static/dist/assets/AppShell-hikzkcql.js +1 -0
  55. package/static/dist/assets/ChatPage-ofqn89h9.js +21 -0
  56. package/static/dist/assets/{HubRoute-qs9vzuh0.js → HubRoute-f2zrs9si.js} +1 -1
  57. package/static/dist/assets/{Markdown-bt9bqd6a.js → Markdown-ftyfzds0.js} +7 -7
  58. package/static/dist/assets/RightPanel-febk3dkd.js +1 -0
  59. package/static/dist/assets/Select-lvx8gihl.js +1 -0
  60. package/static/dist/assets/SessionListPanel-o70ljmik.js +1 -0
  61. package/static/dist/assets/SettingsLayout-pdpmhy1o.js +1 -0
  62. package/static/dist/assets/States-i46tz8an.js +1 -0
  63. package/static/dist/assets/{Toaster-jtn59pjm.js → Toaster-fklyeeyh.js} +1 -1
  64. package/static/dist/assets/_app-j3rors25.js +1 -0
  65. package/static/dist/assets/_app.ext._extensionId-c0ey5ec9.js +1 -0
  66. package/static/dist/assets/_app.index-coy9jrto.js +1 -0
  67. package/static/dist/assets/_app.insights-ebd2k0i5.js +1 -0
  68. package/static/dist/assets/_app.kanban-e2p7ypi5.js +1 -0
  69. package/static/dist/assets/_app.logs-ml21mfyz.js +1 -0
  70. package/static/dist/assets/_app.memory-lhu5mm47.js +1 -0
  71. package/static/dist/assets/_app.profiles-egx0fgjo.js +1 -0
  72. package/static/dist/assets/{_app.session._sessionId-kevi4qd2.js → _app.session._sessionId-n1ggcf5e.js} +1 -1
  73. package/static/dist/assets/_app.settings-d0vqtyid.js +1 -0
  74. package/static/dist/assets/_app.settings._section-cnzj6pli.js +1 -0
  75. package/static/dist/assets/_app.skills-kkug8obu.js +1 -0
  76. package/static/dist/assets/{_app.tasks-b99d98fo.js → _app.tasks-nf285d2o.js} +2 -2
  77. package/static/dist/assets/_app.todos-dxalqp1e.js +1 -0
  78. package/static/dist/assets/_app.workspaces-bqtduujs.js +1 -0
  79. package/static/dist/assets/{abnfDiagram-VCTEODGH-hvrr25lb.js → abnfDiagram-VCTEODGH-i4bgzu83.js} +1 -1
  80. package/static/dist/assets/architecture-7GRP2DOG-o8zqevhv.js +1 -0
  81. package/static/dist/assets/{architectureDiagram-5GKGNRK7-b3r0z8sq.js → architectureDiagram-5GKGNRK7-mx21lwqa.js} +1 -1
  82. package/static/dist/assets/{blockDiagram-I7D4REHJ-hd3gw0cj.js → blockDiagram-I7D4REHJ-o1hhchxv.js} +1 -1
  83. package/static/dist/assets/{c4Diagram-7LVT6UL2-jlmbjiji.js → c4Diagram-7LVT6UL2-jygqtvd8.js} +1 -1
  84. package/static/dist/assets/channel-mw8l793e.js +1 -0
  85. package/static/dist/assets/{chunk-4HAMMTFA-oydbpsin.js → chunk-4HAMMTFA-hjcpubgk.js} +1 -1
  86. package/static/dist/assets/{chunk-75Z2AOVW-j633otj1.js → chunk-75Z2AOVW-eibnex6m.js} +1 -1
  87. package/static/dist/assets/{chunk-DU6HZSFF-ods2l027.js → chunk-DU6HZSFF-k1tp3qnp.js} +3 -3
  88. package/static/dist/assets/{chunk-F27PBJKO-ln4n2cwt.js → chunk-F27PBJKO-lvcgn16f.js} +1 -1
  89. package/static/dist/assets/{chunk-GMAD6QVW-fpz133eh.js → chunk-GMAD6QVW-ldlzruv1.js} +1 -1
  90. package/static/dist/assets/{chunk-GVQU2GXP-abs9ixiy.js → chunk-GVQU2GXP-hs1mva9d.js} +1 -1
  91. package/static/dist/assets/{chunk-IMKFNOWR-j9vukdy1.js → chunk-IMKFNOWR-o7eqx3yb.js} +1 -1
  92. package/static/dist/assets/{chunk-L3NEJ4N5-cfyw87r3.js → chunk-L3NEJ4N5-bp1ofl70.js} +1 -1
  93. package/static/dist/assets/{chunk-OSK3NFVY-baja78wr.js → chunk-OSK3NFVY-o8m6mdz5.js} +1 -1
  94. package/static/dist/assets/{chunk-P2QGCYS3-g66xby8h.js → chunk-P2QGCYS3-kx83fti2.js} +1 -1
  95. package/static/dist/assets/{chunk-POPQ4Y6H-osqu864t.js → chunk-POPQ4Y6H-mwl99e9y.js} +1 -1
  96. package/static/dist/assets/{chunk-PWAF6VOD-egejasv1.js → chunk-PWAF6VOD-oriuokxy.js} +1 -1
  97. package/static/dist/assets/{chunk-SHT3W25Y-l014h8zz.js → chunk-SHT3W25Y-ab5cf00v.js} +1 -1
  98. package/static/dist/assets/{chunk-SVP7TREG-n3z10bp3.js → chunk-SVP7TREG-kqdrpr17.js} +1 -1
  99. package/static/dist/assets/{chunk-TICWLB2K-bzozkjc0.js → chunk-TICWLB2K-kfbnjabk.js} +1 -1
  100. package/static/dist/assets/classDiagram-ZZMXUADV-egajr1wp.js +1 -0
  101. package/static/dist/assets/classDiagram-v2-VYDZK3BY-egajr1wp.js +1 -0
  102. package/static/dist/assets/{copied-cjsttec1.js → copied-dp33szhq.js} +1 -1
  103. package/static/dist/assets/{cynefin-OW5HDTMX-elpt7h1j.js → cynefin-OW5HDTMX-mm5sj9s7.js} +1 -1
  104. package/static/dist/assets/{cynefinDiagram-5FMLGOSQ-5mzl5pi2.js → cynefinDiagram-5FMLGOSQ-nfon5q38.js} +1 -1
  105. package/static/dist/assets/{dagre-GXQ25YYZ-ihwlhmfl.js → dagre-GXQ25YYZ-dzdp9ypb.js} +1 -1
  106. package/static/dist/assets/{diagram-S7CK7UJ4-4z90agaz.js → diagram-S7CK7UJ4-ckxzqo01.js} +1 -1
  107. package/static/dist/assets/{diagram-UQ7AKVKN-eifw0lg7.js → diagram-UQ7AKVKN-hw16h1um.js} +1 -1
  108. package/static/dist/assets/{diagram-VSXAHHWV-ksh3sjqv.js → diagram-VSXAHHWV-mu7wu2zg.js} +1 -1
  109. package/static/dist/assets/{diagram-VX7I27RA-ett72f71.js → diagram-VX7I27RA-k9uprwtj.js} +1 -1
  110. package/static/dist/assets/{diagram-Z3DM3KII-l8scp8nu.js → diagram-Z3DM3KII-csdjzrg9.js} +1 -1
  111. package/static/dist/assets/{ebnfDiagram-PWID7BFC-f0pxcmit.js → ebnfDiagram-PWID7BFC-n8mbw2jj.js} +1 -1
  112. package/static/dist/assets/endpoints-f6ngsyu5.js +64 -0
  113. package/static/dist/assets/{erDiagram-RLTQ6QDP-f47m5xh0.js → erDiagram-RLTQ6QDP-fsfe1uj0.js} +1 -1
  114. package/static/dist/assets/eventmodeling-NTZA5JFV-k3zhqcyl.js +1 -0
  115. package/static/dist/assets/flowDiagram-HODETNUW-bdxes51f.js +1 -0
  116. package/static/dist/assets/{ganttDiagram-EL5Y4UJY-bmktjdi0.js → ganttDiagram-EL5Y4UJY-gsba3iq9.js} +1 -1
  117. package/static/dist/assets/{gitGraph-4MIJSDKK-j2ueg258.js → gitGraph-4MIJSDKK-na86q1pu.js} +1 -1
  118. package/static/dist/assets/{gitGraphDiagram-WWUBYQGX-ms6586v9.js → gitGraphDiagram-WWUBYQGX-g72ew3xz.js} +1 -1
  119. package/static/dist/assets/{highlighted-body-KPVGNVTW-ev52d2si.js → highlighted-body-KPVGNVTW-okn18cp1.js} +1 -1
  120. package/static/dist/assets/index-ab0wj9ik.css +2 -0
  121. package/static/dist/assets/index-fd27xooo.js +11 -0
  122. package/static/dist/assets/{info-A6RAGUB7-j717vyt7.js → info-A6RAGUB7-i01jeltk.js} +1 -1
  123. package/static/dist/assets/{infoDiagram-27XIBGKW-dsbpib36.js → infoDiagram-27XIBGKW-cayiv6fa.js} +1 -1
  124. package/static/dist/assets/{ishikawaDiagram-5VMMS53U-n0sz3i2d.js → ishikawaDiagram-5VMMS53U-g4rf14bv.js} +1 -1
  125. package/static/dist/assets/{journeyDiagram-3NMN7TZE-mcxz1mme.js → journeyDiagram-3NMN7TZE-fusb2424.js} +1 -1
  126. package/static/dist/assets/{kanban-definition-UXKFOSKX-n6hq2lyc.js → kanban-definition-UXKFOSKX-g831pwlr.js} +1 -1
  127. package/static/dist/assets/{lazyRouteComponent-faquioun.js → lazyRouteComponent-ol965zwk.js} +1 -1
  128. package/static/dist/assets/{line-h5xq20s4.js → line-md6em6gc.js} +1 -1
  129. package/static/dist/assets/login-kfclsqd2.js +1 -0
  130. package/static/dist/assets/mermaid-HWGCJPDP-ejk0d88t.js +1 -0
  131. package/static/dist/assets/{mermaid-parser.core-n9cuu5ue.js → mermaid-parser.core-mq3uns9m.js} +3 -3
  132. package/static/dist/assets/{mindmap-definition-YA3MSWOX-gta3u7z4.js → mindmap-definition-YA3MSWOX-oxafrned.js} +1 -1
  133. package/static/dist/assets/onboarding-fo6hnkyv.js +1 -0
  134. package/static/dist/assets/{packet-AYTQ26CC-bpzuq5n6.js → packet-AYTQ26CC-ggxyvbym.js} +1 -1
  135. package/static/dist/assets/{pegDiagram-XKGWAZYB-juye13e5.js → pegDiagram-XKGWAZYB-nk7xcqgp.js} +1 -1
  136. package/static/dist/assets/{pie-WAS4IAKB-odhb8buq.js → pie-WAS4IAKB-ito2razh.js} +1 -1
  137. package/static/dist/assets/{pieDiagram-E7YTZNPT-mgb14ksh.js → pieDiagram-E7YTZNPT-g02xexar.js} +1 -1
  138. package/static/dist/assets/{quadrantDiagram-AXDQQJYC-cg36naog.js → quadrantDiagram-AXDQQJYC-d4jn4xq5.js} +1 -1
  139. package/static/dist/assets/{radar-RG4KPBEZ-lgzfpnef.js → radar-RG4KPBEZ-nebvfdzt.js} +1 -1
  140. package/static/dist/assets/{railroad-74A4TZTK-gmglc3ua.js → railroad-74A4TZTK-ny3gebse.js} +1 -1
  141. package/static/dist/assets/railroad-abnf-HS5TGJTU-gtd38edw.js +1 -0
  142. package/static/dist/assets/railroad-ebnf-LZEXJU2U-o238oax2.js +1 -0
  143. package/static/dist/assets/railroad-peg-WCYAUIDC-c4acqp2j.js +1 -0
  144. package/static/dist/assets/{railroadDiagram-O6MQD6OU-cmxwjt73.js → railroadDiagram-O6MQD6OU-m09mmu67.js} +1 -1
  145. package/static/dist/assets/{refresh-cw-mf192q3f.js → refresh-cw-eitd7yva.js} +1 -1
  146. package/static/dist/assets/{requirementDiagram-BXWQKSXE-ir0s2mek.js → requirementDiagram-BXWQKSXE-evggpcbk.js} +1 -1
  147. package/static/dist/assets/{sankeyDiagram-P5KCCOFB-jo7jcdq0.js → sankeyDiagram-P5KCCOFB-bun89039.js} +1 -1
  148. package/static/dist/assets/{saved-g777hp1t.js → saved-culx14ii.js} +1 -1
  149. package/static/dist/assets/{search-i2kfogyx.js → search-nu6kzq55.js} +1 -1
  150. package/static/dist/assets/{sequenceDiagram-WJ2MYXX4-n0pf6xk5.js → sequenceDiagram-WJ2MYXX4-hhs9f09o.js} +1 -1
  151. package/static/dist/assets/{settings_section_extensions_title-hgvh507l.js → settings_section_extensions_title-mj0gcozh.js} +1 -1
  152. package/static/dist/assets/share._token-k4pcnxqu.js +2 -0
  153. package/static/dist/assets/sliders-horizontal-b9ljga1o.js +1 -0
  154. package/static/dist/assets/{stateDiagram-D77RDMKH-olbrwyv9.js → stateDiagram-D77RDMKH-gi7t1tro.js} +1 -1
  155. package/static/dist/assets/stateDiagram-v2-MP3YSRHH-jtadbol7.js +1 -0
  156. package/static/dist/assets/{swimlanes-42K2YHIH-od14zrw6.js → swimlanes-42K2YHIH-ea5urvph.js} +1 -1
  157. package/static/dist/assets/swimlanesDiagram-VR7AAH4N-ki7weepw.js +8 -0
  158. package/static/dist/assets/{timeline-definition-24CTP7MA-l0qokmnb.js → timeline-definition-24CTP7MA-mq64rm3b.js} +1 -1
  159. package/static/dist/assets/{todoStore-la6uyqzj.js → todoStore-azvfuqbv.js} +1 -1
  160. package/static/dist/assets/{trash-bxz1k9cw.js → trash-osyo895g.js} +1 -1
  161. package/static/dist/assets/{treeView-Q6P3EWNA-hma3ip8m.js → treeView-Q6P3EWNA-cimbtxlz.js} +1 -1
  162. package/static/dist/assets/{treemap-WGGIJYW6-bghjng5m.js → treemap-WGGIJYW6-wig5gi1y.js} +1 -1
  163. package/static/dist/assets/{untitled-nfaw0q7m.js → untitled-mbwemvxf.js} +1 -1
  164. package/static/dist/assets/{useForm-k7fj2zsr.js → useForm-li4bljlv.js} +1 -1
  165. package/static/dist/assets/useLocale-kdew6auf.js +1 -0
  166. package/static/dist/assets/{vennDiagram-4TSXK5OY-bo97zf3z.js → vennDiagram-4TSXK5OY-ez63kh7n.js} +1 -1
  167. package/static/dist/assets/{wardley-WFR3VGLG-pbjl6jav.js → wardley-WFR3VGLG-r7rftz31.js} +1 -1
  168. package/static/dist/assets/{wardleyDiagram-VM6X3IG4-bmy9i09l.js → wardleyDiagram-VM6X3IG4-mvqg9eqc.js} +1 -1
  169. package/static/dist/assets/{xychartDiagram-S5SC5T6Z-eercu0mc.js → xychartDiagram-S5SC5T6Z-eqxdfc91.js} +1 -1
  170. package/static/dist/index.html +1 -1
  171. package/static/dist/sw.js +1 -1
  172. package/static/dist/assets/AppShell-gs0grlbs.js +0 -1
  173. package/static/dist/assets/ChatPage-gi1xlz52.js +0 -18
  174. package/static/dist/assets/RightPanel-dn19xu42.js +0 -1
  175. package/static/dist/assets/Select-y7wtf85g.js +0 -1
  176. package/static/dist/assets/SessionListPanel-igwdj9xr.js +0 -1
  177. package/static/dist/assets/SettingsLayout-n6mu0ghg.js +0 -1
  178. package/static/dist/assets/States-oil2issf.js +0 -1
  179. package/static/dist/assets/_app-k4jrkgfa.js +0 -1
  180. package/static/dist/assets/_app.ext._extensionId-mmf1mepm.js +0 -1
  181. package/static/dist/assets/_app.index-mb41nf00.js +0 -1
  182. package/static/dist/assets/_app.insights-mz5cep8m.js +0 -1
  183. package/static/dist/assets/_app.kanban-ocgw506i.js +0 -1
  184. package/static/dist/assets/_app.logs-lstmoju6.js +0 -1
  185. package/static/dist/assets/_app.memory-dishj9ra.js +0 -1
  186. package/static/dist/assets/_app.profiles-dynpvsio.js +0 -1
  187. package/static/dist/assets/_app.settings-c2czm9v4.js +0 -1
  188. package/static/dist/assets/_app.settings._section-exwflld7.js +0 -1
  189. package/static/dist/assets/_app.skills-j5jh2nug.js +0 -1
  190. package/static/dist/assets/_app.todos-ma8urvn3.js +0 -1
  191. package/static/dist/assets/_app.workspaces-l86cxeod.js +0 -1
  192. package/static/dist/assets/architecture-7GRP2DOG-mqft9w2p.js +0 -1
  193. package/static/dist/assets/channel-ix8cxdld.js +0 -1
  194. package/static/dist/assets/classDiagram-ZZMXUADV-c210y3il.js +0 -1
  195. package/static/dist/assets/classDiagram-v2-VYDZK3BY-c210y3il.js +0 -1
  196. package/static/dist/assets/endpoints-ipnv1hd4.js +0 -64
  197. package/static/dist/assets/eventmodeling-NTZA5JFV-d9b5a0ia.js +0 -1
  198. package/static/dist/assets/flowDiagram-HODETNUW-kfxxsyrk.js +0 -1
  199. package/static/dist/assets/index-bz7g7rhx.css +0 -2
  200. package/static/dist/assets/index-hywfjw88.js +0 -11
  201. package/static/dist/assets/login-mvjys1ol.js +0 -1
  202. package/static/dist/assets/mermaid-HWGCJPDP-gmnkgf1n.js +0 -1
  203. package/static/dist/assets/onboarding-l6quy6gz.js +0 -1
  204. package/static/dist/assets/railroad-abnf-HS5TGJTU-5mjl5wpv.js +0 -1
  205. package/static/dist/assets/railroad-ebnf-LZEXJU2U-bfram8v8.js +0 -1
  206. package/static/dist/assets/railroad-peg-WCYAUIDC-nwfczn88.js +0 -1
  207. package/static/dist/assets/share._token-il3y6js2.js +0 -2
  208. package/static/dist/assets/sliders-horizontal-kd99ffvl.js +0 -1
  209. package/static/dist/assets/stateDiagram-v2-MP3YSRHH-dnzlmdpu.js +0 -1
  210. package/static/dist/assets/swimlanesDiagram-VR7AAH4N-ck0eqqdp.js +0 -8
  211. package/static/dist/assets/useLocale-evhacmv2.js +0 -1
@@ -3,17 +3,19 @@
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; other packaged installs stay manual.
8
+ * Agent: the external checkout follows its own `v*` tags with a
8
9
  * stash/pull or force reset, then the gateway restarts through the sidecar.
9
10
  * Nothing here deletes git locks; `clearLock` only inventories them.
10
11
  */
11
12
  import { readCapped } from '../http/capped.js';
12
13
  import { execFile } from 'node:child_process';
13
14
  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';
15
+ import { existsSync, lstatSync, mkdtempSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, unlinkSync, writeFileSync } from 'node:fs';
16
+ import { delimiter, dirname, isAbsolute, join, resolve } from 'node:path';
16
17
  import { dict } from '../config/agent-config.js';
18
+ import { validateReleaseInfo } from '../release.js';
17
19
  import { str } from '../util.js';
18
20
  export const REPOSITORY = 'MaudeCode/talaria';
19
21
  export const REPOSITORY_URL = `https://github.com/${REPOSITORY}`;
@@ -24,11 +26,13 @@ export const WEB_UPDATE_PATHS = [':(top)web/', ':(top)contracts/'];
24
26
  export const RELEASE_STAMP = 'web/_release.json';
25
27
  const RELEASE_BLOBS = ['sidecar/agent_dependency.json', 'contract_versions.json'];
26
28
  export const CACHE_TTL_S = 1800;
29
+ export const AUTO_UPDATE_INTERVAL_MS = 5 * 60_000;
27
30
  export const RESTART_MAX_WAIT_S = 300;
28
31
  export const DEFAULT_CHANNEL = 'stable';
29
32
  const SHA = /^[a-f0-9]{40}$/;
30
33
  const VERSION = '(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)';
31
34
  const AGENT_TAG_GLOB = 'v*';
35
+ const WEB_NPM_PACKAGE = '@maudecode/talaria-web';
32
36
  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
37
  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
38
  export const normalizeChannel = (channel) => (channel === 'experimental' ? 'experimental' : 'stable');
@@ -62,10 +66,11 @@ export const WEB_BUILD_STEPS = [
62
66
  export const WEB_BUILD_TIMEOUT_MS = 10 * 60_000;
63
67
  /** The artifact the supervisor re-executes; a build that does not leave it behind did not succeed. */
64
68
  export const WEB_SERVER_ENTRY = 'packages/server/dist/bin/talaria-web.js';
65
- export const runNpm = (args, cwd, timeoutMs) => new Promise((done) => {
69
+ const runNpmCommand = (args, cwd, timeoutMs, env) => new Promise((done) => {
66
70
  // Prefer the npm beside the running node so a supervisor started with an absolute node path still finds it.
67
71
  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) => {
72
+ const cli = resolve(dirname(process.execPath), '../lib/node_modules/npm/bin/npm-cli.js');
73
+ 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
74
  if (!error) {
70
75
  done({ out: stdout.trim(), ok: true });
71
76
  return;
@@ -82,6 +87,8 @@ export const runNpm = (args, cwd, timeoutMs) => new Promise((done) => {
82
87
  done({ out: (stderr.trim() || stdout.trim() || `npm exited with status ${String(e.code)}`).slice(-4000), ok: false });
83
88
  });
84
89
  });
90
+ export const runNpm = (args, cwd, timeoutMs) => runNpmCommand(args, cwd, timeoutMs, { ...process.env, NODE_ENV: 'development' });
91
+ export const runPackageNpm = (args, cwd, timeoutMs) => runNpmCommand(args, cwd, timeoutMs, process.env);
85
92
  /** Install and build the checkout's Web packages; null on success, else the message for the caller. */
86
93
  async function buildWeb(root, build) {
87
94
  const cwd = join(root, 'web');
@@ -227,6 +234,9 @@ export async function publishedWebRelease(channel, getJson, now = () => performa
227
234
  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
235
  throw new ReleaseUnavailable('Web release references are mutable or inconsistent');
229
236
  }
237
+ const npm = component.npm;
238
+ if (npm !== undefined && npm !== `${WEB_NPM_PACKAGE}@${String(component.version)}`)
239
+ throw new ReleaseUnavailable('Web npm release identity is inconsistent');
230
240
  const contracts = dict(m.contracts);
231
241
  const supported = {};
232
242
  for (const name of ['appWeb', 'webRelay']) {
@@ -245,6 +255,7 @@ export async function publishedWebRelease(channel, getJson, now = () => performa
245
255
  sourceRevision: source,
246
256
  releaseSet: source,
247
257
  image: component.image,
258
+ npm: typeof npm === 'string' ? npm : null,
248
259
  manifestReleaseSet: str(m.releaseSet),
249
260
  runtime: { tag: componentTag, version: component.version, sourceRevision: source, releaseSet: source, contracts: supported, compatibleAgent: agent },
250
261
  release_url: `${REPOSITORY_URL}/releases/tag/${tag}`,
@@ -283,6 +294,48 @@ export async function checkoutRoot(webRoot, git) {
283
294
  return null;
284
295
  }
285
296
  }
297
+ /** A real global npm install of this package; local, npx, linked and container trees fail closed. */
298
+ export async function npmInstallInfo(webRoot, npm = runPackageNpm, container = existsSync('/.within_container') || existsSync('/.dockerenv') || existsSync('/run/.containerenv')) {
299
+ if (container || !webRoot || !existsSync(webRoot))
300
+ return null;
301
+ let metadata;
302
+ try {
303
+ metadata = dict(JSON.parse(readFileSync(join(webRoot, 'package.json'), 'utf8')));
304
+ }
305
+ catch {
306
+ return null;
307
+ }
308
+ if (metadata.name !== WEB_NPM_PACKAGE || typeof metadata.version !== 'string' || !new RegExp(`^${VERSION}$`).test(metadata.version))
309
+ return null;
310
+ const global = await npm(['root', '--global'], webRoot, 10_000);
311
+ if (!global.ok || !isAbsolute(global.out))
312
+ return null;
313
+ const expected = join(resolve(global.out), '@maudecode', 'talaria-web');
314
+ try {
315
+ if (lstatSync(expected).isSymbolicLink() || realpathSync(expected) !== realpathSync(webRoot))
316
+ return null;
317
+ }
318
+ catch {
319
+ return null;
320
+ }
321
+ return { packageRoot: realpathSync(webRoot), version: metadata.version };
322
+ }
323
+ function compareVersions(installed, latest) {
324
+ const a = installed.split('.').map(Number);
325
+ const b = latest.split('.').map(Number);
326
+ for (let i = 0; i < 3; i += 1)
327
+ if (a[i] !== b[i])
328
+ return Math.sign((a[i] ?? 0) - (b[i] ?? 0));
329
+ return 0;
330
+ }
331
+ function diskRelease(webRoot) {
332
+ try {
333
+ return validateReleaseInfo(JSON.parse(readFileSync(join(webRoot, '_release.json'), 'utf8')), webRoot);
334
+ }
335
+ catch {
336
+ return null;
337
+ }
338
+ }
286
339
  /** Python `verify_release_source`: the stamp Web expects for `release`, computed from immutable blobs at its source revision. */
287
340
  export async function verifyReleaseSource(root, release, git) {
288
341
  const files = {};
@@ -401,8 +454,9 @@ async function checkMainUpdate(root, result, git, id) {
401
454
  return result;
402
455
  }
403
456
  /** Python `check_web_update`. */
404
- export async function checkWebUpdate(webRoot, currentVersion, channel, git, getJson, id) {
457
+ export async function checkWebUpdate(webRoot, currentVersion, channel, git, getJson, id, npmRun = runPackageNpm) {
405
458
  const root = await checkoutRoot(webRoot, git);
459
+ const npmInstall = root === null ? await npmInstallInfo(webRoot, npmRun) : null;
406
460
  const result = { name: 'webui', channel, repo_url: REPOSITORY_URL, current_version: currentVersion, behind: null, no_git: root === null };
407
461
  if (channel === 'experimental' && root !== null)
408
462
  return checkMainUpdate(root, result, git, id);
@@ -417,6 +471,18 @@ export async function checkWebUpdate(webRoot, currentVersion, channel, git, getJ
417
471
  }
418
472
  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
473
  if (root === null) {
474
+ if (npmInstall && channel === 'stable' && release.npm && !str(id.release().tag).startsWith('web-exp-v')) {
475
+ const comparison = compareVersions(npmInstall.version, release.version);
476
+ if (comparison > 0)
477
+ 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.' };
478
+ if (comparison < 0)
479
+ 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.` };
480
+ const installed = diskRelease(npmInstall.packageRoot);
481
+ if (!same(installed, release.runtime))
482
+ 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.' };
483
+ const metadataRepair = !same(id.release(), release.runtime);
484
+ 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.' } : {}) };
485
+ }
420
486
  const current = id.release().sourceRevision ?? null;
421
487
  const version = new RegExp(`^${channel === 'experimental' ? 'web-exp-v' : 'web-v'}${VERSION}$`).exec(currentVersion);
422
488
  let behind = version && current === release.sourceRevision ? 0 : null;
@@ -470,11 +536,86 @@ export async function checkWebUpdate(webRoot, currentVersion, channel, git, getJ
470
536
  Object.assign(result, { manual_update: true, message: 'Commit or remove local changes before updating; Web updates never discard them.' });
471
537
  return result;
472
538
  }
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.' };
544
+ const installed = await npmInstallInfo(webRoot, npmRun);
545
+ if (!installed)
546
+ 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);
550
+ }
551
+ catch {
552
+ return { ok: false, message: 'Cannot resolve a completed Talaria release. Check private-repository read access.' };
553
+ }
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}.` };
567
+ }
568
+ if (!canApply())
569
+ return { ok: false, message: 'Web update deferred because its settings or lifecycle changed.' };
570
+ // Global npm installs keep dependencies inside the package. Build the replacement beside the old package so
571
+ // install/verification failures cannot damage the running installation and the final rename stays on one volume.
572
+ const staging = mkdtempSync(join(dirname(installed.packageRoot), '.talaria-update-'));
573
+ const candidate = join(staging, 'lib/node_modules/@maudecode/talaria-web');
574
+ const backup = join(staging, 'previous');
575
+ let keepBackup = false;
576
+ try {
577
+ const update = await npmRun(['install', '--global', '--prefix', staging, '--no-audit', '--no-fund', release.npm], staging, WEB_BUILD_TIMEOUT_MS);
578
+ if (!update.ok)
579
+ return { ok: false, install_failed: true, target: 'webui', channel, message: `npm update failed: ${sanitizeGitDiagnostic(update.out, 1000) || 'unknown error'}` };
580
+ const metadata = dict(JSON.parse(readFileSync(join(candidate, 'package.json'), 'utf8')));
581
+ const bins = dict(metadata.bin);
582
+ if (metadata.name !== WEB_NPM_PACKAGE || metadata.version !== release.version || !same(diskRelease(candidate), release.runtime)
583
+ || !['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.' };
585
+ }
586
+ const current = await npmInstallInfo(webRoot, npmRun);
587
+ if (!canApply() || current?.packageRoot !== installed.packageRoot || current.version !== installed.version)
588
+ return { ok: false, message: 'The npm installation or update settings changed; retry the update.' };
589
+ renameSync(installed.packageRoot, backup);
590
+ try {
591
+ renameSync(candidate, installed.packageRoot);
592
+ }
593
+ catch (error) {
594
+ // Never delete the only surviving copy if a filesystem error also prevents restoration.
595
+ keepBackup = true;
596
+ renameSync(backup, installed.packageRoot);
597
+ keepBackup = false;
598
+ throw error;
599
+ }
600
+ }
601
+ catch (error) {
602
+ return { ok: false, install_failed: true, message: `npm update failed: ${sanitizeGitDiagnostic(error.message, 1000)}${keepBackup ? `; previous package retained at ${backup}` : ''}` };
603
+ }
604
+ finally {
605
+ if (!keepBackup) {
606
+ try {
607
+ rmSync(staging, { recursive: true, force: true });
608
+ }
609
+ catch { /* a leftover staging directory must not prevent restart after a verified switch */ }
610
+ }
611
+ }
612
+ return { ok: true, target: 'webui', channel, sourceRevision: release.sourceRevision, npm: release.npm, message: `Updated Talaria Web to npm release ${release.version}.` };
613
+ }
473
614
  /** 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) {
615
+ export async function applyWebUpdate(webRoot, channel, git, getJson, id, build = runNpm, npmRun = runPackageNpm, canApply = () => true) {
475
616
  const root = await checkoutRoot(webRoot, git);
476
617
  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.' };
618
+ return applyNpmWebUpdate(webRoot, channel, getJson, id, npmRun, canApply);
478
619
  const status = await git(['status', '--porcelain', '--untracked-files=all'], root);
479
620
  if (!status.ok || status.out)
480
621
  return { ok: false, dirty: true, message: 'Web update refused: the checkout must be clean, including untracked files.' };
@@ -552,6 +693,8 @@ export async function applyWebUpdate(webRoot, channel, git, getJson, id, build =
552
693
  const clean = await git(['status', '--porcelain', '--untracked-files=all'], root);
553
694
  if (!again.ok || again.out !== head || !clean.ok || clean.out)
554
695
  return { ok: false, message: 'The checkout changed during the update; retry after it is clean.' };
696
+ if (!canApply())
697
+ return { ok: false, message: 'Web update deferred because its settings or lifecycle changed.' };
555
698
  if (head !== source) {
556
699
  const merged = await git(['merge', '--ff-only', '--no-stat', '--no-overwrite-ignore', source], root, 30_000);
557
700
  const actual = await git(['rev-parse', 'HEAD'], root);
@@ -587,105 +730,85 @@ export async function applyWebUpdate(webRoot, channel, git, getJson, id, build =
587
730
  }
588
731
  return { ok: true, target: 'webui', channel, sourceRevision: source, message: `Updated Talaria Web to ${tag}.` };
589
732
  }
590
- // ── Agent checkout (external project, `v*` tags, stable only) ────────────────
733
+ // ── Agent checkout: independent Stable releases or Experimental default branch ──
591
734
  async function releaseTags(path, git) {
592
735
  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) : [];
736
+ return out.ok ? out.out.split('\n').map((l) => l.trim()).filter((tag) => /^v\d+\.\d+\.\d+$/.test(tag)) : [];
594
737
  }
595
738
  async function currentReleaseTag(path, git) {
596
- const out = await git(['describe', '--tags', '--abbrev=0', '--match', AGENT_TAG_GLOB], path);
739
+ const out = await git(['describe', '--tags', '--exact-match', '--match', AGENT_TAG_GLOB, 'HEAD'], path);
597
740
  return out.ok && out.out ? out.out : null;
598
741
  }
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;
605
- }
606
742
  const headContainsRef = async (path, ref, git) => (await git(['merge-base', '--is-ancestor', ref, 'HEAD'], path)).ok;
607
743
  const canFastForwardTo = async (path, ref, git) => (await git(['merge-base', '--is-ancestor', 'HEAD', ref], path)).ok;
608
744
  async function detectDefaultBranch(path, git) {
745
+ const refreshed = await git(['remote', 'set-head', 'origin', '--auto'], path, 15_000);
746
+ if (!refreshed.ok)
747
+ throw new Error('Agent default branch could not be refreshed from origin');
609
748
  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';
749
+ const prefix = 'refs/remotes/origin/';
750
+ if (out.ok && out.out.startsWith(prefix) && (await git(['rev-parse', '--verify', `${out.out}^{commit}`], path)).ok)
751
+ return out.out.slice(prefix.length);
752
+ throw new Error('Agent default branch is unavailable');
616
753
  }
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)}`;
754
+ async function agentTarget(path, git, channel) {
755
+ const ref = channel === 'experimental' ? `origin/${await detectDefaultBranch(path, git)}` : (await releaseTags(path, git))[0];
756
+ if (!ref)
757
+ throw new Error('No stable Agent release is available');
758
+ const current = await git(['rev-parse', 'HEAD'], path);
759
+ const latest = await git(['rev-parse', `${ref}^{commit}`], path);
760
+ if (!current.ok || !latest.ok || !SHA.test(current.out) || !SHA.test(latest.out))
761
+ throw new Error('Agent Git identity is unavailable');
762
+ const count = await git(['rev-list', '--count', `${current.out}..${latest.out}`], path);
763
+ const behind = count.ok && /^\d+$/.test(count.out) ? Number.parseInt(count.out, 10) : null;
764
+ const manual = current.out !== latest.out && !(await canFastForwardTo(path, latest.out, git));
765
+ const remote = normalizeRemoteUrl((await git(['remote', 'get-url', 'origin'], path)).out);
766
+ return {
767
+ name: 'agent', channel, branch: ref, current_sha: current.out, latest_sha: latest.out,
768
+ current_version: await currentReleaseTag(path, git) ?? current.out.slice(0, 12), latest_version: ref,
769
+ behind, release_based: channel === 'stable', repo_url: remote, compare_url: compareUrl(remote, current.out, latest.out),
770
+ ...(behind === null ? { error: 'Agent commit count is unavailable' } : {}),
771
+ ...(manual ? { manual_update: true, message: 'Agent checkout is ahead of or divergent from this channel; refusing to rewind it.' } : {}),
772
+ };
620
773
  }
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);
774
+ function agentWarning(info, policy) {
775
+ return { candidate_revision: info.latest_sha, supported_revision: policy.supportedRevision, supported_version: policy.supportedVersion,
776
+ unsupported: info.latest_sha !== policy.supportedRevision };
633
777
  }
634
- async function checkAgentRelease(path, git) {
635
- const tags = await releaseTags(path, git);
636
- const latest = tags[0];
637
- if (!latest)
638
- 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)))
778
+ function confirmAgent(info, policy) {
779
+ // Ordinary apply handles its non-mutating no-op; force still cleans files at the same revision.
780
+ if (!policy)
648
781
  return null;
649
- if (behind > 0 && (await headContainsRef(path, latest, git)))
782
+ if (info.latest_sha === policy.supportedRevision)
783
+ return policy.confirmedRevision && policy.confirmedRevision !== info.latest_sha
784
+ ? { ok: false, message: 'The Agent update target changed. Check for updates and try again.' } : null;
785
+ if (policy.confirmedRevision === info.latest_sha)
650
786
  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) };
787
+ return { ok: false, target: 'agent', agent_channel: info.channel, confirmation_required: true, ...agentWarning(info, policy),
788
+ message: 'This Agent version is not officially supported by Talaria and may cause issues.' };
669
789
  }
670
790
  async function isDirty(path, git) {
671
791
  const out = await git(['diff-index', '--quiet', 'HEAD', '--'], path, 1000);
672
792
  return !out.ok && (out.out === 'git exited with status 1' || !out.out || out.out.startsWith('git exited with status '));
673
793
  }
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) {
794
+ /** Fetch and compare the independently selected Agent channel. */
795
+ export async function checkAgentUpdate(path, git, channel = DEFAULT_CHANNEL) {
676
796
  if (!path || !existsSync(join(path, '.git')))
677
797
  return { name: 'agent', behind: null, no_git: true };
678
798
  const fetched = await git(['fetch', 'origin', '--tags', '--force'], path, 15_000);
679
799
  if (!fetched.ok) {
680
800
  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) };
801
+ return { name: 'agent', channel, behind: null, error: message, stale_check: true, dirty: await isDirty(path, git) };
802
+ }
803
+ try {
804
+ return { ...await agentTarget(path, git, channel), dirty: await isDirty(path, git) };
805
+ }
806
+ catch (error) {
807
+ return { name: 'agent', channel, behind: null, error: error.message };
683
808
  }
684
- const info = (await checkAgentRelease(path, git)) ?? { ...(await checkAgentBranch(path, git)), channel: DEFAULT_CHANNEL };
685
- return { ...info, dirty: await isDirty(path, git) };
686
809
  }
687
- /** Python `_apply_update_inner` (agent branch): fetch, stash, `pull --ff-only`, pop. */
688
- export async function applyAgentUpdate(path, git) {
810
+ /** Fetch, confirm the immutable Agent target, stash, fast-forward, and pop. */
811
+ export async function applyAgentUpdate(path, git, channel = DEFAULT_CHANNEL, policy) {
689
812
  if (!path || !existsSync(join(path, '.git')))
690
813
  return { ok: false, message: 'Not a git repository' };
691
814
  const fetched = await git(['fetch', 'origin', '--quiet', '--tags', '--force'], path, 15_000);
@@ -694,7 +817,22 @@ export async function applyAgentUpdate(path, git) {
694
817
  return { ok: false, message: `Fetch failed due to a repository lock: ${fetched.out.trim()}`, lock_conflict: true };
695
818
  return { ok: false, message: fetchFailureMessage(fetched.out, 'Could not reach the remote repository. Check your internet connection and try again.') };
696
819
  }
697
- const ref = await selectAgentCompareRef(path, git);
820
+ let info;
821
+ try {
822
+ info = await agentTarget(path, git, channel);
823
+ }
824
+ catch (error) {
825
+ return { ok: false, message: error.message };
826
+ }
827
+ if (info.current_sha === info.latest_sha)
828
+ return { ok: true, up_to_date: true, target: 'agent', message: 'Agent is up to date.' };
829
+ if (info.manual_update || info.error)
830
+ return { ...info, ok: false, message: info.message ?? info.error };
831
+ const warning = confirmAgent(info, policy);
832
+ if (warning)
833
+ return warning;
834
+ const ref = str(info.branch);
835
+ const revision = str(info.latest_sha);
698
836
  const status = await git(['status', '--porcelain', '--untracked-files=no'], path);
699
837
  if (!status.ok) {
700
838
  if (isGitLockError(status.out))
@@ -710,9 +848,8 @@ export async function applyAgentUpdate(path, git) {
710
848
  return { ok: false, message: 'Failed to stash local changes' };
711
849
  stashed = true;
712
850
  }
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);
851
+ // Merge the immutable commit that was checked/acknowledged, never re-fetch a moving ref here.
852
+ const pulled = await git(['merge', '--ff-only', revision], path, 30_000);
716
853
  if (!pulled.ok) {
717
854
  let note = '';
718
855
  if (stashed)
@@ -739,21 +876,32 @@ async function restoreStash(path, git, pullOut) {
739
876
  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
877
  }
741
878
  /** 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) {
879
+ export async function forceAgentUpdate(path, git, log, channel = DEFAULT_CHANNEL, policy) {
743
880
  if (!path || !existsSync(join(path, '.git')))
744
881
  return { ok: false, message: 'Not a git repository' };
745
882
  const fetched = await git(['fetch', 'origin', '--quiet', '--tags', '--force'], path, 15_000);
746
883
  if (!fetched.ok)
747
884
  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 };
885
+ let info;
886
+ try {
887
+ info = await agentTarget(path, git, channel);
888
+ }
889
+ catch (error) {
890
+ return { ok: false, message: error.message };
751
891
  }
892
+ const ref = str(info.branch);
893
+ const revision = str(info.latest_sha);
894
+ if ((await headContainsRef(path, revision, git)) && !(await canFastForwardTo(path, revision, git))) {
895
+ 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 };
896
+ }
897
+ const warning = confirmAgent(info, policy);
898
+ if (warning)
899
+ return warning;
752
900
  await git(['checkout', '.'], path);
753
901
  const cleaned = await git(['clean', '-fd'], path);
754
902
  if (!cleaned.ok)
755
903
  log(`[updates] force update: git clean -fd failed (continuing to reset --hard): ${cleaned.out}`);
756
- if (!(await git(['reset', '--hard', ref], path)).ok)
904
+ if (!(await git(['reset', '--hard', revision], path)).ok)
757
905
  return { ok: false, message: `Force reset to ${ref} failed` };
758
906
  return { ok: true, message: `agent force-updated to ${ref}`, target: 'agent', ref };
759
907
  }
@@ -907,9 +1055,15 @@ function summaryPrompt(details) {
907
1055
  }
908
1056
  export class UpdateService {
909
1057
  deps;
910
- cache = { webui: null, agent: null, checked_at: 0, include_agent: true, channel: DEFAULT_CHANNEL };
1058
+ cache = { webui: null, agent: null, checked_at: 0, include_agent: true, channel: DEFAULT_CHANNEL, agent_channel: DEFAULT_CHANNEL };
911
1059
  checking = null;
1060
+ checkingKey = null;
912
1061
  applying = false;
1062
+ autoTimer = null;
1063
+ autoStarted = false;
1064
+ lifecycle = 0;
1065
+ autoRunning = false;
1066
+ autoRestartScheduled = false;
913
1067
  summaries = new Map();
914
1068
  git;
915
1069
  now;
@@ -920,13 +1074,24 @@ export class UpdateService {
920
1074
  this.now = deps.now ?? (() => Date.now() / 1000);
921
1075
  this.sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
922
1076
  }
1077
+ agentChannel() { return this.deps.agentChannel?.() ?? DEFAULT_CHANNEL; }
1078
+ agentPolicy(confirmedRevision) {
1079
+ const supported = dict(this.deps.identity.release().compatibleAgent);
1080
+ return { supportedRevision: str(supported.sourceRevision), supportedVersion: str(supported.version), ...(confirmedRevision ? { confirmedRevision } : {}) };
1081
+ }
923
1082
  /** Python `cached_update_status`: no network, no git mutations. */
924
- cachedStatus(includeAgent = this.deps.includeAgent(), channel = this.deps.channel()) {
1083
+ cachedStatus(includeAgent = this.deps.includeAgent(), channel = this.deps.channel(), agentChannel = this.agentChannel()) {
925
1084
  const cached = { ...this.cache };
926
1085
  if (cached.channel !== channel) {
927
1086
  cached.channel = channel;
1087
+ cached.webui = null;
928
1088
  cached.stale_channel = true;
929
1089
  }
1090
+ if (cached.agent_channel !== agentChannel) {
1091
+ cached.agent_channel = agentChannel;
1092
+ cached.agent = null;
1093
+ cached.stale_agent_channel = true;
1094
+ }
930
1095
  if (cached.include_agent !== includeAgent) {
931
1096
  cached.include_agent = includeAgent;
932
1097
  if (!includeAgent)
@@ -935,27 +1100,93 @@ export class UpdateService {
935
1100
  cached.cached = true;
936
1101
  return cached;
937
1102
  }
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;
1103
+ /** 30 min cache keyed on both channels + include_agent; one in-flight check per key. */
1104
+ async check(force = false, includeAgent = this.deps.includeAgent(), channel = this.deps.channel(), agentChannel = this.agentChannel()) {
1105
+ const key = `${channel}:${agentChannel}:${String(includeAgent)}`;
1106
+ if (this.checking) {
1107
+ if (this.checkingKey === key)
1108
+ return this.checking;
1109
+ await this.checking;
1110
+ return this.check(force, includeAgent, channel, agentChannel);
1111
+ }
1112
+ if (this.applying)
1113
+ return this.cachedStatus(includeAgent, channel, agentChannel);
1114
+ const matches = this.cache.include_agent === includeAgent && this.cache.channel === channel && this.cache.agent_channel === agentChannel;
941
1115
  if (!force && matches && this.now() - Number(this.cache.checked_at) < CACHE_TTL_S)
942
1116
  return { ...this.cache };
943
- if (this.checking && matches)
944
- return this.checking;
1117
+ this.checkingKey = key;
945
1118
  this.checking = (async () => {
946
1119
  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 });
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();
1122
+ if (agent.latest_sha)
1123
+ Object.assign(agent, agentWarning(agent, this.agentPolicy()));
1124
+ Object.assign(this.cache, { webui, agent, checked_at: this.now(), include_agent: includeAgent, channel, agent_channel: agentChannel });
951
1125
  return { ...this.cache };
952
1126
  }
953
1127
  finally {
954
1128
  this.checking = null;
1129
+ this.checkingKey = null;
955
1130
  }
956
1131
  })();
957
1132
  return this.checking;
958
1133
  }
1134
+ startAutoApply(intervalMs = AUTO_UPDATE_INTERVAL_MS) {
1135
+ if (this.autoStarted || this.autoRestartScheduled)
1136
+ return;
1137
+ this.autoStarted = true;
1138
+ const lifecycle = this.lifecycle;
1139
+ const schedule = (delay) => {
1140
+ this.autoTimer = setTimeout(() => {
1141
+ this.autoTimer = null;
1142
+ void this.autoApplyOnce().finally(() => { if (this.autoStarted && lifecycle === this.lifecycle && !this.autoRestartScheduled)
1143
+ schedule(intervalMs); });
1144
+ }, delay);
1145
+ this.autoTimer.unref();
1146
+ };
1147
+ schedule(0);
1148
+ }
1149
+ stopAutoApply() {
1150
+ this.autoStarted = false;
1151
+ this.lifecycle += 1;
1152
+ if (this.autoTimer)
1153
+ clearTimeout(this.autoTimer);
1154
+ this.autoTimer = null;
1155
+ }
1156
+ /** Shared turn admission stays closed from update start through the supervisor restart. */
1157
+ blocksNewWork() { return this.applying || this.autoRestartScheduled; }
1158
+ async autoApplyOnce() {
1159
+ if (this.autoRunning || this.applying || this.autoRestartScheduled || !(this.deps.checkEnabled?.() ?? this.deps.autoApply?.()))
1160
+ return null;
1161
+ this.autoRunning = true;
1162
+ const lifecycle = this.lifecycle;
1163
+ try {
1164
+ const channel = this.deps.channel();
1165
+ const stillEnabled = () => lifecycle === this.lifecycle && Boolean(this.deps.autoApply?.()) && (this.deps.checkEnabled?.() ?? true) && channel === this.deps.channel();
1166
+ const checked = await this.check(true, this.deps.includeAgent(), channel);
1167
+ const web = dict(checked.webui);
1168
+ if (!stillEnabled())
1169
+ return web;
1170
+ if (web.error || web.manual_update) {
1171
+ this.deps.log(`[updates] automatic Web update unavailable: ${str(web.error || web.message)}`);
1172
+ return web;
1173
+ }
1174
+ if (!(Number(web.behind) > 0 || web.metadata_repair === true))
1175
+ return web;
1176
+ const result = await this.apply('webui', channel, stillEnabled);
1177
+ this.deps.log(`[updates] automatic Web update: ${str(result.message || (result.ok ? 'applied' : 'failed'))}`);
1178
+ if (!result.ok)
1179
+ this.cache.webui = { ...web, message: result.message, error: result.restart_blocked ? undefined : result.message };
1180
+ return result;
1181
+ }
1182
+ catch (error) {
1183
+ this.deps.log(`[updates] automatic Web update failed: ${error.message}`);
1184
+ return { ok: false, error: error.message };
1185
+ }
1186
+ finally {
1187
+ this.autoRunning = false;
1188
+ }
1189
+ }
959
1190
  blockedResponse(target) {
960
1191
  const b = this.deps.blockers();
961
1192
  if (!b.restart_blocked)
@@ -965,7 +1196,11 @@ export class UpdateService {
965
1196
  parts.push(`${String(b.active_streams)} active chat stream${b.active_streams === 1 ? '' : 's'}`);
966
1197
  if (b.active_runs)
967
1198
  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 };
1199
+ if (b.active_terminals)
1200
+ parts.push(`${String(b.active_terminals)} open terminal${b.active_terminals === 1 ? '' : 's'}`);
1201
+ if (b.active_cron_jobs)
1202
+ parts.push(`${String(b.active_cron_jobs)} active cron job${b.active_cron_jobs === 1 ? '' : 's'}`);
1203
+ 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
1204
  }
970
1205
  async locked(fn) {
971
1206
  if (this.applying)
@@ -979,17 +1214,28 @@ export class UpdateService {
979
1214
  }
980
1215
  }
981
1216
  /** Python `apply_update`. */
982
- apply(target, channel) {
1217
+ async apply(target, channel, canApply = () => true, agentOptions = {}) {
1218
+ if (this.checking)
1219
+ await this.checking;
1220
+ if (this.autoRestartScheduled)
1221
+ return { ok: true, restart_scheduled: true, message: 'A Web restart is already scheduled.' };
1222
+ if (!canApply())
1223
+ return { ok: false, message: 'Web update settings changed; update deferred.' };
983
1224
  const blocked = this.blockedResponse(target);
984
1225
  if (blocked)
985
- return Promise.resolve(blocked);
986
- return this.locked(() => this.applyInner(target, channel ?? this.deps.channel()));
1226
+ return blocked;
1227
+ const lifecycle = this.lifecycle;
1228
+ return this.locked(() => this.applyInner(target, channel ?? this.deps.channel(), () => lifecycle === this.lifecycle && canApply(), agentOptions));
987
1229
  }
988
- async applyInner(target, channel) {
1230
+ async applyInner(target, channel, canApply = () => true, agentOptions = {}) {
989
1231
  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) {
1232
+ 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);
1234
+ // Settings can cancel before mutation, but a committed replacement must finish restarting unless shutting down.
1235
+ if (result.ok && !result.up_to_date && lifecycle === this.lifecycle) {
992
1236
  this.cache.checked_at = 0;
1237
+ this.autoRestartScheduled = true;
1238
+ this.stopAutoApply();
993
1239
  this.deps.scheduleRestart();
994
1240
  result.restart_scheduled = true;
995
1241
  }
@@ -997,8 +1243,8 @@ export class UpdateService {
997
1243
  }
998
1244
  if (target !== 'agent')
999
1245
  return { ok: false, message: `Unknown target: ${target}` };
1000
- const result = await applyAgentUpdate(this.deps.agentDir(), this.git);
1001
- if (!result.ok)
1246
+ const result = await applyAgentUpdate(this.deps.agentDir(), this.git, agentOptions.agentChannel ?? this.agentChannel(), this.agentPolicy(agentOptions.confirmedRevision));
1247
+ if (!result.ok || result.up_to_date)
1002
1248
  return result;
1003
1249
  return this.finishAgent(result);
1004
1250
  }
@@ -1035,7 +1281,9 @@ export class UpdateService {
1035
1281
  return [false, { ...annotated, message: `${str(first.message) || 'Restart failed'}; recovery retry did not complete: ${str(retry.message) || 'retry did not complete'}` }];
1036
1282
  }
1037
1283
  /** Python `apply_force_update`: Web keeps its clean-only policy; the Agent resets hard. */
1038
- force(target, channel) {
1284
+ async force(target, channel, agentOptions = {}) {
1285
+ if (this.checking)
1286
+ await this.checking;
1039
1287
  if (target === 'webui')
1040
1288
  return this.apply(target, channel);
1041
1289
  const blocked = this.blockedResponse(target);
@@ -1044,12 +1292,12 @@ export class UpdateService {
1044
1292
  return this.locked(async () => {
1045
1293
  if (target !== 'agent')
1046
1294
  return { ok: false, message: `Unknown target: ${target}` };
1047
- const result = await forceAgentUpdate(this.deps.agentDir(), this.git, this.deps.log);
1295
+ const result = await forceAgentUpdate(this.deps.agentDir(), this.git, this.deps.log, agentOptions.agentChannel ?? this.agentChannel(), this.agentPolicy(agentOptions.confirmedRevision));
1048
1296
  return result.ok ? this.finishAgent(result) : result;
1049
1297
  });
1050
1298
  }
1051
1299
  /** Python `apply_clear_lock`: never removes a lock; Web retries the clean path, the Agent gets the manual command. */
1052
- clearLock(target) {
1300
+ clearLock(target, agentOptions = {}) {
1053
1301
  if (target === 'webui')
1054
1302
  return this.apply(target).then((r) => ({ ...r, lock_recovery: { action: 'retry-only' } }));
1055
1303
  const blocked = this.blockedResponse(target);
@@ -1065,7 +1313,7 @@ export class UpdateService {
1065
1313
  const manual = `rm -f ${str(inv.well_known_lock_path)}`;
1066
1314
  if (!inv.well_known_lock_present) {
1067
1315
  this.cache.checked_at = 0;
1068
- const retry = await this.applyInner(target, this.deps.channel());
1316
+ const retry = await this.applyInner(target, this.deps.channel(), () => true, agentOptions);
1069
1317
  return { ...retry, lock_recovery: { action: 'no-lock-found', manual_command: manual, other_locks: inv.other_locks } };
1070
1318
  }
1071
1319
  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 };