local-operator-ui 0.31.27 → 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.
Files changed (114) hide show
  1. package/out/main/index.js +1031 -63
  2. package/out/preload/index.js +37 -0
  3. package/out/renderer/assets/{_basePickBy-oM-LmypH.js → _basePickBy-DL33QdW4.js} +1 -1
  4. package/out/renderer/assets/{_baseUniq-CfyEpPZd.js → _baseUniq-CBRLNfCa.js} +1 -1
  5. package/out/renderer/assets/{agent-details-page-DIBks4X5.js → agent-details-page-JNorVbUk.js} +2 -2
  6. package/out/renderer/assets/{agent-hub-page-oV4gEqC4.js → agent-hub-page-B53zgVHZ.js} +3 -3
  7. package/out/renderer/assets/agents-page-D5VssooM.js +7 -0
  8. package/out/renderer/assets/arc-C1D2LaN2.js +1 -0
  9. package/out/renderer/assets/architectureDiagram-IEHRJDOE-Dm1oPlGu.js +36 -0
  10. package/out/renderer/assets/{blockDiagram-JOT3LUYC-Cn1dvDZU.js → blockDiagram-JOT3LUYC-CeNuHJ3J.js} +2 -2
  11. package/out/renderer/assets/{book-open-P6KBj9Du.js → book-open-C2GJZCxM.js} +1 -1
  12. package/out/renderer/assets/browser-page-DHwPmeL7.js +1 -0
  13. package/out/renderer/assets/browser-webauthn-prompt-Cy_dHOdv.js +1 -0
  14. package/out/renderer/assets/{c4Diagram-VJAJSXHY-B8F1XGHT.js → c4Diagram-VJAJSXHY-Cieumjrn.js} +4 -4
  15. package/out/renderer/assets/channel-BipwWMab.js +1 -0
  16. package/out/renderer/assets/{chunk-4BMEZGHF-CVoXO2u6.js → chunk-4BMEZGHF-CeIWDTcE.js} +1 -1
  17. package/out/renderer/assets/{chunk-A2AXSNBT-LFo0s6P7.js → chunk-A2AXSNBT-nkZ-ZEU8.js} +1 -1
  18. package/out/renderer/assets/{chunk-AEK57VVT-B8s36YJo.js → chunk-AEK57VVT-D3HkgezL.js} +1 -1
  19. package/out/renderer/assets/{chunk-D6G4REZN-BKkRpyOx.js → chunk-D6G4REZN-BZjnLSCT.js} +1 -1
  20. package/out/renderer/assets/{chunk-RZ5BOZE2-qoB87AlE.js → chunk-RZ5BOZE2-RMzqoQiC.js} +1 -1
  21. package/out/renderer/assets/{chunk-XZIHB7SX-BACgoEDe.js → chunk-XZIHB7SX-DJl2yrB3.js} +1 -1
  22. package/out/renderer/assets/classDiagram-GIVACNV2-C7VPQGdP.js +1 -0
  23. package/out/renderer/assets/classDiagram-v2-COTLJTTW-C7VPQGdP.js +1 -0
  24. package/out/renderer/assets/clone-fWuYSDG8.js +1 -0
  25. package/out/renderer/assets/{console-mirror-fXJ5fMxl.js → console-mirror-BhuN6Ypv.js} +1 -1
  26. package/out/renderer/assets/{consoleCapture-Df4uIOqu.js → consoleCapture-BNipqRoj.js} +1 -1
  27. package/out/renderer/assets/conversation-input-sync-BoRhB7VS.js +918 -0
  28. package/out/renderer/assets/conversation-input-sync-CIGg7Rga.css +1 -0
  29. package/out/renderer/assets/dagre-OKDRZEBW-DbaX-Vfd.js +4 -0
  30. package/out/renderer/assets/diagram-SSKATNLV-BP9pThlk.js +43 -0
  31. package/out/renderer/assets/diagram-VNBRO52H-BM5HUD5o.js +24 -0
  32. package/out/renderer/assets/{erDiagram-Q7BY3M3F-CGjlhqbg.js → erDiagram-Q7BY3M3F-CUqUCymj.js} +2 -2
  33. package/out/renderer/assets/error-boundary-CAyz2bjY.js +126 -0
  34. package/out/renderer/assets/{flowDiagram-4HSFHLVR-rb1I18B8.js → flowDiagram-4HSFHLVR-BPz65dD6.js} +2 -2
  35. package/out/renderer/assets/{ganttDiagram-APWFNJXF-D-bSpERZ.js → ganttDiagram-APWFNJXF-ccu__hit.js} +2 -2
  36. package/out/renderer/assets/gitGraphDiagram-7IBYFJ6S-Ct9B7LDY.js +65 -0
  37. package/out/renderer/assets/{graph-BkNT14na.js → graph-DIsum2dA.js} +1 -1
  38. package/out/renderer/assets/index-BWjn66XI.js +1 -0
  39. package/out/renderer/assets/index-Bgxehgfc.js +784 -0
  40. package/out/renderer/assets/index-CCRonkWc.css +1 -0
  41. package/out/renderer/assets/{index-_dbvHdgc.js → index-CTnUXmyg.js} +1 -1
  42. package/out/renderer/assets/index-DHKnfVbX.css +1 -0
  43. package/out/renderer/assets/index-FX4vEaoW.js +7 -0
  44. package/out/renderer/assets/infoDiagram-PH2N3AL5-Bey34vow.js +2 -0
  45. package/out/renderer/assets/installer-Bu8Z9JPe.js +1 -0
  46. package/out/renderer/assets/{journeyDiagram-U35MCT3I-B5bji5kb.js → journeyDiagram-U35MCT3I-jn9ZwYXW.js} +6 -6
  47. package/out/renderer/assets/{kanban-definition-NDS4AKOZ-BGi8PMEB.js → kanban-definition-NDS4AKOZ-C-wz8kvA.js} +4 -4
  48. package/out/renderer/assets/{layout-D3rwe0Wt.js → layout-BREzqltz.js} +1 -1
  49. package/out/renderer/assets/{legacy-agents-page-DnpWTuf8.js → legacy-agents-page-CO-TGqR5.js} +4 -4
  50. package/out/renderer/assets/{mermaid.core-CHiYins7.js → mermaid.core-8-ikrzww.js} +5 -5
  51. package/out/renderer/assets/mesh-page-0CSX8EG-.js +1 -0
  52. package/out/renderer/assets/mindmap-definition-ALO5MXBD-CJzxvyVl.js +95 -0
  53. package/out/renderer/assets/mini-DIRe7eu8.js +1 -0
  54. package/out/renderer/assets/org-surface-gate-C1t_jjzP.js +1 -0
  55. package/out/renderer/assets/{page-header-B3kbzqCp.js → page-header-CKzo9EwH.js} +1 -1
  56. package/out/renderer/assets/pieDiagram-IB7DONF6-B38MVZvk.js +30 -0
  57. package/out/renderer/assets/projects-page-4aBbeqHa.js +55 -0
  58. package/out/renderer/assets/{quadrantDiagram-7GDLP6J5-BZkLgvoE.js → quadrantDiagram-7GDLP6J5-CGwbmj-X.js} +4 -4
  59. package/out/renderer/assets/{radar-MK3ICKWK-QB0E4nFC.js → radar-MK3ICKWK-0J95rurP.js} +1 -1
  60. package/out/renderer/assets/radient-auth-buttons-DlO_vXxS.js +1 -0
  61. package/out/renderer/assets/{requirementDiagram-KVF5MWMF-Cb1rqfe3.js → requirementDiagram-KVF5MWMF-Csqc9PAd.js} +3 -3
  62. package/out/renderer/assets/sankeyDiagram-QLVOVGJD-Lm7B9S0v.js +10 -0
  63. package/out/renderer/assets/{schedules-page-DnHKOQij.js → schedules-page-BI6mduTY.js} +12 -12
  64. package/out/renderer/assets/{sequenceDiagram-X6HHIX6F-Bh-yCDC0.js → sequenceDiagram-X6HHIX6F-Dw8RM08f.js} +2 -2
  65. package/out/renderer/assets/settings-page-BsIAdS9g.js +292 -0
  66. package/out/renderer/assets/{settings-section-C9etJXA9.js → settings-section-DlTh8xfK.js} +1 -1
  67. package/out/renderer/assets/stateDiagram-DGXRK772-DHKpJy8t.js +1 -0
  68. package/out/renderer/assets/stateDiagram-v2-YXO3MK2T-CFuWzCss.js +1 -0
  69. package/out/renderer/assets/{timeline-definition-BDJGKUSR-pgK_gxyk.js → timeline-definition-BDJGKUSR-BxyX64Ja.js} +2 -2
  70. package/out/renderer/assets/ui-preferences-store-C5PakFPc.js +1 -0
  71. package/out/renderer/assets/{use-download-agent-mutation-CXw_g4Rv.js → use-download-agent-mutation-CeaoIrQs.js} +3 -3
  72. package/out/renderer/assets/use-memberships-query-D95x-vrx.js +1 -0
  73. package/out/renderer/assets/{xychartDiagram-VJFVF3MP-CYYxIZAM.js → xychartDiagram-VJFVF3MP-PNx7e-Lx.js} +6 -6
  74. package/out/renderer/console-capture.html +5 -5
  75. package/out/renderer/index.html +9 -9
  76. package/out/renderer/installer.html +4 -5
  77. package/out/renderer/mini.html +7 -6
  78. package/package.json +9 -3
  79. package/out/renderer/assets/agents-page-CZrNPorc.js +0 -7
  80. package/out/renderer/assets/aida-control-CQx1CGx-.js +0 -50
  81. package/out/renderer/assets/arc-DXJiD7SC.js +0 -1
  82. package/out/renderer/assets/architectureDiagram-IEHRJDOE-VHM5BLyo.js +0 -36
  83. package/out/renderer/assets/browser-page-D6eiItUp.js +0 -1
  84. package/out/renderer/assets/browser-webauthn-prompt-CByfVm08.js +0 -1
  85. package/out/renderer/assets/channel-DZQWNfux.js +0 -1
  86. package/out/renderer/assets/classDiagram-GIVACNV2-jw9F6ZK-.js +0 -1
  87. package/out/renderer/assets/classDiagram-v2-COTLJTTW-jw9F6ZK-.js +0 -1
  88. package/out/renderer/assets/clone-DzWAH3PE.js +0 -1
  89. package/out/renderer/assets/dagre-OKDRZEBW-CLMw1D7m.js +0 -4
  90. package/out/renderer/assets/diagram-SSKATNLV-plMydQ5N.js +0 -43
  91. package/out/renderer/assets/diagram-VNBRO52H-BptPse7H.js +0 -24
  92. package/out/renderer/assets/error-boundary-D3Um1PIl.js +0 -6
  93. package/out/renderer/assets/gitGraphDiagram-7IBYFJ6S-DDJQIXgd.js +0 -65
  94. package/out/renderer/assets/index-2qmFWi_F.css +0 -1
  95. package/out/renderer/assets/index-BXamXVGw.js +0 -7
  96. package/out/renderer/assets/index-D3ICBbhi.js +0 -1642
  97. package/out/renderer/assets/index-DHcQTmYa.js +0 -1
  98. package/out/renderer/assets/index-DuUnXH9L.css +0 -1
  99. package/out/renderer/assets/infoDiagram-PH2N3AL5-Bg2dnJEw.js +0 -2
  100. package/out/renderer/assets/installer-D-IQpqXR.js +0 -1
  101. package/out/renderer/assets/mesh-page-B2_ZtHTQ.js +0 -1
  102. package/out/renderer/assets/mindmap-definition-ALO5MXBD-BwO55Ck2.js +0 -95
  103. package/out/renderer/assets/mini-D7VYY0xP.js +0 -1
  104. package/out/renderer/assets/org-surface-gate-Da1VmZbG.js +0 -1
  105. package/out/renderer/assets/pieDiagram-IB7DONF6-j0i7dQ2D.js +0 -30
  106. package/out/renderer/assets/projects-page-f-tkHojn.js +0 -35
  107. package/out/renderer/assets/radient-auth-buttons-2wnsv7f6.js +0 -1
  108. package/out/renderer/assets/sankeyDiagram-QLVOVGJD-ST_5m9Zy.js +0 -10
  109. package/out/renderer/assets/settings-page-D1dF4GZE.js +0 -292
  110. package/out/renderer/assets/stateDiagram-DGXRK772-D6MGh56o.js +0 -1
  111. package/out/renderer/assets/stateDiagram-v2-YXO3MK2T-Dvv5Bcoz.js +0 -1
  112. package/out/renderer/assets/textarea-DeXGhzIK.js +0 -121
  113. package/out/renderer/assets/ui-preferences-store-BLiDaCIL.js +0 -1
  114. package/out/renderer/assets/use-memberships-query-BGPQ6jDl.js +0 -1
package/out/main/index.js CHANGED
@@ -62,12 +62,14 @@ function pairingHasRemedy(cause) {
62
62
  const id$1 = zod.z.string().min(1).max(128).regex(/^[a-zA-Z0-9_-]+$/);
63
63
  const settingKey = zod.z.string().min(1).max(256).regex(/^[a-zA-Z0-9_][a-zA-Z0-9_.-]*$/);
64
64
  const secret = zod.z.string().min(1).max(32768);
65
- const sessionId = zod.z.string().regex(/^[a-f0-9]{12}$/);
65
+ const sessionIdPattern = /^[a-f0-9]{12}$/;
66
+ const sessionId = zod.z.string().regex(sessionIdPattern);
66
67
  const skillCwd = zod.z.string().min(1).max(4096);
67
68
  const mcpServerName = zod.z.string().regex(/^[A-Za-z0-9_.:-]{1,100}$/);
68
69
  const mcpSecretReference = zod.z.string().regex(/^\$\{[A-Za-z_][A-Za-z0-9_]*\}$/);
69
70
  const mcpOperationId = zod.z.string().regex(/^[a-f0-9]{32}$/);
70
- const mcpCatalogCwd = zod.z.string().min(1).max(4096).regex(/^(\/|[A-Za-z]:[\\/])/);
71
+ const mcpCatalogCwdPattern = /^(\/|[A-Za-z]:[\\/])/;
72
+ const mcpCatalogCwd = zod.z.string().min(1).max(4096).regex(mcpCatalogCwdPattern);
71
73
  const SUBSCRIPTION_ID_PATTERN = /^[a-f0-9]{32}$/;
72
74
  const requestId = zod.z.string().regex(/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/);
73
75
  const meshId = zod.z.string().regex(/^[A-Za-z0-9_-]{1,128}$/);
@@ -100,7 +102,17 @@ const profileFields = zod.z.object({
100
102
  instructions: zod.z.string().max(8e3).optional(),
101
103
  tools: zod.z.array(zod.z.string().min(1).max(128)).max(256).optional(),
102
104
  effort: zod.z.string().max(64).optional(),
103
- 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()
104
116
  }).strict();
105
117
  const teamFields = zod.z.object({
106
118
  name: zod.z.string().min(1).max(64).optional(),
@@ -266,6 +278,25 @@ const PROJECT_MILESTONE_NAME_MAX_CHARS = 80;
266
278
  const hubItemKind = zod.z.enum(["agent", "team"]);
267
279
  const desktopRequestUnion = zod.z.discriminatedUnion("op", [
268
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(),
269
300
  zod.z.object({ op: zod.z.literal("profiles.list") }).strict(),
270
301
  zod.z.object({ op: zod.z.literal("profiles.get"), name: profileName }).strict(),
271
302
  zod.z.object({ op: zod.z.literal("profiles.install"), name: profileName, requestId }).strict(),
@@ -755,11 +786,38 @@ const desktopRequestUnion = zod.z.discriminatedUnion("op", [
755
786
  zod.z.object({
756
787
  op: zod.z.literal("sessions.answer"),
757
788
  sessionId,
758
- epoch: id$1,
759
- requestId: id$1,
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(),
760
796
  value: zod.z.string().max(32768).optional(),
761
797
  approved: zod.z.boolean().optional(),
762
- 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()
763
821
  }).strict(),
764
822
  zod.z.object({
765
823
  op: zod.z.literal("sessions.seen"),
@@ -1234,7 +1292,25 @@ const desktopRequestUnion = zod.z.discriminatedUnion("op", [
1234
1292
  sessionId,
1235
1293
  requestId,
1236
1294
  message: zod.z.string().max(2e5).optional(),
1237
- boundary: zod.z.literal("next_safe").optional()
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()
1238
1314
  }).strict(),
1239
1315
  zod.z.object({
1240
1316
  op: zod.z.literal("sessions.stop"),
@@ -1758,6 +1834,20 @@ const desktopRequestUnion = zod.z.discriminatedUnion("op", [
1758
1834
  key: projectKey,
1759
1835
  name: zod.z.string().min(1).max(PROJECT_MILESTONE_NAME_MAX_CHARS)
1760
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(),
1761
1851
  /*
1762
1852
  * AIDA'S CONTROL PLANE: one read and one control op on the same route
1763
1853
  * (`/v1/desktop/aida`), because the rail's row and the composer's `/aida`
@@ -1785,6 +1875,48 @@ const desktopRequestUnion = zod.z.discriminatedUnion("op", [
1785
1875
  ]);
1786
1876
  const desktopRequestSchema = desktopRequestUnion.superRefine(
1787
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
+ }
1788
1920
  if (request.op === "sessions.create" && request.purpose === void 0) {
1789
1921
  if (request.cwd === void 0) {
1790
1922
  ctx.addIssue({
@@ -1980,6 +2112,9 @@ const READ_ONLY_OPS = /* @__PURE__ */ new Set([
1980
2112
  "profiles.get",
1981
2113
  "profiles.list",
1982
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",
1983
2118
  "sessions.aside.get",
1984
2119
  "sessions.checkpoints",
1985
2120
  "sessions.failovers",
@@ -2071,6 +2206,8 @@ function desktopEndpoint(request) {
2071
2206
  switch (request.op) {
2072
2207
  case "capabilities":
2073
2208
  return { path: "/v1/capabilities", method: "GET" };
2209
+ case "runtimes.list":
2210
+ return { path: "/v1/desktop/runtimes?probe=false", method: "GET" };
2074
2211
  /*
2075
2212
  * The mesh reads. Both are plain GETs with no parameters at all - the backend
2076
2213
  * reads THIS device's own relay, so there is nothing for the client to scope it
@@ -2404,6 +2541,16 @@ function desktopEndpoint(request) {
2404
2541
  }
2405
2542
  };
2406
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
+ };
2407
2554
  return {
2408
2555
  path: `/v1/desktop/sessions/${request.sessionId}/answers`,
2409
2556
  method: "POST",
@@ -2852,7 +2999,17 @@ function desktopEndpoint(request) {
2852
2999
  body: {
2853
3000
  request_id: request.requestId,
2854
3001
  message: request.message,
2855
- 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
2856
3013
  }
2857
3014
  };
2858
3015
  case "sessions.stop":
@@ -3086,6 +3243,11 @@ function desktopEndpoint(request) {
3086
3243
  path: `/v1/desktop/projects/${encodeURIComponent(request.key)}/milestones/${encodeURIComponent(request.name)}`,
3087
3244
  method: "DELETE"
3088
3245
  };
3246
+ case "projects.request_update":
3247
+ return {
3248
+ path: `/v1/desktop/projects/${encodeURIComponent(request.key)}/request-update`,
3249
+ method: "POST"
3250
+ };
3089
3251
  case "aida.status":
3090
3252
  return { path: "/v1/desktop/aida", method: "GET" };
3091
3253
  case "aida.control":
@@ -3108,6 +3270,9 @@ const MINI_VIEW_SUMMONED = "mini-view:summoned";
3108
3270
  const MINI_VIEW_DISMISS = "mini-view:dismiss";
3109
3271
  const MINI_VIEW_REGISTRATION = "mini-view:registration";
3110
3272
  const MINI_VIEW_REGISTRATION_GET = "mini-view:registration-get";
3273
+ const MINI_VIEW_RESIZE = "mini-view:resize";
3274
+ const MINI_VIEW_DIALOG = "mini-view:dialog";
3275
+ const MINI_VIEW_PAINTED = "mini-view:painted";
3111
3276
  const MINI_VIEW_DISMISS_REASONS = [
3112
3277
  "escape",
3113
3278
  "blur",
@@ -3119,6 +3284,10 @@ function isMiniViewDismissReason(value) {
3119
3284
  }
3120
3285
  const MINI_VIEW_WIDTH = 640;
3121
3286
  const MINI_VIEW_HEIGHT = 168;
3287
+ const MINI_VIEW_MAX_HEIGHT = 560;
3288
+ function isMiniViewResizePayload(value) {
3289
+ return value !== null && typeof value === "object" && typeof value.height === "number" && Number.isFinite(value.height);
3290
+ }
3122
3291
  const DEFAULT_QUICK_SEND_VALUE = "primary+alt+shift+space";
3123
3292
  const FUNCTION_KEY_TOKEN = /^f([1-9]|1[0-9]|2[0-4])$/;
3124
3293
  function isFunctionKeyToken(token) {
@@ -5724,9 +5893,12 @@ function resolveGlobalInstallPlan(input) {
5724
5893
  * no cost at all before the click (review U3). It named the restart as its
5725
5894
  * cost for the same reason - it was the one destructive thing this path
5726
5895
  * did. It no longer is: on the generation layout the install lands beside
5727
- * the running build and the server keeps serving the build it loaded until
5728
- * its own next idle, so the cost is the install and the wait, and no
5729
- * sentence here may describe a turn being dropped.
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.
5730
5902
  *
5731
5903
  * Source build - the same disclosure with the one difference that matters:
5732
5904
  * this route DOES rewrite a tree a live runtime is reading, so its sentence
@@ -5745,7 +5917,7 @@ function resolveGlobalInstallPlan(input) {
5745
5917
  * panels, and in the by-hand panel the next line was the version sentence,
5746
5918
  * so the promise landed on the wrong line (review D5).
5747
5919
  */
5748
- remedy: managed ? "The app installs the new build beside the one in use and leaves the server you are using on the build it loaded, so nothing in flight is cut off. The server moves onto the new build when it is next idle." : 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.",
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.",
5749
5921
  detail: `${detail}${managed ? provenance : sourceRebuildRoute ? " source build of this machine's checkout; an in-place rebuild." : provenance}`,
5750
5922
  sourceBuild,
5751
5923
  managedRoute: managed ? "entry-point" : sourceRebuildRoute ? "source-build" : null,
@@ -5850,8 +6022,10 @@ function serveRecord(pid, dir) {
5850
6022
  }
5851
6023
  function servingInstallReadings(record) {
5852
6024
  const version = record?.version?.trim() ?? "";
6025
+ const instanceId = record?.instance_id?.trim() ?? "";
5853
6026
  return {
5854
6027
  bootVersion: version === "" ? null : version,
6028
+ instanceId: instanceId === "" ? null : instanceId,
5855
6029
  prefix: record?.prefix?.trim() ?? "",
5856
6030
  installKind: record?.install_kind?.trim() ?? "",
5857
6031
  startedByApp: record?.desktop === true && (record.claim_key ?? "") === ""
@@ -10762,13 +10936,14 @@ class BackendServiceManager {
10762
10936
  * Stop only the captured serve generation, including failed startups. A port
10763
10937
  * or process name is not ownership. Unconfirmed exit blocks replacement.
10764
10938
  */
10765
- async stop(isRestart = false) {
10939
+ async stop(isRestart = false, options = {}) {
10766
10940
  this.startEpoch++;
10767
10941
  if (!isRestart) this.isAppClosing = true;
10768
10942
  this.disposeStreamRelay();
10769
10943
  this.stopHealthCheck();
10770
10944
  const generation = this.ownedServe;
10771
- if (generation) await this.stopGeneration(generation, isRestart);
10945
+ if (generation)
10946
+ await this.stopGeneration(generation, isRestart, options.graceMs);
10772
10947
  await this.startPromise;
10773
10948
  }
10774
10949
  captureServe(child) {
@@ -10826,8 +11001,17 @@ class BackendServiceManager {
10826
11001
  * "we signalled it and moved on" would authorise exactly the double-serve
10827
11002
  * this class exists to prevent. An unconfirmed exit keeps ownership, and the
10828
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.
10829
11013
  */
10830
- stopGeneration(generation, isRestart) {
11014
+ stopGeneration(generation, isRestart, graceMs) {
10831
11015
  if (generation.stop) return generation.stop;
10832
11016
  generation.stop = (async () => {
10833
11017
  const waitForExit = (ms) => new Promise((resolve) => {
@@ -10847,7 +11031,7 @@ class BackendServiceManager {
10847
11031
  });
10848
11032
  });
10849
11033
  if (this.canSignal(generation)) generation.child.kill("SIGTERM");
10850
- const grace = isRestart ? this.shutdownTimeoutMs.restart : this.shutdownTimeoutMs.normal;
11034
+ const grace = graceMs ?? (isRestart ? this.shutdownTimeoutMs.restart : this.shutdownTimeoutMs.normal);
10851
11035
  if (!await waitForExit(grace)) {
10852
11036
  if (this.canSignal(generation)) generation.child.kill("SIGKILL");
10853
11037
  if (!await waitForExit(this.shutdownTimeoutMs.force)) {
@@ -11789,13 +11973,18 @@ class BackendServiceManager {
11789
11973
  * Restart the backend service
11790
11974
  * This method properly handles the restart process to ensure that any pending
11791
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.
11792
11981
  * @returns Promise resolving to true if the restart was successful, false otherwise
11793
11982
  */
11794
- restart() {
11983
+ restart(options = {}) {
11795
11984
  if (this.restartPromise) return this.restartPromise;
11796
11985
  this.restartPromise = (async () => {
11797
11986
  try {
11798
- await this.stop(true);
11987
+ await this.stop(true, { graceMs: options.stopGraceMs });
11799
11988
  if (this.isAppClosing) return false;
11800
11989
  return await this.start();
11801
11990
  } catch (error) {
@@ -13625,6 +13814,14 @@ const METHODS$1 = [
13625
13814
  "tabs",
13626
13815
  "scroll",
13627
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",
13628
13825
  "request_access",
13629
13826
  "await_access",
13630
13827
  "cancel_access",
@@ -13683,7 +13880,13 @@ const ERROR_CODES = [
13683
13880
  // a command that hangs rather than one that fails.
13684
13881
  ...CONSOLE_ERROR_CODES
13685
13882
  ];
13686
- const HOST_CAPABILITIES = ["download", "upload"];
13883
+ const HOST_CAPABILITIES = [
13884
+ "styles",
13885
+ "hit_test",
13886
+ "ancestors",
13887
+ "download",
13888
+ "upload"
13889
+ ];
13687
13890
  const RUN_DIRNAME = path$1.join("run", "ui-browser");
13688
13891
  const STATE_FILENAME = "host.json";
13689
13892
  const STATE_DIR_MODE = 448;
@@ -19305,6 +19508,135 @@ function clampTimeout(requested) {
19305
19508
  if (!Number.isFinite(wanted) || wanted <= 0) return CAPS.downloadTimeoutS;
19306
19509
  return Math.min(wanted, CAPS.downloadTimeoutMaxS);
19307
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
+ }
19308
19640
  const MAX_AGENT_TABS = 8;
19309
19641
  const BACKGROUND_VIEWPORT = {
19310
19642
  x: 0,
@@ -20269,6 +20601,12 @@ function factOf(path2) {
20269
20601
  }
20270
20602
  const DOCUMENT_SCOPED = /* @__PURE__ */ new Set([
20271
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",
20272
20610
  "snapshot",
20273
20611
  "screenshot",
20274
20612
  "click",
@@ -20296,6 +20634,9 @@ const NAVIGATION_ACTIONS = /* @__PURE__ */ new Set(["click", "type"]);
20296
20634
  const TAB_SCOPED = /* @__PURE__ */ new Set([
20297
20635
  "goto",
20298
20636
  "read",
20637
+ "styles",
20638
+ "hit_test",
20639
+ "ancestors",
20299
20640
  "snapshot",
20300
20641
  "screenshot",
20301
20642
  "click",
@@ -20473,6 +20814,12 @@ class BrowserHost {
20473
20814
  return retitle();
20474
20815
  case "read":
20475
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);
20476
20823
  case "snapshot":
20477
20824
  return snapshot(this, params);
20478
20825
  case "scroll":
@@ -24229,6 +24576,7 @@ async function startBrowserHost(options) {
24229
24576
  }
24230
24577
  });
24231
24578
  window.contentView.addChildView(view);
24579
+ view.setBounds(BACKGROUND_VIEWPORT);
24232
24580
  view.setVisible(false);
24233
24581
  return view;
24234
24582
  }
@@ -33246,17 +33594,43 @@ const MINI_DOCUMENT = "mini.html";
33246
33594
  function miniViewUrlFor(rendererUrl) {
33247
33595
  return INDEX_DOCUMENT_SUFFIX.test(rendererUrl) ? rendererUrl.replace(INDEX_DOCUMENT_SUFFIX, `/${MINI_DOCUMENT}`) : `${rendererUrl.replace(TRAILING_SLASH, "")}/${MINI_DOCUMENT}`;
33248
33596
  }
33597
+ const MINI_VIEW_GROUND = "#22201c";
33598
+ const MINI_VIEW_PAINT_TIMEOUT_MS = 1500;
33599
+ function miniViewPresentationFor(input) {
33600
+ if (input.show !== "focus") return "present";
33601
+ if (input.painted) return "present";
33602
+ if (input.timedOut) return "error-surface";
33603
+ return "wait";
33604
+ }
33605
+ function miniViewErrorSurfaceHtml() {
33606
+ return `<!doctype html>
33607
+ <html><head><meta charset="utf-8" /><title>Quick send</title>
33608
+ <style>
33609
+ :root { color-scheme: dark; }
33610
+ html, body { margin: 0; height: 100%; background: ${MINI_VIEW_GROUND}; }
33611
+ body { display: flex; align-items: center; padding: 0 16px;
33612
+ font: 13px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
33613
+ color: #c2bcb0; }
33614
+ p { margin: 0; }
33615
+ strong { font-weight: 600; color: #ef8078; }
33616
+ </style></head>
33617
+ <body><p><strong>Quick send could not open.</strong> Try the hotkey again, or restart Local Operator if it keeps failing.</p></body></html>`;
33618
+ }
33249
33619
  function createMiniView(options) {
33250
33620
  const window = new electron.BrowserWindow({
33251
33621
  width: MINI_VIEW_WIDTH,
33252
33622
  height: MINI_VIEW_HEIGHT,
33253
33623
  /*
33254
- * Sized in content, not window, terms: the design fixes the COMPOSER at
33255
- * 640x168 and the frames are captured and reviewed at that size, so the
33256
- * page's viewport is the number that must be exact on every platform.
33624
+ * Sized in content, not window, terms, and the content size is now the
33625
+ * BASE of a measured range rather than the whole story: the restyle's
33626
+ * chrome (previews, the readings strip, the picker sheet) asks for more
33627
+ * through `MINI_VIEW_RESIZE` below, and this call is what makes the
33628
+ * page's viewport the number every frame is captured at.
33257
33629
  */
33258
33630
  useContentSize: true,
33259
33631
  show: false,
33632
+ /* The dark ground under an as-yet-unpainted document; see MINI_VIEW_GROUND. */
33633
+ backgroundColor: MINI_VIEW_GROUND,
33260
33634
  // A composer that floats over other apps carries no frame of its own:
33261
33635
  // the app's own rows are the entire surface, and the one sanctioned
33262
33636
  // elevation step is the card's ground.
@@ -33304,7 +33678,91 @@ function createMiniView(options) {
33304
33678
  if (process.platform === "darwin") {
33305
33679
  window.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
33306
33680
  }
33681
+ let paintedForLoad = false;
33682
+ let paintTimer = null;
33683
+ let paintWaiters = [];
33684
+ const settlePaint = (ready) => {
33685
+ if (paintTimer !== null) {
33686
+ clearTimeout(paintTimer);
33687
+ paintTimer = null;
33688
+ }
33689
+ paintedForLoad = ready;
33690
+ const waiting = paintWaiters;
33691
+ paintWaiters = [];
33692
+ for (const resolve of waiting) resolve();
33693
+ };
33694
+ const showErrorSurface = () => {
33695
+ if (disposed || window.isDestroyed()) return;
33696
+ settlePaint(false);
33697
+ void window.loadURL(
33698
+ `data:text/html;charset=utf-8,${encodeURIComponent(miniViewErrorSurfaceHtml())}`
33699
+ );
33700
+ window.webContents.once("did-finish-load", () => {
33701
+ if (disposed || window.isDestroyed()) return;
33702
+ positionForCursorDisplay();
33703
+ presentMiniView(window, options.show, {
33704
+ trigger: "mini-view",
33705
+ report: options.report
33706
+ });
33707
+ if (options.show === "focus") {
33708
+ window.webContents.send(MINI_VIEW_SUMMONED, {
33709
+ at: Date.now()
33710
+ });
33711
+ }
33712
+ });
33713
+ };
33714
+ window.webContents.on("did-start-loading", () => {
33715
+ paintedForLoad = false;
33716
+ });
33717
+ window.webContents.on(
33718
+ "did-fail-load",
33719
+ (_event, errorCode, errorDescription, validatedURL, isMainFrame) => {
33720
+ if (!isMainFrame) return;
33721
+ options.report?.(
33722
+ `[mini-view] load failed (${errorCode} ${errorDescription}) for ${validatedURL}; showing the error card`
33723
+ );
33724
+ showErrorSurface();
33725
+ }
33726
+ );
33727
+ window.webContents.on("render-process-gone", (_event, details) => {
33728
+ options.report?.(
33729
+ `[mini-view] renderer gone (${details.reason}); showing the error card`
33730
+ );
33731
+ showErrorSurface();
33732
+ });
33307
33733
  void window.loadURL(options.url);
33734
+ const onResize = (event, payload) => {
33735
+ if (window.isDestroyed() || event.sender !== window.webContents || event.senderFrame !== window.webContents.mainFrame) {
33736
+ return;
33737
+ }
33738
+ if (!isMiniViewResizePayload(payload)) return;
33739
+ const area = electron.screen.getDisplayMatching(window.getBounds()).workArea;
33740
+ const ceiling = Math.max(
33741
+ MINI_VIEW_HEIGHT,
33742
+ Math.min(MINI_VIEW_MAX_HEIGHT, area.height - 64)
33743
+ );
33744
+ const height = Math.max(
33745
+ MINI_VIEW_HEIGHT,
33746
+ Math.min(Math.round(payload.height), ceiling)
33747
+ );
33748
+ const bounds = window.getContentBounds();
33749
+ if (bounds.height === height) return;
33750
+ window.setContentSize(MINI_VIEW_WIDTH, height);
33751
+ const bottom = bounds.y + height;
33752
+ if (bottom > area.y + area.height) {
33753
+ window.setPosition(
33754
+ bounds.x,
33755
+ Math.max(area.y, area.y + area.height - height)
33756
+ );
33757
+ }
33758
+ };
33759
+ electron.ipcMain.on(MINI_VIEW_RESIZE, onResize);
33760
+ const onPainted = (event) => {
33761
+ if (window.isDestroyed() || event.sender !== window.webContents || event.senderFrame !== window.webContents.mainFrame)
33762
+ return;
33763
+ settlePaint(true);
33764
+ };
33765
+ electron.ipcMain.on(MINI_VIEW_PAINTED, onPainted);
33308
33766
  let disposed = false;
33309
33767
  function hide() {
33310
33768
  if (disposed || window.isDestroyed()) return;
@@ -33334,42 +33792,86 @@ function createMiniView(options) {
33334
33792
  }
33335
33793
  hide();
33336
33794
  });
33337
- function toggle() {
33338
- if (disposed) return;
33339
- if (!window.isDestroyed() && window.isVisible()) {
33340
- hide();
33795
+ function presentWhenReady() {
33796
+ if (disposed || window.isDestroyed()) return;
33797
+ const decision = miniViewPresentationFor({
33798
+ show: options.show,
33799
+ painted: paintedForLoad,
33800
+ timedOut: false
33801
+ });
33802
+ if (decision === "error-surface") {
33803
+ showErrorSurface();
33341
33804
  return;
33342
33805
  }
33806
+ if (decision === "present") {
33807
+ presentNow();
33808
+ return;
33809
+ }
33810
+ paintWaiters.push(() => {
33811
+ if (miniViewPresentationFor({
33812
+ show: options.show,
33813
+ painted: paintedForLoad,
33814
+ timedOut: false
33815
+ }) === "present")
33816
+ presentNow();
33817
+ });
33818
+ if (paintTimer === null) {
33819
+ paintTimer = setTimeout(() => {
33820
+ paintTimer = null;
33821
+ if (miniViewPresentationFor({
33822
+ show: options.show,
33823
+ painted: paintedForLoad,
33824
+ timedOut: true
33825
+ }) === "error-surface")
33826
+ showErrorSurface();
33827
+ }, MINI_VIEW_PAINT_TIMEOUT_MS);
33828
+ }
33829
+ }
33830
+ function presentNow() {
33831
+ if (disposed || window.isDestroyed()) return;
33343
33832
  positionForCursorDisplay();
33344
33833
  presentMiniView(window, options.show, {
33345
33834
  trigger: "mini-view",
33346
33835
  report: options.report
33347
33836
  });
33348
- if (options.show === "focus" && !window.isDestroyed()) {
33349
- const payload = { at: Date.now() };
33350
- window.webContents.send(MINI_VIEW_SUMMONED, payload);
33837
+ if (options.show === "focus") {
33838
+ window.webContents.send(MINI_VIEW_SUMMONED, {
33839
+ at: Date.now()
33840
+ });
33351
33841
  }
33352
33842
  }
33843
+ function toggle() {
33844
+ if (disposed) return;
33845
+ if (!window.isDestroyed() && window.isVisible()) {
33846
+ hide();
33847
+ return;
33848
+ }
33849
+ presentWhenReady();
33850
+ }
33353
33851
  function positionForCursorDisplay() {
33354
33852
  if (window.isDestroyed()) return;
33355
33853
  const display = electron.screen.getDisplayNearestPoint(
33356
33854
  electron.screen.getCursorScreenPoint()
33357
33855
  );
33358
33856
  const area = display.workArea;
33857
+ const height = window.isDestroyed() ? MINI_VIEW_HEIGHT : window.getContentBounds().height;
33359
33858
  const x = Math.round(area.x + (area.width - MINI_VIEW_WIDTH) / 2);
33360
- const desiredY = area.y + area.height / 3 - MINI_VIEW_HEIGHT / 2;
33859
+ const desiredY = area.y + area.height / 3 - height / 2;
33361
33860
  const y = Math.round(
33362
- Math.min(
33363
- Math.max(desiredY, area.y),
33364
- area.y + area.height - MINI_VIEW_HEIGHT
33365
- )
33861
+ Math.min(Math.max(desiredY, area.y), area.y + area.height - height)
33366
33862
  );
33367
33863
  window.setPosition(x, y);
33368
33864
  }
33369
33865
  function dispose() {
33370
33866
  if (disposed) return;
33371
33867
  disposed = true;
33868
+ electron.ipcMain.removeListener(MINI_VIEW_RESIZE, onResize);
33869
+ electron.ipcMain.removeListener(MINI_VIEW_PAINTED, onPainted);
33372
33870
  electron.ipcMain.removeHandler(MINI_VIEW_DISMISS);
33871
+ if (paintTimer !== null) {
33872
+ clearTimeout(paintTimer);
33873
+ paintTimer = null;
33874
+ }
33373
33875
  if (!window.isDestroyed()) window.destroy();
33374
33876
  }
33375
33877
  return { window, toggle, hide, dispose };
@@ -33436,6 +33938,123 @@ function titlebarOptions(platform, chrome) {
33436
33938
  }
33437
33939
  };
33438
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
+ }
33439
34058
  const CHROMIUM_TRANSPORT_CODES = [
33440
34059
  "ERR_INTERNET_DISCONNECTED",
33441
34060
  "ERR_NETWORK_CHANGED",
@@ -34225,6 +34844,7 @@ const BETA_VERSION_REGEX = /v\d+\.\d+\.\d+\.beta\.\d+/;
34225
34844
  const ARCHITECTURE_SEPARATOR_REGEX = /\s+/;
34226
34845
  const GLOBAL_UPDATE_TIMEOUT_MS = 15 * 60 * 1e3;
34227
34846
  const SOURCE_REBUILD_TIMEOUT_MS = 30 * 60 * 1e3;
34847
+ const UPDATE_RESTART_STOP_GRACE_MS = 5e3;
34228
34848
  const BUNDLE_ID_REGEX = /^[\w.-]+$/;
34229
34849
  const INSTALL_IN_FLIGHT_RECHECK_MS = 1e4;
34230
34850
  const LAUNCHCTL_NOT_LOADED_REGEX = /could not find|no such process/i;
@@ -38714,6 +39334,219 @@ ${heal.removed.join("\n")}`,
38714
39334
  async readFleetSnapshot(backend) {
38715
39335
  return backend.servingSessionFleet();
38716
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
+ }
38717
39550
  /**
38718
39551
  * The build the daemon serving this app BOOTED with, or null when its record
38719
39552
  * carries no reading.
@@ -38735,7 +39568,7 @@ ${heal.removed.join("\n")}`,
38735
39568
  * the same pair rather than about two different questions.
38736
39569
  */
38737
39570
  servingBootVersion() {
38738
- return this.backendService?.servingInstall().readings.bootVersion ?? null;
39571
+ return this.servingRecordReadings().bootVersion;
38739
39572
  }
38740
39573
  /**
38741
39574
  * The running reading an update event carries to the renderer.
@@ -39106,6 +39939,7 @@ ${heal.removed.join("\n")}`,
39106
39939
  sourceRebuild: rebuildPath
39107
39940
  });
39108
39941
  const rebuildRoute = freshPlan.managedRoute === "source-build" && rebuildPath !== null;
39942
+ const generationInstall = freshPlan.managedRoute === "entry-point";
39109
39943
  if (!consolePath && !rebuildRoute) {
39110
39944
  this.sendToRenderer("backend-update-error", {
39111
39945
  message: "The global install could not be found on this machine, so the update did not run. See the update service log.",
@@ -39118,6 +39952,8 @@ ${heal.removed.join("\n")}`,
39118
39952
  const before = this.readInstallVersionAt(installPath) ?? plan.installedInstallVersion;
39119
39953
  const markerBefore = rebuildRoute ? this.readRebuildMarkerState(installPath) : null;
39120
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;
39121
39957
  if (rebuildRoute && !await this.drainFleetForUpdate({
39122
39958
  backend,
39123
39959
  command: freshPlan.updateCommand ?? null
@@ -39216,9 +40052,63 @@ ${heal.removed.join("\n")}`,
39216
40052
  `Global install updated: ${before ?? "unknown"} -> ${after ?? "unknown"}`,
39217
40053
  LogFileType.UPDATE_SERVICE
39218
40054
  );
39219
- const generationInstall = freshPlan.managedRoute === "entry-point";
39220
- const installAlreadyCurrent = !rebuildRoute && before !== null && after !== null && before === after;
39221
- if (backend.isUsingExternalBackend() || generationInstall && !installAlreadyCurrent) {
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()) {
39222
40112
  const health = await this.getInstalledBackendVersion();
39223
40113
  const running2 = this.eventRunningVersion(health);
39224
40114
  logger.info(
@@ -39229,6 +40119,7 @@ ${heal.removed.join("\n")}`,
39229
40119
  installVersion: after,
39230
40120
  runningVersion: running2,
39231
40121
  restarted: false,
40122
+ moved: false,
39232
40123
  restartable: this.backendIsAppOwned(),
39233
40124
  /*
39234
40125
  * FALSE BY CONSTRUCTION, and stated rather than left absent: this arm is
@@ -39239,13 +40130,21 @@ ${heal.removed.join("\n")}`,
39239
40130
  });
39240
40131
  return true;
39241
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
+ }
39242
40139
  logger.info(
39243
40140
  "Restarting backend service onto the updated install...",
39244
40141
  LogFileType.UPDATE_SERVICE
39245
40142
  );
39246
40143
  this.sendToRenderer("backend-update-progress", { phase: "restarting" });
39247
40144
  const fleetBeforeRestart = await this.readFleetSnapshot(backend);
39248
- const restartSuccess = await backend.restart();
40145
+ const restartSuccess = await backend.restart({
40146
+ stopGraceMs: UPDATE_RESTART_STOP_GRACE_MS
40147
+ });
39249
40148
  if (!restartSuccess) {
39250
40149
  logger.error(
39251
40150
  "Backend service restart failed after the global install update",
@@ -39302,12 +40201,43 @@ ${heal.removed.join("\n")}`,
39302
40201
  fleetBeforeInstall,
39303
40202
  fleetBeforeRestart
39304
40203
  );
39305
- const reengage = reengageSnapshot !== null ? await this.reengageFleetAfterRestart(backend, reengageSnapshot) : null;
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
+ }
39306
40228
  this.sendToRenderer("backend-update-completed", {
39307
40229
  installVersion: after,
39308
40230
  runningVersion: reported,
39309
40231
  restarted: true,
39310
- sessionsOnOldBuild: reengage?.stillResident.length ?? null
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
39311
40241
  });
39312
40242
  return true;
39313
40243
  }
@@ -40516,9 +41446,15 @@ if (process.platform === "darwin" && electron.app.dock) {
40516
41446
  if (windowLaunch.hideDock) {
40517
41447
  electron.app.dock.hide();
40518
41448
  } else {
41449
+ electron.app.dock.show();
40519
41450
  electron.app.dock.setIcon(image);
40520
41451
  }
40521
41452
  }
41453
+ function reassertDockParticipation() {
41454
+ if (process.platform !== "darwin" || !electron.app.dock) return;
41455
+ if (windowLaunch.hideDock) return;
41456
+ electron.app.dock.show();
41457
+ }
40522
41458
  const telemetryLaunch = resolveTelemetryLaunch({
40523
41459
  env: launchEnv,
40524
41460
  projectKey: backendConfig.VITE_PUBLIC_POSTHOG_KEY
@@ -41706,24 +42642,37 @@ electron.app.whenReady().then(async () => {
41706
42642
  return false;
41707
42643
  }
41708
42644
  });
41709
- electron.ipcMain.handle("show-open-dialog", async (_, options) => {
41710
- if (!mainWindow) {
42645
+ electron.ipcMain.handle("show-open-dialog", async (event, options) => {
42646
+ const parent = electron.BrowserWindow.fromWebContents(event.sender) ?? mainWindow;
42647
+ if (!parent) {
41711
42648
  logger.error(
41712
- "Cannot show open dialog: mainWindow is not available.",
42649
+ "Cannot show open dialog: no window can own it.",
41713
42650
  LogFileType.BACKEND
41714
42651
  );
41715
42652
  return { canceled: true, filePaths: [] };
41716
42653
  }
41717
- const result = await electron.dialog.showOpenDialog(
41718
- mainWindow,
41719
- withRememberedDirectory(
41720
- "open-file",
41721
- options,
41722
- pickerFallbackDirectory()
41723
- )
41724
- );
41725
- rememberPickedDirectory("open-file", result.filePaths);
41726
- return result;
42654
+ const mini = miniView;
42655
+ const fromMini = mini !== null && !mini.window.isDestroyed() && event.sender === mini.window.webContents;
42656
+ const dialogPayload = { open: true };
42657
+ if (fromMini)
42658
+ mini.window.webContents.send(MINI_VIEW_DIALOG, dialogPayload);
42659
+ try {
42660
+ const result = await electron.dialog.showOpenDialog(
42661
+ parent,
42662
+ withRememberedDirectory(
42663
+ "open-file",
42664
+ options,
42665
+ pickerFallbackDirectory()
42666
+ )
42667
+ );
42668
+ rememberPickedDirectory("open-file", result.filePaths);
42669
+ return result;
42670
+ } finally {
42671
+ if (fromMini && mini !== null && !mini.window.isDestroyed())
42672
+ mini.window.webContents.send(MINI_VIEW_DIALOG, {
42673
+ open: false
42674
+ });
42675
+ }
41727
42676
  });
41728
42677
  electron.ipcMain.handle("select-directory", async () => {
41729
42678
  if (!mainWindow) {
@@ -42132,6 +43081,7 @@ electron.app.whenReady().then(async () => {
42132
43081
  };
42133
43082
  setupMainWindowWithUpdateService(launchSession, launchCatalogue);
42134
43083
  electron.app.on("activate", () => {
43084
+ reassertDockParticipation();
42135
43085
  if (electron.BrowserWindow.getAllWindows().length === 0) {
42136
43086
  if (quitState.isQuitting()) {
42137
43087
  reportSkippedWhileQuitting(
@@ -42171,6 +43121,13 @@ electron.app.on("window-all-closed", () => {
42171
43121
  electron.app.quit();
42172
43122
  return;
42173
43123
  }
43124
+ if (quitState.isQuitting()) {
43125
+ logger.info(
43126
+ "All windows closed as part of a quit in progress; will-quit owns the completion",
43127
+ LogFileType.BACKEND
43128
+ );
43129
+ return;
43130
+ }
42174
43131
  logger.info(
42175
43132
  "All windows closed, but keeping app active (macOS platform)",
42176
43133
  LogFileType.BACKEND
@@ -42210,7 +43167,7 @@ electron.app.on("will-quit", (event) => {
42210
43167
  failsafe.unref();
42211
43168
  void backendService.stop(false).then(() => {
42212
43169
  clearTimeout(failsafe);
42213
- electron.app.quit();
43170
+ electron.app.exit(0);
42214
43171
  }).catch((error) => {
42215
43172
  clearTimeout(failsafe);
42216
43173
  logger.error(
@@ -42229,22 +43186,33 @@ const holdQuitForSessionCookieSnapshot = createSessionCookieQuitHold({
42229
43186
  });
42230
43187
  electron.app.on("before-quit", async (event) => {
42231
43188
  quitState.begin();
43189
+ electron.globalShortcut.unregisterAll();
43190
+ miniViewRegistrar?.dispose();
43191
+ quickSendWatcher?.stop();
43192
+ miniView?.dispose();
43193
+ if (windowLaunch.mode === "headless") {
43194
+ launcherWatch?.stop();
43195
+ armHeadlessExitDeadline("the app is quitting");
43196
+ }
42232
43197
  if (await holdQuitForSessionCookieSnapshot(event)) return;
42233
43198
  logger.info("App is about to quit", LogFileType.BACKEND);
43199
+ if (!backendService.isOwnedCleanupComplete()) {
43200
+ try {
43201
+ await backendService.stop(false);
43202
+ } catch (error) {
43203
+ logger.error(
43204
+ "Owned backend stop rejected during quit; continuing the shutdown",
43205
+ LogFileType.BACKEND,
43206
+ error
43207
+ );
43208
+ }
43209
+ }
42234
43210
  if (parkedLaunches.length > 0) {
42235
43211
  reportParksAtQuit(
42236
43212
  parkedLaunches.map((parked) => parked.session),
42237
43213
  reportRaise
42238
43214
  );
42239
43215
  }
42240
- if (windowLaunch.mode === "headless") {
42241
- launcherWatch?.stop();
42242
- armHeadlessExitDeadline("the app is quitting");
42243
- }
42244
- electron.globalShortcut.unregisterAll();
42245
- miniViewRegistrar?.dispose();
42246
- quickSendWatcher?.stop();
42247
- miniView?.dispose();
42248
43216
  activeUpdateService?.quitForInFlightInstall("app quit");
42249
43217
  void stopBrowserHost();
42250
43218
  });