local-operator-ui 0.31.28 → 0.31.30
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 +931 -31
- package/out/renderer/assets/{_basePickBy-D16oopNi.js → _basePickBy-Y9GC34gi.js} +1 -1
- package/out/renderer/assets/{_baseUniq-BYKYfwtj.js → _baseUniq-B0peOdjF.js} +1 -1
- package/out/renderer/assets/{agent-details-page-CpwkLhdq.js → agent-details-page-DVzsvKvr.js} +1 -1
- package/out/renderer/assets/{agent-hub-page-BYeba0zC.js → agent-hub-page-Dp8llcze.js} +1 -1
- package/out/renderer/assets/agents-page-DlN3w0Rq.js +7 -0
- package/out/renderer/assets/{arc-BYg3WIzZ.js → arc-DQzRvMGZ.js} +1 -1
- package/out/renderer/assets/{architectureDiagram-IEHRJDOE-np6HwkQz.js → architectureDiagram-IEHRJDOE-BHLUr3Vi.js} +1 -1
- package/out/renderer/assets/{blockDiagram-JOT3LUYC-CaR7YwVl.js → blockDiagram-JOT3LUYC-CVK0U-3j.js} +1 -1
- package/out/renderer/assets/{book-open--g1VlVMp.js → book-open-j4JS3yvI.js} +1 -1
- package/out/renderer/assets/browser-page-BI7a4ZtV.js +1 -0
- package/out/renderer/assets/{browser-webauthn-prompt-Bsk0eXNZ.js → browser-webauthn-prompt-nwdqBWnv.js} +1 -1
- package/out/renderer/assets/{c4Diagram-VJAJSXHY-DqBT3rl5.js → c4Diagram-VJAJSXHY-BqLTGVHe.js} +1 -1
- package/out/renderer/assets/channel-BP8e6HYH.js +1 -0
- package/out/renderer/assets/{chunk-4BMEZGHF-B7-MzFPi.js → chunk-4BMEZGHF-BSPZPecM.js} +1 -1
- package/out/renderer/assets/{chunk-A2AXSNBT-B9IqNrNg.js → chunk-A2AXSNBT-CEEG3uIi.js} +1 -1
- package/out/renderer/assets/{chunk-AEK57VVT-DFzBJNd9.js → chunk-AEK57VVT-CvyIUWUR.js} +1 -1
- package/out/renderer/assets/{chunk-D6G4REZN-DW-XzsAv.js → chunk-D6G4REZN-D4lVOdW7.js} +1 -1
- package/out/renderer/assets/{chunk-RZ5BOZE2-C7el6yre.js → chunk-RZ5BOZE2-znegx9P6.js} +1 -1
- package/out/renderer/assets/{chunk-XZIHB7SX-BGYrq1Wn.js → chunk-XZIHB7SX-DPz_D8Nf.js} +1 -1
- package/out/renderer/assets/classDiagram-GIVACNV2-DVXt0ycE.js +1 -0
- package/out/renderer/assets/classDiagram-v2-COTLJTTW-DVXt0ycE.js +1 -0
- package/out/renderer/assets/clone-D0eJwRGh.js +1 -0
- package/out/renderer/assets/{console-mirror-ffbsozOd.js → console-mirror-GUGtzmyW.js} +1 -1
- package/out/renderer/assets/{consoleCapture-CiApa6fi.js → consoleCapture-D8nStIVt.js} +1 -1
- package/out/renderer/assets/{conversation-input-sync-DQUYkOfh.js → conversation-input-sync-CGaXAqah.js} +246 -239
- package/out/renderer/assets/{dagre-OKDRZEBW-BVScRDvj.js → dagre-OKDRZEBW-ByuMXl-x.js} +1 -1
- package/out/renderer/assets/{diagram-SSKATNLV-B0iQ6YY1.js → diagram-SSKATNLV-DrN1Clbm.js} +1 -1
- package/out/renderer/assets/{diagram-VNBRO52H-Co6YPtUT.js → diagram-VNBRO52H-BU95qC98.js} +1 -1
- package/out/renderer/assets/{erDiagram-Q7BY3M3F-C9XIuSQl.js → erDiagram-Q7BY3M3F-CLttNK5z.js} +1 -1
- package/out/renderer/assets/{error-boundary-DH4MhqhL.js → error-boundary-BR1z8pWF.js} +5 -5
- package/out/renderer/assets/{flowDiagram-4HSFHLVR-QTHrGPk1.js → flowDiagram-4HSFHLVR-BCsFW1Gf.js} +1 -1
- package/out/renderer/assets/{ganttDiagram-APWFNJXF-qC1VnBIz.js → ganttDiagram-APWFNJXF-Cdg1Q_-7.js} +1 -1
- package/out/renderer/assets/{gitGraphDiagram-7IBYFJ6S-D7UGP8uH.js → gitGraphDiagram-7IBYFJ6S-0tWQBOZl.js} +1 -1
- package/out/renderer/assets/{graph-BIUuz2bp.js → graph-BOvjqm1y.js} +1 -1
- package/out/renderer/assets/{index-Ckvy9Zge.js → index-Bbr0EFhm.js} +1 -1
- package/out/renderer/assets/index-Cy3MjKxd.css +1 -0
- package/out/renderer/assets/{index-vK67PDia.js → index-D09I1LXM.js} +196 -196
- package/out/renderer/assets/{index-DTc-sJ8S.js → index-DbuNRNvX.js} +1 -1
- package/out/renderer/assets/{infoDiagram-PH2N3AL5-DYtVGJC7.js → infoDiagram-PH2N3AL5-cyj8Lve1.js} +1 -1
- package/out/renderer/assets/{installer-BcSiJA6N.js → installer-DDGjYv9v.js} +1 -1
- package/out/renderer/assets/{journeyDiagram-U35MCT3I-vVg2sFTp.js → journeyDiagram-U35MCT3I-DWBKDniU.js} +1 -1
- package/out/renderer/assets/{kanban-definition-NDS4AKOZ-BE36wSk2.js → kanban-definition-NDS4AKOZ-Cl5Gtazo.js} +1 -1
- package/out/renderer/assets/{layout-bOQrw0b8.js → layout-CxuQX-mj.js} +1 -1
- package/out/renderer/assets/{legacy-agents-page-BSKz_UrV.js → legacy-agents-page-EmhBvGX8.js} +1 -1
- package/out/renderer/assets/{mermaid.core-DpvY6Bhk.js → mermaid.core-eIMT1oSc.js} +5 -5
- package/out/renderer/assets/{mesh-page-9Q8Sc9J_.js → mesh-page-B8pGIWvO.js} +1 -1
- package/out/renderer/assets/{mindmap-definition-ALO5MXBD-B9PPN6sb.js → mindmap-definition-ALO5MXBD-zPRivaIU.js} +1 -1
- package/out/renderer/assets/{mini-ySULdryU.js → mini-D6nPFjTs.js} +1 -1
- package/out/renderer/assets/{org-surface-gate-Dl4zcl5q.js → org-surface-gate-BeX5voGq.js} +1 -1
- package/out/renderer/assets/{page-header-DazB-GQz.js → page-header-BLBgjVqc.js} +1 -1
- package/out/renderer/assets/{pieDiagram-IB7DONF6-DkioNhQY.js → pieDiagram-IB7DONF6-CUTT0oqK.js} +1 -1
- package/out/renderer/assets/projects-page-D60z6cvI.js +55 -0
- package/out/renderer/assets/{quadrantDiagram-7GDLP6J5-4m3m0N6F.js → quadrantDiagram-7GDLP6J5-B6ksoL9Y.js} +1 -1
- package/out/renderer/assets/{radar-MK3ICKWK-DS8YGAOb.js → radar-MK3ICKWK-BpDoqWc2.js} +1 -1
- package/out/renderer/assets/{radient-auth-buttons-DZeHRL7u.js → radient-auth-buttons-Ep1e0Z5I.js} +1 -1
- package/out/renderer/assets/{requirementDiagram-KVF5MWMF-DsBdUu3C.js → requirementDiagram-KVF5MWMF-DQwmYS7R.js} +1 -1
- package/out/renderer/assets/{sankeyDiagram-QLVOVGJD-CnaPYKR6.js → sankeyDiagram-QLVOVGJD-Dc8VJLoV.js} +1 -1
- package/out/renderer/assets/{schedules-page-B0dSGpXt.js → schedules-page-DyMkIr1h.js} +1 -1
- package/out/renderer/assets/{sequenceDiagram-X6HHIX6F-C3eja1QY.js → sequenceDiagram-X6HHIX6F-SsVDN37O.js} +1 -1
- package/out/renderer/assets/{settings-page-CrMsVX3d.js → settings-page-C_1ja3uN.js} +6 -6
- package/out/renderer/assets/{settings-section-BGG4XzV0.js → settings-section-BLUoPALO.js} +1 -1
- package/out/renderer/assets/{stateDiagram-DGXRK772-CjFKiyXn.js → stateDiagram-DGXRK772-vwQ-TB_f.js} +1 -1
- package/out/renderer/assets/stateDiagram-v2-YXO3MK2T-D30uaRbQ.js +1 -0
- package/out/renderer/assets/{timeline-definition-BDJGKUSR-CAXy0nQD.js → timeline-definition-BDJGKUSR-CQ_pAZms.js} +1 -1
- package/out/renderer/assets/{ui-preferences-store-B6Fr0UX9.js → ui-preferences-store-CxJfZcpf.js} +1 -1
- package/out/renderer/assets/{use-download-agent-mutation-LsNre_-6.js → use-download-agent-mutation-DIujvdLj.js} +1 -1
- package/out/renderer/assets/{use-memberships-query-BgiSfYgr.js → use-memberships-query-BhO5nUlo.js} +1 -1
- package/out/renderer/assets/{xychartDiagram-VJFVF3MP-CfNm8SWG.js → xychartDiagram-VJFVF3MP-Cs3B-KEl.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-CSNQxVPq.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) {
|
|
@@ -12264,14 +12444,22 @@ function reportParksAtQuit(sessions, report) {
|
|
|
12264
12444
|
].join(" ")
|
|
12265
12445
|
);
|
|
12266
12446
|
}
|
|
12267
|
-
function reportSkippedWhileQuitting(context, requested) {
|
|
12447
|
+
function reportSkippedWhileQuitting(context, requested, deferredReopen = false) {
|
|
12268
12448
|
context.report?.(
|
|
12269
12449
|
[
|
|
12270
12450
|
`trigger=${context.trigger}`,
|
|
12271
12451
|
`mode=${MODE_OF_SHOW[requested]}`,
|
|
12272
12452
|
`requested=${requested}`,
|
|
12273
12453
|
...requesterFields(context),
|
|
12274
|
-
"applied=skipped+quitting"
|
|
12454
|
+
"applied=skipped+quitting",
|
|
12455
|
+
/*
|
|
12456
|
+
* THE COMPLETION TOKEN (#755): this refusal was recorded and will be
|
|
12457
|
+
* completed by one successor instance at the quit's terminal. Present
|
|
12458
|
+
* exactly when the recorder took the record — the create-gate refusals
|
|
12459
|
+
* (banner click, consent reopen, viewer verbs) are not completed and say
|
|
12460
|
+
* nothing extra, and neither does a `never` request.
|
|
12461
|
+
*/
|
|
12462
|
+
...deferredReopen ? ["reopen=deferred"] : []
|
|
12275
12463
|
].join(" ")
|
|
12276
12464
|
);
|
|
12277
12465
|
}
|
|
@@ -12314,13 +12502,15 @@ function readSecondLaunchRequest(input) {
|
|
|
12314
12502
|
}
|
|
12315
12503
|
function applySecondLaunch(request, target2) {
|
|
12316
12504
|
if (target2.quitting) {
|
|
12505
|
+
const deferredReopen = target2.onRefusedWhileQuitting?.(request) === true;
|
|
12317
12506
|
reportSkippedWhileQuitting(
|
|
12318
12507
|
{
|
|
12319
12508
|
trigger: "second-instance",
|
|
12320
12509
|
requester: request.requester ?? void 0,
|
|
12321
12510
|
report: target2.report
|
|
12322
12511
|
},
|
|
12323
|
-
request.show
|
|
12512
|
+
request.show,
|
|
12513
|
+
deferredReopen
|
|
12324
12514
|
);
|
|
12325
12515
|
return;
|
|
12326
12516
|
}
|
|
@@ -13634,6 +13824,14 @@ const METHODS$1 = [
|
|
|
13634
13824
|
"tabs",
|
|
13635
13825
|
"scroll",
|
|
13636
13826
|
"logs",
|
|
13827
|
+
// The structured read actions (`styles`, `hit_test`, `ancestors`), in
|
|
13828
|
+
// `protocol.py`'s tuple order: page-geometry reads answered by fixed
|
|
13829
|
+
// self-contained page functions (the vendored `driver/geometry-read.ts`),
|
|
13830
|
+
// capped and rounded in the page. This array is the wire's closed gate, so
|
|
13831
|
+
// it must stay byte-comparable with the Python source it mirrors.
|
|
13832
|
+
"styles",
|
|
13833
|
+
"hit_test",
|
|
13834
|
+
"ancestors",
|
|
13637
13835
|
"request_access",
|
|
13638
13836
|
"await_access",
|
|
13639
13837
|
"cancel_access",
|
|
@@ -13692,7 +13890,13 @@ const ERROR_CODES = [
|
|
|
13692
13890
|
// a command that hangs rather than one that fails.
|
|
13693
13891
|
...CONSOLE_ERROR_CODES
|
|
13694
13892
|
];
|
|
13695
|
-
const HOST_CAPABILITIES = [
|
|
13893
|
+
const HOST_CAPABILITIES = [
|
|
13894
|
+
"styles",
|
|
13895
|
+
"hit_test",
|
|
13896
|
+
"ancestors",
|
|
13897
|
+
"download",
|
|
13898
|
+
"upload"
|
|
13899
|
+
];
|
|
13696
13900
|
const RUN_DIRNAME = path$1.join("run", "ui-browser");
|
|
13697
13901
|
const STATE_FILENAME = "host.json";
|
|
13698
13902
|
const STATE_DIR_MODE = 448;
|
|
@@ -19314,6 +19518,135 @@ function clampTimeout(requested) {
|
|
|
19314
19518
|
if (!Number.isFinite(wanted) || wanted <= 0) return CAPS.downloadTimeoutS;
|
|
19315
19519
|
return Math.min(wanted, CAPS.downloadTimeoutMaxS);
|
|
19316
19520
|
}
|
|
19521
|
+
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}';
|
|
19522
|
+
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}';
|
|
19523
|
+
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}';
|
|
19524
|
+
const MAX_REQUESTED_PROPERTIES = 20;
|
|
19525
|
+
const DEFAULT_DEPTH = 12;
|
|
19526
|
+
const MAX_DEPTH = 16;
|
|
19527
|
+
async function runFixed(record, source, args, label) {
|
|
19528
|
+
const code = `(${source})(${args.map((arg) => JSON.stringify(arg)).join(", ")})`;
|
|
19529
|
+
return await deadline(
|
|
19530
|
+
record.view.webContents.executeJavaScriptInIsolatedWorld(
|
|
19531
|
+
ISOLATED_WORLD_ID,
|
|
19532
|
+
[{ code }]
|
|
19533
|
+
),
|
|
19534
|
+
SCRIPTING_DEADLINE_MS,
|
|
19535
|
+
label
|
|
19536
|
+
);
|
|
19537
|
+
}
|
|
19538
|
+
function selectorError(selector, error) {
|
|
19539
|
+
if (error instanceof Error && INVALID_SELECTOR.test(error.message)) {
|
|
19540
|
+
return new BridgeCommandError(
|
|
19541
|
+
"element_not_found",
|
|
19542
|
+
`selector ${selector} is not valid`
|
|
19543
|
+
);
|
|
19544
|
+
}
|
|
19545
|
+
return error;
|
|
19546
|
+
}
|
|
19547
|
+
function structuredResult(name, result) {
|
|
19548
|
+
if (typeof result !== "object" || result === null) {
|
|
19549
|
+
throw new BridgeCommandError(
|
|
19550
|
+
"internal",
|
|
19551
|
+
`the page returned no structured ${name} result`
|
|
19552
|
+
);
|
|
19553
|
+
}
|
|
19554
|
+
return result;
|
|
19555
|
+
}
|
|
19556
|
+
function requiredSelector(params) {
|
|
19557
|
+
const selector = stringParam(params, "selector");
|
|
19558
|
+
if (!selector) {
|
|
19559
|
+
throw new BridgeCommandError("element_not_found", "selector is required");
|
|
19560
|
+
}
|
|
19561
|
+
return selector;
|
|
19562
|
+
}
|
|
19563
|
+
function requestedProperties(params) {
|
|
19564
|
+
if (!Array.isArray(params.properties)) return [];
|
|
19565
|
+
return params.properties.filter((entry) => typeof entry === "string").slice(0, MAX_REQUESTED_PROPERTIES);
|
|
19566
|
+
}
|
|
19567
|
+
async function styles(ctx, params) {
|
|
19568
|
+
const record = ctx.registry.requireSurface(params.tab);
|
|
19569
|
+
const selector = requiredSelector(params);
|
|
19570
|
+
const properties = requestedProperties(params);
|
|
19571
|
+
let result;
|
|
19572
|
+
try {
|
|
19573
|
+
result = await runFixed(
|
|
19574
|
+
record,
|
|
19575
|
+
READ_STYLES_SOURCE,
|
|
19576
|
+
[selector, properties],
|
|
19577
|
+
`styles(${selector})`
|
|
19578
|
+
);
|
|
19579
|
+
} catch (error) {
|
|
19580
|
+
throw selectorError(selector, error);
|
|
19581
|
+
}
|
|
19582
|
+
if (result === null || result === void 0) {
|
|
19583
|
+
throw new BridgeCommandError(
|
|
19584
|
+
"element_not_found",
|
|
19585
|
+
`selector ${selector} matched nothing`
|
|
19586
|
+
);
|
|
19587
|
+
}
|
|
19588
|
+
ctx.registry.touch(record);
|
|
19589
|
+
return { ...structuredResult("styles", result), ...pageOf(record.view) };
|
|
19590
|
+
}
|
|
19591
|
+
async function hitTest(ctx, params) {
|
|
19592
|
+
const record = ctx.registry.requireSurface(params.tab);
|
|
19593
|
+
const x = numberParam(params, "x");
|
|
19594
|
+
const y = numberParam(params, "y");
|
|
19595
|
+
if (x === void 0 || y === void 0) {
|
|
19596
|
+
throw new BridgeCommandError(
|
|
19597
|
+
"internal",
|
|
19598
|
+
"'hit_test' needs numeric x and y (viewport coordinates)"
|
|
19599
|
+
);
|
|
19600
|
+
}
|
|
19601
|
+
const result = await runFixed(
|
|
19602
|
+
record,
|
|
19603
|
+
HIT_TEST_SOURCE,
|
|
19604
|
+
[x, y],
|
|
19605
|
+
`hit_test(${x}, ${y})`
|
|
19606
|
+
);
|
|
19607
|
+
if (result === null || result === void 0) {
|
|
19608
|
+
throw new BridgeCommandError(
|
|
19609
|
+
"element_not_found",
|
|
19610
|
+
`no element at point (${x}, ${y})`
|
|
19611
|
+
);
|
|
19612
|
+
}
|
|
19613
|
+
ctx.registry.touch(record);
|
|
19614
|
+
return {
|
|
19615
|
+
...structuredResult("hit_test", result),
|
|
19616
|
+
...pageOf(record.view)
|
|
19617
|
+
};
|
|
19618
|
+
}
|
|
19619
|
+
async function ancestors(ctx, params) {
|
|
19620
|
+
const record = ctx.registry.requireSurface(params.tab);
|
|
19621
|
+
const selector = requiredSelector(params);
|
|
19622
|
+
const raw = numberParam(params, "depth");
|
|
19623
|
+
const depth = Math.max(
|
|
19624
|
+
1,
|
|
19625
|
+
Math.min(MAX_DEPTH, raw === void 0 ? DEFAULT_DEPTH : Math.floor(raw))
|
|
19626
|
+
);
|
|
19627
|
+
let result;
|
|
19628
|
+
try {
|
|
19629
|
+
result = await runFixed(
|
|
19630
|
+
record,
|
|
19631
|
+
ANCESTORS_SOURCE,
|
|
19632
|
+
[selector, depth],
|
|
19633
|
+
`ancestors(${selector})`
|
|
19634
|
+
);
|
|
19635
|
+
} catch (error) {
|
|
19636
|
+
throw selectorError(selector, error);
|
|
19637
|
+
}
|
|
19638
|
+
if (result === null || result === void 0) {
|
|
19639
|
+
throw new BridgeCommandError(
|
|
19640
|
+
"element_not_found",
|
|
19641
|
+
`selector ${selector} matched nothing`
|
|
19642
|
+
);
|
|
19643
|
+
}
|
|
19644
|
+
ctx.registry.touch(record);
|
|
19645
|
+
return {
|
|
19646
|
+
...structuredResult("ancestors", result),
|
|
19647
|
+
...pageOf(record.view)
|
|
19648
|
+
};
|
|
19649
|
+
}
|
|
19317
19650
|
const MAX_AGENT_TABS = 8;
|
|
19318
19651
|
const BACKGROUND_VIEWPORT = {
|
|
19319
19652
|
x: 0,
|
|
@@ -20278,6 +20611,12 @@ function factOf(path2) {
|
|
|
20278
20611
|
}
|
|
20279
20612
|
const DOCUMENT_SCOPED = /* @__PURE__ */ new Set([
|
|
20280
20613
|
"read",
|
|
20614
|
+
// The structured reads (`styles`, `hit_test`, `ancestors`) describe the
|
|
20615
|
+
// CURRENT document exactly as `read` does, so a result that raced a
|
|
20616
|
+
// navigation it did not cause must be discarded the same way.
|
|
20617
|
+
"styles",
|
|
20618
|
+
"hit_test",
|
|
20619
|
+
"ancestors",
|
|
20281
20620
|
"snapshot",
|
|
20282
20621
|
"screenshot",
|
|
20283
20622
|
"click",
|
|
@@ -20305,6 +20644,9 @@ const NAVIGATION_ACTIONS = /* @__PURE__ */ new Set(["click", "type"]);
|
|
|
20305
20644
|
const TAB_SCOPED = /* @__PURE__ */ new Set([
|
|
20306
20645
|
"goto",
|
|
20307
20646
|
"read",
|
|
20647
|
+
"styles",
|
|
20648
|
+
"hit_test",
|
|
20649
|
+
"ancestors",
|
|
20308
20650
|
"snapshot",
|
|
20309
20651
|
"screenshot",
|
|
20310
20652
|
"click",
|
|
@@ -20482,6 +20824,12 @@ class BrowserHost {
|
|
|
20482
20824
|
return retitle();
|
|
20483
20825
|
case "read":
|
|
20484
20826
|
return read(this, params);
|
|
20827
|
+
case "styles":
|
|
20828
|
+
return styles(this, params);
|
|
20829
|
+
case "hit_test":
|
|
20830
|
+
return hitTest(this, params);
|
|
20831
|
+
case "ancestors":
|
|
20832
|
+
return ancestors(this, params);
|
|
20485
20833
|
case "snapshot":
|
|
20486
20834
|
return snapshot(this, params);
|
|
20487
20835
|
case "scroll":
|
|
@@ -24238,6 +24586,7 @@ async function startBrowserHost(options) {
|
|
|
24238
24586
|
}
|
|
24239
24587
|
});
|
|
24240
24588
|
window.contentView.addChildView(view);
|
|
24589
|
+
view.setBounds(BACKGROUND_VIEWPORT);
|
|
24241
24590
|
view.setVisible(false);
|
|
24242
24591
|
return view;
|
|
24243
24592
|
}
|
|
@@ -33564,6 +33913,49 @@ function createQuitState() {
|
|
|
33564
33913
|
isQuitting: () => quitting
|
|
33565
33914
|
};
|
|
33566
33915
|
}
|
|
33916
|
+
function successorArgs(entry, facts) {
|
|
33917
|
+
if (entry.kind === "activate") {
|
|
33918
|
+
if (!facts.defaultApp) return [];
|
|
33919
|
+
return withModePin(facts.argv.slice(1), entry.show);
|
|
33920
|
+
}
|
|
33921
|
+
if (entry.argv === null) return [];
|
|
33922
|
+
return withModePin(entry.argv.slice(1), entry.show);
|
|
33923
|
+
}
|
|
33924
|
+
function withModePin(args, show) {
|
|
33925
|
+
const namesAMode = args.some(
|
|
33926
|
+
(argument) => argument === WINDOW_MODE_FLAG || argument.startsWith(`${WINDOW_MODE_FLAG}=`)
|
|
33927
|
+
);
|
|
33928
|
+
return namesAMode ? [...args] : [...args, `${WINDOW_MODE_FLAG}=${MODE_OF_SHOW[show]}`];
|
|
33929
|
+
}
|
|
33930
|
+
function defaultSuccessorFacts() {
|
|
33931
|
+
return { defaultApp: process.defaultApp === true, argv: process.argv };
|
|
33932
|
+
}
|
|
33933
|
+
function createRelaunchPending(options = {}) {
|
|
33934
|
+
const facts = options.facts ?? defaultSuccessorFacts;
|
|
33935
|
+
let pending = null;
|
|
33936
|
+
let scheduled = false;
|
|
33937
|
+
return {
|
|
33938
|
+
hasPending: () => pending !== null,
|
|
33939
|
+
record: (entry) => {
|
|
33940
|
+
if (!canCreateWindowFor(entry.show)) return false;
|
|
33941
|
+
pending = {
|
|
33942
|
+
...entry,
|
|
33943
|
+
argv: entry.argv === null ? null : [...entry.argv]
|
|
33944
|
+
};
|
|
33945
|
+
return true;
|
|
33946
|
+
},
|
|
33947
|
+
clear: () => {
|
|
33948
|
+
pending = null;
|
|
33949
|
+
},
|
|
33950
|
+
scheduleOnExit: ({ relaunch }) => {
|
|
33951
|
+
if (scheduled) return;
|
|
33952
|
+
const entry = pending;
|
|
33953
|
+
if (entry === null) return;
|
|
33954
|
+
scheduled = true;
|
|
33955
|
+
relaunch(successorArgs(entry, facts()));
|
|
33956
|
+
}
|
|
33957
|
+
};
|
|
33958
|
+
}
|
|
33567
33959
|
function titlebarOptions(platform, chrome) {
|
|
33568
33960
|
if (platform === "darwin") {
|
|
33569
33961
|
return { titleBarStyle: "hidden" };
|
|
@@ -33599,6 +33991,123 @@ function titlebarOptions(platform, chrome) {
|
|
|
33599
33991
|
}
|
|
33600
33992
|
};
|
|
33601
33993
|
}
|
|
33994
|
+
const UPDATE_REPORT_KEY = "update_report";
|
|
33995
|
+
const UPDATE_REPORT_SCHEMA_PREFIX = "update.report.";
|
|
33996
|
+
const DAEMON_STATUSES = /* @__PURE__ */ new Set([
|
|
33997
|
+
"refreshed",
|
|
33998
|
+
"already",
|
|
33999
|
+
"failed",
|
|
34000
|
+
"unsupervised"
|
|
34001
|
+
]);
|
|
34002
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
34003
|
+
function parseUpdateReport(stdout) {
|
|
34004
|
+
let found = null;
|
|
34005
|
+
for (const line of stdout.split("\n")) {
|
|
34006
|
+
const trimmed = line.trim();
|
|
34007
|
+
if (!trimmed.startsWith("{") || !trimmed.includes(UPDATE_REPORT_KEY))
|
|
34008
|
+
continue;
|
|
34009
|
+
let payload;
|
|
34010
|
+
try {
|
|
34011
|
+
payload = JSON.parse(trimmed);
|
|
34012
|
+
} catch {
|
|
34013
|
+
continue;
|
|
34014
|
+
}
|
|
34015
|
+
const report = reportFromPayload(payload);
|
|
34016
|
+
if (report) found = report;
|
|
34017
|
+
}
|
|
34018
|
+
return found;
|
|
34019
|
+
}
|
|
34020
|
+
function reportFromPayload(payload) {
|
|
34021
|
+
if (!isRecord(payload)) return null;
|
|
34022
|
+
const raw = payload[UPDATE_REPORT_KEY];
|
|
34023
|
+
if (!isRecord(raw)) return null;
|
|
34024
|
+
const schema2 = raw.schema;
|
|
34025
|
+
if (typeof schema2 !== "string" || !schema2.startsWith(UPDATE_REPORT_SCHEMA_PREFIX))
|
|
34026
|
+
return null;
|
|
34027
|
+
const installVersion = raw.install_version;
|
|
34028
|
+
const target2 = raw.target;
|
|
34029
|
+
if (typeof installVersion !== "string" || typeof target2 !== "string")
|
|
34030
|
+
return null;
|
|
34031
|
+
const serve = raw.serve;
|
|
34032
|
+
const daemons = raw.daemons;
|
|
34033
|
+
if (!Array.isArray(serve) || !Array.isArray(daemons)) return null;
|
|
34034
|
+
const serveEntries = [];
|
|
34035
|
+
for (const entry of serve) {
|
|
34036
|
+
if (!isRecord(entry)) return null;
|
|
34037
|
+
const { pid, from, to, moved, instance } = entry;
|
|
34038
|
+
if (typeof pid !== "number" || typeof from !== "string" || typeof to !== "string" || typeof moved !== "boolean" || typeof instance !== "string")
|
|
34039
|
+
return null;
|
|
34040
|
+
serveEntries.push({ pid, from, to, moved, instance });
|
|
34041
|
+
}
|
|
34042
|
+
const daemonEntries = [];
|
|
34043
|
+
for (const entry of daemons) {
|
|
34044
|
+
if (!isRecord(entry)) return null;
|
|
34045
|
+
const { name, status: status2, version } = entry;
|
|
34046
|
+
if (typeof name !== "string" || typeof status2 !== "string" || !DAEMON_STATUSES.has(status2) || typeof version !== "string")
|
|
34047
|
+
return null;
|
|
34048
|
+
daemonEntries.push({
|
|
34049
|
+
name,
|
|
34050
|
+
status: status2,
|
|
34051
|
+
version
|
|
34052
|
+
});
|
|
34053
|
+
}
|
|
34054
|
+
return {
|
|
34055
|
+
schema: schema2,
|
|
34056
|
+
install_version: installVersion,
|
|
34057
|
+
target: target2,
|
|
34058
|
+
serve: serveEntries,
|
|
34059
|
+
daemons: daemonEntries
|
|
34060
|
+
};
|
|
34061
|
+
}
|
|
34062
|
+
function serveMovedOntoBuild(input) {
|
|
34063
|
+
if (!input.healthOk) return false;
|
|
34064
|
+
const before = input.instanceBefore?.trim() ?? "";
|
|
34065
|
+
if (before === "") return false;
|
|
34066
|
+
const after = input.recordAfter.instanceId?.trim() || input.reportServe?.instance.trim() || "";
|
|
34067
|
+
if (after === "" || after === before) return false;
|
|
34068
|
+
return serveOnTarget(
|
|
34069
|
+
input.recordAfter.bootVersion,
|
|
34070
|
+
input.installVersion,
|
|
34071
|
+
input.target
|
|
34072
|
+
);
|
|
34073
|
+
}
|
|
34074
|
+
function serveOnTarget(bootVersion, installVersion, target2) {
|
|
34075
|
+
const boot = bootVersion?.trim() ?? "";
|
|
34076
|
+
if (boot === "") return false;
|
|
34077
|
+
const wanted = target2?.trim() ?? "";
|
|
34078
|
+
if (wanted !== "") {
|
|
34079
|
+
const order2 = compareVersions(boot, wanted);
|
|
34080
|
+
return order2 !== null && order2 >= 0;
|
|
34081
|
+
}
|
|
34082
|
+
const installed = installVersion?.trim() ?? "";
|
|
34083
|
+
if (installed === "") return false;
|
|
34084
|
+
const order = compareVersions(boot, installed);
|
|
34085
|
+
return order === null ? boot === installed : order >= 0;
|
|
34086
|
+
}
|
|
34087
|
+
function runtimeStragglerCount(body, current) {
|
|
34088
|
+
const currentBuild = current?.trim() ?? "";
|
|
34089
|
+
if (currentBuild === "") return null;
|
|
34090
|
+
if (!isRecord(body)) return null;
|
|
34091
|
+
const result = body.result;
|
|
34092
|
+
if (!isRecord(result)) return null;
|
|
34093
|
+
const runtimes = result.runtimes;
|
|
34094
|
+
if (!Array.isArray(runtimes)) return null;
|
|
34095
|
+
let count = 0;
|
|
34096
|
+
for (const row of runtimes) {
|
|
34097
|
+
if (!isRecord(row)) continue;
|
|
34098
|
+
const build = typeof row.build_version === "string" ? row.build_version.trim() : "";
|
|
34099
|
+
if (build === "") continue;
|
|
34100
|
+
if (build !== currentBuild) count += 1;
|
|
34101
|
+
}
|
|
34102
|
+
return count;
|
|
34103
|
+
}
|
|
34104
|
+
function daemonMoveOutcome(report) {
|
|
34105
|
+
if (report === null) return null;
|
|
34106
|
+
const failed = report.daemons.filter(
|
|
34107
|
+
(daemon) => daemon.status === "failed" || daemon.status === "unsupervised"
|
|
34108
|
+
).map((daemon) => daemon.name);
|
|
34109
|
+
return { source: "report", moved: failed.length === 0, failed };
|
|
34110
|
+
}
|
|
33602
34111
|
const CHROMIUM_TRANSPORT_CODES = [
|
|
33603
34112
|
"ERR_INTERNET_DISCONNECTED",
|
|
33604
34113
|
"ERR_NETWORK_CHANGED",
|
|
@@ -34388,6 +34897,7 @@ const BETA_VERSION_REGEX = /v\d+\.\d+\.\d+\.beta\.\d+/;
|
|
|
34388
34897
|
const ARCHITECTURE_SEPARATOR_REGEX = /\s+/;
|
|
34389
34898
|
const GLOBAL_UPDATE_TIMEOUT_MS = 15 * 60 * 1e3;
|
|
34390
34899
|
const SOURCE_REBUILD_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
34900
|
+
const UPDATE_RESTART_STOP_GRACE_MS = 5e3;
|
|
34391
34901
|
const BUNDLE_ID_REGEX = /^[\w.-]+$/;
|
|
34392
34902
|
const INSTALL_IN_FLIGHT_RECHECK_MS = 1e4;
|
|
34393
34903
|
const LAUNCHCTL_NOT_LOADED_REGEX = /could not find|no such process/i;
|
|
@@ -35847,6 +36357,21 @@ class UpdateService {
|
|
|
35847
36357
|
this.ensureWatchdogAfterInFlightQuit(marker);
|
|
35848
36358
|
return true;
|
|
35849
36359
|
}
|
|
36360
|
+
/**
|
|
36361
|
+
* Whether this quit's RELAUNCH is already owned by the update lane, as a
|
|
36362
|
+
* read-only question for the quit's own terminal.
|
|
36363
|
+
*
|
|
36364
|
+
* The same fact `quitForInFlightInstall` decides: when a live pending install
|
|
36365
|
+
* exists, the app quits so Squirrel's swap can proceed and the relaunch
|
|
36366
|
+
* watchdog is what brings it back. A second return path arriving at that swap
|
|
36367
|
+
* is exactly the failure both lanes guard against — Squirrel's last check
|
|
36368
|
+
* aborts an install when any instance of the target app is running — so #755's
|
|
36369
|
+
* successor launch (a reopen refused during the quit, completed with
|
|
36370
|
+
* `app.relaunch` at the quit terminal) must stand down while this is true.
|
|
36371
|
+
*/
|
|
36372
|
+
inFlightInstallOwnsRelaunch() {
|
|
36373
|
+
return this.inFlightQuitWatchdogEnsured;
|
|
36374
|
+
}
|
|
35850
36375
|
/**
|
|
35851
36376
|
* Kill a watchdog still running from the recorded install attempt.
|
|
35852
36377
|
*
|
|
@@ -38877,6 +39402,219 @@ ${heal.removed.join("\n")}`,
|
|
|
38877
39402
|
async readFleetSnapshot(backend) {
|
|
38878
39403
|
return backend.servingSessionFleet();
|
|
38879
39404
|
}
|
|
39405
|
+
/**
|
|
39406
|
+
* The serving process's own record, both fields from ONE read.
|
|
39407
|
+
*
|
|
39408
|
+
* The post-swap move proof compares an instance token against the pre-swap
|
|
39409
|
+
* one, and a pair assembled from two reads can straddle a republish (a new
|
|
39410
|
+
* instance under the OLD version string, or the reverse) - so the two fields
|
|
39411
|
+
* come out of a single `servingInstall()` call, which reads the record file
|
|
39412
|
+
* once. Absence is both-fields-null, never a papered-over reading: a record
|
|
39413
|
+
* that could not be read proves nothing and the verdict says so.
|
|
39414
|
+
*/
|
|
39415
|
+
servingRecordReadings() {
|
|
39416
|
+
const readings = this.backendService?.servingInstall().readings ?? servingInstallReadings(null);
|
|
39417
|
+
return {
|
|
39418
|
+
bootVersion: readings.bootVersion,
|
|
39419
|
+
instanceId: readings.instanceId
|
|
39420
|
+
};
|
|
39421
|
+
}
|
|
39422
|
+
/**
|
|
39423
|
+
* The pre-swap rows still resident in a fresh read, or null when unreadable.
|
|
39424
|
+
*
|
|
39425
|
+
* The liveness count's own arithmetic, in one place because two callers need
|
|
39426
|
+
* it in different shapes (the restart fallback counts from this, the moved
|
|
39427
|
+
* path's fallback counts THROUGH it). A read that could not be taken is null
|
|
39428
|
+
* rather than an empty set: "nothing resident" would be a measured zero, and
|
|
39429
|
+
* an unanswered roster is not a measurement. A cold row (an empty
|
|
39430
|
+
* `liveState`, the catalogue's spelling for a session with no runtime) is not
|
|
39431
|
+
* resident - `isLiveRow` owns that rule, the same one the engagement diff
|
|
39432
|
+
* uses.
|
|
39433
|
+
*/
|
|
39434
|
+
stillResidentRows(before, after) {
|
|
39435
|
+
if (after === null) return null;
|
|
39436
|
+
const liveNow = new Set(
|
|
39437
|
+
after.filter((row) => isLiveRow(row)).map((row) => row.sessionId)
|
|
39438
|
+
);
|
|
39439
|
+
return before.filter((row) => isLiveRow(row) && liveNow.has(row.sessionId));
|
|
39440
|
+
}
|
|
39441
|
+
/**
|
|
39442
|
+
* The BUILD-accurate straggler count: runtimes not on the build the server
|
|
39443
|
+
* is on, from the machine's runtime roster, or null when it did not answer.
|
|
39444
|
+
*
|
|
39445
|
+
* THIS IS THE PRIMARY CENSUS since 2026-09-30 (design §5): `GET
|
|
39446
|
+
* /v1/desktop/runtimes?probe=false` returns one row per live runtime with the
|
|
39447
|
+
* `build_version` the runtime's own record (or boot record) carries, so the
|
|
39448
|
+
* count answers the question the notice's sentence makes - "still on the old
|
|
39449
|
+
* version" - from a build reading rather than from a liveness proxy. An empty
|
|
39450
|
+
* `build_version` is UNKNOWN and is not counted (the roster journal's own
|
|
39451
|
+
* rule), and a row whose build equals the current one has already rolled.
|
|
39452
|
+
*
|
|
39453
|
+
* Null (unreadable body, non-200, a transport failure, or no current build to
|
|
39454
|
+
* compare against) sends the caller to the liveness fallback; the failure is
|
|
39455
|
+
* logged here, where the transport error is still in hand. The read is
|
|
39456
|
+
* deliberately per-call rather than cached: it is the one reading whose whole
|
|
39457
|
+
* point is that it moves as runtimes roll, so every completion asks the
|
|
39458
|
+
* machine again. A notice-reopen re-read is NOT wired (review round 1's A1):
|
|
39459
|
+
* no renderer->main census channel exists yet, so the count travels with the
|
|
39460
|
+
* completion it was measured for - recorded as deferred on the PR rather
|
|
39461
|
+
* than claimed here.
|
|
39462
|
+
*/
|
|
39463
|
+
async readRuntimeCensus(backend, current) {
|
|
39464
|
+
try {
|
|
39465
|
+
const response = await backend.requestDesktop({ op: "runtimes.list" });
|
|
39466
|
+
if (response.status !== 200) {
|
|
39467
|
+
logger.info(
|
|
39468
|
+
`Runtime roster did not answer (${response.status}); the straggler count falls back to the liveness reading`,
|
|
39469
|
+
LogFileType.UPDATE_SERVICE
|
|
39470
|
+
);
|
|
39471
|
+
return null;
|
|
39472
|
+
}
|
|
39473
|
+
const count = runtimeStragglerCount(response.body, current);
|
|
39474
|
+
if (count === null) {
|
|
39475
|
+
logger.info(
|
|
39476
|
+
"Runtime roster answered with no usable rows; the straggler count falls back to the liveness reading",
|
|
39477
|
+
LogFileType.UPDATE_SERVICE
|
|
39478
|
+
);
|
|
39479
|
+
}
|
|
39480
|
+
return count;
|
|
39481
|
+
} catch (error) {
|
|
39482
|
+
logger.info(
|
|
39483
|
+
`Runtime roster read failed (${error instanceof Error ? error.message : String(error)}); the straggler count falls back to the liveness reading`,
|
|
39484
|
+
LogFileType.UPDATE_SERVICE
|
|
39485
|
+
);
|
|
39486
|
+
return null;
|
|
39487
|
+
}
|
|
39488
|
+
}
|
|
39489
|
+
/**
|
|
39490
|
+
* The straggler count and where it came from: build first, liveness second.
|
|
39491
|
+
*
|
|
39492
|
+
* The build count (`readRuntimeCensus`) is the primary. When the roster does
|
|
39493
|
+
* not answer, the fallback is the EXISTING liveness reading, in whichever
|
|
39494
|
+
* shape the path already has it: the restart fallback passes the rows its own
|
|
39495
|
+
* post-restart read found resident (`resident`), while the moved path passes
|
|
39496
|
+
* the pre-update snapshot (`before`) and this takes one fresh fleet read to
|
|
39497
|
+
* count it. A path with neither - no snapshot, or a fleet read that failed -
|
|
39498
|
+
* answers null/null, which the notice renders as the numberless sentence: a
|
|
39499
|
+
* count that cannot be measured is not a zero.
|
|
39500
|
+
*
|
|
39501
|
+
* The source travels with the count for the RECORD, and for any later reader
|
|
39502
|
+
* that needs to know how it was measured - it does not choose the notice's
|
|
39503
|
+
* words. The notice keeps ONE session-family sentence for both sources
|
|
39504
|
+
* (designer copy consult, 2026-09-30: the producer's vocabulary belongs in
|
|
39505
|
+
* the payload field, never in the reader's sentence), so a build count is not
|
|
39506
|
+
* printed as "N runtimes" and the liveness reading's documented over-count
|
|
39507
|
+
* rides the field beside the count instead of changing the sentence.
|
|
39508
|
+
*/
|
|
39509
|
+
async readStragglerCensus(input) {
|
|
39510
|
+
const build = await this.readRuntimeCensus(input.backend, input.current);
|
|
39511
|
+
if (build !== null) return { count: build, source: "build" };
|
|
39512
|
+
let resident;
|
|
39513
|
+
if (input.resident !== void 0) {
|
|
39514
|
+
resident = input.resident;
|
|
39515
|
+
} else if (input.before != null) {
|
|
39516
|
+
resident = this.stillResidentRows(
|
|
39517
|
+
input.before,
|
|
39518
|
+
await this.readFleetSnapshot(input.backend)
|
|
39519
|
+
);
|
|
39520
|
+
} else {
|
|
39521
|
+
resident = null;
|
|
39522
|
+
}
|
|
39523
|
+
if (resident === null) return { count: null, source: null };
|
|
39524
|
+
logger.info(
|
|
39525
|
+
`Straggler count from the liveness reading: ${resident.length} pre-swap session(s) still resident`,
|
|
39526
|
+
LogFileType.UPDATE_SERVICE
|
|
39527
|
+
);
|
|
39528
|
+
return { count: resident.length, source: "liveness" };
|
|
39529
|
+
}
|
|
39530
|
+
/**
|
|
39531
|
+
* The restart fallback's tail for a generation install: ONE read, the
|
|
39532
|
+
* engage, and the resident set - no retire wait (2026-09-30).
|
|
39533
|
+
*
|
|
39534
|
+
* WHY THERE IS NO WAIT HERE. The rebuild route's `reengageDisplacedSessions`
|
|
39535
|
+
* waits for the pre-swap set to retire because a rebuild rewrites the tree a
|
|
39536
|
+
* runtime is reading, so runtimes really do die under it and the wave takes
|
|
39537
|
+
* ~35 s to pass. A serve restart displaces nothing by itself - session
|
|
39538
|
+
* runtimes are detached and survived the bounce (they retire on their own
|
|
39539
|
+
* build check, whenever their next idle is) - and the measured ordinary shape
|
|
39540
|
+
* was an idle-gated fleet that never moved, so the wait burned its full 60 s
|
|
39541
|
+
* grace for an engage it never performed. The re-engage opportunity it
|
|
39542
|
+
* represented is idle's job now (the 2026-09-29 directive), and the honest
|
|
39543
|
+
* statement replaces the wait: engage only the sessions THIS read shows
|
|
39544
|
+
* without a runtime, and count the rest as still resident.
|
|
39545
|
+
*
|
|
39546
|
+
* IDEMPOTENT BY THE SAME READ: the engage set is the difference the one read
|
|
39547
|
+
* shows, so a runtime that came back on its own is never spawned twice.
|
|
39548
|
+
* Returns null when the read could not be taken - not measured, which the
|
|
39549
|
+
* census and the caller's log both carry as such.
|
|
39550
|
+
*/
|
|
39551
|
+
async engageDisplacedAfterFallbackRestart(backend, snapshot2) {
|
|
39552
|
+
if (snapshot2 === null) return null;
|
|
39553
|
+
if (snapshot2.length === 0) return [];
|
|
39554
|
+
const after = await this.readFleetSnapshot(backend);
|
|
39555
|
+
const displaced = displacedSessions(snapshot2, after);
|
|
39556
|
+
const engaged = [];
|
|
39557
|
+
const failed = [];
|
|
39558
|
+
for (const row of displaced) {
|
|
39559
|
+
try {
|
|
39560
|
+
if (await this.engageSessionRuntime(backend, row))
|
|
39561
|
+
engaged.push(row.sessionId);
|
|
39562
|
+
else
|
|
39563
|
+
failed.push({
|
|
39564
|
+
sessionId: row.sessionId,
|
|
39565
|
+
reason: "the server did not take the engage"
|
|
39566
|
+
});
|
|
39567
|
+
} catch (error) {
|
|
39568
|
+
failed.push({
|
|
39569
|
+
sessionId: row.sessionId,
|
|
39570
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
39571
|
+
});
|
|
39572
|
+
}
|
|
39573
|
+
}
|
|
39574
|
+
logger.info(
|
|
39575
|
+
`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(", ")})` : ""}`,
|
|
39576
|
+
LogFileType.UPDATE_SERVICE
|
|
39577
|
+
);
|
|
39578
|
+
if (after === null) return null;
|
|
39579
|
+
return this.stillResidentRows(snapshot2, after);
|
|
39580
|
+
}
|
|
39581
|
+
/**
|
|
39582
|
+
* Say the rollover's evidence, once, where a post-mortem reads it.
|
|
39583
|
+
*
|
|
39584
|
+
* THE HONEST DEGRADE IS THE POINT (task §3): with the updater's structured
|
|
39585
|
+
* report absent - any core build before `update.report.v1` - serve is proven
|
|
39586
|
+
* by its own record and the daemons are BEST-EFFORT. That is said in exactly
|
|
39587
|
+
* those words rather than faked from a line that is not there, because the
|
|
39588
|
+
* alternative (treating an absent report as "daemons fine") is how a
|
|
39589
|
+
* user-visible claim gets made with no evidence behind it. A report that NAMES a
|
|
39590
|
+
* failed daemon is surfaced as a warning beside the same line.
|
|
39591
|
+
*
|
|
39592
|
+
* Names the three inputs the verdict read - the instance pair, the boot
|
|
39593
|
+
* reading, the `/health` answer - so a wrong verdict can be reconstructed
|
|
39594
|
+
* from the log without re-running anything.
|
|
39595
|
+
*/
|
|
39596
|
+
logRolloverEvidence(input) {
|
|
39597
|
+
const verdict = input.moved ? "proven moved" : "not proven moved";
|
|
39598
|
+
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"}`;
|
|
39599
|
+
const daemons = daemonMoveOutcome(input.report);
|
|
39600
|
+
if (daemons === null) {
|
|
39601
|
+
logger.info(
|
|
39602
|
+
`Rollover evidence: serve ${verdict} (${facts}); the updater printed no update.report.v1 line, so the daemons are best-effort with no evidence either way`,
|
|
39603
|
+
LogFileType.UPDATE_SERVICE
|
|
39604
|
+
);
|
|
39605
|
+
return;
|
|
39606
|
+
}
|
|
39607
|
+
logger.info(
|
|
39608
|
+
`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(", ")}`}`,
|
|
39609
|
+
LogFileType.UPDATE_SERVICE
|
|
39610
|
+
);
|
|
39611
|
+
if (!daemons.moved) {
|
|
39612
|
+
logger.warn(
|
|
39613
|
+
`Daemon refresh incomplete after the update: ${daemons.failed.join(", ")} (named by the update.report.v1 line; serve's own move is a separate reading)`,
|
|
39614
|
+
LogFileType.UPDATE_SERVICE
|
|
39615
|
+
);
|
|
39616
|
+
}
|
|
39617
|
+
}
|
|
38880
39618
|
/**
|
|
38881
39619
|
* The build the daemon serving this app BOOTED with, or null when its record
|
|
38882
39620
|
* carries no reading.
|
|
@@ -38898,7 +39636,7 @@ ${heal.removed.join("\n")}`,
|
|
|
38898
39636
|
* the same pair rather than about two different questions.
|
|
38899
39637
|
*/
|
|
38900
39638
|
servingBootVersion() {
|
|
38901
|
-
return this.
|
|
39639
|
+
return this.servingRecordReadings().bootVersion;
|
|
38902
39640
|
}
|
|
38903
39641
|
/**
|
|
38904
39642
|
* The running reading an update event carries to the renderer.
|
|
@@ -39269,6 +40007,7 @@ ${heal.removed.join("\n")}`,
|
|
|
39269
40007
|
sourceRebuild: rebuildPath
|
|
39270
40008
|
});
|
|
39271
40009
|
const rebuildRoute = freshPlan.managedRoute === "source-build" && rebuildPath !== null;
|
|
40010
|
+
const generationInstall = freshPlan.managedRoute === "entry-point";
|
|
39272
40011
|
if (!consolePath && !rebuildRoute) {
|
|
39273
40012
|
this.sendToRenderer("backend-update-error", {
|
|
39274
40013
|
message: "The global install could not be found on this machine, so the update did not run. See the update service log.",
|
|
@@ -39281,6 +40020,8 @@ ${heal.removed.join("\n")}`,
|
|
|
39281
40020
|
const before = this.readInstallVersionAt(installPath) ?? plan.installedInstallVersion;
|
|
39282
40021
|
const markerBefore = rebuildRoute ? this.readRebuildMarkerState(installPath) : null;
|
|
39283
40022
|
const fleetBeforeInstall = rebuildRoute ? await this.readFleetSnapshot(backend) : null;
|
|
40023
|
+
const serveInstanceBefore = generationInstall ? this.servingRecordReadings().instanceId : null;
|
|
40024
|
+
const fleetBeforeUpdate = generationInstall ? await this.readFleetSnapshot(backend) : null;
|
|
39284
40025
|
if (rebuildRoute && !await this.drainFleetForUpdate({
|
|
39285
40026
|
backend,
|
|
39286
40027
|
command: freshPlan.updateCommand ?? null
|
|
@@ -39379,9 +40120,63 @@ ${heal.removed.join("\n")}`,
|
|
|
39379
40120
|
`Global install updated: ${before ?? "unknown"} -> ${after ?? "unknown"}`,
|
|
39380
40121
|
LogFileType.UPDATE_SERVICE
|
|
39381
40122
|
);
|
|
39382
|
-
const
|
|
39383
|
-
|
|
39384
|
-
|
|
40123
|
+
const updateReport = generationInstall ? parseUpdateReport(run.stdout) : null;
|
|
40124
|
+
let serveRecordAfter = { bootVersion: null, instanceId: null };
|
|
40125
|
+
let serveHealthOk = null;
|
|
40126
|
+
let serveMovedWithoutRestart = false;
|
|
40127
|
+
if (generationInstall) {
|
|
40128
|
+
serveRecordAfter = this.servingRecordReadings();
|
|
40129
|
+
serveHealthOk = await this.checkBackendHealth();
|
|
40130
|
+
serveMovedWithoutRestart = serveMovedOntoBuild({
|
|
40131
|
+
instanceBefore: serveInstanceBefore,
|
|
40132
|
+
recordAfter: serveRecordAfter,
|
|
40133
|
+
installVersion: after,
|
|
40134
|
+
target: target2,
|
|
40135
|
+
reportServe: updateReport?.serve[0] ?? null,
|
|
40136
|
+
healthOk: serveHealthOk
|
|
40137
|
+
});
|
|
40138
|
+
this.logRolloverEvidence({
|
|
40139
|
+
report: updateReport,
|
|
40140
|
+
moved: serveMovedWithoutRestart,
|
|
40141
|
+
instanceBefore: serveInstanceBefore,
|
|
40142
|
+
recordAfter: serveRecordAfter,
|
|
40143
|
+
healthOk: serveHealthOk
|
|
40144
|
+
});
|
|
40145
|
+
}
|
|
40146
|
+
if (serveMovedWithoutRestart) {
|
|
40147
|
+
const census = await this.readStragglerCensus({
|
|
40148
|
+
backend,
|
|
40149
|
+
current: serveRecordAfter.bootVersion ?? after ?? target2,
|
|
40150
|
+
before: fleetBeforeUpdate
|
|
40151
|
+
});
|
|
40152
|
+
logger.info(
|
|
40153
|
+
`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`,
|
|
40154
|
+
LogFileType.UPDATE_SERVICE
|
|
40155
|
+
);
|
|
40156
|
+
this.sendToRenderer("backend-update-completed", {
|
|
40157
|
+
installVersion: after,
|
|
40158
|
+
/*
|
|
40159
|
+
* THE RECORD's reading, not `/health`'s (round 2, T1): the panel
|
|
40160
|
+
* compares these two strings, and only the process's own record
|
|
40161
|
+
* names the build it actually loaded. The moved verdict requires
|
|
40162
|
+
* a boot reading, so this arm cannot send a null it proved.
|
|
40163
|
+
*/
|
|
40164
|
+
runningVersion: serveRecordAfter.bootVersion,
|
|
40165
|
+
moved: true,
|
|
40166
|
+
restarted: false,
|
|
40167
|
+
restartable: this.backendIsAppOwned(),
|
|
40168
|
+
/*
|
|
40169
|
+
* FALSE BY CONSTRUCTION, and stated rather than left absent: this arm is
|
|
40170
|
+
* the global install's own updater, so the environment it moved is never
|
|
40171
|
+
* the app's managed one (design D5).
|
|
40172
|
+
*/
|
|
40173
|
+
appOwnedEnvironment: false,
|
|
40174
|
+
sessionsOnOldBuild: census.count,
|
|
40175
|
+
sessionsOnOldBuildSource: census.source
|
|
40176
|
+
});
|
|
40177
|
+
return true;
|
|
40178
|
+
}
|
|
40179
|
+
if (backend.isUsingExternalBackend()) {
|
|
39385
40180
|
const health = await this.getInstalledBackendVersion();
|
|
39386
40181
|
const running2 = this.eventRunningVersion(health);
|
|
39387
40182
|
logger.info(
|
|
@@ -39392,6 +40187,7 @@ ${heal.removed.join("\n")}`,
|
|
|
39392
40187
|
installVersion: after,
|
|
39393
40188
|
runningVersion: running2,
|
|
39394
40189
|
restarted: false,
|
|
40190
|
+
moved: false,
|
|
39395
40191
|
restartable: this.backendIsAppOwned(),
|
|
39396
40192
|
/*
|
|
39397
40193
|
* FALSE BY CONSTRUCTION, and stated rather than left absent: this arm is
|
|
@@ -39402,13 +40198,21 @@ ${heal.removed.join("\n")}`,
|
|
|
39402
40198
|
});
|
|
39403
40199
|
return true;
|
|
39404
40200
|
}
|
|
40201
|
+
if (generationInstall) {
|
|
40202
|
+
logger.info(
|
|
40203
|
+
`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`,
|
|
40204
|
+
LogFileType.UPDATE_SERVICE
|
|
40205
|
+
);
|
|
40206
|
+
}
|
|
39405
40207
|
logger.info(
|
|
39406
40208
|
"Restarting backend service onto the updated install...",
|
|
39407
40209
|
LogFileType.UPDATE_SERVICE
|
|
39408
40210
|
);
|
|
39409
40211
|
this.sendToRenderer("backend-update-progress", { phase: "restarting" });
|
|
39410
40212
|
const fleetBeforeRestart = await this.readFleetSnapshot(backend);
|
|
39411
|
-
const restartSuccess = await backend.restart(
|
|
40213
|
+
const restartSuccess = await backend.restart({
|
|
40214
|
+
stopGraceMs: UPDATE_RESTART_STOP_GRACE_MS
|
|
40215
|
+
});
|
|
39412
40216
|
if (!restartSuccess) {
|
|
39413
40217
|
logger.error(
|
|
39414
40218
|
"Backend service restart failed after the global install update",
|
|
@@ -39465,12 +40269,43 @@ ${heal.removed.join("\n")}`,
|
|
|
39465
40269
|
fleetBeforeInstall,
|
|
39466
40270
|
fleetBeforeRestart
|
|
39467
40271
|
);
|
|
39468
|
-
|
|
40272
|
+
let sessionsOnOldBuild;
|
|
40273
|
+
let sessionsOnOldBuildSource;
|
|
40274
|
+
if (rebuildRoute) {
|
|
40275
|
+
const reengage = reengageSnapshot !== null ? await this.reengageFleetAfterRestart(backend, reengageSnapshot) : null;
|
|
40276
|
+
const census = await this.readStragglerCensus({
|
|
40277
|
+
backend,
|
|
40278
|
+
current: after ?? target2,
|
|
40279
|
+
resident: reengage !== null ? reengage.stillResident : null
|
|
40280
|
+
});
|
|
40281
|
+
sessionsOnOldBuild = census.count;
|
|
40282
|
+
sessionsOnOldBuildSource = census.source;
|
|
40283
|
+
} else {
|
|
40284
|
+
const resident = await this.engageDisplacedAfterFallbackRestart(
|
|
40285
|
+
backend,
|
|
40286
|
+
reengageSnapshot
|
|
40287
|
+
);
|
|
40288
|
+
const census = await this.readStragglerCensus({
|
|
40289
|
+
backend,
|
|
40290
|
+
current: reported ?? after ?? target2,
|
|
40291
|
+
resident
|
|
40292
|
+
});
|
|
40293
|
+
sessionsOnOldBuild = census.count;
|
|
40294
|
+
sessionsOnOldBuildSource = census.source;
|
|
40295
|
+
}
|
|
39469
40296
|
this.sendToRenderer("backend-update-completed", {
|
|
39470
40297
|
installVersion: after,
|
|
39471
40298
|
runningVersion: reported,
|
|
39472
40299
|
restarted: true,
|
|
39473
|
-
|
|
40300
|
+
/*
|
|
40301
|
+
* The app moved this daemon onto the new build with the restart, and the
|
|
40302
|
+
* distinction is stated where it was proven (`reported` was held
|
|
40303
|
+
* at-or-past the target above). Absent on the rebuild route, which keeps
|
|
40304
|
+
* its version and its own sentence.
|
|
40305
|
+
*/
|
|
40306
|
+
...rebuildRoute ? {} : { moved: true },
|
|
40307
|
+
sessionsOnOldBuild,
|
|
40308
|
+
sessionsOnOldBuildSource
|
|
39474
40309
|
});
|
|
39475
40310
|
return true;
|
|
39476
40311
|
}
|
|
@@ -40655,6 +41490,7 @@ function armHeadlessExitDeadline(why) {
|
|
|
40655
41490
|
const line = `${why}; the quit did not finish within ${LAUNCHER_EXIT_DEADLINE_MS} ms, exiting`;
|
|
40656
41491
|
logger.error(`[window-mode] ${line}`, LogFileType.BACKEND);
|
|
40657
41492
|
console.log(`[window-mode] ${line}`);
|
|
41493
|
+
scheduleReopenAtQuitTerminal();
|
|
40658
41494
|
electron.app.exit(0);
|
|
40659
41495
|
}, LAUNCHER_EXIT_DEADLINE_MS);
|
|
40660
41496
|
deadline2.unref();
|
|
@@ -41110,10 +41946,17 @@ const userShellPath = createUserShellPath({
|
|
|
41110
41946
|
});
|
|
41111
41947
|
const backendService = new BackendServiceManager({ userShellPath });
|
|
41112
41948
|
const quitState = createQuitState();
|
|
41949
|
+
const relaunchPending = createRelaunchPending();
|
|
41113
41950
|
const backendInstaller = new BackendInstaller({
|
|
41114
41951
|
// The installer's one route back from a cancelled quit: the declined dialog
|
|
41115
|
-
// answer (`./quit-state` owns why there is exactly one such site).
|
|
41116
|
-
|
|
41952
|
+
// answer (`./quit-state` owns why there is exactly one such site). The
|
|
41953
|
+
// refused reopen's record is let go BESIDE that release: a cancelled quit
|
|
41954
|
+
// must not leak a spawn into some later, unrelated exit
|
|
41955
|
+
// (`./relaunch-pending` owns why scheduling happens only at the terminal).
|
|
41956
|
+
onQuitCancelled: () => {
|
|
41957
|
+
quitState.cancel();
|
|
41958
|
+
relaunchPending.clear();
|
|
41959
|
+
}
|
|
41117
41960
|
});
|
|
41118
41961
|
const reportBackendFailure = (message, fileType) => {
|
|
41119
41962
|
if (backendService.isShuttingDown()) {
|
|
@@ -41338,6 +42181,20 @@ if (!gotTheLock) {
|
|
|
41338
42181
|
* `window-raise.ts` owns the refusal and its line).
|
|
41339
42182
|
*/
|
|
41340
42183
|
quitting: quitState.isQuitting(),
|
|
42184
|
+
/*
|
|
42185
|
+
* #755: A REFUSED SECOND LAUNCH LEAVES A RECORD. The request that would have been
|
|
42186
|
+
* refused whole is one the user made, so beside the refusal it is recorded and
|
|
42187
|
+
* the quit's terminal completes it with one successor instance
|
|
42188
|
+
* (`./relaunch-pending` owns the shape and the exactly-once rule). `commandLine`
|
|
42189
|
+
* is THIS request's own, so what the successor replays cannot drift from what
|
|
42190
|
+
* was refused, and the recorder's answer is what makes the refusal line say
|
|
42191
|
+
* `reopen=deferred` (`window-raise.ts` owns the token).
|
|
42192
|
+
*/
|
|
42193
|
+
onRefusedWhileQuitting: (request) => relaunchPending.record({
|
|
42194
|
+
kind: "second-instance",
|
|
42195
|
+
show: request.show,
|
|
42196
|
+
argv: commandLine
|
|
42197
|
+
}),
|
|
41341
42198
|
openConversation: openConversationInWindow ? (session, request) => openConversationInWindow?.(
|
|
41342
42199
|
session,
|
|
41343
42200
|
secondInstanceRequest(request)
|
|
@@ -42317,9 +43174,15 @@ electron.app.whenReady().then(async () => {
|
|
|
42317
43174
|
reassertDockParticipation();
|
|
42318
43175
|
if (electron.BrowserWindow.getAllWindows().length === 0) {
|
|
42319
43176
|
if (quitState.isQuitting()) {
|
|
43177
|
+
const deferredReopen = relaunchPending.record({
|
|
43178
|
+
kind: "activate",
|
|
43179
|
+
show: OPERATOR_SHOW,
|
|
43180
|
+
argv: null
|
|
43181
|
+
});
|
|
42320
43182
|
reportSkippedWhileQuitting(
|
|
42321
43183
|
{ trigger: "activate", report: reportRaise },
|
|
42322
|
-
OPERATOR_SHOW
|
|
43184
|
+
OPERATOR_SHOW,
|
|
43185
|
+
deferredReopen
|
|
42323
43186
|
);
|
|
42324
43187
|
return;
|
|
42325
43188
|
}
|
|
@@ -42383,9 +43246,40 @@ electron.app.on("window-all-closed", () => {
|
|
|
42383
43246
|
const QUIT_FAILSAFE_MARGIN_MS = 5e3;
|
|
42384
43247
|
const QUIT_CLEANUP_FAILSAFE_MS = LAUNCHER_PROBE_WORST_MS + INTERPRETER_RESOLUTION_WORST_MS + OWNED_STOP_WORST_MS + READINESS_POLL_INTERVAL_MS + QUIT_FAILSAFE_MARGIN_MS;
|
|
42385
43248
|
let backendQuitPending = false;
|
|
43249
|
+
let relaunchStandDownLogged = false;
|
|
43250
|
+
const scheduleReopenAtQuitTerminal = () => {
|
|
43251
|
+
if (activeUpdateService?.inFlightInstallOwnsRelaunch() === true) {
|
|
43252
|
+
if (relaunchPending.hasPending() && !relaunchStandDownLogged) {
|
|
43253
|
+
relaunchStandDownLogged = true;
|
|
43254
|
+
logger.info(
|
|
43255
|
+
"[relaunch] not scheduling a successor: an in-flight update install owns this quit's relaunch",
|
|
43256
|
+
LogFileType.BACKEND
|
|
43257
|
+
);
|
|
43258
|
+
}
|
|
43259
|
+
return;
|
|
43260
|
+
}
|
|
43261
|
+
relaunchPending.scheduleOnExit({
|
|
43262
|
+
relaunch: (args) => {
|
|
43263
|
+
try {
|
|
43264
|
+
electron.app.relaunch({ args: [...args] });
|
|
43265
|
+
logger.info(
|
|
43266
|
+
`[relaunch] successor scheduled to complete the deferred reopen: ${args.length === 0 ? "(plain reopen)" : args.join(" ")}`,
|
|
43267
|
+
LogFileType.BACKEND
|
|
43268
|
+
);
|
|
43269
|
+
} catch (error) {
|
|
43270
|
+
logger.error(
|
|
43271
|
+
"[relaunch] scheduling the successor failed; the quit continues",
|
|
43272
|
+
LogFileType.BACKEND,
|
|
43273
|
+
error
|
|
43274
|
+
);
|
|
43275
|
+
}
|
|
43276
|
+
}
|
|
43277
|
+
});
|
|
43278
|
+
};
|
|
42386
43279
|
electron.app.on("will-quit", (event) => {
|
|
42387
43280
|
viewerEndpoint?.close();
|
|
42388
43281
|
viewerRecord?.stop();
|
|
43282
|
+
scheduleReopenAtQuitTerminal();
|
|
42389
43283
|
if (backendService.isOwnedCleanupComplete()) return;
|
|
42390
43284
|
event.preventDefault();
|
|
42391
43285
|
if (backendQuitPending) return;
|
|
@@ -42395,11 +43289,13 @@ electron.app.on("will-quit", (event) => {
|
|
|
42395
43289
|
`Owned backend cleanup did not finish within ${QUIT_CLEANUP_FAILSAFE_MS} ms; exiting with failure`,
|
|
42396
43290
|
LogFileType.BACKEND
|
|
42397
43291
|
);
|
|
43292
|
+
scheduleReopenAtQuitTerminal();
|
|
42398
43293
|
electron.app.exit(1);
|
|
42399
43294
|
}, QUIT_CLEANUP_FAILSAFE_MS);
|
|
42400
43295
|
failsafe.unref();
|
|
42401
43296
|
void backendService.stop(false).then(() => {
|
|
42402
43297
|
clearTimeout(failsafe);
|
|
43298
|
+
scheduleReopenAtQuitTerminal();
|
|
42403
43299
|
electron.app.exit(0);
|
|
42404
43300
|
}).catch((error) => {
|
|
42405
43301
|
clearTimeout(failsafe);
|
|
@@ -42408,6 +43304,7 @@ electron.app.on("will-quit", (event) => {
|
|
|
42408
43304
|
LogFileType.BACKEND,
|
|
42409
43305
|
error
|
|
42410
43306
|
);
|
|
43307
|
+
scheduleReopenAtQuitTerminal();
|
|
42411
43308
|
electron.app.exit(1);
|
|
42412
43309
|
});
|
|
42413
43310
|
});
|
|
@@ -42469,5 +43366,8 @@ process.on("uncaughtException", (error) => {
|
|
|
42469
43366
|
LogFileType.BACKEND,
|
|
42470
43367
|
stopError
|
|
42471
43368
|
);
|
|
42472
|
-
}).finally(() =>
|
|
43369
|
+
}).finally(() => {
|
|
43370
|
+
scheduleReopenAtQuitTerminal();
|
|
43371
|
+
process.exit(1);
|
|
43372
|
+
});
|
|
42473
43373
|
});
|