local-operator-ui 0.31.28 → 0.31.29
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/out/main/index.js +791 -24
- package/out/renderer/assets/{_basePickBy-D16oopNi.js → _basePickBy-DL33QdW4.js} +1 -1
- package/out/renderer/assets/{_baseUniq-BYKYfwtj.js → _baseUniq-CBRLNfCa.js} +1 -1
- package/out/renderer/assets/{agent-details-page-CpwkLhdq.js → agent-details-page-JNorVbUk.js} +1 -1
- package/out/renderer/assets/{agent-hub-page-BYeba0zC.js → agent-hub-page-B53zgVHZ.js} +1 -1
- package/out/renderer/assets/agents-page-D5VssooM.js +7 -0
- package/out/renderer/assets/{arc-BYg3WIzZ.js → arc-C1D2LaN2.js} +1 -1
- package/out/renderer/assets/{architectureDiagram-IEHRJDOE-np6HwkQz.js → architectureDiagram-IEHRJDOE-Dm1oPlGu.js} +1 -1
- package/out/renderer/assets/{blockDiagram-JOT3LUYC-CaR7YwVl.js → blockDiagram-JOT3LUYC-CeNuHJ3J.js} +1 -1
- package/out/renderer/assets/{book-open--g1VlVMp.js → book-open-C2GJZCxM.js} +1 -1
- package/out/renderer/assets/browser-page-DHwPmeL7.js +1 -0
- package/out/renderer/assets/{browser-webauthn-prompt-Bsk0eXNZ.js → browser-webauthn-prompt-Cy_dHOdv.js} +1 -1
- package/out/renderer/assets/{c4Diagram-VJAJSXHY-DqBT3rl5.js → c4Diagram-VJAJSXHY-Cieumjrn.js} +1 -1
- package/out/renderer/assets/channel-BipwWMab.js +1 -0
- package/out/renderer/assets/{chunk-4BMEZGHF-B7-MzFPi.js → chunk-4BMEZGHF-CeIWDTcE.js} +1 -1
- package/out/renderer/assets/{chunk-A2AXSNBT-B9IqNrNg.js → chunk-A2AXSNBT-nkZ-ZEU8.js} +1 -1
- package/out/renderer/assets/{chunk-AEK57VVT-DFzBJNd9.js → chunk-AEK57VVT-D3HkgezL.js} +1 -1
- package/out/renderer/assets/{chunk-D6G4REZN-DW-XzsAv.js → chunk-D6G4REZN-BZjnLSCT.js} +1 -1
- package/out/renderer/assets/{chunk-RZ5BOZE2-C7el6yre.js → chunk-RZ5BOZE2-RMzqoQiC.js} +1 -1
- package/out/renderer/assets/{chunk-XZIHB7SX-BGYrq1Wn.js → chunk-XZIHB7SX-DJl2yrB3.js} +1 -1
- package/out/renderer/assets/classDiagram-GIVACNV2-C7VPQGdP.js +1 -0
- package/out/renderer/assets/classDiagram-v2-COTLJTTW-C7VPQGdP.js +1 -0
- package/out/renderer/assets/clone-fWuYSDG8.js +1 -0
- package/out/renderer/assets/{console-mirror-ffbsozOd.js → console-mirror-BhuN6Ypv.js} +1 -1
- package/out/renderer/assets/{consoleCapture-CiApa6fi.js → consoleCapture-BNipqRoj.js} +1 -1
- package/out/renderer/assets/{conversation-input-sync-DQUYkOfh.js → conversation-input-sync-BoRhB7VS.js} +245 -238
- package/out/renderer/assets/{dagre-OKDRZEBW-BVScRDvj.js → dagre-OKDRZEBW-DbaX-Vfd.js} +1 -1
- package/out/renderer/assets/{diagram-SSKATNLV-B0iQ6YY1.js → diagram-SSKATNLV-BP9pThlk.js} +1 -1
- package/out/renderer/assets/{diagram-VNBRO52H-Co6YPtUT.js → diagram-VNBRO52H-BM5HUD5o.js} +1 -1
- package/out/renderer/assets/{erDiagram-Q7BY3M3F-C9XIuSQl.js → erDiagram-Q7BY3M3F-CUqUCymj.js} +1 -1
- package/out/renderer/assets/{error-boundary-DH4MhqhL.js → error-boundary-CAyz2bjY.js} +5 -5
- package/out/renderer/assets/{flowDiagram-4HSFHLVR-QTHrGPk1.js → flowDiagram-4HSFHLVR-BPz65dD6.js} +1 -1
- package/out/renderer/assets/{ganttDiagram-APWFNJXF-qC1VnBIz.js → ganttDiagram-APWFNJXF-ccu__hit.js} +1 -1
- package/out/renderer/assets/{gitGraphDiagram-7IBYFJ6S-D7UGP8uH.js → gitGraphDiagram-7IBYFJ6S-Ct9B7LDY.js} +1 -1
- package/out/renderer/assets/{graph-BIUuz2bp.js → graph-DIsum2dA.js} +1 -1
- package/out/renderer/assets/{index-Ckvy9Zge.js → index-BWjn66XI.js} +1 -1
- package/out/renderer/assets/{index-vK67PDia.js → index-Bgxehgfc.js} +195 -195
- package/out/renderer/assets/index-CCRonkWc.css +1 -0
- package/out/renderer/assets/{index-DTc-sJ8S.js → index-FX4vEaoW.js} +1 -1
- package/out/renderer/assets/{infoDiagram-PH2N3AL5-DYtVGJC7.js → infoDiagram-PH2N3AL5-Bey34vow.js} +1 -1
- package/out/renderer/assets/{installer-BcSiJA6N.js → installer-Bu8Z9JPe.js} +1 -1
- package/out/renderer/assets/{journeyDiagram-U35MCT3I-vVg2sFTp.js → journeyDiagram-U35MCT3I-jn9ZwYXW.js} +1 -1
- package/out/renderer/assets/{kanban-definition-NDS4AKOZ-BE36wSk2.js → kanban-definition-NDS4AKOZ-C-wz8kvA.js} +1 -1
- package/out/renderer/assets/{layout-bOQrw0b8.js → layout-BREzqltz.js} +1 -1
- package/out/renderer/assets/{legacy-agents-page-BSKz_UrV.js → legacy-agents-page-CO-TGqR5.js} +1 -1
- package/out/renderer/assets/{mermaid.core-DpvY6Bhk.js → mermaid.core-8-ikrzww.js} +5 -5
- package/out/renderer/assets/{mesh-page-9Q8Sc9J_.js → mesh-page-0CSX8EG-.js} +1 -1
- package/out/renderer/assets/{mindmap-definition-ALO5MXBD-B9PPN6sb.js → mindmap-definition-ALO5MXBD-CJzxvyVl.js} +1 -1
- package/out/renderer/assets/{mini-ySULdryU.js → mini-DIRe7eu8.js} +1 -1
- package/out/renderer/assets/{org-surface-gate-Dl4zcl5q.js → org-surface-gate-C1t_jjzP.js} +1 -1
- package/out/renderer/assets/{page-header-DazB-GQz.js → page-header-CKzo9EwH.js} +1 -1
- package/out/renderer/assets/{pieDiagram-IB7DONF6-DkioNhQY.js → pieDiagram-IB7DONF6-B38MVZvk.js} +1 -1
- package/out/renderer/assets/projects-page-4aBbeqHa.js +55 -0
- package/out/renderer/assets/{quadrantDiagram-7GDLP6J5-4m3m0N6F.js → quadrantDiagram-7GDLP6J5-CGwbmj-X.js} +1 -1
- package/out/renderer/assets/{radar-MK3ICKWK-DS8YGAOb.js → radar-MK3ICKWK-0J95rurP.js} +1 -1
- package/out/renderer/assets/{radient-auth-buttons-DZeHRL7u.js → radient-auth-buttons-DlO_vXxS.js} +1 -1
- package/out/renderer/assets/{requirementDiagram-KVF5MWMF-DsBdUu3C.js → requirementDiagram-KVF5MWMF-Csqc9PAd.js} +1 -1
- package/out/renderer/assets/{sankeyDiagram-QLVOVGJD-CnaPYKR6.js → sankeyDiagram-QLVOVGJD-Lm7B9S0v.js} +1 -1
- package/out/renderer/assets/{schedules-page-B0dSGpXt.js → schedules-page-BI6mduTY.js} +1 -1
- package/out/renderer/assets/{sequenceDiagram-X6HHIX6F-C3eja1QY.js → sequenceDiagram-X6HHIX6F-Dw8RM08f.js} +1 -1
- package/out/renderer/assets/{settings-page-CrMsVX3d.js → settings-page-BsIAdS9g.js} +6 -6
- package/out/renderer/assets/{settings-section-BGG4XzV0.js → settings-section-DlTh8xfK.js} +1 -1
- package/out/renderer/assets/{stateDiagram-DGXRK772-CjFKiyXn.js → stateDiagram-DGXRK772-DHKpJy8t.js} +1 -1
- package/out/renderer/assets/stateDiagram-v2-YXO3MK2T-CFuWzCss.js +1 -0
- package/out/renderer/assets/{timeline-definition-BDJGKUSR-CAXy0nQD.js → timeline-definition-BDJGKUSR-BxyX64Ja.js} +1 -1
- package/out/renderer/assets/{ui-preferences-store-B6Fr0UX9.js → ui-preferences-store-C5PakFPc.js} +1 -1
- package/out/renderer/assets/{use-download-agent-mutation-LsNre_-6.js → use-download-agent-mutation-CeaoIrQs.js} +1 -1
- package/out/renderer/assets/{use-memberships-query-BgiSfYgr.js → use-memberships-query-D95x-vrx.js} +1 -1
- package/out/renderer/assets/{xychartDiagram-VJFVF3MP-CfNm8SWG.js → xychartDiagram-VJFVF3MP-PNx7e-Lx.js} +1 -1
- package/out/renderer/console-capture.html +5 -5
- package/out/renderer/index.html +7 -7
- package/out/renderer/installer.html +4 -4
- package/out/renderer/mini.html +6 -6
- package/package.json +10 -3
- package/out/renderer/assets/agents-page-BQdR1fgV.js +0 -7
- package/out/renderer/assets/browser-page-BPwZPQY2.js +0 -1
- package/out/renderer/assets/channel-mWTr8anL.js +0 -1
- package/out/renderer/assets/classDiagram-GIVACNV2-DsNAZ0y6.js +0 -1
- package/out/renderer/assets/classDiagram-v2-COTLJTTW-DsNAZ0y6.js +0 -1
- package/out/renderer/assets/clone-48M_nlg0.js +0 -1
- package/out/renderer/assets/index-C5Ighs_C.css +0 -1
- package/out/renderer/assets/projects-page-C37RiUJz.js +0 -35
- package/out/renderer/assets/stateDiagram-v2-YXO3MK2T-BsbYpgkp.js +0 -1
- /package/out/renderer/assets/{index-D-TXozHA.js → index-CTnUXmyg.js} +0 -0
package/out/main/index.js
CHANGED
|
@@ -102,7 +102,17 @@ const profileFields = zod.z.object({
|
|
|
102
102
|
instructions: zod.z.string().max(8e3).optional(),
|
|
103
103
|
tools: zod.z.array(zod.z.string().min(1).max(128)).max(256).optional(),
|
|
104
104
|
effort: zod.z.string().max(64).optional(),
|
|
105
|
-
delegate: zod.z.boolean().optional()
|
|
105
|
+
delegate: zod.z.boolean().optional(),
|
|
106
|
+
/*
|
|
107
|
+
* The action class (`reactive` | `proactive`), the field the Class control
|
|
108
|
+
* writes. An ENUM rather than a free string, because the route takes a
|
|
109
|
+
* `Literal` and a spelling this schema let through would be a 422 the
|
|
110
|
+
* caller could not tell from a bug — the same reasoning `scheduleUnit`
|
|
111
|
+
* above states. Optional like every other field here: an omitted key means
|
|
112
|
+
* "leave the class alone" on an update, which is what lets the control send
|
|
113
|
+
* the class and nothing else.
|
|
114
|
+
*/
|
|
115
|
+
action_class: zod.z.enum(["reactive", "proactive"]).optional()
|
|
106
116
|
}).strict();
|
|
107
117
|
const teamFields = zod.z.object({
|
|
108
118
|
name: zod.z.string().min(1).max(64).optional(),
|
|
@@ -268,6 +278,25 @@ const PROJECT_MILESTONE_NAME_MAX_CHARS = 80;
|
|
|
268
278
|
const hubItemKind = zod.z.enum(["agent", "team"]);
|
|
269
279
|
const desktopRequestUnion = zod.z.discriminatedUnion("op", [
|
|
270
280
|
zod.z.object({ op: zod.z.literal("capabilities") }).strict(),
|
|
281
|
+
/*
|
|
282
|
+
* THE MACHINE'S RUNTIME ROSTER, read for the straggler census a completed
|
|
283
|
+
* server update reports (`GET /v1/desktop/runtimes`). A GET with a fixed
|
|
284
|
+
* shape, so the whole request is the op.
|
|
285
|
+
*
|
|
286
|
+
* It reads the INVENTORY (`probe=false` in the path composer): the census
|
|
287
|
+
* asks every row what build it booted with, and the route's per-row loopback
|
|
288
|
+
* connects answer a different question ("did it answer?") that nothing here
|
|
289
|
+
* acts on. Skipping them also keeps the response inside the control budget
|
|
290
|
+
* without depending on the probe pool's own deadline logic.
|
|
291
|
+
*
|
|
292
|
+
* THE CONTROL BUDGET IS THE RIGHT ONE, and it is a bound rather than a hope:
|
|
293
|
+
* the route's own ceiling is the two external reads in front of the
|
|
294
|
+
* composition (5 s process table + 3 s socket table, stated in `roster.py`),
|
|
295
|
+
* and `probe=false` skips the per-row connects entirely - so the worst case
|
|
296
|
+
* is ~8 s, well under the 20 s control deadline. It is NOT on the long-read
|
|
297
|
+
* list: nothing here fans out to a provider or walks a ledger.
|
|
298
|
+
*/
|
|
299
|
+
zod.z.object({ op: zod.z.literal("runtimes.list") }).strict(),
|
|
271
300
|
zod.z.object({ op: zod.z.literal("profiles.list") }).strict(),
|
|
272
301
|
zod.z.object({ op: zod.z.literal("profiles.get"), name: profileName }).strict(),
|
|
273
302
|
zod.z.object({ op: zod.z.literal("profiles.install"), name: profileName, requestId }).strict(),
|
|
@@ -757,11 +786,38 @@ const desktopRequestUnion = zod.z.discriminatedUnion("op", [
|
|
|
757
786
|
zod.z.object({
|
|
758
787
|
op: zod.z.literal("sessions.answer"),
|
|
759
788
|
sessionId,
|
|
760
|
-
|
|
761
|
-
|
|
789
|
+
/**
|
|
790
|
+
* The owner epoch, for the two GATE shapes. Optional here because a queued
|
|
791
|
+
* ask has no epoch to give — `ask_id` is the other selector and the checks
|
|
792
|
+
* below require exactly one of the pair.
|
|
793
|
+
*/
|
|
794
|
+
epoch: id$1.optional(),
|
|
795
|
+
requestId: id$1.optional(),
|
|
762
796
|
value: zod.z.string().max(32768).optional(),
|
|
763
797
|
approved: zod.z.boolean().optional(),
|
|
764
|
-
questionIndex: zod.z.number().int().min(0).optional()
|
|
798
|
+
questionIndex: zod.z.number().int().min(0).optional(),
|
|
799
|
+
/**
|
|
800
|
+
* The queued ask's id (`a-3f9c`). Its presence selects the THIRD body shape —
|
|
801
|
+
* `{ask_id, answers}` or `{ask_id, decline}` — and, per the backend's own
|
|
802
|
+
* contract, makes `epoch` irrelevant rather than optional-but-checked: an ask
|
|
803
|
+
* outlives the owner that queued it, so requiring an epoch would refuse
|
|
804
|
+
* exactly the case the feature exists for (a cold session whose asks are
|
|
805
|
+
* still open). The single-winner rule that the epoch check used to provide
|
|
806
|
+
* now lives on the ask log, where the first `answered` event wins.
|
|
807
|
+
*/
|
|
808
|
+
askId: id$1.optional(),
|
|
809
|
+
/**
|
|
810
|
+
* The WHOLE ask's answers, keyed by question id, each a list because a
|
|
811
|
+
* question may be multi-select.
|
|
812
|
+
*
|
|
813
|
+
* One atomic body rather than a per-question stream: the blocking path
|
|
814
|
+
* answered one question at a time over the wire, and a client that died
|
|
815
|
+
* part-way left an ask half-settled — the exact state the atomic submit
|
|
816
|
+
* exists to make unrepresentable.
|
|
817
|
+
*/
|
|
818
|
+
answers: zod.z.record(zod.z.string(), zod.z.array(zod.z.string().max(32768))).optional(),
|
|
819
|
+
/** "No answer — decide yourself", the explicit form of today's Esc. */
|
|
820
|
+
decline: zod.z.boolean().optional()
|
|
765
821
|
}).strict(),
|
|
766
822
|
zod.z.object({
|
|
767
823
|
op: zod.z.literal("sessions.seen"),
|
|
@@ -1236,7 +1292,25 @@ const desktopRequestUnion = zod.z.discriminatedUnion("op", [
|
|
|
1236
1292
|
sessionId,
|
|
1237
1293
|
requestId,
|
|
1238
1294
|
message: zod.z.string().max(2e5).optional(),
|
|
1239
|
-
|
|
1295
|
+
/*
|
|
1296
|
+
* WHERE THE COPY STOPS. `next_safe` is the historical form and the default
|
|
1297
|
+
* (the whole committed conversation, waiting for a turn boundary);
|
|
1298
|
+
* `at_entry` cuts through `entryId` and needs no boundary, because a named
|
|
1299
|
+
* point is already committed.
|
|
1300
|
+
*
|
|
1301
|
+
* `entryId` is bounded rather than patterned for the route's reason -
|
|
1302
|
+
* entry ids are minted in more than one shape and existence in the
|
|
1303
|
+
* CONVERSATION is what decides whether one is real (the route answers an
|
|
1304
|
+
* unknown or foreign id with a refusal, never a fork of the wrong
|
|
1305
|
+
* history) - so this client validates the shape it can and lets the
|
|
1306
|
+
* backend adjudicate the rest.
|
|
1307
|
+
*
|
|
1308
|
+
* BOTH ARE OPTIONAL AND ABSENT IS THE OLD CALL, key for key: a typed
|
|
1309
|
+
* `/fork` and the palette's fork send neither, and the body builder below
|
|
1310
|
+
* omits `entry_id` entirely rather than sending it as null.
|
|
1311
|
+
*/
|
|
1312
|
+
boundary: zod.z.enum(["next_safe", "at_entry"]).optional(),
|
|
1313
|
+
entryId: zod.z.string().max(128).optional()
|
|
1240
1314
|
}).strict(),
|
|
1241
1315
|
zod.z.object({
|
|
1242
1316
|
op: zod.z.literal("sessions.stop"),
|
|
@@ -1760,6 +1834,20 @@ const desktopRequestUnion = zod.z.discriminatedUnion("op", [
|
|
|
1760
1834
|
key: projectKey,
|
|
1761
1835
|
name: zod.z.string().min(1).max(PROJECT_MILESTONE_NAME_MAX_CHARS)
|
|
1762
1836
|
}).strict(),
|
|
1837
|
+
/*
|
|
1838
|
+
* The check-in fan-out (`POST /v1/desktop/projects/{project}/request-update`):
|
|
1839
|
+
* ask every linked session to post a progress update. APPENDED to the union
|
|
1840
|
+
* like its siblings, and gated by its OWN capability key
|
|
1841
|
+
* (`features.projects_request_update`) rather than a bump of `projects`: the
|
|
1842
|
+
* tab renders perfectly well against a backend that cannot ask its sessions
|
|
1843
|
+
* for anything, so the version would hide a working surface behind an update
|
|
1844
|
+
* it does not need (the `session_search` rule above). The key is the row's
|
|
1845
|
+
* address (id or name) - the same shape every other projects op takes.
|
|
1846
|
+
*/
|
|
1847
|
+
zod.z.object({
|
|
1848
|
+
op: zod.z.literal("projects.request_update"),
|
|
1849
|
+
key: projectKey
|
|
1850
|
+
}).strict(),
|
|
1763
1851
|
/*
|
|
1764
1852
|
* AIDA'S CONTROL PLANE: one read and one control op on the same route
|
|
1765
1853
|
* (`/v1/desktop/aida`), because the rail's row and the composer's `/aida`
|
|
@@ -1787,6 +1875,48 @@ const desktopRequestUnion = zod.z.discriminatedUnion("op", [
|
|
|
1787
1875
|
]);
|
|
1788
1876
|
const desktopRequestSchema = desktopRequestUnion.superRefine(
|
|
1789
1877
|
(request, ctx) => {
|
|
1878
|
+
if (request.op === "sessions.answer") {
|
|
1879
|
+
if (request.askId !== void 0) {
|
|
1880
|
+
if (!request.askId)
|
|
1881
|
+
ctx.addIssue({
|
|
1882
|
+
code: zod.z.ZodIssueCode.custom,
|
|
1883
|
+
message: "ask_id must be a non-empty string",
|
|
1884
|
+
path: ["askId"]
|
|
1885
|
+
});
|
|
1886
|
+
if (request.decline !== true && !request.answers)
|
|
1887
|
+
ctx.addIssue({
|
|
1888
|
+
code: zod.z.ZodIssueCode.custom,
|
|
1889
|
+
message: "A queued-ask answer needs answers or decline",
|
|
1890
|
+
path: ["answers"]
|
|
1891
|
+
});
|
|
1892
|
+
else if (request.decline === true && request.answers !== void 0)
|
|
1893
|
+
ctx.addIssue({
|
|
1894
|
+
code: zod.z.ZodIssueCode.custom,
|
|
1895
|
+
message: "Supply either answers or decline",
|
|
1896
|
+
path: ["decline"]
|
|
1897
|
+
});
|
|
1898
|
+
else if (request.answers !== void 0 && Object.keys(request.answers).length === 0)
|
|
1899
|
+
ctx.addIssue({
|
|
1900
|
+
code: zod.z.ZodIssueCode.custom,
|
|
1901
|
+
message: "answers must name at least one question",
|
|
1902
|
+
path: ["answers"]
|
|
1903
|
+
});
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
if (!request.epoch)
|
|
1907
|
+
ctx.addIssue({
|
|
1908
|
+
code: zod.z.ZodIssueCode.custom,
|
|
1909
|
+
message: "An epoch is required to answer a gate",
|
|
1910
|
+
path: ["epoch"]
|
|
1911
|
+
});
|
|
1912
|
+
if (!request.requestId)
|
|
1913
|
+
ctx.addIssue({
|
|
1914
|
+
code: zod.z.ZodIssueCode.custom,
|
|
1915
|
+
message: "A request id is required to answer a gate",
|
|
1916
|
+
path: ["requestId"]
|
|
1917
|
+
});
|
|
1918
|
+
return;
|
|
1919
|
+
}
|
|
1790
1920
|
if (request.op === "sessions.create" && request.purpose === void 0) {
|
|
1791
1921
|
if (request.cwd === void 0) {
|
|
1792
1922
|
ctx.addIssue({
|
|
@@ -1982,6 +2112,9 @@ const READ_ONLY_OPS = /* @__PURE__ */ new Set([
|
|
|
1982
2112
|
"profiles.get",
|
|
1983
2113
|
"profiles.list",
|
|
1984
2114
|
"providers.list",
|
|
2115
|
+
// A reader that changes nothing (the route's own docstring): the straggler
|
|
2116
|
+
// census, and the app re-reads it rather than caching a stale count.
|
|
2117
|
+
"runtimes.list",
|
|
1985
2118
|
"sessions.aside.get",
|
|
1986
2119
|
"sessions.checkpoints",
|
|
1987
2120
|
"sessions.failovers",
|
|
@@ -2073,6 +2206,8 @@ function desktopEndpoint(request) {
|
|
|
2073
2206
|
switch (request.op) {
|
|
2074
2207
|
case "capabilities":
|
|
2075
2208
|
return { path: "/v1/capabilities", method: "GET" };
|
|
2209
|
+
case "runtimes.list":
|
|
2210
|
+
return { path: "/v1/desktop/runtimes?probe=false", method: "GET" };
|
|
2076
2211
|
/*
|
|
2077
2212
|
* The mesh reads. Both are plain GETs with no parameters at all - the backend
|
|
2078
2213
|
* reads THIS device's own relay, so there is nothing for the client to scope it
|
|
@@ -2406,6 +2541,16 @@ function desktopEndpoint(request) {
|
|
|
2406
2541
|
}
|
|
2407
2542
|
};
|
|
2408
2543
|
case "sessions.answer":
|
|
2544
|
+
if (request.askId !== void 0)
|
|
2545
|
+
return {
|
|
2546
|
+
path: `/v1/desktop/sessions/${request.sessionId}/answers`,
|
|
2547
|
+
method: "POST",
|
|
2548
|
+
body: {
|
|
2549
|
+
ask_id: request.askId,
|
|
2550
|
+
answers: request.answers,
|
|
2551
|
+
decline: request.decline
|
|
2552
|
+
}
|
|
2553
|
+
};
|
|
2409
2554
|
return {
|
|
2410
2555
|
path: `/v1/desktop/sessions/${request.sessionId}/answers`,
|
|
2411
2556
|
method: "POST",
|
|
@@ -2854,7 +2999,17 @@ function desktopEndpoint(request) {
|
|
|
2854
2999
|
body: {
|
|
2855
3000
|
request_id: request.requestId,
|
|
2856
3001
|
message: request.message,
|
|
2857
|
-
boundary: request.boundary
|
|
3002
|
+
boundary: request.boundary,
|
|
3003
|
+
/*
|
|
3004
|
+
* The route validates the two TOGETHER: `entry_id` with `next_safe`
|
|
3005
|
+
* is a 422, and `at_entry` without one is a 422. This builder does
|
|
3006
|
+
* not police that pairing (the schema above is what a renderer may
|
|
3007
|
+
* send; the route is what adjudicates it), but it does keep the
|
|
3008
|
+
* absent case byte-identical to the call that shipped before the
|
|
3009
|
+
* cut existed - `undefined` is dropped by `JSON.stringify`, so a
|
|
3010
|
+
* `next_safe` fork sends no `entry_id` key at all.
|
|
3011
|
+
*/
|
|
3012
|
+
entry_id: request.entryId
|
|
2858
3013
|
}
|
|
2859
3014
|
};
|
|
2860
3015
|
case "sessions.stop":
|
|
@@ -3088,6 +3243,11 @@ function desktopEndpoint(request) {
|
|
|
3088
3243
|
path: `/v1/desktop/projects/${encodeURIComponent(request.key)}/milestones/${encodeURIComponent(request.name)}`,
|
|
3089
3244
|
method: "DELETE"
|
|
3090
3245
|
};
|
|
3246
|
+
case "projects.request_update":
|
|
3247
|
+
return {
|
|
3248
|
+
path: `/v1/desktop/projects/${encodeURIComponent(request.key)}/request-update`,
|
|
3249
|
+
method: "POST"
|
|
3250
|
+
};
|
|
3091
3251
|
case "aida.status":
|
|
3092
3252
|
return { path: "/v1/desktop/aida", method: "GET" };
|
|
3093
3253
|
case "aida.control":
|
|
@@ -5733,9 +5893,12 @@ function resolveGlobalInstallPlan(input) {
|
|
|
5733
5893
|
* no cost at all before the click (review U3). It named the restart as its
|
|
5734
5894
|
* cost for the same reason - it was the one destructive thing this path
|
|
5735
5895
|
* did. It no longer is: on the generation layout the install lands beside
|
|
5736
|
-
* the running build and the server
|
|
5737
|
-
*
|
|
5738
|
-
*
|
|
5896
|
+
* the running build and the server MOVES ONTO IT as part of the update -
|
|
5897
|
+
* the in-place serve reload, verified by the serving process's own record
|
|
5898
|
+
* (a restart remains only the fallback for a reload that cannot be proven
|
|
5899
|
+
* to land, and even that cuts no turn) - so the cost is the install and
|
|
5900
|
+
* the wait, and no sentence here may describe a turn being dropped or
|
|
5901
|
+
* defer the move to some later idle.
|
|
5739
5902
|
*
|
|
5740
5903
|
* Source build - the same disclosure with the one difference that matters:
|
|
5741
5904
|
* this route DOES rewrite a tree a live runtime is reading, so its sentence
|
|
@@ -5754,7 +5917,7 @@ function resolveGlobalInstallPlan(input) {
|
|
|
5754
5917
|
* panels, and in the by-hand panel the next line was the version sentence,
|
|
5755
5918
|
* so the promise landed on the wrong line (review D5).
|
|
5756
5919
|
*/
|
|
5757
|
-
remedy: managed ? "The app installs the new build beside the one in use and
|
|
5920
|
+
remedy: managed ? "The app installs the new build beside the one in use and moves the server onto it immediately, so nothing in flight is cut off." : sourceRebuildRoute ? "Rebuilds this checkout with `lop-update`. The app waits for the turns running on this machine to finish first, and the rebuild then reinstalls this install in place - so a turn started while it runs can still be interrupted - and it can take up to half an hour. This install keeps reporting the checkout's version, not the release the app offered." : "This install predates the non-disruptive installer, so update it once from your terminal; that updater rewrites the shared environment in place, which can interrupt sessions mid-turn. The app manages updates after that.",
|
|
5758
5921
|
detail: `${detail}${managed ? provenance : sourceRebuildRoute ? " source build of this machine's checkout; an in-place rebuild." : provenance}`,
|
|
5759
5922
|
sourceBuild,
|
|
5760
5923
|
managedRoute: managed ? "entry-point" : sourceRebuildRoute ? "source-build" : null,
|
|
@@ -5859,8 +6022,10 @@ function serveRecord(pid, dir) {
|
|
|
5859
6022
|
}
|
|
5860
6023
|
function servingInstallReadings(record) {
|
|
5861
6024
|
const version = record?.version?.trim() ?? "";
|
|
6025
|
+
const instanceId = record?.instance_id?.trim() ?? "";
|
|
5862
6026
|
return {
|
|
5863
6027
|
bootVersion: version === "" ? null : version,
|
|
6028
|
+
instanceId: instanceId === "" ? null : instanceId,
|
|
5864
6029
|
prefix: record?.prefix?.trim() ?? "",
|
|
5865
6030
|
installKind: record?.install_kind?.trim() ?? "",
|
|
5866
6031
|
startedByApp: record?.desktop === true && (record.claim_key ?? "") === ""
|
|
@@ -10771,13 +10936,14 @@ class BackendServiceManager {
|
|
|
10771
10936
|
* Stop only the captured serve generation, including failed startups. A port
|
|
10772
10937
|
* or process name is not ownership. Unconfirmed exit blocks replacement.
|
|
10773
10938
|
*/
|
|
10774
|
-
async stop(isRestart = false) {
|
|
10939
|
+
async stop(isRestart = false, options = {}) {
|
|
10775
10940
|
this.startEpoch++;
|
|
10776
10941
|
if (!isRestart) this.isAppClosing = true;
|
|
10777
10942
|
this.disposeStreamRelay();
|
|
10778
10943
|
this.stopHealthCheck();
|
|
10779
10944
|
const generation = this.ownedServe;
|
|
10780
|
-
if (generation)
|
|
10945
|
+
if (generation)
|
|
10946
|
+
await this.stopGeneration(generation, isRestart, options.graceMs);
|
|
10781
10947
|
await this.startPromise;
|
|
10782
10948
|
}
|
|
10783
10949
|
captureServe(child) {
|
|
@@ -10835,8 +11001,17 @@ class BackendServiceManager {
|
|
|
10835
11001
|
* "we signalled it and moved on" would authorise exactly the double-serve
|
|
10836
11002
|
* this class exists to prevent. An unconfirmed exit keeps ownership, and the
|
|
10837
11003
|
* caller reports failure instead of proceeding.
|
|
11004
|
+
*
|
|
11005
|
+
* `graceMs` OVERRIDES THE RESTART GRACE FOR ONE CALL (2026-09-30), added for
|
|
11006
|
+
* the update fallback: a serve being stopped because its reload could not be
|
|
11007
|
+
* proven drains for as long as uvicorn waits for its connections (the
|
|
11008
|
+
* server's own wait has no deadline), and the caller that knows the process
|
|
11009
|
+
* is about to be replaced may bound that wait. The ESCALATION is unchanged -
|
|
11010
|
+
* SIGTERM, the (possibly shorter) grace, SIGKILL, the force wait - so an
|
|
11011
|
+
* unresponsive process still fails closed, and a caller that passes nothing
|
|
11012
|
+
* gets the same grace it always got.
|
|
10838
11013
|
*/
|
|
10839
|
-
stopGeneration(generation, isRestart) {
|
|
11014
|
+
stopGeneration(generation, isRestart, graceMs) {
|
|
10840
11015
|
if (generation.stop) return generation.stop;
|
|
10841
11016
|
generation.stop = (async () => {
|
|
10842
11017
|
const waitForExit = (ms) => new Promise((resolve) => {
|
|
@@ -10856,7 +11031,7 @@ class BackendServiceManager {
|
|
|
10856
11031
|
});
|
|
10857
11032
|
});
|
|
10858
11033
|
if (this.canSignal(generation)) generation.child.kill("SIGTERM");
|
|
10859
|
-
const grace = isRestart ? this.shutdownTimeoutMs.restart : this.shutdownTimeoutMs.normal;
|
|
11034
|
+
const grace = graceMs ?? (isRestart ? this.shutdownTimeoutMs.restart : this.shutdownTimeoutMs.normal);
|
|
10860
11035
|
if (!await waitForExit(grace)) {
|
|
10861
11036
|
if (this.canSignal(generation)) generation.child.kill("SIGKILL");
|
|
10862
11037
|
if (!await waitForExit(this.shutdownTimeoutMs.force)) {
|
|
@@ -11798,13 +11973,18 @@ class BackendServiceManager {
|
|
|
11798
11973
|
* Restart the backend service
|
|
11799
11974
|
* This method properly handles the restart process to ensure that any pending
|
|
11800
11975
|
* timeouts from the stop operation don't affect the newly started process
|
|
11976
|
+
*
|
|
11977
|
+
* `options.stopGraceMs` bounds ONE restart's SIGTERM grace (see
|
|
11978
|
+
* `stopGeneration`); it exists for the update fallback, whose process is
|
|
11979
|
+
* already known to be replaced, and callers that pass nothing keep the
|
|
11980
|
+
* default grace exactly as before.
|
|
11801
11981
|
* @returns Promise resolving to true if the restart was successful, false otherwise
|
|
11802
11982
|
*/
|
|
11803
|
-
restart() {
|
|
11983
|
+
restart(options = {}) {
|
|
11804
11984
|
if (this.restartPromise) return this.restartPromise;
|
|
11805
11985
|
this.restartPromise = (async () => {
|
|
11806
11986
|
try {
|
|
11807
|
-
await this.stop(true);
|
|
11987
|
+
await this.stop(true, { graceMs: options.stopGraceMs });
|
|
11808
11988
|
if (this.isAppClosing) return false;
|
|
11809
11989
|
return await this.start();
|
|
11810
11990
|
} catch (error) {
|
|
@@ -13634,6 +13814,14 @@ const METHODS$1 = [
|
|
|
13634
13814
|
"tabs",
|
|
13635
13815
|
"scroll",
|
|
13636
13816
|
"logs",
|
|
13817
|
+
// The structured read actions (`styles`, `hit_test`, `ancestors`), in
|
|
13818
|
+
// `protocol.py`'s tuple order: page-geometry reads answered by fixed
|
|
13819
|
+
// self-contained page functions (the vendored `driver/geometry-read.ts`),
|
|
13820
|
+
// capped and rounded in the page. This array is the wire's closed gate, so
|
|
13821
|
+
// it must stay byte-comparable with the Python source it mirrors.
|
|
13822
|
+
"styles",
|
|
13823
|
+
"hit_test",
|
|
13824
|
+
"ancestors",
|
|
13637
13825
|
"request_access",
|
|
13638
13826
|
"await_access",
|
|
13639
13827
|
"cancel_access",
|
|
@@ -13692,7 +13880,13 @@ const ERROR_CODES = [
|
|
|
13692
13880
|
// a command that hangs rather than one that fails.
|
|
13693
13881
|
...CONSOLE_ERROR_CODES
|
|
13694
13882
|
];
|
|
13695
|
-
const HOST_CAPABILITIES = [
|
|
13883
|
+
const HOST_CAPABILITIES = [
|
|
13884
|
+
"styles",
|
|
13885
|
+
"hit_test",
|
|
13886
|
+
"ancestors",
|
|
13887
|
+
"download",
|
|
13888
|
+
"upload"
|
|
13889
|
+
];
|
|
13696
13890
|
const RUN_DIRNAME = path$1.join("run", "ui-browser");
|
|
13697
13891
|
const STATE_FILENAME = "host.json";
|
|
13698
13892
|
const STATE_DIR_MODE = 448;
|
|
@@ -19314,6 +19508,135 @@ function clampTimeout(requested) {
|
|
|
19314
19508
|
if (!Number.isFinite(wanted) || wanted <= 0) return CAPS.downloadTimeoutS;
|
|
19315
19509
|
return Math.min(wanted, CAPS.downloadTimeoutMaxS);
|
|
19316
19510
|
}
|
|
19511
|
+
const READ_STYLES_SOURCE = '(selector, properties) => {\n const DEFAULTS = [\n "display",\n "position",\n "visibility",\n "opacity",\n "z-index",\n "transform",\n "transform-origin",\n "top",\n "right",\n "bottom",\n "left",\n "width",\n "height",\n "contain",\n "filter",\n "will-change",\n "overflow",\n "overflow-x",\n "overflow-y",\n "pointer-events"\n ];\n const MAX_MATCHES = 5;\n const MAX_STYLES = 30;\n const MAX_INLINE = 30;\n const MAX_IDENTITY = 120;\n const MAX_VALUE = 200;\n const all = document.querySelectorAll(selector);\n if (!all || all.length === 0) return null;\n const names = DEFAULTS.slice();\n if (Array.isArray(properties)) {\n for (let i = 0; i < properties.length && names.length < MAX_STYLES; i++) {\n const name = String(properties[i] ?? "").trim();\n if (name && names.indexOf(name) === -1) names.push(name);\n }\n }\n const round2 = (value) => {\n const rounded = Math.round(value * 100) / 100;\n return rounded === 0 ? 0 : rounded;\n };\n const clip = (value, limit) => value.length > limit ? value.slice(0, limit - 1) + "\\u2026" : value;\n const rectOf = (el) => {\n const r = el.getBoundingClientRect();\n return {\n x: round2(r.x),\n y: round2(r.y),\n top: round2(r.top),\n right: round2(r.right),\n bottom: round2(r.bottom),\n width: round2(r.width),\n height: round2(r.height)\n };\n };\n const attrOf = (el, name) => clip(el.getAttribute(name) || "", MAX_IDENTITY);\n const matches = [];\n const first = Array.from(all).slice(0, MAX_MATCHES);\n for (const el of first) {\n const computed = getComputedStyle(el);\n const styles = {};\n for (const name of names) {\n styles[name] = clip(String(computed.getPropertyValue(name) || "").trim(), MAX_VALUE);\n }\n const inline = {};\n const declaration = el.style;\n let inlineCount = 0;\n for (let j = 0; j < declaration.length && inlineCount < MAX_INLINE; j++) {\n const name = declaration.item(j);\n if (name && name.indexOf("--") === 0) {\n const key = clip(name, MAX_IDENTITY);\n if (!(key in inline)) {\n inline[key] = clip(\n String(declaration.getPropertyValue(name) || "").trim(),\n MAX_VALUE\n );\n inlineCount += 1;\n }\n }\n }\n matches.push({\n tag: clip(el.tagName.toLowerCase(), MAX_IDENTITY),\n id: attrOf(el, "id"),\n role: attrOf(el, "role"),\n className: attrOf(el, "class"),\n rect: rectOf(el),\n styles,\n inline\n });\n }\n return { count: matches.length, matches, truncated: all.length > matches.length };\n}';
|
|
19512
|
+
const HIT_TEST_SOURCE = '(x, y) => {\n const PROPS = ["display", "position", "visibility", "opacity", "z-index", "pointer-events"];\n const MAX_ELEMENTS = 8;\n const MAX_IDENTITY = 120;\n const MAX_VALUE = 200;\n const px = Number(x);\n const py = Number(y);\n if (!Number.isFinite(px) || !Number.isFinite(py)) return null;\n const stack = document.elementsFromPoint(px, py);\n if (!stack || stack.length === 0) return null;\n const round2 = (value) => {\n const rounded = Math.round(value * 100) / 100;\n return rounded === 0 ? 0 : rounded;\n };\n const clip = (value, limit) => value.length > limit ? value.slice(0, limit - 1) + "\\u2026" : value;\n const attrOf = (el, name) => clip(el.getAttribute(name) || "", MAX_IDENTITY);\n const elements = [];\n for (const el of stack.slice(0, MAX_ELEMENTS)) {\n const computed = getComputedStyle(el);\n const styles = {};\n for (const name of PROPS) {\n styles[name] = clip(String(computed.getPropertyValue(name) || "").trim(), MAX_VALUE);\n }\n const r = el.getBoundingClientRect();\n elements.push({\n tag: clip(el.tagName.toLowerCase(), MAX_IDENTITY),\n id: attrOf(el, "id"),\n role: attrOf(el, "role"),\n className: attrOf(el, "class"),\n rect: {\n x: round2(r.x),\n y: round2(r.y),\n top: round2(r.top),\n right: round2(r.right),\n bottom: round2(r.bottom),\n width: round2(r.width),\n height: round2(r.height)\n },\n styles\n });\n }\n return { count: elements.length, elements };\n}';
|
|
19513
|
+
const ANCESTORS_SOURCE = '(selector, depth) => {\n const PROPS = [\n "display",\n "position",\n "visibility",\n "overflow",\n "overflow-x",\n "overflow-y",\n "transform",\n "transform-origin",\n "contain",\n "filter",\n "will-change",\n "z-index",\n "isolation",\n "top",\n "left"\n ];\n const DEFAULT_DEPTH = 12;\n const MAX_DEPTH = 16;\n const MAX_IDENTITY = 120;\n const MAX_VALUE = 200;\n const el = document.querySelector(selector);\n if (!el) return null;\n const wanted = typeof depth === "number" && Number.isFinite(depth) ? Math.floor(depth) : DEFAULT_DEPTH;\n const bound = Math.max(1, Math.min(MAX_DEPTH, wanted));\n const round2 = (value) => {\n const rounded = Math.round(value * 100) / 100;\n return rounded === 0 ? 0 : rounded;\n };\n const clip = (value, limit) => value.length > limit ? value.slice(0, limit - 1) + "\\u2026" : value;\n const attrOf = (el2, name) => clip(el2.getAttribute(name) || "", MAX_IDENTITY);\n const chain = [];\n let node = el;\n while (node && chain.length < bound) {\n const computed = getComputedStyle(node);\n const styles = {};\n for (const name of PROPS) {\n styles[name] = clip(String(computed.getPropertyValue(name) || "").trim(), MAX_VALUE);\n }\n const r = node.getBoundingClientRect();\n chain.push({\n tag: clip(node.tagName.toLowerCase(), MAX_IDENTITY),\n id: attrOf(node, "id"),\n role: attrOf(node, "role"),\n className: attrOf(node, "class"),\n rect: {\n x: round2(r.x),\n y: round2(r.y),\n top: round2(r.top),\n right: round2(r.right),\n bottom: round2(r.bottom),\n width: round2(r.width),\n height: round2(r.height)\n },\n styles\n });\n if (node === document.documentElement) break;\n node = node.parentElement;\n }\n return { count: chain.length, chain };\n}';
|
|
19514
|
+
const MAX_REQUESTED_PROPERTIES = 20;
|
|
19515
|
+
const DEFAULT_DEPTH = 12;
|
|
19516
|
+
const MAX_DEPTH = 16;
|
|
19517
|
+
async function runFixed(record, source, args, label) {
|
|
19518
|
+
const code = `(${source})(${args.map((arg) => JSON.stringify(arg)).join(", ")})`;
|
|
19519
|
+
return await deadline(
|
|
19520
|
+
record.view.webContents.executeJavaScriptInIsolatedWorld(
|
|
19521
|
+
ISOLATED_WORLD_ID,
|
|
19522
|
+
[{ code }]
|
|
19523
|
+
),
|
|
19524
|
+
SCRIPTING_DEADLINE_MS,
|
|
19525
|
+
label
|
|
19526
|
+
);
|
|
19527
|
+
}
|
|
19528
|
+
function selectorError(selector, error) {
|
|
19529
|
+
if (error instanceof Error && INVALID_SELECTOR.test(error.message)) {
|
|
19530
|
+
return new BridgeCommandError(
|
|
19531
|
+
"element_not_found",
|
|
19532
|
+
`selector ${selector} is not valid`
|
|
19533
|
+
);
|
|
19534
|
+
}
|
|
19535
|
+
return error;
|
|
19536
|
+
}
|
|
19537
|
+
function structuredResult(name, result) {
|
|
19538
|
+
if (typeof result !== "object" || result === null) {
|
|
19539
|
+
throw new BridgeCommandError(
|
|
19540
|
+
"internal",
|
|
19541
|
+
`the page returned no structured ${name} result`
|
|
19542
|
+
);
|
|
19543
|
+
}
|
|
19544
|
+
return result;
|
|
19545
|
+
}
|
|
19546
|
+
function requiredSelector(params) {
|
|
19547
|
+
const selector = stringParam(params, "selector");
|
|
19548
|
+
if (!selector) {
|
|
19549
|
+
throw new BridgeCommandError("element_not_found", "selector is required");
|
|
19550
|
+
}
|
|
19551
|
+
return selector;
|
|
19552
|
+
}
|
|
19553
|
+
function requestedProperties(params) {
|
|
19554
|
+
if (!Array.isArray(params.properties)) return [];
|
|
19555
|
+
return params.properties.filter((entry) => typeof entry === "string").slice(0, MAX_REQUESTED_PROPERTIES);
|
|
19556
|
+
}
|
|
19557
|
+
async function styles(ctx, params) {
|
|
19558
|
+
const record = ctx.registry.requireSurface(params.tab);
|
|
19559
|
+
const selector = requiredSelector(params);
|
|
19560
|
+
const properties = requestedProperties(params);
|
|
19561
|
+
let result;
|
|
19562
|
+
try {
|
|
19563
|
+
result = await runFixed(
|
|
19564
|
+
record,
|
|
19565
|
+
READ_STYLES_SOURCE,
|
|
19566
|
+
[selector, properties],
|
|
19567
|
+
`styles(${selector})`
|
|
19568
|
+
);
|
|
19569
|
+
} catch (error) {
|
|
19570
|
+
throw selectorError(selector, error);
|
|
19571
|
+
}
|
|
19572
|
+
if (result === null || result === void 0) {
|
|
19573
|
+
throw new BridgeCommandError(
|
|
19574
|
+
"element_not_found",
|
|
19575
|
+
`selector ${selector} matched nothing`
|
|
19576
|
+
);
|
|
19577
|
+
}
|
|
19578
|
+
ctx.registry.touch(record);
|
|
19579
|
+
return { ...structuredResult("styles", result), ...pageOf(record.view) };
|
|
19580
|
+
}
|
|
19581
|
+
async function hitTest(ctx, params) {
|
|
19582
|
+
const record = ctx.registry.requireSurface(params.tab);
|
|
19583
|
+
const x = numberParam(params, "x");
|
|
19584
|
+
const y = numberParam(params, "y");
|
|
19585
|
+
if (x === void 0 || y === void 0) {
|
|
19586
|
+
throw new BridgeCommandError(
|
|
19587
|
+
"internal",
|
|
19588
|
+
"'hit_test' needs numeric x and y (viewport coordinates)"
|
|
19589
|
+
);
|
|
19590
|
+
}
|
|
19591
|
+
const result = await runFixed(
|
|
19592
|
+
record,
|
|
19593
|
+
HIT_TEST_SOURCE,
|
|
19594
|
+
[x, y],
|
|
19595
|
+
`hit_test(${x}, ${y})`
|
|
19596
|
+
);
|
|
19597
|
+
if (result === null || result === void 0) {
|
|
19598
|
+
throw new BridgeCommandError(
|
|
19599
|
+
"element_not_found",
|
|
19600
|
+
`no element at point (${x}, ${y})`
|
|
19601
|
+
);
|
|
19602
|
+
}
|
|
19603
|
+
ctx.registry.touch(record);
|
|
19604
|
+
return {
|
|
19605
|
+
...structuredResult("hit_test", result),
|
|
19606
|
+
...pageOf(record.view)
|
|
19607
|
+
};
|
|
19608
|
+
}
|
|
19609
|
+
async function ancestors(ctx, params) {
|
|
19610
|
+
const record = ctx.registry.requireSurface(params.tab);
|
|
19611
|
+
const selector = requiredSelector(params);
|
|
19612
|
+
const raw = numberParam(params, "depth");
|
|
19613
|
+
const depth = Math.max(
|
|
19614
|
+
1,
|
|
19615
|
+
Math.min(MAX_DEPTH, raw === void 0 ? DEFAULT_DEPTH : Math.floor(raw))
|
|
19616
|
+
);
|
|
19617
|
+
let result;
|
|
19618
|
+
try {
|
|
19619
|
+
result = await runFixed(
|
|
19620
|
+
record,
|
|
19621
|
+
ANCESTORS_SOURCE,
|
|
19622
|
+
[selector, depth],
|
|
19623
|
+
`ancestors(${selector})`
|
|
19624
|
+
);
|
|
19625
|
+
} catch (error) {
|
|
19626
|
+
throw selectorError(selector, error);
|
|
19627
|
+
}
|
|
19628
|
+
if (result === null || result === void 0) {
|
|
19629
|
+
throw new BridgeCommandError(
|
|
19630
|
+
"element_not_found",
|
|
19631
|
+
`selector ${selector} matched nothing`
|
|
19632
|
+
);
|
|
19633
|
+
}
|
|
19634
|
+
ctx.registry.touch(record);
|
|
19635
|
+
return {
|
|
19636
|
+
...structuredResult("ancestors", result),
|
|
19637
|
+
...pageOf(record.view)
|
|
19638
|
+
};
|
|
19639
|
+
}
|
|
19317
19640
|
const MAX_AGENT_TABS = 8;
|
|
19318
19641
|
const BACKGROUND_VIEWPORT = {
|
|
19319
19642
|
x: 0,
|
|
@@ -20278,6 +20601,12 @@ function factOf(path2) {
|
|
|
20278
20601
|
}
|
|
20279
20602
|
const DOCUMENT_SCOPED = /* @__PURE__ */ new Set([
|
|
20280
20603
|
"read",
|
|
20604
|
+
// The structured reads (`styles`, `hit_test`, `ancestors`) describe the
|
|
20605
|
+
// CURRENT document exactly as `read` does, so a result that raced a
|
|
20606
|
+
// navigation it did not cause must be discarded the same way.
|
|
20607
|
+
"styles",
|
|
20608
|
+
"hit_test",
|
|
20609
|
+
"ancestors",
|
|
20281
20610
|
"snapshot",
|
|
20282
20611
|
"screenshot",
|
|
20283
20612
|
"click",
|
|
@@ -20305,6 +20634,9 @@ const NAVIGATION_ACTIONS = /* @__PURE__ */ new Set(["click", "type"]);
|
|
|
20305
20634
|
const TAB_SCOPED = /* @__PURE__ */ new Set([
|
|
20306
20635
|
"goto",
|
|
20307
20636
|
"read",
|
|
20637
|
+
"styles",
|
|
20638
|
+
"hit_test",
|
|
20639
|
+
"ancestors",
|
|
20308
20640
|
"snapshot",
|
|
20309
20641
|
"screenshot",
|
|
20310
20642
|
"click",
|
|
@@ -20482,6 +20814,12 @@ class BrowserHost {
|
|
|
20482
20814
|
return retitle();
|
|
20483
20815
|
case "read":
|
|
20484
20816
|
return read(this, params);
|
|
20817
|
+
case "styles":
|
|
20818
|
+
return styles(this, params);
|
|
20819
|
+
case "hit_test":
|
|
20820
|
+
return hitTest(this, params);
|
|
20821
|
+
case "ancestors":
|
|
20822
|
+
return ancestors(this, params);
|
|
20485
20823
|
case "snapshot":
|
|
20486
20824
|
return snapshot(this, params);
|
|
20487
20825
|
case "scroll":
|
|
@@ -24238,6 +24576,7 @@ async function startBrowserHost(options) {
|
|
|
24238
24576
|
}
|
|
24239
24577
|
});
|
|
24240
24578
|
window.contentView.addChildView(view);
|
|
24579
|
+
view.setBounds(BACKGROUND_VIEWPORT);
|
|
24241
24580
|
view.setVisible(false);
|
|
24242
24581
|
return view;
|
|
24243
24582
|
}
|
|
@@ -33599,6 +33938,123 @@ function titlebarOptions(platform, chrome) {
|
|
|
33599
33938
|
}
|
|
33600
33939
|
};
|
|
33601
33940
|
}
|
|
33941
|
+
const UPDATE_REPORT_KEY = "update_report";
|
|
33942
|
+
const UPDATE_REPORT_SCHEMA_PREFIX = "update.report.";
|
|
33943
|
+
const DAEMON_STATUSES = /* @__PURE__ */ new Set([
|
|
33944
|
+
"refreshed",
|
|
33945
|
+
"already",
|
|
33946
|
+
"failed",
|
|
33947
|
+
"unsupervised"
|
|
33948
|
+
]);
|
|
33949
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
33950
|
+
function parseUpdateReport(stdout) {
|
|
33951
|
+
let found = null;
|
|
33952
|
+
for (const line of stdout.split("\n")) {
|
|
33953
|
+
const trimmed = line.trim();
|
|
33954
|
+
if (!trimmed.startsWith("{") || !trimmed.includes(UPDATE_REPORT_KEY))
|
|
33955
|
+
continue;
|
|
33956
|
+
let payload;
|
|
33957
|
+
try {
|
|
33958
|
+
payload = JSON.parse(trimmed);
|
|
33959
|
+
} catch {
|
|
33960
|
+
continue;
|
|
33961
|
+
}
|
|
33962
|
+
const report = reportFromPayload(payload);
|
|
33963
|
+
if (report) found = report;
|
|
33964
|
+
}
|
|
33965
|
+
return found;
|
|
33966
|
+
}
|
|
33967
|
+
function reportFromPayload(payload) {
|
|
33968
|
+
if (!isRecord(payload)) return null;
|
|
33969
|
+
const raw = payload[UPDATE_REPORT_KEY];
|
|
33970
|
+
if (!isRecord(raw)) return null;
|
|
33971
|
+
const schema2 = raw.schema;
|
|
33972
|
+
if (typeof schema2 !== "string" || !schema2.startsWith(UPDATE_REPORT_SCHEMA_PREFIX))
|
|
33973
|
+
return null;
|
|
33974
|
+
const installVersion = raw.install_version;
|
|
33975
|
+
const target2 = raw.target;
|
|
33976
|
+
if (typeof installVersion !== "string" || typeof target2 !== "string")
|
|
33977
|
+
return null;
|
|
33978
|
+
const serve = raw.serve;
|
|
33979
|
+
const daemons = raw.daemons;
|
|
33980
|
+
if (!Array.isArray(serve) || !Array.isArray(daemons)) return null;
|
|
33981
|
+
const serveEntries = [];
|
|
33982
|
+
for (const entry of serve) {
|
|
33983
|
+
if (!isRecord(entry)) return null;
|
|
33984
|
+
const { pid, from, to, moved, instance } = entry;
|
|
33985
|
+
if (typeof pid !== "number" || typeof from !== "string" || typeof to !== "string" || typeof moved !== "boolean" || typeof instance !== "string")
|
|
33986
|
+
return null;
|
|
33987
|
+
serveEntries.push({ pid, from, to, moved, instance });
|
|
33988
|
+
}
|
|
33989
|
+
const daemonEntries = [];
|
|
33990
|
+
for (const entry of daemons) {
|
|
33991
|
+
if (!isRecord(entry)) return null;
|
|
33992
|
+
const { name, status: status2, version } = entry;
|
|
33993
|
+
if (typeof name !== "string" || typeof status2 !== "string" || !DAEMON_STATUSES.has(status2) || typeof version !== "string")
|
|
33994
|
+
return null;
|
|
33995
|
+
daemonEntries.push({
|
|
33996
|
+
name,
|
|
33997
|
+
status: status2,
|
|
33998
|
+
version
|
|
33999
|
+
});
|
|
34000
|
+
}
|
|
34001
|
+
return {
|
|
34002
|
+
schema: schema2,
|
|
34003
|
+
install_version: installVersion,
|
|
34004
|
+
target: target2,
|
|
34005
|
+
serve: serveEntries,
|
|
34006
|
+
daemons: daemonEntries
|
|
34007
|
+
};
|
|
34008
|
+
}
|
|
34009
|
+
function serveMovedOntoBuild(input) {
|
|
34010
|
+
if (!input.healthOk) return false;
|
|
34011
|
+
const before = input.instanceBefore?.trim() ?? "";
|
|
34012
|
+
if (before === "") return false;
|
|
34013
|
+
const after = input.recordAfter.instanceId?.trim() || input.reportServe?.instance.trim() || "";
|
|
34014
|
+
if (after === "" || after === before) return false;
|
|
34015
|
+
return serveOnTarget(
|
|
34016
|
+
input.recordAfter.bootVersion,
|
|
34017
|
+
input.installVersion,
|
|
34018
|
+
input.target
|
|
34019
|
+
);
|
|
34020
|
+
}
|
|
34021
|
+
function serveOnTarget(bootVersion, installVersion, target2) {
|
|
34022
|
+
const boot = bootVersion?.trim() ?? "";
|
|
34023
|
+
if (boot === "") return false;
|
|
34024
|
+
const wanted = target2?.trim() ?? "";
|
|
34025
|
+
if (wanted !== "") {
|
|
34026
|
+
const order2 = compareVersions(boot, wanted);
|
|
34027
|
+
return order2 !== null && order2 >= 0;
|
|
34028
|
+
}
|
|
34029
|
+
const installed = installVersion?.trim() ?? "";
|
|
34030
|
+
if (installed === "") return false;
|
|
34031
|
+
const order = compareVersions(boot, installed);
|
|
34032
|
+
return order === null ? boot === installed : order >= 0;
|
|
34033
|
+
}
|
|
34034
|
+
function runtimeStragglerCount(body, current) {
|
|
34035
|
+
const currentBuild = current?.trim() ?? "";
|
|
34036
|
+
if (currentBuild === "") return null;
|
|
34037
|
+
if (!isRecord(body)) return null;
|
|
34038
|
+
const result = body.result;
|
|
34039
|
+
if (!isRecord(result)) return null;
|
|
34040
|
+
const runtimes = result.runtimes;
|
|
34041
|
+
if (!Array.isArray(runtimes)) return null;
|
|
34042
|
+
let count = 0;
|
|
34043
|
+
for (const row of runtimes) {
|
|
34044
|
+
if (!isRecord(row)) continue;
|
|
34045
|
+
const build = typeof row.build_version === "string" ? row.build_version.trim() : "";
|
|
34046
|
+
if (build === "") continue;
|
|
34047
|
+
if (build !== currentBuild) count += 1;
|
|
34048
|
+
}
|
|
34049
|
+
return count;
|
|
34050
|
+
}
|
|
34051
|
+
function daemonMoveOutcome(report) {
|
|
34052
|
+
if (report === null) return null;
|
|
34053
|
+
const failed = report.daemons.filter(
|
|
34054
|
+
(daemon) => daemon.status === "failed" || daemon.status === "unsupervised"
|
|
34055
|
+
).map((daemon) => daemon.name);
|
|
34056
|
+
return { source: "report", moved: failed.length === 0, failed };
|
|
34057
|
+
}
|
|
33602
34058
|
const CHROMIUM_TRANSPORT_CODES = [
|
|
33603
34059
|
"ERR_INTERNET_DISCONNECTED",
|
|
33604
34060
|
"ERR_NETWORK_CHANGED",
|
|
@@ -34388,6 +34844,7 @@ const BETA_VERSION_REGEX = /v\d+\.\d+\.\d+\.beta\.\d+/;
|
|
|
34388
34844
|
const ARCHITECTURE_SEPARATOR_REGEX = /\s+/;
|
|
34389
34845
|
const GLOBAL_UPDATE_TIMEOUT_MS = 15 * 60 * 1e3;
|
|
34390
34846
|
const SOURCE_REBUILD_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
34847
|
+
const UPDATE_RESTART_STOP_GRACE_MS = 5e3;
|
|
34391
34848
|
const BUNDLE_ID_REGEX = /^[\w.-]+$/;
|
|
34392
34849
|
const INSTALL_IN_FLIGHT_RECHECK_MS = 1e4;
|
|
34393
34850
|
const LAUNCHCTL_NOT_LOADED_REGEX = /could not find|no such process/i;
|
|
@@ -38877,6 +39334,219 @@ ${heal.removed.join("\n")}`,
|
|
|
38877
39334
|
async readFleetSnapshot(backend) {
|
|
38878
39335
|
return backend.servingSessionFleet();
|
|
38879
39336
|
}
|
|
39337
|
+
/**
|
|
39338
|
+
* The serving process's own record, both fields from ONE read.
|
|
39339
|
+
*
|
|
39340
|
+
* The post-swap move proof compares an instance token against the pre-swap
|
|
39341
|
+
* one, and a pair assembled from two reads can straddle a republish (a new
|
|
39342
|
+
* instance under the OLD version string, or the reverse) - so the two fields
|
|
39343
|
+
* come out of a single `servingInstall()` call, which reads the record file
|
|
39344
|
+
* once. Absence is both-fields-null, never a papered-over reading: a record
|
|
39345
|
+
* that could not be read proves nothing and the verdict says so.
|
|
39346
|
+
*/
|
|
39347
|
+
servingRecordReadings() {
|
|
39348
|
+
const readings = this.backendService?.servingInstall().readings ?? servingInstallReadings(null);
|
|
39349
|
+
return {
|
|
39350
|
+
bootVersion: readings.bootVersion,
|
|
39351
|
+
instanceId: readings.instanceId
|
|
39352
|
+
};
|
|
39353
|
+
}
|
|
39354
|
+
/**
|
|
39355
|
+
* The pre-swap rows still resident in a fresh read, or null when unreadable.
|
|
39356
|
+
*
|
|
39357
|
+
* The liveness count's own arithmetic, in one place because two callers need
|
|
39358
|
+
* it in different shapes (the restart fallback counts from this, the moved
|
|
39359
|
+
* path's fallback counts THROUGH it). A read that could not be taken is null
|
|
39360
|
+
* rather than an empty set: "nothing resident" would be a measured zero, and
|
|
39361
|
+
* an unanswered roster is not a measurement. A cold row (an empty
|
|
39362
|
+
* `liveState`, the catalogue's spelling for a session with no runtime) is not
|
|
39363
|
+
* resident - `isLiveRow` owns that rule, the same one the engagement diff
|
|
39364
|
+
* uses.
|
|
39365
|
+
*/
|
|
39366
|
+
stillResidentRows(before, after) {
|
|
39367
|
+
if (after === null) return null;
|
|
39368
|
+
const liveNow = new Set(
|
|
39369
|
+
after.filter((row) => isLiveRow(row)).map((row) => row.sessionId)
|
|
39370
|
+
);
|
|
39371
|
+
return before.filter((row) => isLiveRow(row) && liveNow.has(row.sessionId));
|
|
39372
|
+
}
|
|
39373
|
+
/**
|
|
39374
|
+
* The BUILD-accurate straggler count: runtimes not on the build the server
|
|
39375
|
+
* is on, from the machine's runtime roster, or null when it did not answer.
|
|
39376
|
+
*
|
|
39377
|
+
* THIS IS THE PRIMARY CENSUS since 2026-09-30 (design §5): `GET
|
|
39378
|
+
* /v1/desktop/runtimes?probe=false` returns one row per live runtime with the
|
|
39379
|
+
* `build_version` the runtime's own record (or boot record) carries, so the
|
|
39380
|
+
* count answers the question the notice's sentence makes - "still on the old
|
|
39381
|
+
* version" - from a build reading rather than from a liveness proxy. An empty
|
|
39382
|
+
* `build_version` is UNKNOWN and is not counted (the roster journal's own
|
|
39383
|
+
* rule), and a row whose build equals the current one has already rolled.
|
|
39384
|
+
*
|
|
39385
|
+
* Null (unreadable body, non-200, a transport failure, or no current build to
|
|
39386
|
+
* compare against) sends the caller to the liveness fallback; the failure is
|
|
39387
|
+
* logged here, where the transport error is still in hand. The read is
|
|
39388
|
+
* deliberately per-call rather than cached: it is the one reading whose whole
|
|
39389
|
+
* point is that it moves as runtimes roll, so every completion asks the
|
|
39390
|
+
* machine again. A notice-reopen re-read is NOT wired (review round 1's A1):
|
|
39391
|
+
* no renderer->main census channel exists yet, so the count travels with the
|
|
39392
|
+
* completion it was measured for - recorded as deferred on the PR rather
|
|
39393
|
+
* than claimed here.
|
|
39394
|
+
*/
|
|
39395
|
+
async readRuntimeCensus(backend, current) {
|
|
39396
|
+
try {
|
|
39397
|
+
const response = await backend.requestDesktop({ op: "runtimes.list" });
|
|
39398
|
+
if (response.status !== 200) {
|
|
39399
|
+
logger.info(
|
|
39400
|
+
`Runtime roster did not answer (${response.status}); the straggler count falls back to the liveness reading`,
|
|
39401
|
+
LogFileType.UPDATE_SERVICE
|
|
39402
|
+
);
|
|
39403
|
+
return null;
|
|
39404
|
+
}
|
|
39405
|
+
const count = runtimeStragglerCount(response.body, current);
|
|
39406
|
+
if (count === null) {
|
|
39407
|
+
logger.info(
|
|
39408
|
+
"Runtime roster answered with no usable rows; the straggler count falls back to the liveness reading",
|
|
39409
|
+
LogFileType.UPDATE_SERVICE
|
|
39410
|
+
);
|
|
39411
|
+
}
|
|
39412
|
+
return count;
|
|
39413
|
+
} catch (error) {
|
|
39414
|
+
logger.info(
|
|
39415
|
+
`Runtime roster read failed (${error instanceof Error ? error.message : String(error)}); the straggler count falls back to the liveness reading`,
|
|
39416
|
+
LogFileType.UPDATE_SERVICE
|
|
39417
|
+
);
|
|
39418
|
+
return null;
|
|
39419
|
+
}
|
|
39420
|
+
}
|
|
39421
|
+
/**
|
|
39422
|
+
* The straggler count and where it came from: build first, liveness second.
|
|
39423
|
+
*
|
|
39424
|
+
* The build count (`readRuntimeCensus`) is the primary. When the roster does
|
|
39425
|
+
* not answer, the fallback is the EXISTING liveness reading, in whichever
|
|
39426
|
+
* shape the path already has it: the restart fallback passes the rows its own
|
|
39427
|
+
* post-restart read found resident (`resident`), while the moved path passes
|
|
39428
|
+
* the pre-update snapshot (`before`) and this takes one fresh fleet read to
|
|
39429
|
+
* count it. A path with neither - no snapshot, or a fleet read that failed -
|
|
39430
|
+
* answers null/null, which the notice renders as the numberless sentence: a
|
|
39431
|
+
* count that cannot be measured is not a zero.
|
|
39432
|
+
*
|
|
39433
|
+
* The source travels with the count for the RECORD, and for any later reader
|
|
39434
|
+
* that needs to know how it was measured - it does not choose the notice's
|
|
39435
|
+
* words. The notice keeps ONE session-family sentence for both sources
|
|
39436
|
+
* (designer copy consult, 2026-09-30: the producer's vocabulary belongs in
|
|
39437
|
+
* the payload field, never in the reader's sentence), so a build count is not
|
|
39438
|
+
* printed as "N runtimes" and the liveness reading's documented over-count
|
|
39439
|
+
* rides the field beside the count instead of changing the sentence.
|
|
39440
|
+
*/
|
|
39441
|
+
async readStragglerCensus(input) {
|
|
39442
|
+
const build = await this.readRuntimeCensus(input.backend, input.current);
|
|
39443
|
+
if (build !== null) return { count: build, source: "build" };
|
|
39444
|
+
let resident;
|
|
39445
|
+
if (input.resident !== void 0) {
|
|
39446
|
+
resident = input.resident;
|
|
39447
|
+
} else if (input.before != null) {
|
|
39448
|
+
resident = this.stillResidentRows(
|
|
39449
|
+
input.before,
|
|
39450
|
+
await this.readFleetSnapshot(input.backend)
|
|
39451
|
+
);
|
|
39452
|
+
} else {
|
|
39453
|
+
resident = null;
|
|
39454
|
+
}
|
|
39455
|
+
if (resident === null) return { count: null, source: null };
|
|
39456
|
+
logger.info(
|
|
39457
|
+
`Straggler count from the liveness reading: ${resident.length} pre-swap session(s) still resident`,
|
|
39458
|
+
LogFileType.UPDATE_SERVICE
|
|
39459
|
+
);
|
|
39460
|
+
return { count: resident.length, source: "liveness" };
|
|
39461
|
+
}
|
|
39462
|
+
/**
|
|
39463
|
+
* The restart fallback's tail for a generation install: ONE read, the
|
|
39464
|
+
* engage, and the resident set - no retire wait (2026-09-30).
|
|
39465
|
+
*
|
|
39466
|
+
* WHY THERE IS NO WAIT HERE. The rebuild route's `reengageDisplacedSessions`
|
|
39467
|
+
* waits for the pre-swap set to retire because a rebuild rewrites the tree a
|
|
39468
|
+
* runtime is reading, so runtimes really do die under it and the wave takes
|
|
39469
|
+
* ~35 s to pass. A serve restart displaces nothing by itself - session
|
|
39470
|
+
* runtimes are detached and survived the bounce (they retire on their own
|
|
39471
|
+
* build check, whenever their next idle is) - and the measured ordinary shape
|
|
39472
|
+
* was an idle-gated fleet that never moved, so the wait burned its full 60 s
|
|
39473
|
+
* grace for an engage it never performed. The re-engage opportunity it
|
|
39474
|
+
* represented is idle's job now (the 2026-09-29 directive), and the honest
|
|
39475
|
+
* statement replaces the wait: engage only the sessions THIS read shows
|
|
39476
|
+
* without a runtime, and count the rest as still resident.
|
|
39477
|
+
*
|
|
39478
|
+
* IDEMPOTENT BY THE SAME READ: the engage set is the difference the one read
|
|
39479
|
+
* shows, so a runtime that came back on its own is never spawned twice.
|
|
39480
|
+
* Returns null when the read could not be taken - not measured, which the
|
|
39481
|
+
* census and the caller's log both carry as such.
|
|
39482
|
+
*/
|
|
39483
|
+
async engageDisplacedAfterFallbackRestart(backend, snapshot2) {
|
|
39484
|
+
if (snapshot2 === null) return null;
|
|
39485
|
+
if (snapshot2.length === 0) return [];
|
|
39486
|
+
const after = await this.readFleetSnapshot(backend);
|
|
39487
|
+
const displaced = displacedSessions(snapshot2, after);
|
|
39488
|
+
const engaged = [];
|
|
39489
|
+
const failed = [];
|
|
39490
|
+
for (const row of displaced) {
|
|
39491
|
+
try {
|
|
39492
|
+
if (await this.engageSessionRuntime(backend, row))
|
|
39493
|
+
engaged.push(row.sessionId);
|
|
39494
|
+
else
|
|
39495
|
+
failed.push({
|
|
39496
|
+
sessionId: row.sessionId,
|
|
39497
|
+
reason: "the server did not take the engage"
|
|
39498
|
+
});
|
|
39499
|
+
} catch (error) {
|
|
39500
|
+
failed.push({
|
|
39501
|
+
sessionId: row.sessionId,
|
|
39502
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
39503
|
+
});
|
|
39504
|
+
}
|
|
39505
|
+
}
|
|
39506
|
+
logger.info(
|
|
39507
|
+
`Re-engaged ${engaged.length} of ${displaced.length} session(s) displaced by the restart${failed.length > 0 ? `; ${failed.length} did not answer (${failed.map((row) => row.sessionId).join(", ")})` : ""}`,
|
|
39508
|
+
LogFileType.UPDATE_SERVICE
|
|
39509
|
+
);
|
|
39510
|
+
if (after === null) return null;
|
|
39511
|
+
return this.stillResidentRows(snapshot2, after);
|
|
39512
|
+
}
|
|
39513
|
+
/**
|
|
39514
|
+
* Say the rollover's evidence, once, where a post-mortem reads it.
|
|
39515
|
+
*
|
|
39516
|
+
* THE HONEST DEGRADE IS THE POINT (task §3): with the updater's structured
|
|
39517
|
+
* report absent - any core build before `update.report.v1` - serve is proven
|
|
39518
|
+
* by its own record and the daemons are BEST-EFFORT. That is said in exactly
|
|
39519
|
+
* those words rather than faked from a line that is not there, because the
|
|
39520
|
+
* alternative (treating an absent report as "daemons fine") is how a
|
|
39521
|
+
* user-visible claim gets made with no evidence behind it. A report that NAMES a
|
|
39522
|
+
* failed daemon is surfaced as a warning beside the same line.
|
|
39523
|
+
*
|
|
39524
|
+
* Names the three inputs the verdict read - the instance pair, the boot
|
|
39525
|
+
* reading, the `/health` answer - so a wrong verdict can be reconstructed
|
|
39526
|
+
* from the log without re-running anything.
|
|
39527
|
+
*/
|
|
39528
|
+
logRolloverEvidence(input) {
|
|
39529
|
+
const verdict = input.moved ? "proven moved" : "not proven moved";
|
|
39530
|
+
const facts = `instance ${input.instanceBefore ?? "no reading"} -> ${input.recordAfter.instanceId ?? "no reading"}, boot ${input.recordAfter.bootVersion ?? "no reading"}, /health ${input.healthOk ? "answered" : "did not answer"}`;
|
|
39531
|
+
const daemons = daemonMoveOutcome(input.report);
|
|
39532
|
+
if (daemons === null) {
|
|
39533
|
+
logger.info(
|
|
39534
|
+
`Rollover evidence: serve ${verdict} (${facts}); the updater printed no update.report.v1 line, so the daemons are best-effort with no evidence either way`,
|
|
39535
|
+
LogFileType.UPDATE_SERVICE
|
|
39536
|
+
);
|
|
39537
|
+
return;
|
|
39538
|
+
}
|
|
39539
|
+
logger.info(
|
|
39540
|
+
`Rollover evidence: serve ${verdict} (${facts}); the updater's report says ${daemons.moved ? "every supervised daemon is refreshed or already current" : `these daemons did not refresh: ${daemons.failed.join(", ")}`}`,
|
|
39541
|
+
LogFileType.UPDATE_SERVICE
|
|
39542
|
+
);
|
|
39543
|
+
if (!daemons.moved) {
|
|
39544
|
+
logger.warn(
|
|
39545
|
+
`Daemon refresh incomplete after the update: ${daemons.failed.join(", ")} (named by the update.report.v1 line; serve's own move is a separate reading)`,
|
|
39546
|
+
LogFileType.UPDATE_SERVICE
|
|
39547
|
+
);
|
|
39548
|
+
}
|
|
39549
|
+
}
|
|
38880
39550
|
/**
|
|
38881
39551
|
* The build the daemon serving this app BOOTED with, or null when its record
|
|
38882
39552
|
* carries no reading.
|
|
@@ -38898,7 +39568,7 @@ ${heal.removed.join("\n")}`,
|
|
|
38898
39568
|
* the same pair rather than about two different questions.
|
|
38899
39569
|
*/
|
|
38900
39570
|
servingBootVersion() {
|
|
38901
|
-
return this.
|
|
39571
|
+
return this.servingRecordReadings().bootVersion;
|
|
38902
39572
|
}
|
|
38903
39573
|
/**
|
|
38904
39574
|
* The running reading an update event carries to the renderer.
|
|
@@ -39269,6 +39939,7 @@ ${heal.removed.join("\n")}`,
|
|
|
39269
39939
|
sourceRebuild: rebuildPath
|
|
39270
39940
|
});
|
|
39271
39941
|
const rebuildRoute = freshPlan.managedRoute === "source-build" && rebuildPath !== null;
|
|
39942
|
+
const generationInstall = freshPlan.managedRoute === "entry-point";
|
|
39272
39943
|
if (!consolePath && !rebuildRoute) {
|
|
39273
39944
|
this.sendToRenderer("backend-update-error", {
|
|
39274
39945
|
message: "The global install could not be found on this machine, so the update did not run. See the update service log.",
|
|
@@ -39281,6 +39952,8 @@ ${heal.removed.join("\n")}`,
|
|
|
39281
39952
|
const before = this.readInstallVersionAt(installPath) ?? plan.installedInstallVersion;
|
|
39282
39953
|
const markerBefore = rebuildRoute ? this.readRebuildMarkerState(installPath) : null;
|
|
39283
39954
|
const fleetBeforeInstall = rebuildRoute ? await this.readFleetSnapshot(backend) : null;
|
|
39955
|
+
const serveInstanceBefore = generationInstall ? this.servingRecordReadings().instanceId : null;
|
|
39956
|
+
const fleetBeforeUpdate = generationInstall ? await this.readFleetSnapshot(backend) : null;
|
|
39284
39957
|
if (rebuildRoute && !await this.drainFleetForUpdate({
|
|
39285
39958
|
backend,
|
|
39286
39959
|
command: freshPlan.updateCommand ?? null
|
|
@@ -39379,9 +40052,63 @@ ${heal.removed.join("\n")}`,
|
|
|
39379
40052
|
`Global install updated: ${before ?? "unknown"} -> ${after ?? "unknown"}`,
|
|
39380
40053
|
LogFileType.UPDATE_SERVICE
|
|
39381
40054
|
);
|
|
39382
|
-
const
|
|
39383
|
-
|
|
39384
|
-
|
|
40055
|
+
const updateReport = generationInstall ? parseUpdateReport(run.stdout) : null;
|
|
40056
|
+
let serveRecordAfter = { bootVersion: null, instanceId: null };
|
|
40057
|
+
let serveHealthOk = null;
|
|
40058
|
+
let serveMovedWithoutRestart = false;
|
|
40059
|
+
if (generationInstall) {
|
|
40060
|
+
serveRecordAfter = this.servingRecordReadings();
|
|
40061
|
+
serveHealthOk = await this.checkBackendHealth();
|
|
40062
|
+
serveMovedWithoutRestart = serveMovedOntoBuild({
|
|
40063
|
+
instanceBefore: serveInstanceBefore,
|
|
40064
|
+
recordAfter: serveRecordAfter,
|
|
40065
|
+
installVersion: after,
|
|
40066
|
+
target: target2,
|
|
40067
|
+
reportServe: updateReport?.serve[0] ?? null,
|
|
40068
|
+
healthOk: serveHealthOk
|
|
40069
|
+
});
|
|
40070
|
+
this.logRolloverEvidence({
|
|
40071
|
+
report: updateReport,
|
|
40072
|
+
moved: serveMovedWithoutRestart,
|
|
40073
|
+
instanceBefore: serveInstanceBefore,
|
|
40074
|
+
recordAfter: serveRecordAfter,
|
|
40075
|
+
healthOk: serveHealthOk
|
|
40076
|
+
});
|
|
40077
|
+
}
|
|
40078
|
+
if (serveMovedWithoutRestart) {
|
|
40079
|
+
const census = await this.readStragglerCensus({
|
|
40080
|
+
backend,
|
|
40081
|
+
current: serveRecordAfter.bootVersion ?? after ?? target2,
|
|
40082
|
+
before: fleetBeforeUpdate
|
|
40083
|
+
});
|
|
40084
|
+
logger.info(
|
|
40085
|
+
`The server serving this app moved onto ${serveRecordAfter.bootVersion ?? "the new build"} without a restart (instance changed, /health answered, ${census.count ?? "no"} straggler(s) counted from ${census.source ?? "no"} reading); completing without bouncing it`,
|
|
40086
|
+
LogFileType.UPDATE_SERVICE
|
|
40087
|
+
);
|
|
40088
|
+
this.sendToRenderer("backend-update-completed", {
|
|
40089
|
+
installVersion: after,
|
|
40090
|
+
/*
|
|
40091
|
+
* THE RECORD's reading, not `/health`'s (round 2, T1): the panel
|
|
40092
|
+
* compares these two strings, and only the process's own record
|
|
40093
|
+
* names the build it actually loaded. The moved verdict requires
|
|
40094
|
+
* a boot reading, so this arm cannot send a null it proved.
|
|
40095
|
+
*/
|
|
40096
|
+
runningVersion: serveRecordAfter.bootVersion,
|
|
40097
|
+
moved: true,
|
|
40098
|
+
restarted: false,
|
|
40099
|
+
restartable: this.backendIsAppOwned(),
|
|
40100
|
+
/*
|
|
40101
|
+
* FALSE BY CONSTRUCTION, and stated rather than left absent: this arm is
|
|
40102
|
+
* the global install's own updater, so the environment it moved is never
|
|
40103
|
+
* the app's managed one (design D5).
|
|
40104
|
+
*/
|
|
40105
|
+
appOwnedEnvironment: false,
|
|
40106
|
+
sessionsOnOldBuild: census.count,
|
|
40107
|
+
sessionsOnOldBuildSource: census.source
|
|
40108
|
+
});
|
|
40109
|
+
return true;
|
|
40110
|
+
}
|
|
40111
|
+
if (backend.isUsingExternalBackend()) {
|
|
39385
40112
|
const health = await this.getInstalledBackendVersion();
|
|
39386
40113
|
const running2 = this.eventRunningVersion(health);
|
|
39387
40114
|
logger.info(
|
|
@@ -39392,6 +40119,7 @@ ${heal.removed.join("\n")}`,
|
|
|
39392
40119
|
installVersion: after,
|
|
39393
40120
|
runningVersion: running2,
|
|
39394
40121
|
restarted: false,
|
|
40122
|
+
moved: false,
|
|
39395
40123
|
restartable: this.backendIsAppOwned(),
|
|
39396
40124
|
/*
|
|
39397
40125
|
* FALSE BY CONSTRUCTION, and stated rather than left absent: this arm is
|
|
@@ -39402,13 +40130,21 @@ ${heal.removed.join("\n")}`,
|
|
|
39402
40130
|
});
|
|
39403
40131
|
return true;
|
|
39404
40132
|
}
|
|
40133
|
+
if (generationInstall) {
|
|
40134
|
+
logger.info(
|
|
40135
|
+
`Install moved to ${after ?? "unknown"}; the serving process's own record does not prove it moved onto the new build (instance ${serveInstanceBefore ?? "no reading"} -> ${serveRecordAfter.instanceId ?? "no reading"}, boot ${serveRecordAfter.bootVersion ?? "no reading"}, /health ${serveHealthOk ? "answered" : "did not answer"}); restarting the daemon this app owns onto it`,
|
|
40136
|
+
LogFileType.UPDATE_SERVICE
|
|
40137
|
+
);
|
|
40138
|
+
}
|
|
39405
40139
|
logger.info(
|
|
39406
40140
|
"Restarting backend service onto the updated install...",
|
|
39407
40141
|
LogFileType.UPDATE_SERVICE
|
|
39408
40142
|
);
|
|
39409
40143
|
this.sendToRenderer("backend-update-progress", { phase: "restarting" });
|
|
39410
40144
|
const fleetBeforeRestart = await this.readFleetSnapshot(backend);
|
|
39411
|
-
const restartSuccess = await backend.restart(
|
|
40145
|
+
const restartSuccess = await backend.restart({
|
|
40146
|
+
stopGraceMs: UPDATE_RESTART_STOP_GRACE_MS
|
|
40147
|
+
});
|
|
39412
40148
|
if (!restartSuccess) {
|
|
39413
40149
|
logger.error(
|
|
39414
40150
|
"Backend service restart failed after the global install update",
|
|
@@ -39465,12 +40201,43 @@ ${heal.removed.join("\n")}`,
|
|
|
39465
40201
|
fleetBeforeInstall,
|
|
39466
40202
|
fleetBeforeRestart
|
|
39467
40203
|
);
|
|
39468
|
-
|
|
40204
|
+
let sessionsOnOldBuild;
|
|
40205
|
+
let sessionsOnOldBuildSource;
|
|
40206
|
+
if (rebuildRoute) {
|
|
40207
|
+
const reengage = reengageSnapshot !== null ? await this.reengageFleetAfterRestart(backend, reengageSnapshot) : null;
|
|
40208
|
+
const census = await this.readStragglerCensus({
|
|
40209
|
+
backend,
|
|
40210
|
+
current: after ?? target2,
|
|
40211
|
+
resident: reengage !== null ? reengage.stillResident : null
|
|
40212
|
+
});
|
|
40213
|
+
sessionsOnOldBuild = census.count;
|
|
40214
|
+
sessionsOnOldBuildSource = census.source;
|
|
40215
|
+
} else {
|
|
40216
|
+
const resident = await this.engageDisplacedAfterFallbackRestart(
|
|
40217
|
+
backend,
|
|
40218
|
+
reengageSnapshot
|
|
40219
|
+
);
|
|
40220
|
+
const census = await this.readStragglerCensus({
|
|
40221
|
+
backend,
|
|
40222
|
+
current: reported ?? after ?? target2,
|
|
40223
|
+
resident
|
|
40224
|
+
});
|
|
40225
|
+
sessionsOnOldBuild = census.count;
|
|
40226
|
+
sessionsOnOldBuildSource = census.source;
|
|
40227
|
+
}
|
|
39469
40228
|
this.sendToRenderer("backend-update-completed", {
|
|
39470
40229
|
installVersion: after,
|
|
39471
40230
|
runningVersion: reported,
|
|
39472
40231
|
restarted: true,
|
|
39473
|
-
|
|
40232
|
+
/*
|
|
40233
|
+
* The app moved this daemon onto the new build with the restart, and the
|
|
40234
|
+
* distinction is stated where it was proven (`reported` was held
|
|
40235
|
+
* at-or-past the target above). Absent on the rebuild route, which keeps
|
|
40236
|
+
* its version and its own sentence.
|
|
40237
|
+
*/
|
|
40238
|
+
...rebuildRoute ? {} : { moved: true },
|
|
40239
|
+
sessionsOnOldBuild,
|
|
40240
|
+
sessionsOnOldBuildSource
|
|
39474
40241
|
});
|
|
39475
40242
|
return true;
|
|
39476
40243
|
}
|