@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.
- package/_release.json +20 -0
- package/dist/api/chat-router.d.ts +120 -3
- package/dist/api/chat-router.js +11 -8
- package/dist/api/chat-router.js.map +1 -1
- package/dist/api/router.d.ts +10 -0
- package/dist/api/router.js +2 -1
- package/dist/api/router.js.map +1 -1
- package/dist/api/sessions-router.d.ts +691 -40
- package/dist/api/sessions-router.js +2 -2
- package/dist/api/sessions-router.js.map +1 -1
- package/dist/api/settings-router.d.ts +8 -0
- package/dist/api/settings-router.js +13 -5
- package/dist/api/settings-router.js.map +1 -1
- package/dist/api/sse-routes.js +53 -9
- package/dist/api/sse-routes.js.map +1 -1
- package/dist/api/tools-router.d.ts +420 -0
- package/dist/api/tools-router.js +98 -5
- package/dist/api/tools-router.js.map +1 -1
- package/dist/app.d.ts +1247 -43
- package/dist/auth/oidc.js +2 -0
- package/dist/auth/oidc.js.map +1 -1
- package/dist/auth/store.d.ts +17 -0
- package/dist/auth/store.js +77 -19
- package/dist/auth/store.js.map +1 -1
- package/dist/bin/talaria-web.js +4 -1
- package/dist/bin/talaria-web.js.map +1 -1
- package/dist/cli/supervise.js +2 -1
- package/dist/cli/supervise.js.map +1 -1
- package/dist/fs/atomic.d.ts +8 -1
- package/dist/fs/atomic.js +52 -3
- package/dist/fs/atomic.js.map +1 -1
- package/dist/http/context.d.ts +2 -0
- package/dist/http/context.js.map +1 -1
- package/dist/redact.d.ts +15 -0
- package/dist/redact.js +1393 -9
- package/dist/redact.js.map +1 -1
- package/dist/release.js +3 -1
- package/dist/release.js.map +1 -1
- package/dist/runtime.js +28 -11
- package/dist/runtime.js.map +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +11 -5
- package/dist/server.js.map +1 -1
- package/dist/sessions/anchor.d.ts +59 -5
- package/dist/sessions/anchor.js +373 -32
- package/dist/sessions/anchor.js.map +1 -1
- package/dist/sessions/file-changes.d.ts +15 -0
- package/dist/sessions/file-changes.js +58 -0
- package/dist/sessions/file-changes.js.map +1 -0
- package/dist/sessions/journal.d.ts +6 -1
- package/dist/sessions/journal.js +9 -15
- package/dist/sessions/journal.js.map +1 -1
- package/dist/sessions/list.d.ts +21 -1
- package/dist/sessions/list.js +81 -17
- package/dist/sessions/list.js.map +1 -1
- package/dist/sessions/merge.d.ts +72 -2
- package/dist/sessions/merge.js +326 -13
- package/dist/sessions/merge.js.map +1 -1
- package/dist/sessions/pending.d.ts +11 -2
- package/dist/sessions/pending.js +52 -3
- package/dist/sessions/pending.js.map +1 -1
- package/dist/sessions/service.d.ts +18 -3
- package/dist/sessions/service.js +115 -62
- package/dist/sessions/service.js.map +1 -1
- package/dist/sessions/session.js +1 -1
- package/dist/sessions/session.js.map +1 -1
- package/dist/sessions/state-db.d.ts +5 -0
- package/dist/sessions/state-db.js +34 -0
- package/dist/sessions/state-db.js.map +1 -1
- package/dist/sessions/streams.d.ts +4 -1
- package/dist/sessions/streams.js +3 -0
- package/dist/sessions/streams.js.map +1 -1
- package/dist/sessions/tool-display.d.ts +21 -0
- package/dist/sessions/tool-display.js +113 -0
- package/dist/sessions/tool-display.js.map +1 -0
- package/dist/sessions/turn.d.ts +39 -7
- package/dist/sessions/turn.js +288 -98
- package/dist/sessions/turn.js.map +1 -1
- package/dist/sessions/window.d.ts +1 -0
- package/dist/sessions/window.js +1 -1
- package/dist/sessions/window.js.map +1 -1
- package/dist/settings.js +5 -0
- package/dist/settings.js.map +1 -1
- package/dist/sidecar/client.d.ts +1 -1
- package/dist/sidecar/client.js +15 -4
- package/dist/sidecar/client.js.map +1 -1
- package/dist/sidecar/fake.js +3 -2
- package/dist/sidecar/fake.js.map +1 -1
- package/dist/spa.d.ts +2 -0
- package/dist/spa.js +9 -0
- package/dist/spa.js.map +1 -1
- package/dist/test/harness.d.ts +2 -0
- package/dist/test/harness.js +1 -1
- package/dist/test/harness.js.map +1 -1
- package/dist/tools/crons.js +55 -43
- package/dist/tools/crons.js.map +1 -1
- package/dist/tools/terminal.d.ts +6 -0
- package/dist/tools/terminal.js +24 -3
- package/dist/tools/terminal.js.map +1 -1
- package/dist/tools/update-notifications.d.ts +120 -0
- package/dist/tools/update-notifications.js +387 -0
- package/dist/tools/update-notifications.js.map +1 -0
- package/dist/tools/updates.d.ts +97 -13
- package/dist/tools/updates.js +638 -124
- package/dist/tools/updates.js.map +1 -1
- package/package.json +2 -2
- package/sidecar/agent_dependency.json +4 -3
- package/sidecar/scripts/replay_sidecar.py +17 -0
- package/sidecar/talaria_sidecar/__init__.py +1 -1
- package/sidecar/talaria_sidecar/home.py +51 -19
- package/sidecar/talaria_sidecar/methods/chat.py +113 -4
- package/sidecar/talaria_sidecar/methods/config.py +5 -3
- package/sidecar/talaria_sidecar/runtime.py +3 -9
- package/static/dist/FILES.txt +115 -116
- package/static/dist/assets/AppShell-m4w420jl.js +1 -0
- package/static/dist/assets/ChatPage-cuqcnisv.js +20 -0
- package/static/dist/assets/HubRoute-hqgqtm35.js +2 -0
- package/static/dist/assets/{Markdown-bt9bqd6a.js → Markdown-j2hlu30v.js} +7 -7
- package/static/dist/assets/RightPanel-oid5q557.js +1 -0
- package/static/dist/assets/Select-ot2zr0il.js +1 -0
- package/static/dist/assets/SessionListPanel-o3sgjnrq.js +1 -0
- package/static/dist/assets/SettingsLayout-b0idn649.js +1 -0
- package/static/dist/assets/States-htz1oee5.js +1 -0
- package/static/dist/assets/Toaster-h49pk1hy.js +1 -0
- package/static/dist/assets/_app-h7eaezs7.js +1 -0
- package/static/dist/assets/_app.ext._extensionId-h8oaqzsj.js +1 -0
- package/static/dist/assets/_app.index-xinao3xh.js +1 -0
- package/static/dist/assets/_app.insights-cxplvhkm.js +1 -0
- package/static/dist/assets/_app.kanban-hhli8h0h.js +1 -0
- package/static/dist/assets/_app.logs-f6srwjfl.js +1 -0
- package/static/dist/assets/_app.memory-lntcea85.js +1 -0
- package/static/dist/assets/_app.profiles-lftyp6of.js +1 -0
- package/static/dist/assets/{_app.session._sessionId-kevi4qd2.js → _app.session._sessionId-hhpcqih4.js} +1 -1
- package/static/dist/assets/_app.settings-rs93zpvw.js +1 -0
- package/static/dist/assets/_app.settings._section-nafqsbwm.js +1 -0
- package/static/dist/assets/_app.skills-jzoi1ivd.js +1 -0
- package/static/dist/assets/_app.tasks-ijphsw56.js +2 -0
- package/static/dist/assets/_app.todos-emyh8inv.js +1 -0
- package/static/dist/assets/_app.workspaces-fwr1cum8.js +1 -0
- package/static/dist/assets/{abnfDiagram-VCTEODGH-hvrr25lb.js → abnfDiagram-VCTEODGH-egykbz8h.js} +1 -1
- package/static/dist/assets/architecture-7GRP2DOG-l6y92mbn.js +1 -0
- package/static/dist/assets/{architectureDiagram-5GKGNRK7-b3r0z8sq.js → architectureDiagram-5GKGNRK7-b2e0wk3y.js} +1 -1
- package/static/dist/assets/{blockDiagram-I7D4REHJ-hd3gw0cj.js → blockDiagram-I7D4REHJ-i3cq67dn.js} +1 -1
- package/static/dist/assets/{c4Diagram-7LVT6UL2-jlmbjiji.js → c4Diagram-7LVT6UL2-lg1p1s7a.js} +1 -1
- package/static/dist/assets/channel-gq8cwu71.js +1 -0
- package/static/dist/assets/{chunk-4HAMMTFA-oydbpsin.js → chunk-4HAMMTFA-i0s1s7zi.js} +1 -1
- package/static/dist/assets/{chunk-75Z2AOVW-j633otj1.js → chunk-75Z2AOVW-kwinpapo.js} +1 -1
- package/static/dist/assets/{chunk-DU6HZSFF-ods2l027.js → chunk-DU6HZSFF-hb8qrbc3.js} +3 -3
- package/static/dist/assets/{chunk-F27PBJKO-ln4n2cwt.js → chunk-F27PBJKO-dy2tqtcs.js} +1 -1
- package/static/dist/assets/{chunk-GMAD6QVW-fpz133eh.js → chunk-GMAD6QVW-fqox5yvt.js} +1 -1
- package/static/dist/assets/{chunk-GVQU2GXP-abs9ixiy.js → chunk-GVQU2GXP-bo3x6ze4.js} +1 -1
- package/static/dist/assets/{chunk-IMKFNOWR-j9vukdy1.js → chunk-IMKFNOWR-nxs6rrcq.js} +1 -1
- package/static/dist/assets/{chunk-L3NEJ4N5-cfyw87r3.js → chunk-L3NEJ4N5-f9yeq1fi.js} +1 -1
- package/static/dist/assets/{chunk-OSK3NFVY-baja78wr.js → chunk-OSK3NFVY-c7df44qv.js} +1 -1
- package/static/dist/assets/{chunk-P2QGCYS3-g66xby8h.js → chunk-P2QGCYS3-eyjzf5o5.js} +1 -1
- package/static/dist/assets/{chunk-POPQ4Y6H-osqu864t.js → chunk-POPQ4Y6H-iy3hp9rc.js} +1 -1
- package/static/dist/assets/{chunk-PWAF6VOD-egejasv1.js → chunk-PWAF6VOD-co2yoefz.js} +1 -1
- package/static/dist/assets/{chunk-SHT3W25Y-l014h8zz.js → chunk-SHT3W25Y-obzqmn9k.js} +1 -1
- package/static/dist/assets/{chunk-SVP7TREG-n3z10bp3.js → chunk-SVP7TREG-hqwdo6vt.js} +1 -1
- package/static/dist/assets/{chunk-TICWLB2K-bzozkjc0.js → chunk-TICWLB2K-l8t1mdfd.js} +1 -1
- package/static/dist/assets/classDiagram-ZZMXUADV-fg9sc5r8.js +1 -0
- package/static/dist/assets/classDiagram-v2-VYDZK3BY-fg9sc5r8.js +1 -0
- package/static/dist/assets/{copied-cjsttec1.js → copied-hurn8vn4.js} +1 -1
- package/static/dist/assets/{cynefin-OW5HDTMX-elpt7h1j.js → cynefin-OW5HDTMX-ngx2ihay.js} +1 -1
- package/static/dist/assets/{cynefinDiagram-5FMLGOSQ-5mzl5pi2.js → cynefinDiagram-5FMLGOSQ-fiyooys3.js} +1 -1
- package/static/dist/assets/{dagre-GXQ25YYZ-ihwlhmfl.js → dagre-GXQ25YYZ-fctryel6.js} +1 -1
- package/static/dist/assets/{diagram-S7CK7UJ4-4z90agaz.js → diagram-S7CK7UJ4-m27jopok.js} +1 -1
- package/static/dist/assets/{diagram-UQ7AKVKN-eifw0lg7.js → diagram-UQ7AKVKN-ojgkbmp4.js} +1 -1
- package/static/dist/assets/{diagram-VSXAHHWV-ksh3sjqv.js → diagram-VSXAHHWV-gfafpgyj.js} +1 -1
- package/static/dist/assets/{diagram-VX7I27RA-ett72f71.js → diagram-VX7I27RA-daobicg4.js} +1 -1
- package/static/dist/assets/{diagram-Z3DM3KII-l8scp8nu.js → diagram-Z3DM3KII-mi475zvk.js} +1 -1
- package/static/dist/assets/{ebnfDiagram-PWID7BFC-f0pxcmit.js → ebnfDiagram-PWID7BFC-daf4zvsy.js} +1 -1
- package/static/dist/assets/endpoints-cyjhnrbn.js +64 -0
- package/static/dist/assets/{erDiagram-RLTQ6QDP-f47m5xh0.js → erDiagram-RLTQ6QDP-gxfhxvfp.js} +1 -1
- package/static/dist/assets/eventmodeling-NTZA5JFV-manga0bq.js +1 -0
- package/static/dist/assets/flowDiagram-HODETNUW-bpt13uny.js +1 -0
- package/static/dist/assets/{ganttDiagram-EL5Y4UJY-bmktjdi0.js → ganttDiagram-EL5Y4UJY-k3208e0p.js} +1 -1
- package/static/dist/assets/{gitGraph-4MIJSDKK-j2ueg258.js → gitGraph-4MIJSDKK-gjuhj4zk.js} +1 -1
- package/static/dist/assets/{gitGraphDiagram-WWUBYQGX-ms6586v9.js → gitGraphDiagram-WWUBYQGX-jglxvux1.js} +1 -1
- package/static/dist/assets/{highlighted-body-KPVGNVTW-ev52d2si.js → highlighted-body-KPVGNVTW-gh24xc9c.js} +1 -1
- package/static/dist/assets/index-gsg78cb4.css +2 -0
- package/static/dist/assets/{index-hywfjw88.js → index-jb47los5.js} +4 -4
- package/static/dist/assets/{info-A6RAGUB7-j717vyt7.js → info-A6RAGUB7-huf0xaoq.js} +1 -1
- package/static/dist/assets/{infoDiagram-27XIBGKW-dsbpib36.js → infoDiagram-27XIBGKW-jk3n96ix.js} +1 -1
- package/static/dist/assets/{ishikawaDiagram-5VMMS53U-n0sz3i2d.js → ishikawaDiagram-5VMMS53U-o95uo5dx.js} +1 -1
- package/static/dist/assets/{journeyDiagram-3NMN7TZE-mcxz1mme.js → journeyDiagram-3NMN7TZE-h5ye2l70.js} +1 -1
- package/static/dist/assets/{kanban-definition-UXKFOSKX-n6hq2lyc.js → kanban-definition-UXKFOSKX-lgrpk5yi.js} +1 -1
- package/static/dist/assets/{lazyRouteComponent-faquioun.js → lazyRouteComponent-owywngyt.js} +1 -1
- package/static/dist/assets/{line-h5xq20s4.js → line-cihmc0pn.js} +1 -1
- package/static/dist/assets/login-g063apx6.js +1 -0
- package/static/dist/assets/mermaid-HWGCJPDP-okd1mnvj.js +1 -0
- package/static/dist/assets/{mermaid-parser.core-n9cuu5ue.js → mermaid-parser.core-d1609aga.js} +3 -3
- package/static/dist/assets/{mindmap-definition-YA3MSWOX-gta3u7z4.js → mindmap-definition-YA3MSWOX-lbvtir31.js} +1 -1
- package/static/dist/assets/onboarding-ngw0x0pr.js +1 -0
- package/static/dist/assets/{packet-AYTQ26CC-bpzuq5n6.js → packet-AYTQ26CC-o4cp5z3k.js} +1 -1
- package/static/dist/assets/{pegDiagram-XKGWAZYB-juye13e5.js → pegDiagram-XKGWAZYB-e8xqu1qj.js} +1 -1
- package/static/dist/assets/{pie-WAS4IAKB-odhb8buq.js → pie-WAS4IAKB-hm9icuov.js} +1 -1
- package/static/dist/assets/{pieDiagram-E7YTZNPT-mgb14ksh.js → pieDiagram-E7YTZNPT-hj96x7hn.js} +1 -1
- package/static/dist/assets/{quadrantDiagram-AXDQQJYC-cg36naog.js → quadrantDiagram-AXDQQJYC-80utbxhm.js} +1 -1
- package/static/dist/assets/{radar-RG4KPBEZ-lgzfpnef.js → radar-RG4KPBEZ-k39b6100.js} +1 -1
- package/static/dist/assets/{railroad-74A4TZTK-gmglc3ua.js → railroad-74A4TZTK-ewyqz67j.js} +1 -1
- package/static/dist/assets/railroad-abnf-HS5TGJTU-dx2q0evz.js +1 -0
- package/static/dist/assets/railroad-ebnf-LZEXJU2U-l8xt49t5.js +1 -0
- package/static/dist/assets/railroad-peg-WCYAUIDC-kuxha9yz.js +1 -0
- package/static/dist/assets/{railroadDiagram-O6MQD6OU-cmxwjt73.js → railroadDiagram-O6MQD6OU-iwv9dby8.js} +1 -1
- package/static/dist/assets/{refresh-cw-mf192q3f.js → refresh-cw-hjzomwlo.js} +1 -1
- package/static/dist/assets/{requirementDiagram-BXWQKSXE-ir0s2mek.js → requirementDiagram-BXWQKSXE-cdgh5jha.js} +1 -1
- package/static/dist/assets/{sankeyDiagram-P5KCCOFB-jo7jcdq0.js → sankeyDiagram-P5KCCOFB-dbu6p97m.js} +1 -1
- package/static/dist/assets/{saved-g777hp1t.js → saved-feqhycco.js} +1 -1
- package/static/dist/assets/{search-i2kfogyx.js → search-n3jdc20p.js} +1 -1
- package/static/dist/assets/{sequenceDiagram-WJ2MYXX4-n0pf6xk5.js → sequenceDiagram-WJ2MYXX4-b0wchifg.js} +1 -1
- package/static/dist/assets/{settings_section_extensions_title-hgvh507l.js → settings_section_extensions_title-mtyviexd.js} +1 -1
- package/static/dist/assets/share._token-cwov8uqh.js +2 -0
- package/static/dist/assets/sliders-horizontal-i1lseyg0.js +1 -0
- package/static/dist/assets/{stateDiagram-D77RDMKH-olbrwyv9.js → stateDiagram-D77RDMKH-mb4b38xh.js} +1 -1
- package/static/dist/assets/stateDiagram-v2-MP3YSRHH-cn1fnqrg.js +1 -0
- package/static/dist/assets/{swimlanes-42K2YHIH-od14zrw6.js → swimlanes-42K2YHIH-ofrpfwj0.js} +1 -1
- package/static/dist/assets/swimlanesDiagram-VR7AAH4N-d652ummv.js +8 -0
- package/static/dist/assets/{timeline-definition-24CTP7MA-l0qokmnb.js → timeline-definition-24CTP7MA-hlw0jg6o.js} +1 -1
- package/static/dist/assets/todoStore-mfy9lpio.js +1 -0
- package/static/dist/assets/{treeView-Q6P3EWNA-hma3ip8m.js → treeView-Q6P3EWNA-d93wr19c.js} +1 -1
- package/static/dist/assets/{treemap-WGGIJYW6-bghjng5m.js → treemap-WGGIJYW6-g7ezdedz.js} +1 -1
- package/static/dist/assets/{untitled-nfaw0q7m.js → untitled-mqxhd1eh.js} +1 -1
- package/static/dist/assets/{useForm-k7fj2zsr.js → useForm-hh33yrwa.js} +1 -1
- package/static/dist/assets/useLocale-cr30vl7r.js +1 -0
- package/static/dist/assets/{vennDiagram-4TSXK5OY-bo97zf3z.js → vennDiagram-4TSXK5OY-m5vgeba5.js} +1 -1
- package/static/dist/assets/{wardley-WFR3VGLG-pbjl6jav.js → wardley-WFR3VGLG-gpao9bxv.js} +1 -1
- package/static/dist/assets/{wardleyDiagram-VM6X3IG4-bmy9i09l.js → wardleyDiagram-VM6X3IG4-g2yzwshm.js} +1 -1
- package/static/dist/assets/{xychartDiagram-S5SC5T6Z-eercu0mc.js → xychartDiagram-S5SC5T6Z-itakhpic.js} +1 -1
- package/static/dist/index.html +1 -1
- package/static/dist/sw.js +114 -152
- package/static/dist/assets/AppShell-gs0grlbs.js +0 -1
- package/static/dist/assets/ChatPage-gi1xlz52.js +0 -18
- package/static/dist/assets/HubRoute-qs9vzuh0.js +0 -2
- package/static/dist/assets/RightPanel-dn19xu42.js +0 -1
- package/static/dist/assets/Select-y7wtf85g.js +0 -1
- package/static/dist/assets/SessionListPanel-igwdj9xr.js +0 -1
- package/static/dist/assets/SettingsLayout-n6mu0ghg.js +0 -1
- package/static/dist/assets/States-oil2issf.js +0 -1
- package/static/dist/assets/Toaster-jtn59pjm.js +0 -1
- package/static/dist/assets/_app-k4jrkgfa.js +0 -1
- package/static/dist/assets/_app.ext._extensionId-mmf1mepm.js +0 -1
- package/static/dist/assets/_app.index-mb41nf00.js +0 -1
- package/static/dist/assets/_app.insights-mz5cep8m.js +0 -1
- package/static/dist/assets/_app.kanban-ocgw506i.js +0 -1
- package/static/dist/assets/_app.logs-lstmoju6.js +0 -1
- package/static/dist/assets/_app.memory-dishj9ra.js +0 -1
- package/static/dist/assets/_app.profiles-dynpvsio.js +0 -1
- package/static/dist/assets/_app.settings-c2czm9v4.js +0 -1
- package/static/dist/assets/_app.settings._section-exwflld7.js +0 -1
- package/static/dist/assets/_app.skills-j5jh2nug.js +0 -1
- package/static/dist/assets/_app.tasks-b99d98fo.js +0 -2
- package/static/dist/assets/_app.todos-ma8urvn3.js +0 -1
- package/static/dist/assets/_app.workspaces-l86cxeod.js +0 -1
- package/static/dist/assets/architecture-7GRP2DOG-mqft9w2p.js +0 -1
- package/static/dist/assets/channel-ix8cxdld.js +0 -1
- package/static/dist/assets/classDiagram-ZZMXUADV-c210y3il.js +0 -1
- package/static/dist/assets/classDiagram-v2-VYDZK3BY-c210y3il.js +0 -1
- package/static/dist/assets/endpoints-ipnv1hd4.js +0 -64
- package/static/dist/assets/eventmodeling-NTZA5JFV-d9b5a0ia.js +0 -1
- package/static/dist/assets/flowDiagram-HODETNUW-kfxxsyrk.js +0 -1
- package/static/dist/assets/index-bz7g7rhx.css +0 -2
- package/static/dist/assets/login-mvjys1ol.js +0 -1
- package/static/dist/assets/mermaid-HWGCJPDP-gmnkgf1n.js +0 -1
- package/static/dist/assets/onboarding-l6quy6gz.js +0 -1
- package/static/dist/assets/railroad-abnf-HS5TGJTU-5mjl5wpv.js +0 -1
- package/static/dist/assets/railroad-ebnf-LZEXJU2U-bfram8v8.js +0 -1
- package/static/dist/assets/railroad-peg-WCYAUIDC-nwfczn88.js +0 -1
- package/static/dist/assets/share._token-il3y6js2.js +0 -2
- package/static/dist/assets/sliders-horizontal-kd99ffvl.js +0 -1
- package/static/dist/assets/stateDiagram-v2-MP3YSRHH-dnzlmdpu.js +0 -1
- package/static/dist/assets/swimlanesDiagram-VR7AAH4N-ck0eqqdp.js +0 -8
- package/static/dist/assets/todoStore-la6uyqzj.js +0 -1
- package/static/dist/assets/trash-bxz1k9cw.js +0 -1
- package/static/dist/assets/useLocale-evhacmv2.js +0 -1
package/dist/tools/updates.js
CHANGED
|
@@ -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
|
|
7
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
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', '--
|
|
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
|
-
|
|
600
|
-
|
|
601
|
-
if (!
|
|
602
|
-
return
|
|
603
|
-
|
|
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
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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
|
|
618
|
-
const
|
|
619
|
-
|
|
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
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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
|
-
|
|
650
|
-
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
|
|
682
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
714
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
749
|
-
|
|
750
|
-
|
|
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',
|
|
1123
|
+
if (!(await git(['reset', '--hard', revision], path)).ok)
|
|
757
1124
|
return { ok: false, message: `Force reset to ${ref} failed` };
|
|
758
|
-
|
|
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
|
-
/**
|
|
939
|
-
async check(force = false, includeAgent = this.deps.includeAgent(), channel = this.deps.channel()) {
|
|
940
|
-
const
|
|
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
|
-
|
|
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
|
|
949
|
-
|
|
950
|
-
|
|
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
|
-
|
|
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
|
|
986
|
-
|
|
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
|
|
991
|
-
|
|
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 };
|