@bivy/bivy 0.20.9-staging.2 → 0.20.9-staging.21

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 (131) hide show
  1. package/README.md +6 -5
  2. package/bin/bivy.mjs +13 -56
  3. package/bin/cli-commands.mjs +2 -4
  4. package/bin/guides/talk-to-the-user.md +13 -24
  5. package/dist/agent-instructions.js +12 -14
  6. package/dist/agents/pi/integration.js +1 -1
  7. package/dist/apps/remote-preview.js +3 -0
  8. package/dist/apps/service.js +6 -1
  9. package/dist/apps/styles.css +40 -102
  10. package/dist/automation-reseal.js +30 -0
  11. package/dist/certification/generated.js +1 -1
  12. package/dist/device-registry.js +27 -0
  13. package/dist/ephemeral-teardown.js +11 -1
  14. package/dist/exec.js +4 -1
  15. package/dist/runtime/model-catalog.js +16 -0
  16. package/dist/server.js +73 -62
  17. package/dist/session/event-log.js +98 -41
  18. package/dist/session/inline-image-fetch.js +24 -33
  19. package/dist/session/message-components.js +323 -0
  20. package/dist/session/run-terminal.js +9 -7
  21. package/dist/session/session-tokens.js +1 -1
  22. package/dist/session/transcript-persistence.js +106 -11
  23. package/package.json +6 -6
  24. package/web/assets/AutomationsView-Cobo4Kep.js +51 -0
  25. package/web/assets/{ChangesCard-DcSRy5g_.js → ChangesCard-Bo6E8C_y.js} +3 -3
  26. package/web/assets/LibraryView-BTeMc6BO.js +2 -0
  27. package/web/assets/{ProviderConnect-cpGwBMrU.js → ProviderConnect-CsidS7kq.js} +1 -1
  28. package/web/assets/SessionList-B7IbeXSa.js +1 -0
  29. package/web/assets/Settings-BQRQsQ36.js +10 -0
  30. package/web/assets/{Sheet-D-5t3lJ0.js → Sheet-C0BIPbMr.js} +1 -1
  31. package/web/assets/{Terminal-CjMjKB1f.js → Terminal-BTRgonB5.js} +1 -1
  32. package/web/assets/{VoiceSettings-xv6rjaY2.js → VoiceSettings-UZ1SA0lQ.js} +1 -1
  33. package/web/assets/{abnfDiagram-VCTEODGH-BLT3MLyK.js → abnfDiagram-VCTEODGH-CJoWsvVk.js} +1 -1
  34. package/web/assets/architecture-7GRP2DOG-CuzcodTO.js +1 -0
  35. package/web/assets/{architectureDiagram-5GKGNRK7-BGinLETW.js → architectureDiagram-5GKGNRK7-_qpa4O1v.js} +1 -1
  36. package/web/assets/{blockDiagram-I7D4REHJ-BNiY2HLB.js → blockDiagram-I7D4REHJ-DmLgwSr-.js} +1 -1
  37. package/web/assets/{c4Diagram-7LVT6UL2-46aGfRMd.js → c4Diagram-7LVT6UL2-DeZuOOSk.js} +1 -1
  38. package/web/assets/channel-B0OL-aPd.js +1 -0
  39. package/web/assets/{chunk-4HAMMTFA-CRzer-vm.js → chunk-4HAMMTFA-Cr6O6lBG.js} +1 -1
  40. package/web/assets/{chunk-75Z2AOVW-MFkgf2Ar.js → chunk-75Z2AOVW-gSgNIYMY.js} +1 -1
  41. package/web/assets/{chunk-DU6HZSFF-Bpze6ID8.js → chunk-DU6HZSFF-BacnRf2Y.js} +1 -1
  42. package/web/assets/{chunk-F27PBJKO-Dq3OmNXG.js → chunk-F27PBJKO-DRrpfF1z.js} +1 -1
  43. package/web/assets/{chunk-GMAD6QVW-umDWdG9g.js → chunk-GMAD6QVW-DXCi6v6B.js} +1 -1
  44. package/web/assets/{chunk-GVQU2GXP-DuT1Awt9.js → chunk-GVQU2GXP-Yd74hIeT.js} +1 -1
  45. package/web/assets/{chunk-IMKFNOWR-CpFFccVb.js → chunk-IMKFNOWR-D5xz0-QC.js} +1 -1
  46. package/web/assets/{chunk-L3NEJ4N5-0jRWrASa.js → chunk-L3NEJ4N5-BrS0CGPM.js} +1 -1
  47. package/web/assets/{chunk-OSK3NFVY-CBErW4-p.js → chunk-OSK3NFVY-C6d1DSJe.js} +1 -1
  48. package/web/assets/{chunk-P2QGCYS3-BPMii7LM.js → chunk-P2QGCYS3-BrKgIngm.js} +1 -1
  49. package/web/assets/{chunk-POPQ4Y6H-CHTRMb_C.js → chunk-POPQ4Y6H-Bkwx5BZs.js} +1 -1
  50. package/web/assets/{chunk-PWAF6VOD-BOcNKrmu.js → chunk-PWAF6VOD-AeCCeE0F.js} +1 -1
  51. package/web/assets/{chunk-SHT3W25Y-Dvb-oEDN.js → chunk-SHT3W25Y-01NYjozy.js} +1 -1
  52. package/web/assets/{chunk-SVP7TREG-B4AQIUTK.js → chunk-SVP7TREG-CCh7wXAI.js} +1 -1
  53. package/web/assets/{chunk-TICWLB2K-CWzLTsHW.js → chunk-TICWLB2K-CcWKqwdM.js} +1 -1
  54. package/web/assets/classDiagram-ZZMXUADV-D8dSdFQt.js +1 -0
  55. package/web/assets/classDiagram-v2-VYDZK3BY-D8dSdFQt.js +1 -0
  56. package/web/assets/{cynefin-OW5HDTMX-BPkKT48j.js → cynefin-OW5HDTMX-CYYc_tBE.js} +1 -1
  57. package/web/assets/{cynefinDiagram-5FMLGOSQ-Ck2qLmpR.js → cynefinDiagram-5FMLGOSQ-Dnz7OfxK.js} +1 -1
  58. package/web/assets/{dagre-GXQ25YYZ-BPKEs_49.js → dagre-GXQ25YYZ-di5CyaMt.js} +1 -1
  59. package/web/assets/{diagram-S7CK7UJ4-ou-L5HdJ.js → diagram-S7CK7UJ4-BVC93OLB.js} +1 -1
  60. package/web/assets/{diagram-UQ7AKVKN-DihVn6fF.js → diagram-UQ7AKVKN-D7pXHAvQ.js} +1 -1
  61. package/web/assets/{diagram-VSXAHHWV-BkLtxbP7.js → diagram-VSXAHHWV-d0zW-IUz.js} +1 -1
  62. package/web/assets/{diagram-VX7I27RA-BZ-Z-CwP.js → diagram-VX7I27RA-BRlMo9oa.js} +1 -1
  63. package/web/assets/{diagram-Z3DM3KII-BQN-bGqD.js → diagram-Z3DM3KII-BmfntB1i.js} +1 -1
  64. package/web/assets/{ebnfDiagram-PWID7BFC-aKK1a_it.js → ebnfDiagram-PWID7BFC-BxUYvqsz.js} +1 -1
  65. package/web/assets/{erDiagram-RLTQ6QDP-BGsXtJl-.js → erDiagram-RLTQ6QDP-CbUl1Cqa.js} +1 -1
  66. package/web/assets/eventmodeling-NTZA5JFV-sTVSUyNU.js +1 -0
  67. package/web/assets/flowDiagram-HODETNUW-Beo3zBEU.js +1 -0
  68. package/web/assets/{ganttDiagram-EL5Y4UJY-lrYuyNAe.js → ganttDiagram-EL5Y4UJY-CkpR0avQ.js} +1 -1
  69. package/web/assets/{gitGraph-4MIJSDKK-raj_IxZ8.js → gitGraph-4MIJSDKK-CmbJxSaM.js} +1 -1
  70. package/web/assets/{gitGraphDiagram-WWUBYQGX-DzqRGXuk.js → gitGraphDiagram-WWUBYQGX-iyO408KF.js} +1 -1
  71. package/web/assets/index-B4HaCD-n.css +1 -0
  72. package/web/assets/{index-B6GPr2Pt.js → index-fdEPLcPs.js} +2 -2
  73. package/web/assets/{info-A6RAGUB7-DJVi9SAN.js → info-A6RAGUB7--6JCM0xX.js} +1 -1
  74. package/web/assets/{infoDiagram-27XIBGKW-B2HrGywP.js → infoDiagram-27XIBGKW-Dchx6QcB.js} +1 -1
  75. package/web/assets/{ishikawaDiagram-5VMMS53U-CczIkvWw.js → ishikawaDiagram-5VMMS53U-DOY3izUi.js} +1 -1
  76. package/web/assets/{journeyDiagram-3NMN7TZE-D-h-kwep.js → journeyDiagram-3NMN7TZE-oqZgC9oJ.js} +1 -1
  77. package/web/assets/{kanban-definition-UXKFOSKX-GClrFWfH.js → kanban-definition-UXKFOSKX-HCRC0D9X.js} +1 -1
  78. package/web/assets/{line-pweWV0oc.js → line-qcY2eok7.js} +1 -1
  79. package/web/assets/{mermaid-parser.core-C1vHZCtC.js → mermaid-parser.core-Bullp1xl.js} +3 -3
  80. package/web/assets/{mermaid.core-Df8DJ7AE.js → mermaid.core-FBmsAtMC.js} +4 -4
  81. package/web/assets/{mindmap-definition-YA3MSWOX-BpqA307n.js → mindmap-definition-YA3MSWOX-CfgOAkVQ.js} +1 -1
  82. package/web/assets/mount-IY4-2elJ.js +31 -0
  83. package/web/assets/{packet-AYTQ26CC-afE6T0Hc.js → packet-AYTQ26CC-COAmiopo.js} +1 -1
  84. package/web/assets/{pegDiagram-XKGWAZYB-AAEw7Khy.js → pegDiagram-XKGWAZYB-BYafVbeG.js} +1 -1
  85. package/web/assets/{pie-WAS4IAKB-vtwSUVVK.js → pie-WAS4IAKB-ZYe346n6.js} +1 -1
  86. package/web/assets/{pieDiagram-E7YTZNPT-CDg0SkhS.js → pieDiagram-E7YTZNPT-DnRbaA7i.js} +1 -1
  87. package/web/assets/{quadrantDiagram-AXDQQJYC-_o0rOR26.js → quadrantDiagram-AXDQQJYC-B2uxyKLd.js} +1 -1
  88. package/web/assets/{radar-RG4KPBEZ-_Tf7tpz8.js → radar-RG4KPBEZ-DwFlqYgB.js} +1 -1
  89. package/web/assets/{railroad-74A4TZTK-DM_a0HtL.js → railroad-74A4TZTK-DBv5jh9o.js} +1 -1
  90. package/web/assets/railroad-abnf-HS5TGJTU-VQxaaUVU.js +1 -0
  91. package/web/assets/railroad-ebnf-LZEXJU2U-DWKO1s90.js +1 -0
  92. package/web/assets/railroad-peg-WCYAUIDC-DvViPquF.js +1 -0
  93. package/web/assets/{railroadDiagram-O6MQD6OU-BpQZeQ-f.js → railroadDiagram-O6MQD6OU-CIUFwDWd.js} +1 -1
  94. package/web/assets/{requirementDiagram-BXWQKSXE-DC4US4K_.js → requirementDiagram-BXWQKSXE-BXwDlf9Q.js} +1 -1
  95. package/web/assets/{sankeyDiagram-P5KCCOFB-BUJ9KIqV.js → sankeyDiagram-P5KCCOFB-DTiRcy5E.js} +1 -1
  96. package/web/assets/{sequenceDiagram-WJ2MYXX4-By7D5XFY.js → sequenceDiagram-WJ2MYXX4-DFPRDr4a.js} +1 -1
  97. package/web/assets/settingsRoute-3489vHS0.js +1 -0
  98. package/web/assets/{stateDiagram-D77RDMKH-D0BA3EeM.js → stateDiagram-D77RDMKH-CFQ-4xlY.js} +1 -1
  99. package/web/assets/stateDiagram-v2-MP3YSRHH-DIIQkE2r.js +1 -0
  100. package/web/assets/{swimlanes-42K2YHIH-BNUI3ghm.js → swimlanes-42K2YHIH-Bke6VXs0.js} +1 -1
  101. package/web/assets/swimlanesDiagram-VR7AAH4N-Drds9YUy.js +8 -0
  102. package/web/assets/{timeline-definition-24CTP7MA-DVzeoz4F.js → timeline-definition-24CTP7MA-Cs2-dpWN.js} +1 -1
  103. package/web/assets/{treeView-Q6P3EWNA-CnvdZw-U.js → treeView-Q6P3EWNA-Dr8vsD3U.js} +1 -1
  104. package/web/assets/{treemap-WGGIJYW6-Ax_RXzPk.js → treemap-WGGIJYW6-DbvC3nsA.js} +1 -1
  105. package/web/assets/useStore-BDEjghiw.js +23 -0
  106. package/web/assets/{vennDiagram-4TSXK5OY-CNauH8S4.js → vennDiagram-4TSXK5OY-CdUoLWPo.js} +1 -1
  107. package/web/assets/{wardley-WFR3VGLG-q8WnmSMu.js → wardley-WFR3VGLG-BL7j6fb-.js} +1 -1
  108. package/web/assets/{wardleyDiagram-VM6X3IG4-DKOY4P11.js → wardleyDiagram-VM6X3IG4-BEVYmHWI.js} +1 -1
  109. package/web/assets/{xychartDiagram-S5SC5T6Z-m8KaPfml.js → xychartDiagram-S5SC5T6Z-C9zn_1JB.js} +1 -1
  110. package/web/index.html +2 -2
  111. package/web/sw.js +1 -1
  112. package/dist/ephemeral-exec.js +0 -108
  113. package/dist/session/suggestions.js +0 -17
  114. package/web/assets/AutomationsView-DEild20v.js +0 -51
  115. package/web/assets/LibraryView-Cu0tU0gU.js +0 -2
  116. package/web/assets/Settings-BoTU83-i.js +0 -10
  117. package/web/assets/architecture-7GRP2DOG-s28sMQ0-.js +0 -1
  118. package/web/assets/channel-CPSM7rj7.js +0 -1
  119. package/web/assets/classDiagram-ZZMXUADV-CZ-So67n.js +0 -1
  120. package/web/assets/classDiagram-v2-VYDZK3BY-CZ-So67n.js +0 -1
  121. package/web/assets/eventmodeling-NTZA5JFV-Dd2mk6iz.js +0 -1
  122. package/web/assets/flowDiagram-HODETNUW-D7f1jYIL.js +0 -1
  123. package/web/assets/index-D76QD5dl.css +0 -1
  124. package/web/assets/mount-ISRAcaLu.js +0 -33
  125. package/web/assets/railroad-abnf-HS5TGJTU-BzFZIgti.js +0 -1
  126. package/web/assets/railroad-ebnf-LZEXJU2U-BkKCtTE4.js +0 -1
  127. package/web/assets/railroad-peg-WCYAUIDC-CuXWl9dG.js +0 -1
  128. package/web/assets/settingsRoute-DSj-6Pdo.js +0 -1
  129. package/web/assets/stateDiagram-v2-MP3YSRHH-2_bDqGCB.js +0 -1
  130. package/web/assets/swimlanesDiagram-VR7AAH4N-SmXx3nAj.js +0 -8
  131. package/web/assets/useStore-BznE40dl.js +0 -29
@@ -7,6 +7,7 @@ import { generatePairingKeypair, generateRoomKey, generatePairSecret, deriveWrap
7
7
  import { seal, open } from "./e2e.js";
8
8
  const DEFAULT_PAIR_TTL_MS = 5 * 60_000;
9
9
  const ROOM_KEY_BYTES = 32;
10
+ const MAX_RETIRED_ROOM_KEYS = 8;
10
11
  /**
11
12
  * Validate + canonicalize a pre-shared room-key seed. Returns the canonical
12
13
  * base64 of a well-formed 32-byte key, or undefined for anything malformed
@@ -81,6 +82,7 @@ export class PairingStore {
81
82
  return new PairingStore(filePath, {
82
83
  nodeKeypair: parsed.nodeKeypair,
83
84
  roomKeyB64: parsed.roomKeyB64,
85
+ ...(Array.isArray(parsed.retiredRoomKeysB64) && parsed.retiredRoomKeysB64.length ? { retiredRoomKeysB64: parsed.retiredRoomKeysB64 } : {}),
84
86
  devices: Array.isArray(parsed.devices) ? parsed.devices : [],
85
87
  });
86
88
  }
@@ -130,6 +132,30 @@ export class PairingStore {
130
132
  roomKey() {
131
133
  return Buffer.from(this.data.roomKeyB64, "base64");
132
134
  }
135
+ /** Room keys replaced by earlier rotations that still guard stored data. */
136
+ retiredRoomKeys() {
137
+ return (this.data.retiredRoomKeysB64 ?? []).map((key) => Buffer.from(key, "base64"));
138
+ }
139
+ /** Drop retired room keys once nothing stored is sealed under them. */
140
+ forgetRetiredRoomKeys() {
141
+ if (!this.data.retiredRoomKeysB64)
142
+ return;
143
+ delete this.data.retiredRoomKeysB64;
144
+ this.persist();
145
+ }
146
+ /** Open a payload sealed under the current room key or a retired one. */
147
+ openSealed(payload) {
148
+ let lastError;
149
+ for (const key of [this.roomKey(), ...this.retiredRoomKeys()]) {
150
+ try {
151
+ return open(key, payload);
152
+ }
153
+ catch (error) {
154
+ lastError = error;
155
+ }
156
+ }
157
+ throw lastError;
158
+ }
133
159
  /** Issue a single-use, expiring pairing secret to embed in a linking QR. */
134
160
  issuePairSecret(ttlMs = DEFAULT_PAIR_TTL_MS) {
135
161
  this.sweepSecrets();
@@ -215,6 +241,7 @@ export class PairingStore {
215
241
  }
216
242
  /** Rotate the room key and re-wrap it for all current devices. */
217
243
  rotateRoomKey() {
244
+ this.data.retiredRoomKeysB64 = [this.data.roomKeyB64, ...(this.data.retiredRoomKeysB64 ?? [])].slice(0, MAX_RETIRED_ROOM_KEYS);
218
245
  this.data.roomKeyB64 = generateRoomKey().toString("base64");
219
246
  this.persist();
220
247
  const roomKey = this.roomKey();
@@ -9,6 +9,7 @@ export function readEphemeralTeardownConfig(env = process.env) {
9
9
  onFinish: env.BIVY_TEARDOWN_ON_FINISH === "1",
10
10
  finishGraceMs: Number(env.BIVY_TEARDOWN_FINISH_GRACE_MS) || 10_000,
11
11
  idleGraceMs: Number(env.BIVY_SESSION_IDLE_CLOSE_MS) || 30 * 60 * 1000,
12
+ sleep: env.BIVY_EPHEMERAL_SLEEP === "1",
12
13
  };
13
14
  }
14
15
  /**
@@ -20,8 +21,10 @@ export function readEphemeralTeardownConfig(env = process.env) {
20
21
  export function shouldSelfTeardown(cfg, state) {
21
22
  if (!cfg.enabled)
22
23
  return false;
24
+ // A fresh box waits for its first work. A sleeping machine woken for
25
+ // nothing goes back to sleep after the idle window instead of running to TTL.
23
26
  if (!state.everBusy)
24
- return false;
27
+ return cfg.sleep && state.idleForMs >= cfg.idleGraceMs;
25
28
  if (state.anyWorking || state.anyRemoteActive || state.inFlightWork > 0)
26
29
  return false;
27
30
  const grace = cfg.onFinish ? cfg.finishGraceMs : cfg.idleGraceMs;
@@ -50,6 +53,13 @@ export async function performSelfTeardown(deps) {
50
53
  torndown = true;
51
54
  const log = deps.log ?? ((m) => console.log(`[ephemeral-teardown] ${m}`));
52
55
  const exit = deps.exit ?? ((c) => process.exit(c));
56
+ if (deps.sleep) {
57
+ // Exit only: the provider keeps the stopped machine and its disk, and a
58
+ // wake starts it again. Signalling settled would get it reaped.
59
+ log(`machine idle — sleeping (provider=${deps.provider})`);
60
+ exit(0);
61
+ return;
62
+ }
53
63
  log(`machine idle — self-teardown (provider=${deps.provider})`);
54
64
  try {
55
65
  await deps.signalSettled?.();
package/dist/exec.js CHANGED
@@ -14,6 +14,7 @@
14
14
  // A turn that ends without any assistant text because the agent FAILED (e.g. an
15
15
  // unauthenticated CLI) exits non-zero with the error on stderr, rather than
16
16
  // reporting a silent empty success.
17
+ import { randomUUID } from "node:crypto";
17
18
  import { WebSocket } from "ws";
18
19
  const USAGE = `Usage: bivy exec [options] "<prompt>" (or pipe the prompt on stdin, or pass -)
19
20
 
@@ -223,7 +224,9 @@ async function main() {
223
224
  try {
224
225
  await api(args.url, args.token, "/api/session/prompt", {
225
226
  method: "POST",
226
- body: JSON.stringify({ sessionId, text: prompt, clientMessageId: `exec-${sessionId}` }),
227
+ // One id per exec run: the node drops a repeated id as a duplicate send
228
+ // for a day, so an id shared by every run in a session ate follow-ups.
229
+ body: JSON.stringify({ sessionId, text: prompt, clientMessageId: `exec-${randomUUID()}` }),
227
230
  });
228
231
  }
229
232
  catch (error) {
@@ -147,3 +147,19 @@ export async function catalogReplyEvent(load, sessionId) {
147
147
  return { type: "session.error", sessionId, error: `Couldn't read this machine's model catalog: ${error instanceof Error ? error.message : String(error)}` };
148
148
  }
149
149
  }
150
+ /**
151
+ * Give a session's agent up to `timeoutMs` to publish its real model catalog
152
+ * before a caller reads it. A not-yet-started agent otherwise answers from a
153
+ * placeholder list. Never throws; a runtime without warmModels() is a no-op.
154
+ */
155
+ export async function warmSessionCatalog(session, timeoutMs) {
156
+ if (!session.warmModels)
157
+ return;
158
+ let timer;
159
+ await Promise.race([
160
+ session.warmModels().catch(() => { }),
161
+ new Promise((resolve) => { timer = setTimeout(resolve, timeoutMs); }),
162
+ ]);
163
+ if (timer)
164
+ clearTimeout(timer);
165
+ }
package/dist/server.js CHANGED
@@ -44,7 +44,7 @@ import { createAppCommands } from "./controllers/app-commands.js";
44
44
  import { createFileCommands } from "./controllers/file-commands.js";
45
45
  import { bindClientCommandRoutes } from "./http/client-command-routes.js";
46
46
  import { collectDiscoveredSessions, planNativeAdoption } from "./runtime/native-session-discovery.js";
47
- import { aggregateModelCatalog, catalogReplyEvent, mergeProviderCatalog } from "./runtime/model-catalog.js";
47
+ import { aggregateModelCatalog, catalogReplyEvent, warmSessionCatalog, mergeProviderCatalog } from "./runtime/model-catalog.js";
48
48
  import { resolveModelRef } from "./runtime/model-ref.js";
49
49
  import { RuntimeHost, enforcementLevelFor, remoteRuntimeEnabled } from "./runtime/host.js";
50
50
  import { RemoteRuntime, RemoteRuntimeSession } from "./runtime/remote.js";
@@ -54,6 +54,7 @@ import { ControlPlaneSessionLocationRegistry, LayeredSessionLocationRegistry } f
54
54
  import { attachAdoptedSessions, classifyAttachFailure } from "./runtime/adoption.js";
55
55
  import { createCredentialStore, testProviderCredential } from "./runtime/credentials.js";
56
56
  import { decodeAutomationTemplate } from "./automation-template.js";
57
+ import { resealAutomationTemplates } from "./automation-reseal.js";
57
58
  import { passWebhookFilter } from "./automation-filter-gate.js";
58
59
  import { isModelAuthError, authProviderForSession, classifyModelAuthError } from "./runtime/auth-errors.js";
59
60
  import { createCredentialVault, migrateVaultDir } from "./runtime/credential-store.js";
@@ -80,9 +81,7 @@ import { exportProviderAuth, exportAccountApiKeys, exportAccountOAuthCredentials
80
81
  import { listProviders } from "./runtime/provider-catalog.js";
81
82
  import { exportLocalModels, importLocalModels } from "./runtime/local-model-store.js";
82
83
  import { sessionLikeFields } from "./session/start-like.js";
83
- import { MAX_SUGGESTION_TEXT, MAX_SUGGESTION_TITLE, SUGGESTION_RUNS, isTaskSuggestion } from "./session/suggestions.js";
84
84
  import { BIVY_AGENT_NOTE, mergeSyncedAgentInstructions, readAgentInstructions, sessionInstructions, writeAgentInstructions, MAX_AGENT_INSTRUCTIONS_BYTES } from "./agent-instructions.js";
85
- import { execEphemeralRequest } from "./ephemeral-exec.js";
86
85
  import { ApprovalManager } from "./approval.js";
87
86
  import { QuestionManager, validQuestions, isAskUserQuestionTool, formatQuestionResult } from "./question.js";
88
87
  import { NodeIdentity } from "./identity.js";
@@ -143,7 +142,7 @@ import { AgentQuestions, askQuestionsFrom } from "./session/agent-questions.js";
143
142
  import { AutomationProposals, describeChange } from "./session/automation-proposals.js";
144
143
  import { createSessionTokenCodec, isSessionToken, sessionTokenAllows } from "./session/session-tokens.js";
145
144
  import { setSessionTokenSigner } from "./runtime/session-env.js";
146
- import { MAX_NOTICE_TEXT, isAgentNotice, noticePush } from "./session/notices.js";
145
+ import { MAX_NOTICE_TEXT, noticePush } from "./session/notices.js";
147
146
  import { createForkCommands } from "./controllers/fork-commands.js";
148
147
  import { createGithubCommands } from "./controllers/github-commands.js";
149
148
  import { createCredentialCommands } from "./controllers/credential-commands.js";
@@ -1082,6 +1081,7 @@ const linkedDevices = createLinkedDeviceController({
1082
1081
  syncPairingMetadata();
1083
1082
  relay?.pushRotate(deliveries);
1084
1083
  broadcast({ type: "devices.updated", devices });
1084
+ void resealRetiredAutomationTemplates();
1085
1085
  },
1086
1086
  });
1087
1087
  const accessDevices = createAccessDeviceController({
@@ -2268,7 +2268,7 @@ const notAgentServers = (offers) => offers.filter((offer) => !agentCommands.has(
2268
2268
  const appService = new AppService(appRegistry, appGateway ?? remotePreview, {
2269
2269
  start: async (spec) => {
2270
2270
  let failure = "Could not start the app terminal.";
2271
- const id = await runTerms.openRunTerminal({ ...spec, agent: "app", label: spec.name }, (event) => {
2271
+ const id = await runTerms.openRunTerminal({ ...spec, agent: "app", label: spec.name, background: true }, (event) => {
2272
2272
  const e = event;
2273
2273
  if (e.type === "terminal.error" && e.error)
2274
2274
  failure = e.error;
@@ -2898,6 +2898,13 @@ const RELAY_COMMANDS = {
2898
2898
  if (!requestedSessionId && wantedRuntimeId && record?.runtimeId !== wantedRuntimeId)
2899
2899
  record = null;
2900
2900
  record ??= await sessionForModelQuery(wantedRuntimeId);
2901
+ // A session whose agent hasn't started yet answers from the runtime's
2902
+ // placeholder catalog, which can be missing the account's current models —
2903
+ // a freshly launched machine then rejected a saved model it actually has.
2904
+ // Warm the real catalog first (a no-op re-read once the agent is running),
2905
+ // bounded so a slow spawn still answers with what is known.
2906
+ if (requestedSessionId)
2907
+ await warmSessionCatalog(record.session, SESSION_MODELS_WARM_MS);
2901
2908
  // Tag the list with the runtime it was resolved for so the client can
2902
2909
  // tell whether it belongs to the agent it currently has selected. Without
2903
2910
  // this a models.list answered for one agent (e.g. Codex's models) could
@@ -3188,22 +3195,6 @@ const RELAY_COMMANDS = {
3188
3195
  return ctx.reply({ type: "session.promote.result", requestId, ok: false, error: "Promotion lost the epoch race" });
3189
3196
  ctx.reply({ type: "session.promote.result", requestId, ok: true, sessionId, epoch });
3190
3197
  },
3191
- // Ephemeral provisioning transport (node-broker path). A remote device that
3192
- // holds the user's cloud credentials asks this node to make ONE allowlisted
3193
- // HTTPS request to a provider (Fly/Hetzner/AWS/...) on its behalf. The
3194
- // token/credentials ride in the request headers and are used transiently —
3195
- // never persisted here — so the provisioning stays end-to-end (the control
3196
- // plane never sees it). The host allowlist is the SSRF guard.
3197
- async "ephemeral.exec"(msg) {
3198
- const requestId = String(msg.requestId ?? "");
3199
- try {
3200
- const result = await execEphemeralRequest(msg.request);
3201
- relay?.sendEvent({ type: "ephemeral.exec.result", requestId, ...result });
3202
- }
3203
- catch (error) {
3204
- relay?.sendEvent({ type: "ephemeral.exec.result", requestId, error: error instanceof Error ? error.message : String(error) });
3205
- }
3206
- },
3207
3198
  // Subscription / OAuth login driven from a remote device. The node runs the
3208
3199
  // provider's device-code (or paste-back) flow and reports the verification
3209
3200
  // link + code over the relay; the phone never needs the node's localhost.
@@ -3548,6 +3539,7 @@ function startRelayIfConfigured() {
3548
3539
  if (config.controlPlaneUrl && config.enrollmentToken) {
3549
3540
  sessionAdvertiseTarget = { controlPlaneUrl: config.controlPlaneUrl, enrollmentToken: config.enrollmentToken };
3550
3541
  void syncModelAuthFromControlPlane();
3542
+ void resealRetiredAutomationTemplates();
3551
3543
  if (remoteRuntimeEnabled()) {
3552
3544
  // Stage 3: layer a control-plane-backed location registry UNDER the in-memory
3553
3545
  // one (so a lookup surviving a restart resolves from durable state), then
@@ -3709,6 +3701,43 @@ async function modelAuthFetch(pathname, init = {}) {
3709
3701
  headers.set("content-type", "application/json");
3710
3702
  return fetch(`${sessionAdvertiseTarget.controlPlaneUrl.replace(/\/$/, "")}${pathname}`, { ...init, headers });
3711
3703
  }
3704
+ // A device revoke rotates the room key. Re-seal this node's stored automation
3705
+ // instructions under the new key so the node and its devices can still read
3706
+ // them, then drop the retired keys. Retried at startup until it completes.
3707
+ async function resealRetiredAutomationTemplates() {
3708
+ const retired = pairingStore.retiredRoomKeys();
3709
+ if (!retired.length || !sessionAdvertiseTarget)
3710
+ return;
3711
+ const current = pairingStore.roomKey();
3712
+ try {
3713
+ const res = await modelAuthFetch("/node/automation-templates");
3714
+ if (!res?.ok)
3715
+ throw new Error(`control plane returned ${res?.status}`);
3716
+ const { templates } = await res.json();
3717
+ const result = await resealAutomationTemplates({
3718
+ nodeId: identity.nodeId,
3719
+ current,
3720
+ retired,
3721
+ templates,
3722
+ save: async (id, templateCiphertext) => {
3723
+ const saved = await modelAuthFetch(`/node/automation-templates/${encodeURIComponent(id)}`, { method: "PUT", body: JSON.stringify({ templateCiphertext }) });
3724
+ if (!saved?.ok)
3725
+ throw new Error(`control plane returned ${saved?.status}`);
3726
+ },
3727
+ });
3728
+ if (result.resealed)
3729
+ console.log(`[automation-reseal] re-sealed ${result.resealed} automation template(s) under the rotated room key`);
3730
+ if (result.unreadable.length)
3731
+ console.warn(`[automation-reseal] ${result.unreadable.length} automation template(s) were sealed under a key this node no longer has: ${result.unreadable.join(", ")}`);
3732
+ // Another rotation during this pass retired `current`; run again for it.
3733
+ if (!pairingStore.roomKey().equals(current))
3734
+ return void resealRetiredAutomationTemplates();
3735
+ pairingStore.forgetRetiredRoomKeys();
3736
+ }
3737
+ catch (error) {
3738
+ console.warn("[automation-reseal] will retry at next start:", error.message);
3739
+ }
3740
+ }
3712
3741
  const remoteSessionAdmission = createRemoteSessionAdmission(async (idempotencyKey) => {
3713
3742
  // Only remote scopes reach this callback. Self-hosted control planes answer
3714
3743
  // allowed without an extension; losing enrollment mid-launch must fail closed.
@@ -5565,7 +5594,7 @@ async function executeWorkItem(item, report, signal) {
5565
5594
  throw new Error("automation instructions were encrypted for a different node");
5566
5595
  }
5567
5596
  try {
5568
- const template = decodeAutomationTemplate(open(pairingStore.roomKey(), payload.join(":")));
5597
+ const template = decodeAutomationTemplate(pairingStore.openSealed(payload.join(":")));
5569
5598
  credentialLabels = template.credentialLabels;
5570
5599
  filter = template.filter;
5571
5600
  item = { ...item, body: template.instructions };
@@ -7620,7 +7649,7 @@ function evaluateEphemeralTeardown() {
7620
7649
  const records = new Set(openSessions.values());
7621
7650
  const anyWorking = [...records].some((r) => r.isWorking);
7622
7651
  const anyRemoteActive = clients.size > 0 || (relay?.clientCount ?? 0) > 0
7623
- || [...records].some((r) => r.remoteActive);
7652
+ || [...records].some((r) => r.remoteActive) || remotePreview.inUse;
7624
7653
  const inFlightWork = controlPlanePoller?.inFlightCount() ?? 0;
7625
7654
  if (anyWorking || anyRemoteActive || inFlightWork > 0) {
7626
7655
  ephemeralEverBusy = true;
@@ -7643,6 +7672,7 @@ function evaluateEphemeralTeardown() {
7643
7672
  }
7644
7673
  await performSelfTeardown({
7645
7674
  provider: ephemeralTeardownCfg.provider,
7675
+ sleep: ephemeralTeardownCfg.sleep,
7646
7676
  signalSettled: signalSettledToControlPlane,
7647
7677
  shutdown: () => { try {
7648
7678
  spawnSync("shutdown", ["-h", "now"], { stdio: "ignore" });
@@ -7657,7 +7687,7 @@ if (ephemeralTeardownCfg.enabled) {
7657
7687
  const ephemeralEvalMs = Math.max(2_000, Math.min(ephemeralTeardownCfg.finishGraceMs, 15_000));
7658
7688
  const ephemeralTeardownTimer = setInterval(() => evaluateEphemeralTeardown(), ephemeralEvalMs);
7659
7689
  ephemeralTeardownTimer.unref?.();
7660
- console.log(`[ephemeral-teardown] armed: provider=${ephemeralTeardownCfg.provider} onFinish=${ephemeralTeardownCfg.onFinish} ttl=${ephemeralTeardownCfg.ttlMin}m`);
7690
+ console.log(`[ephemeral-teardown] armed: provider=${ephemeralTeardownCfg.provider} onFinish=${ephemeralTeardownCfg.onFinish} sleep=${ephemeralTeardownCfg.sleep} ttl=${ephemeralTeardownCfg.ttlMin}m`);
7661
7691
  }
7662
7692
  setTimeout(() => void sweepDiskGuardrails(), 30_000).unref?.();
7663
7693
  // One sweep shortly after boot clears ghosts left by a previous run before any
@@ -8130,14 +8160,17 @@ function attachSessionListeners(record) {
8130
8160
  if (event.type === "turn_start" || event.type === "message_boundary" || event.type === "message_end" || event.type === "turn_end") {
8131
8161
  transcripts.persistTranscriptSnapshot(record);
8132
8162
  }
8133
- // A finalized assistant message may reference a remote image via markdown
8134
- // (`![alt](https://…)`) — fetch and store it now so the chat can render it
8135
- // (see resolveInlineImages). Checked on both events: message_end is the
8136
- // precise "this assistant message is done" signal most runtimes emit, but
8137
- // turn_end is a safety net for one that only surfaces the final text there.
8138
- // Fire-and-forget and internally deduped, so checking on both costs nothing.
8163
+ // A finalized assistant message may reference an image via markdown — a
8164
+ // remote URL (`![alt](https://…)`) to fetch, or a file the agent just
8165
+ // produced in its workspace (`![alt](out/chart.png)`) to read. Resolve and
8166
+ // store either now, so the chat can render it and history keeps it after the
8167
+ // workspace is gone. Checked on both events: message_end is the precise
8168
+ // "this assistant message is done" signal most runtimes emit, but turn_end
8169
+ // is a safety net for one that only surfaces the final text there. Both
8170
+ // resolvers are internally deduped, so checking twice costs nothing.
8139
8171
  if (event.type === "message_end" || event.type === "turn_end") {
8140
8172
  transcripts.resolveInlineImages(record);
8173
+ transcripts.resolveWorkspaceRefs(record, harnessDirFor(record));
8141
8174
  }
8142
8175
  // Durably persist the throttled sidecars at the turn boundary so a crash
8143
8176
  // loses at most the in-flight turn's UI detail, not the whole turn. A
@@ -9069,6 +9102,9 @@ async function sessionForModelQuery(runtimeId) {
9069
9102
  modelQueryScratchPending.set(wanted, build);
9070
9103
  return build;
9071
9104
  }
9105
+ /** How long a session's models.list waits for its agent's real catalog before
9106
+ * answering with what is known (below the client's 15 s launch timeout). */
9107
+ const SESSION_MODELS_WARM_MS = 8_000;
9072
9108
  // A fresh scratch answers its first models.list from the runtime's placeholder
9073
9109
  // list (no live agent yet). Warm the real catalog in the background — for Claude
9074
9110
  // Code this spins up the agent subprocess just far enough for supportedModels()
@@ -11741,10 +11777,9 @@ app.get("/api/session/:id/context", (req, res) => {
11741
11777
  previewAvailable,
11742
11778
  }));
11743
11779
  });
11744
- // `bivy notify "<message>"`: a card in the chat, and a push naming the session
11745
- // (never the text) when nobody has the app open, or when it's urgent. At most
11746
- // one push a minute per session; later notices still post a card. A `bivy run`
11747
- // terminal has no chat to hold the card, so its notice is the push alone.
11780
+ // `bivy notify`: a push naming the session (never any text) when nobody has the
11781
+ // app open, or when it's urgent. At most one push a minute per session. It adds
11782
+ // nothing to the chat: what the agent has to say belongs in its reply there.
11748
11783
  const lastNotifyPush = new Map();
11749
11784
  app.post("/api/session/:id/notify", (req, res) => {
11750
11785
  const record = openSessions.get(String(req.params.id));
@@ -11752,16 +11787,9 @@ app.post("/api/session/:id/notify", (req, res) => {
11752
11787
  const sessionId = record?.id ?? run?.sessionId;
11753
11788
  if (!sessionId)
11754
11789
  return res.status(404).json({ error: "Session not found" });
11755
- const text = typeof req.body?.text === "string" ? req.body.text.trim() : "";
11756
- const notice = { id: `notice-${randomBytes(8).toString("hex")}`, text, ...(req.body?.urgent === true ? { urgent: true } : {}) };
11757
- if (!isAgentNotice(notice))
11758
- return res.status(400).json({ error: `A notice needs text (up to ${MAX_NOTICE_TEXT} characters).` });
11759
- if (record) {
11760
- eventLog.appendNotice(record.id, { afterMessageCount: record.session.getMessages().length, notice });
11761
- broadcast(stampSessionEvent({ type: "session.event", sessionId: record.id, event: { type: "notice", id: notice.id, notice } }));
11762
- }
11790
+ const urgent = req.body?.urgent === true;
11763
11791
  const watching = Boolean(record) && (clients.size > 0 || (relay?.clientCount ?? 0) > 0);
11764
- const push = noticePush({ urgent: notice.urgent, userWatching: watching, lastPushAt: lastNotifyPush.get(sessionId), now: Date.now(), pushConfigured: Boolean(sessionAdvertiseTarget) });
11792
+ const push = noticePush({ urgent, userWatching: watching, lastPushAt: lastNotifyPush.get(sessionId), now: Date.now(), pushConfigured: Boolean(sessionAdvertiseTarget) });
11765
11793
  if (push === "sent") {
11766
11794
  lastNotifyPush.set(sessionId, Date.now());
11767
11795
  void sendNotificationHint({
@@ -11769,10 +11797,10 @@ app.post("/api/session/:id/notify", (req, res) => {
11769
11797
  sessionId,
11770
11798
  targetSessionId: sessionId,
11771
11799
  title: sessionNotifyLabel(record, run?.name || "A terminal session"),
11772
- body: "Has a message for you — tap to read it.",
11800
+ body: "Wants you to take a look — tap to open the session.",
11773
11801
  });
11774
11802
  }
11775
- res.json({ ok: true, id: notice.id, posted: Boolean(record), push, userWatching: watching });
11803
+ res.json({ ok: true, push, userWatching: watching });
11776
11804
  });
11777
11805
  // `bivy ask`: the question card and "needs your input" push, for any agent.
11778
11806
  // Returns at once with an id; `/wait` blocks up to 240s per call (under common
@@ -11830,23 +11858,6 @@ app.post("/api/session/:id/title", (req, res) => {
11830
11858
  sessionNamer.setSessionName(record, title);
11831
11859
  res.json({ ok: true, title });
11832
11860
  });
11833
- // `bivy suggest "<task>"`: the agent proposes a task the user can start in one
11834
- // tap, here, through this agent's sub-agents, or in a parallel session; `run`
11835
- // is the one the agent recommends (see packages/web SuggestionCard).
11836
- app.post("/api/session/:id/suggest", (req, res) => {
11837
- const record = openSessions.get(String(req.params.id));
11838
- if (!record)
11839
- return res.status(404).json({ error: "Session not found" });
11840
- const text = typeof req.body?.text === "string" ? req.body.text.trim() : "";
11841
- const title = typeof req.body?.title === "string" && req.body.title.trim() ? req.body.title.trim() : undefined;
11842
- const run = req.body?.run || undefined;
11843
- const suggestion = { id: `suggestion-${randomBytes(8).toString("hex")}`, text, ...(title ? { title } : {}), ...(run ? { run } : {}) };
11844
- if (!isTaskSuggestion(suggestion))
11845
- return res.status(400).json({ error: `A suggestion needs text (up to ${MAX_SUGGESTION_TEXT} characters), an optional title (up to ${MAX_SUGGESTION_TITLE}) and an optional run (${SUGGESTION_RUNS.join(", ")}).` });
11846
- eventLog.appendSuggestion(record.id, { afterMessageCount: record.session.getMessages().length, suggestion });
11847
- broadcast(stampSessionEvent({ type: "session.event", sessionId: record.id, event: { type: "suggestion", id: suggestion.id, suggestion } }));
11848
- res.json({ ok: true, id: suggestion.id });
11849
- });
11850
11861
  // Explicit child Run API for first-party/user-directed workflows. These routes
11851
11862
  // are intentionally not advertised to agents as tools; the service still
11852
11863
  // authorizes every status lookup against this parent Session's provenance.