@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.
- package/_release.json +20 -0
- package/dist/api/chat-router.d.ts +54 -1
- package/dist/api/sessions-router.d.ts +296 -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 +1 -1
- package/dist/api/sse-routes.js.map +1 -1
- package/dist/api/tools-router.d.ts +91 -0
- package/dist/api/tools-router.js +4 -4
- package/dist/api/tools-router.js.map +1 -1
- package/dist/app.d.ts +449 -2
- 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/runtime.js +8 -2
- package/dist/runtime.js.map +1 -1
- package/dist/sessions/anchor.d.ts +57 -5
- package/dist/sessions/anchor.js +368 -32
- package/dist/sessions/anchor.js.map +1 -1
- package/dist/sessions/merge.d.ts +6 -0
- package/dist/sessions/merge.js +35 -4
- package/dist/sessions/merge.js.map +1 -1
- package/dist/sessions/service.d.ts +2 -0
- package/dist/sessions/service.js +31 -16
- 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/turn.d.ts +14 -0
- package/dist/sessions/turn.js +147 -20
- 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/tools/crons.js +55 -43
- package/dist/tools/crons.js.map +1 -1
- package/dist/tools/updates.d.ts +51 -13
- package/dist/tools/updates.js +362 -114
- package/dist/tools/updates.js.map +1 -1
- package/package.json +2 -2
- package/sidecar/agent_dependency.json +4 -3
- package/sidecar/talaria_sidecar/home.py +29 -20
- package/sidecar/talaria_sidecar/methods/config.py +5 -3
- package/sidecar/talaria_sidecar/runtime.py +3 -9
- package/static/dist/FILES.txt +116 -116
- package/static/dist/assets/AppShell-hikzkcql.js +1 -0
- package/static/dist/assets/ChatPage-ofqn89h9.js +21 -0
- package/static/dist/assets/{HubRoute-qs9vzuh0.js → HubRoute-f2zrs9si.js} +1 -1
- package/static/dist/assets/{Markdown-bt9bqd6a.js → Markdown-ftyfzds0.js} +7 -7
- package/static/dist/assets/RightPanel-febk3dkd.js +1 -0
- package/static/dist/assets/Select-lvx8gihl.js +1 -0
- package/static/dist/assets/SessionListPanel-o70ljmik.js +1 -0
- package/static/dist/assets/SettingsLayout-pdpmhy1o.js +1 -0
- package/static/dist/assets/States-i46tz8an.js +1 -0
- package/static/dist/assets/{Toaster-jtn59pjm.js → Toaster-fklyeeyh.js} +1 -1
- package/static/dist/assets/_app-j3rors25.js +1 -0
- package/static/dist/assets/_app.ext._extensionId-c0ey5ec9.js +1 -0
- package/static/dist/assets/_app.index-coy9jrto.js +1 -0
- package/static/dist/assets/_app.insights-ebd2k0i5.js +1 -0
- package/static/dist/assets/_app.kanban-e2p7ypi5.js +1 -0
- package/static/dist/assets/_app.logs-ml21mfyz.js +1 -0
- package/static/dist/assets/_app.memory-lhu5mm47.js +1 -0
- package/static/dist/assets/_app.profiles-egx0fgjo.js +1 -0
- package/static/dist/assets/{_app.session._sessionId-kevi4qd2.js → _app.session._sessionId-n1ggcf5e.js} +1 -1
- package/static/dist/assets/_app.settings-d0vqtyid.js +1 -0
- package/static/dist/assets/_app.settings._section-cnzj6pli.js +1 -0
- package/static/dist/assets/_app.skills-kkug8obu.js +1 -0
- package/static/dist/assets/{_app.tasks-b99d98fo.js → _app.tasks-nf285d2o.js} +2 -2
- package/static/dist/assets/_app.todos-dxalqp1e.js +1 -0
- package/static/dist/assets/_app.workspaces-bqtduujs.js +1 -0
- package/static/dist/assets/{abnfDiagram-VCTEODGH-hvrr25lb.js → abnfDiagram-VCTEODGH-i4bgzu83.js} +1 -1
- package/static/dist/assets/architecture-7GRP2DOG-o8zqevhv.js +1 -0
- package/static/dist/assets/{architectureDiagram-5GKGNRK7-b3r0z8sq.js → architectureDiagram-5GKGNRK7-mx21lwqa.js} +1 -1
- package/static/dist/assets/{blockDiagram-I7D4REHJ-hd3gw0cj.js → blockDiagram-I7D4REHJ-o1hhchxv.js} +1 -1
- package/static/dist/assets/{c4Diagram-7LVT6UL2-jlmbjiji.js → c4Diagram-7LVT6UL2-jygqtvd8.js} +1 -1
- package/static/dist/assets/channel-mw8l793e.js +1 -0
- package/static/dist/assets/{chunk-4HAMMTFA-oydbpsin.js → chunk-4HAMMTFA-hjcpubgk.js} +1 -1
- package/static/dist/assets/{chunk-75Z2AOVW-j633otj1.js → chunk-75Z2AOVW-eibnex6m.js} +1 -1
- package/static/dist/assets/{chunk-DU6HZSFF-ods2l027.js → chunk-DU6HZSFF-k1tp3qnp.js} +3 -3
- package/static/dist/assets/{chunk-F27PBJKO-ln4n2cwt.js → chunk-F27PBJKO-lvcgn16f.js} +1 -1
- package/static/dist/assets/{chunk-GMAD6QVW-fpz133eh.js → chunk-GMAD6QVW-ldlzruv1.js} +1 -1
- package/static/dist/assets/{chunk-GVQU2GXP-abs9ixiy.js → chunk-GVQU2GXP-hs1mva9d.js} +1 -1
- package/static/dist/assets/{chunk-IMKFNOWR-j9vukdy1.js → chunk-IMKFNOWR-o7eqx3yb.js} +1 -1
- package/static/dist/assets/{chunk-L3NEJ4N5-cfyw87r3.js → chunk-L3NEJ4N5-bp1ofl70.js} +1 -1
- package/static/dist/assets/{chunk-OSK3NFVY-baja78wr.js → chunk-OSK3NFVY-o8m6mdz5.js} +1 -1
- package/static/dist/assets/{chunk-P2QGCYS3-g66xby8h.js → chunk-P2QGCYS3-kx83fti2.js} +1 -1
- package/static/dist/assets/{chunk-POPQ4Y6H-osqu864t.js → chunk-POPQ4Y6H-mwl99e9y.js} +1 -1
- package/static/dist/assets/{chunk-PWAF6VOD-egejasv1.js → chunk-PWAF6VOD-oriuokxy.js} +1 -1
- package/static/dist/assets/{chunk-SHT3W25Y-l014h8zz.js → chunk-SHT3W25Y-ab5cf00v.js} +1 -1
- package/static/dist/assets/{chunk-SVP7TREG-n3z10bp3.js → chunk-SVP7TREG-kqdrpr17.js} +1 -1
- package/static/dist/assets/{chunk-TICWLB2K-bzozkjc0.js → chunk-TICWLB2K-kfbnjabk.js} +1 -1
- package/static/dist/assets/classDiagram-ZZMXUADV-egajr1wp.js +1 -0
- package/static/dist/assets/classDiagram-v2-VYDZK3BY-egajr1wp.js +1 -0
- package/static/dist/assets/{copied-cjsttec1.js → copied-dp33szhq.js} +1 -1
- package/static/dist/assets/{cynefin-OW5HDTMX-elpt7h1j.js → cynefin-OW5HDTMX-mm5sj9s7.js} +1 -1
- package/static/dist/assets/{cynefinDiagram-5FMLGOSQ-5mzl5pi2.js → cynefinDiagram-5FMLGOSQ-nfon5q38.js} +1 -1
- package/static/dist/assets/{dagre-GXQ25YYZ-ihwlhmfl.js → dagre-GXQ25YYZ-dzdp9ypb.js} +1 -1
- package/static/dist/assets/{diagram-S7CK7UJ4-4z90agaz.js → diagram-S7CK7UJ4-ckxzqo01.js} +1 -1
- package/static/dist/assets/{diagram-UQ7AKVKN-eifw0lg7.js → diagram-UQ7AKVKN-hw16h1um.js} +1 -1
- package/static/dist/assets/{diagram-VSXAHHWV-ksh3sjqv.js → diagram-VSXAHHWV-mu7wu2zg.js} +1 -1
- package/static/dist/assets/{diagram-VX7I27RA-ett72f71.js → diagram-VX7I27RA-k9uprwtj.js} +1 -1
- package/static/dist/assets/{diagram-Z3DM3KII-l8scp8nu.js → diagram-Z3DM3KII-csdjzrg9.js} +1 -1
- package/static/dist/assets/{ebnfDiagram-PWID7BFC-f0pxcmit.js → ebnfDiagram-PWID7BFC-n8mbw2jj.js} +1 -1
- package/static/dist/assets/endpoints-f6ngsyu5.js +64 -0
- package/static/dist/assets/{erDiagram-RLTQ6QDP-f47m5xh0.js → erDiagram-RLTQ6QDP-fsfe1uj0.js} +1 -1
- package/static/dist/assets/eventmodeling-NTZA5JFV-k3zhqcyl.js +1 -0
- package/static/dist/assets/flowDiagram-HODETNUW-bdxes51f.js +1 -0
- package/static/dist/assets/{ganttDiagram-EL5Y4UJY-bmktjdi0.js → ganttDiagram-EL5Y4UJY-gsba3iq9.js} +1 -1
- package/static/dist/assets/{gitGraph-4MIJSDKK-j2ueg258.js → gitGraph-4MIJSDKK-na86q1pu.js} +1 -1
- package/static/dist/assets/{gitGraphDiagram-WWUBYQGX-ms6586v9.js → gitGraphDiagram-WWUBYQGX-g72ew3xz.js} +1 -1
- package/static/dist/assets/{highlighted-body-KPVGNVTW-ev52d2si.js → highlighted-body-KPVGNVTW-okn18cp1.js} +1 -1
- package/static/dist/assets/index-ab0wj9ik.css +2 -0
- package/static/dist/assets/index-fd27xooo.js +11 -0
- package/static/dist/assets/{info-A6RAGUB7-j717vyt7.js → info-A6RAGUB7-i01jeltk.js} +1 -1
- package/static/dist/assets/{infoDiagram-27XIBGKW-dsbpib36.js → infoDiagram-27XIBGKW-cayiv6fa.js} +1 -1
- package/static/dist/assets/{ishikawaDiagram-5VMMS53U-n0sz3i2d.js → ishikawaDiagram-5VMMS53U-g4rf14bv.js} +1 -1
- package/static/dist/assets/{journeyDiagram-3NMN7TZE-mcxz1mme.js → journeyDiagram-3NMN7TZE-fusb2424.js} +1 -1
- package/static/dist/assets/{kanban-definition-UXKFOSKX-n6hq2lyc.js → kanban-definition-UXKFOSKX-g831pwlr.js} +1 -1
- package/static/dist/assets/{lazyRouteComponent-faquioun.js → lazyRouteComponent-ol965zwk.js} +1 -1
- package/static/dist/assets/{line-h5xq20s4.js → line-md6em6gc.js} +1 -1
- package/static/dist/assets/login-kfclsqd2.js +1 -0
- package/static/dist/assets/mermaid-HWGCJPDP-ejk0d88t.js +1 -0
- package/static/dist/assets/{mermaid-parser.core-n9cuu5ue.js → mermaid-parser.core-mq3uns9m.js} +3 -3
- package/static/dist/assets/{mindmap-definition-YA3MSWOX-gta3u7z4.js → mindmap-definition-YA3MSWOX-oxafrned.js} +1 -1
- package/static/dist/assets/onboarding-fo6hnkyv.js +1 -0
- package/static/dist/assets/{packet-AYTQ26CC-bpzuq5n6.js → packet-AYTQ26CC-ggxyvbym.js} +1 -1
- package/static/dist/assets/{pegDiagram-XKGWAZYB-juye13e5.js → pegDiagram-XKGWAZYB-nk7xcqgp.js} +1 -1
- package/static/dist/assets/{pie-WAS4IAKB-odhb8buq.js → pie-WAS4IAKB-ito2razh.js} +1 -1
- package/static/dist/assets/{pieDiagram-E7YTZNPT-mgb14ksh.js → pieDiagram-E7YTZNPT-g02xexar.js} +1 -1
- package/static/dist/assets/{quadrantDiagram-AXDQQJYC-cg36naog.js → quadrantDiagram-AXDQQJYC-d4jn4xq5.js} +1 -1
- package/static/dist/assets/{radar-RG4KPBEZ-lgzfpnef.js → radar-RG4KPBEZ-nebvfdzt.js} +1 -1
- package/static/dist/assets/{railroad-74A4TZTK-gmglc3ua.js → railroad-74A4TZTK-ny3gebse.js} +1 -1
- package/static/dist/assets/railroad-abnf-HS5TGJTU-gtd38edw.js +1 -0
- package/static/dist/assets/railroad-ebnf-LZEXJU2U-o238oax2.js +1 -0
- package/static/dist/assets/railroad-peg-WCYAUIDC-c4acqp2j.js +1 -0
- package/static/dist/assets/{railroadDiagram-O6MQD6OU-cmxwjt73.js → railroadDiagram-O6MQD6OU-m09mmu67.js} +1 -1
- package/static/dist/assets/{refresh-cw-mf192q3f.js → refresh-cw-eitd7yva.js} +1 -1
- package/static/dist/assets/{requirementDiagram-BXWQKSXE-ir0s2mek.js → requirementDiagram-BXWQKSXE-evggpcbk.js} +1 -1
- package/static/dist/assets/{sankeyDiagram-P5KCCOFB-jo7jcdq0.js → sankeyDiagram-P5KCCOFB-bun89039.js} +1 -1
- package/static/dist/assets/{saved-g777hp1t.js → saved-culx14ii.js} +1 -1
- package/static/dist/assets/{search-i2kfogyx.js → search-nu6kzq55.js} +1 -1
- package/static/dist/assets/{sequenceDiagram-WJ2MYXX4-n0pf6xk5.js → sequenceDiagram-WJ2MYXX4-hhs9f09o.js} +1 -1
- package/static/dist/assets/{settings_section_extensions_title-hgvh507l.js → settings_section_extensions_title-mj0gcozh.js} +1 -1
- package/static/dist/assets/share._token-k4pcnxqu.js +2 -0
- package/static/dist/assets/sliders-horizontal-b9ljga1o.js +1 -0
- package/static/dist/assets/{stateDiagram-D77RDMKH-olbrwyv9.js → stateDiagram-D77RDMKH-gi7t1tro.js} +1 -1
- package/static/dist/assets/stateDiagram-v2-MP3YSRHH-jtadbol7.js +1 -0
- package/static/dist/assets/{swimlanes-42K2YHIH-od14zrw6.js → swimlanes-42K2YHIH-ea5urvph.js} +1 -1
- package/static/dist/assets/swimlanesDiagram-VR7AAH4N-ki7weepw.js +8 -0
- package/static/dist/assets/{timeline-definition-24CTP7MA-l0qokmnb.js → timeline-definition-24CTP7MA-mq64rm3b.js} +1 -1
- package/static/dist/assets/{todoStore-la6uyqzj.js → todoStore-azvfuqbv.js} +1 -1
- package/static/dist/assets/{trash-bxz1k9cw.js → trash-osyo895g.js} +1 -1
- package/static/dist/assets/{treeView-Q6P3EWNA-hma3ip8m.js → treeView-Q6P3EWNA-cimbtxlz.js} +1 -1
- package/static/dist/assets/{treemap-WGGIJYW6-bghjng5m.js → treemap-WGGIJYW6-wig5gi1y.js} +1 -1
- package/static/dist/assets/{untitled-nfaw0q7m.js → untitled-mbwemvxf.js} +1 -1
- package/static/dist/assets/{useForm-k7fj2zsr.js → useForm-li4bljlv.js} +1 -1
- package/static/dist/assets/useLocale-kdew6auf.js +1 -0
- package/static/dist/assets/{vennDiagram-4TSXK5OY-bo97zf3z.js → vennDiagram-4TSXK5OY-ez63kh7n.js} +1 -1
- package/static/dist/assets/{wardley-WFR3VGLG-pbjl6jav.js → wardley-WFR3VGLG-r7rftz31.js} +1 -1
- package/static/dist/assets/{wardleyDiagram-VM6X3IG4-bmy9i09l.js → wardleyDiagram-VM6X3IG4-mvqg9eqc.js} +1 -1
- package/static/dist/assets/{xychartDiagram-S5SC5T6Z-eercu0mc.js → xychartDiagram-S5SC5T6Z-eqxdfc91.js} +1 -1
- package/static/dist/index.html +1 -1
- package/static/dist/sw.js +1 -1
- package/static/dist/assets/AppShell-gs0grlbs.js +0 -1
- package/static/dist/assets/ChatPage-gi1xlz52.js +0 -18
- 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/_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.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/index-hywfjw88.js +0 -11
- 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/useLocale-evhacmv2.js +0 -1
package/dist/tools/updates.js
CHANGED
|
@@ -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
|
|
7
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
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', '--
|
|
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
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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
|
|
618
|
-
const
|
|
619
|
-
|
|
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
|
-
|
|
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);
|
|
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
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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 (
|
|
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
|
-
|
|
652
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
682
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
714
|
-
const
|
|
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
|
-
|
|
749
|
-
|
|
750
|
-
|
|
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',
|
|
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
|
-
/**
|
|
939
|
-
async check(force = false, includeAgent = this.deps.includeAgent(), channel = this.deps.channel()) {
|
|
940
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
949
|
-
|
|
950
|
-
|
|
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
|
-
|
|
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
|
|
986
|
-
|
|
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
|
|
991
|
-
|
|
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 };
|