@bivy/bivy 0.20.8 → 0.20.9-staging.10

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 (120) 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 +4 -10
  6. package/dist/agents/pi/integration.js +1 -1
  7. package/dist/apps/styles.css +14 -47
  8. package/dist/certification/generated.js +1 -1
  9. package/dist/ephemeral-teardown.js +11 -1
  10. package/dist/exec.js +4 -1
  11. package/dist/server.js +10 -52
  12. package/dist/session/event-log.js +15 -18
  13. package/dist/session/inline-image-fetch.js +1 -3
  14. package/dist/session/session-tokens.js +1 -1
  15. package/package.json +6 -6
  16. package/web/assets/AutomationsView-CqKikjyc.js +51 -0
  17. package/web/assets/{ChangesCard-CPnPUh6N.js → ChangesCard-C6LeCjVH.js} +1 -1
  18. package/web/assets/{LibraryView-CbEXd0eI.js → LibraryView-BR9OMhze.js} +1 -1
  19. package/web/assets/{ProviderConnect-CoqAK5Wk.js → ProviderConnect-CKYP_-dW.js} +1 -1
  20. package/web/assets/Settings-D9myJbO-.js +10 -0
  21. package/web/assets/{Sheet-DDK1zw1r.js → Sheet-CL3dk1Sa.js} +1 -1
  22. package/web/assets/{Terminal-1RXw5d-8.js → Terminal-DuzTWmq_.js} +1 -1
  23. package/web/assets/{VoiceSettings-XKgK-rHp.js → VoiceSettings-C5xiXXg-.js} +1 -1
  24. package/web/assets/{abnfDiagram-VCTEODGH-mGY2XgpR.js → abnfDiagram-VCTEODGH-CiuYisMe.js} +1 -1
  25. package/web/assets/architecture-7GRP2DOG-CmGiKLJd.js +1 -0
  26. package/web/assets/{architectureDiagram-5GKGNRK7-iODllUCA.js → architectureDiagram-5GKGNRK7-DQEZKeJf.js} +1 -1
  27. package/web/assets/{blockDiagram-I7D4REHJ-Ge7J4mPm.js → blockDiagram-I7D4REHJ-BXifaI4s.js} +1 -1
  28. package/web/assets/{c4Diagram-7LVT6UL2-f-kX3zo7.js → c4Diagram-7LVT6UL2-BUI1_QJ6.js} +1 -1
  29. package/web/assets/channel-GfgWPXVn.js +1 -0
  30. package/web/assets/{chunk-4HAMMTFA-DgLlEu-R.js → chunk-4HAMMTFA-CAqAnSt0.js} +1 -1
  31. package/web/assets/{chunk-75Z2AOVW-Dml242zI.js → chunk-75Z2AOVW-CmAfksq6.js} +1 -1
  32. package/web/assets/{chunk-DU6HZSFF-DHGHAQj_.js → chunk-DU6HZSFF-CBn3E1kV.js} +1 -1
  33. package/web/assets/{chunk-F27PBJKO-DYJWgQwW.js → chunk-F27PBJKO-Cqca621p.js} +1 -1
  34. package/web/assets/{chunk-GMAD6QVW-Be6XrE8S.js → chunk-GMAD6QVW-DuNHDMjw.js} +1 -1
  35. package/web/assets/{chunk-GVQU2GXP-CrJtrbng.js → chunk-GVQU2GXP-DadmdXvU.js} +1 -1
  36. package/web/assets/{chunk-IMKFNOWR-BNoFxzwg.js → chunk-IMKFNOWR-D7xvXSnw.js} +1 -1
  37. package/web/assets/{chunk-L3NEJ4N5-8rFD5H7o.js → chunk-L3NEJ4N5-Drxtq2Z1.js} +1 -1
  38. package/web/assets/{chunk-OSK3NFVY-Df7-4Mnn.js → chunk-OSK3NFVY-UWX78yLv.js} +1 -1
  39. package/web/assets/{chunk-P2QGCYS3-rHBlhB9b.js → chunk-P2QGCYS3-D6hNBxaT.js} +1 -1
  40. package/web/assets/{chunk-POPQ4Y6H-TtIAtZss.js → chunk-POPQ4Y6H-CmUjfAOD.js} +1 -1
  41. package/web/assets/{chunk-PWAF6VOD-CkwX1I_u.js → chunk-PWAF6VOD-CTJIWOj-.js} +1 -1
  42. package/web/assets/{chunk-SHT3W25Y-QK-L20Uq.js → chunk-SHT3W25Y-CIPO0-2c.js} +1 -1
  43. package/web/assets/{chunk-SVP7TREG-r_Z2GtSi.js → chunk-SVP7TREG-DkeeA16U.js} +1 -1
  44. package/web/assets/{chunk-TICWLB2K-CUKfLR5q.js → chunk-TICWLB2K-CCCgUMTk.js} +1 -1
  45. package/web/assets/classDiagram-ZZMXUADV-AnSFdiZo.js +1 -0
  46. package/web/assets/classDiagram-v2-VYDZK3BY-AnSFdiZo.js +1 -0
  47. package/web/assets/{cynefin-OW5HDTMX-Dt3Y2U_d.js → cynefin-OW5HDTMX-lsnbNwYx.js} +1 -1
  48. package/web/assets/{cynefinDiagram-5FMLGOSQ-Y5k7qfOG.js → cynefinDiagram-5FMLGOSQ-CmT6IHRO.js} +1 -1
  49. package/web/assets/{dagre-GXQ25YYZ-D6uSyP7i.js → dagre-GXQ25YYZ-CaVNKxQq.js} +1 -1
  50. package/web/assets/{diagram-S7CK7UJ4-cy3iJG3K.js → diagram-S7CK7UJ4-DOPXiiOU.js} +1 -1
  51. package/web/assets/{diagram-UQ7AKVKN-D-Dujcue.js → diagram-UQ7AKVKN-Bxe6jYPE.js} +1 -1
  52. package/web/assets/{diagram-VSXAHHWV-BmI9Opjx.js → diagram-VSXAHHWV-D2IPWUER.js} +1 -1
  53. package/web/assets/{diagram-VX7I27RA-Ca81_y4A.js → diagram-VX7I27RA-DPA8OR7z.js} +1 -1
  54. package/web/assets/{diagram-Z3DM3KII-BBMq_KK4.js → diagram-Z3DM3KII-qlgZN9mg.js} +1 -1
  55. package/web/assets/{ebnfDiagram-PWID7BFC-Bc-yp1-u.js → ebnfDiagram-PWID7BFC-BG3oYfMg.js} +1 -1
  56. package/web/assets/{erDiagram-RLTQ6QDP-C4not1_u.js → erDiagram-RLTQ6QDP-39wsY2SV.js} +1 -1
  57. package/web/assets/eventmodeling-NTZA5JFV-COSR-_D4.js +1 -0
  58. package/web/assets/flowDiagram-HODETNUW-Bv4slDx7.js +1 -0
  59. package/web/assets/{ganttDiagram-EL5Y4UJY-eOqP4w8u.js → ganttDiagram-EL5Y4UJY-9m79cMnC.js} +1 -1
  60. package/web/assets/{gitGraph-4MIJSDKK-CNyrdfwo.js → gitGraph-4MIJSDKK-B5lfxsxx.js} +1 -1
  61. package/web/assets/{gitGraphDiagram-WWUBYQGX-DWlo-lrH.js → gitGraphDiagram-WWUBYQGX-DWl1V9gk.js} +1 -1
  62. package/web/assets/{index-C1fVIjSt.js → index-DKaLVMgX.js} +2 -2
  63. package/web/assets/index-DZb12uRj.css +1 -0
  64. package/web/assets/{info-A6RAGUB7-CUQ_micy.js → info-A6RAGUB7-CVPplCTQ.js} +1 -1
  65. package/web/assets/{infoDiagram-27XIBGKW-BGpJ5FNT.js → infoDiagram-27XIBGKW-B-yaJrP2.js} +1 -1
  66. package/web/assets/{ishikawaDiagram-5VMMS53U-BPHiUQm6.js → ishikawaDiagram-5VMMS53U-DtJ480L8.js} +1 -1
  67. package/web/assets/{journeyDiagram-3NMN7TZE-B4LBaWaP.js → journeyDiagram-3NMN7TZE-CsvTgPXC.js} +1 -1
  68. package/web/assets/{kanban-definition-UXKFOSKX-DNVvNHrT.js → kanban-definition-UXKFOSKX-CqTcD1hf.js} +1 -1
  69. package/web/assets/{line-DEBH51YO.js → line-D2guJ01j.js} +1 -1
  70. package/web/assets/{mermaid-parser.core-DKGY5TTe.js → mermaid-parser.core-NJMkrhfu.js} +3 -3
  71. package/web/assets/{mermaid.core-SY5t-W54.js → mermaid.core-D-4da6CZ.js} +4 -4
  72. package/web/assets/{mindmap-definition-YA3MSWOX-DNzu0vrE.js → mindmap-definition-YA3MSWOX-CsHyNaAw.js} +1 -1
  73. package/web/assets/mount-DAtUokCH.js +31 -0
  74. package/web/assets/{packet-AYTQ26CC-BFmJxIPS.js → packet-AYTQ26CC-CXMWsuib.js} +1 -1
  75. package/web/assets/{pegDiagram-XKGWAZYB-Cg4XSQXq.js → pegDiagram-XKGWAZYB-ByQqRbph.js} +1 -1
  76. package/web/assets/{pie-WAS4IAKB-BjugvaCZ.js → pie-WAS4IAKB-eRy6tqdo.js} +1 -1
  77. package/web/assets/{pieDiagram-E7YTZNPT-CZUoS4VP.js → pieDiagram-E7YTZNPT-D9CMUNwI.js} +1 -1
  78. package/web/assets/{quadrantDiagram-AXDQQJYC-Dt2ha5S3.js → quadrantDiagram-AXDQQJYC-BqrSq-tU.js} +1 -1
  79. package/web/assets/{radar-RG4KPBEZ-wvkY1jFj.js → radar-RG4KPBEZ-BASdVQAe.js} +1 -1
  80. package/web/assets/{railroad-74A4TZTK-C0urSGwW.js → railroad-74A4TZTK-MbyNUFW2.js} +1 -1
  81. package/web/assets/railroad-abnf-HS5TGJTU-DzZc8bRA.js +1 -0
  82. package/web/assets/railroad-ebnf-LZEXJU2U-d86-7YjJ.js +1 -0
  83. package/web/assets/railroad-peg-WCYAUIDC-yBlZPQK8.js +1 -0
  84. package/web/assets/{railroadDiagram-O6MQD6OU-CMPDMFEF.js → railroadDiagram-O6MQD6OU-CPhS-1_c.js} +1 -1
  85. package/web/assets/{requirementDiagram-BXWQKSXE-CgG1fTgc.js → requirementDiagram-BXWQKSXE-CtX1iQsg.js} +1 -1
  86. package/web/assets/{sankeyDiagram-P5KCCOFB-CqQc0C6b.js → sankeyDiagram-P5KCCOFB-BBSZVi3s.js} +1 -1
  87. package/web/assets/{sequenceDiagram-WJ2MYXX4-CCar7eyP.js → sequenceDiagram-WJ2MYXX4-Zwj2R6Ua.js} +1 -1
  88. package/web/assets/{settingsRoute-D75Kl5qJ.js → settingsRoute-DYF97n3g.js} +1 -1
  89. package/web/assets/{stateDiagram-D77RDMKH-hZdV5Olo.js → stateDiagram-D77RDMKH-DTB9F1f2.js} +1 -1
  90. package/web/assets/stateDiagram-v2-MP3YSRHH-BlWD3kYd.js +1 -0
  91. package/web/assets/{swimlanes-42K2YHIH-BOjTGDck.js → swimlanes-42K2YHIH-Bf5m1Adz.js} +1 -1
  92. package/web/assets/swimlanesDiagram-VR7AAH4N-B4wYjUsJ.js +8 -0
  93. package/web/assets/{timeline-definition-24CTP7MA-EQ9J2i8-.js → timeline-definition-24CTP7MA-CehYDFxs.js} +1 -1
  94. package/web/assets/{treeView-Q6P3EWNA-C_5CbmSf.js → treeView-Q6P3EWNA-CQmjQcQJ.js} +1 -1
  95. package/web/assets/{treemap-WGGIJYW6-C37qSFRc.js → treemap-WGGIJYW6-DMV5jymC.js} +1 -1
  96. package/web/assets/useStore-DrZiS5P_.js +22 -0
  97. package/web/assets/{vennDiagram-4TSXK5OY-Dz_rYEly.js → vennDiagram-4TSXK5OY-CJGwwuUA.js} +1 -1
  98. package/web/assets/{wardley-WFR3VGLG-ByC5Xl0N.js → wardley-WFR3VGLG-S6KEgq1P.js} +1 -1
  99. package/web/assets/{wardleyDiagram-VM6X3IG4-ZORHthei.js → wardleyDiagram-VM6X3IG4-B1eaMBr1.js} +1 -1
  100. package/web/assets/{xychartDiagram-S5SC5T6Z-BsEewrR8.js → xychartDiagram-S5SC5T6Z-C5emgjT-.js} +1 -1
  101. package/web/index.html +2 -2
  102. package/web/sw.js +1 -1
  103. package/dist/ephemeral-exec.js +0 -108
  104. package/dist/session/suggestions.js +0 -17
  105. package/web/assets/AutomationsView-CI8mce5b.js +0 -51
  106. package/web/assets/Settings-DzwwvcJY.js +0 -10
  107. package/web/assets/architecture-7GRP2DOG-iTGIo6ed.js +0 -1
  108. package/web/assets/channel-CZ9L6iYi.js +0 -1
  109. package/web/assets/classDiagram-ZZMXUADV-D_pQOsdw.js +0 -1
  110. package/web/assets/classDiagram-v2-VYDZK3BY-D_pQOsdw.js +0 -1
  111. package/web/assets/eventmodeling-NTZA5JFV-kTJMsPPZ.js +0 -1
  112. package/web/assets/flowDiagram-HODETNUW-BKXsEnEc.js +0 -1
  113. package/web/assets/index-D76QD5dl.css +0 -1
  114. package/web/assets/mount-BAMZ2poC.js +0 -33
  115. package/web/assets/railroad-abnf-HS5TGJTU-D3fB8KBr.js +0 -1
  116. package/web/assets/railroad-ebnf-LZEXJU2U-DxtPt5hF.js +0 -1
  117. package/web/assets/railroad-peg-WCYAUIDC-DSr1P8IJ.js +0 -1
  118. package/web/assets/stateDiagram-v2-MP3YSRHH-DSUfNYnq.js +0 -1
  119. package/web/assets/swimlanesDiagram-VR7AAH4N-6bUIqkAR.js +0 -8
  120. package/web/assets/useStore-BjrbW265.js +0 -29
package/README.md CHANGED
@@ -285,10 +285,9 @@ Every session gets the `bivy` CLI, so any agent with a shell can talk back
285
285
  through the app, not only agents with their own built-in tools:
286
286
 
287
287
  ```bash
288
- bivy notify "Tests are green, PR is up" # chat card + phone push when you're away
288
+ bivy notify # push to your phone so you come back
289
289
  bivy ask "Ship to staging?" --option Yes --option No # waits for your answer
290
290
  bivy attach report.png --caption "Before/after" # show a file in the chat
291
- bivy suggest "Add a dark theme to settings" # a next task you start in one tap
292
291
  bivy context --json # session, workspace, machine, apps
293
292
  ```
294
293
 
@@ -527,6 +526,8 @@ use, modify, and self-host it under that license. If users interact with your
527
526
  modified version over a network, section 13 requires you to offer its
528
527
  corresponding source. See [LICENSE](LICENSE).
529
528
 
530
- Bivy Cloud is the hosted operation of that stack plus billing and plans, in a
531
- separate private repository. Contributions use the
532
- [DCO](CONTRIBUTING.md#certificate-of-origin); there is no CLA.
529
+ Bivy Cloud is the hosted operation of that stack. Its plans, billing and
530
+ managed-compute operations live in a separate private service behind Core's
531
+ deployment extension. Contributions are made under the
532
+ [Contributor License Agreement](CLA.md). The Bivy name and logo are covered by
533
+ the [trademark policy](TRADEMARKS.md).
package/bin/bivy.mjs CHANGED
@@ -2595,38 +2595,6 @@ async function cmdContext(args = []) {
2595
2595
  console.log(`\nEverything else: ${c.cyan("bivy help")} (or ${c.cyan("bivy help --json")}).`);
2596
2596
  }
2597
2597
 
2598
- // `bivy suggest "<task>" [--title "…"] [--run here|subagents|new] [--session <id>]`
2599
- // — propose a task the user can start in one tap, in this session or beside it. For an agent
2600
- // offering next steps: write the task as a complete instruction.
2601
- async function cmdSuggest(args = []) {
2602
- const usage = 'Usage: bivy suggest "<task>" [--title "short label"] [--run here|subagents|new] [--session <id>] [--json]';
2603
- if (args.includes("-h") || args.includes("--help")) {
2604
- console.log(`${usage}\n\nPost a task the user can start in one tap: in this session, through your sub-agents, or in a parallel session that works in its own copy of the project. Write it as a complete instruction, with paths relative to the project root.\n\n--run says where you recommend running it, which becomes the card's main button: here (builds on this conversation), subagents (independent tasks you can split across your own sub-agents; only if you have them), or new (bigger independent work the user will want to follow in its own session). Without it, one card recommends here and several recommend new.\n\n--json prints {"ok","id"}.`);
2605
- return;
2606
- }
2607
- const json = wantsJson(args);
2608
- const fail = (error) => cliError(error, { json, paint: c.red });
2609
- const flagsWithValue = new Set(["--session", "--title", "--run"]);
2610
- const flag = (name) => {
2611
- const i = args.indexOf(name);
2612
- return i >= 0 && i + 1 < args.length ? args[i + 1] : undefined;
2613
- };
2614
- const text = args.filter((a, i) => !a.startsWith("-") && !(i > 0 && flagsWithValue.has(args[i - 1]))).join(" ").trim();
2615
- const sessionId = resolveAttachSessionId({ sessionFlag: flag("--session"), env: process.env });
2616
- if (!text) return fail({ code: "usage", message: usage, exit: EXIT.usage });
2617
- const run = flag("--run");
2618
- if (run !== undefined && !["here", "subagents", "new"].includes(run)) return fail({ code: "usage", message: `--run is here, subagents or new (got "${run}").`, exit: EXIT.usage });
2619
- if (!sessionId) return fail({ code: "no_session", message: "No session id.", hint: "Run inside an agent session ($BIVY_SESSION_ID) or pass --session <id>.", next: "bivy sessions --json", exit: EXIT.usage });
2620
- const config = loadConfig();
2621
- if (!(await ensureNodeRunning(config))) return fail({ code: "node_unreachable", message: `Could not reach the Bivy node at ${url(config)}.`, next: "bivy status", exit: EXIT.unavailable });
2622
- const res = await sessionPost(config, sessionId, "suggest", { text, title: flag("--title"), run }).catch((error) => error);
2623
- if (res instanceof Error) return fail({ code: "node_unreachable", message: `Could not reach the Bivy node: ${res.message}`, next: "bivy status", exit: EXIT.unavailable });
2624
- const body = await res.json().catch(() => ({}));
2625
- if (!res.ok) return fail(sessionHttpError("Suggest", res.status, body));
2626
- if (json) { console.log(JSON.stringify(body)); return; }
2627
- console.log(c.green("Suggested in the chat. The user can start it in one tap."));
2628
- }
2629
-
2630
2598
  // `bivy title "<title>" [--session <id>]` — rename the session the agent runs in.
2631
2599
  async function cmdTitle(args = []) {
2632
2600
  const usage = 'Usage: bivy title "<title>" [--session <id>] [--json]';
@@ -2674,45 +2642,37 @@ function durationSeconds(text) {
2674
2642
  return m ? Number(m[1]) * (m[2] === "h" ? 3600 : m[2] === "m" ? 60 : 1) : NaN;
2675
2643
  }
2676
2644
 
2677
- // `bivy notify "<message>" [--urgent]` — reach the user: a card in the chat,
2678
- // and a push naming the session (never the text) when nobody has the app open.
2645
+ // `bivy notify [--urgent]` — bring the user back: a push naming the session
2646
+ // (never any text) when nobody has the app open. Nothing is posted in the chat.
2679
2647
  async function cmdNotify(args = []) {
2680
- const usage = 'Usage: bivy notify "<message>" [--urgent] [--session <id>] [--json]';
2648
+ const usage = "Usage: bivy notify [--urgent] [--session <id>] [--json]";
2681
2649
  if (args.includes("-h") || args.includes("--help")) {
2682
2650
  console.log(`${usage}
2683
2651
 
2684
- Send the user a message: it appears as a card in the chat, and their devices get
2685
- a push notification (naming this session, not the text) when nobody has the app
2686
- open. --urgent pushes even while they do. At most one push a minute per session;
2687
- later messages still reach the chat. Use it when you finish long work, get
2688
- blocked, or need the user to look at something. --json prints
2689
- {"ok","id","push":"sent"|"user_watching"|"rate_limited","userWatching"}.`);
2652
+ Push to the user's devices so they come back to this session. The push names the
2653
+ session, nothing else, and nothing is posted in the chat: say what you need in
2654
+ your reply. It is sent only when nobody has the app open; --urgent pushes even
2655
+ while they do. At most one push a minute per session. You don't need it when you
2656
+ finish: Bivy already pushes when a turn ends while the user is away. --json prints
2657
+ {"ok","push":"sent"|"user_watching"|"rate_limited"|"unavailable","userWatching"}.`);
2690
2658
  return;
2691
2659
  }
2692
2660
  const json = wantsJson(args);
2693
2661
  const fail = (error) => cliError(error, { json, paint: c.red });
2694
2662
  const parsed = parseSessionArgs(args, ["--session"]);
2695
2663
  if (parsed.error) return fail({ code: "usage", message: parsed.error, exit: EXIT.usage });
2696
- const text = parsed.positional.join(" ").trim();
2697
- if (!text) return fail({ code: "usage", message: usage, exit: EXIT.usage });
2698
2664
  const sessionId = resolveAttachSessionId({ sessionFlag: parsed.one("--session"), env: process.env });
2699
2665
  if (!sessionId) return fail({ code: "no_session", message: "No session id.", hint: "Run inside an agent session ($BIVY_SESSION_ID) or pass --session <id>.", next: "bivy sessions --json", exit: EXIT.usage });
2700
2666
  const config = loadConfig();
2701
2667
  if (!(await ensureNodeRunning(config))) return fail({ code: "node_unreachable", message: `Could not reach the Bivy node at ${url(config)}.`, next: "bivy status", exit: EXIT.unavailable });
2702
- const res = await sessionPost(config, sessionId, "notify", { text, urgent: parsed.has("--urgent") }).catch((error) => error);
2668
+ const res = await sessionPost(config, sessionId, "notify", { urgent: parsed.has("--urgent") }).catch((error) => error);
2703
2669
  if (res instanceof Error) return fail({ code: "node_unreachable", message: `Could not reach the Bivy node: ${res.message}`, next: "bivy status", exit: EXIT.unavailable });
2704
2670
  const body = await res.json().catch(() => ({}));
2705
2671
  if (!res.ok) return fail(sessionHttpError("Notify", res.status, body));
2706
2672
  if (json) { console.log(JSON.stringify(body)); return; }
2707
- const noPush = { user_watching: "the user has the app open; add --urgent to push anyway", rate_limited: "this session pushed less than a minute ago", unavailable: "this machine isn't signed in to a Bivy account" }[body.push];
2708
- // A terminal run (`bivy run`) has no chat to hold a card: the push is the notice.
2709
- if (body.posted === false) {
2710
- if (body.push === "sent") console.log(c.green("Pushed to the user's devices."));
2711
- else console.log(c.yellow(`Not delivered: a terminal run has no chat to post in, and no push was sent (${noPush ?? body.push}).`));
2712
- return;
2713
- }
2714
- const pushed = body.push === "sent" ? "and pushed to the user's devices" : noPush ? `(no push: ${noPush})` : "";
2715
- console.log(c.green(`Posted in the chat ${pushed}.`.replace(" .", ".")));
2673
+ if (body.push === "sent") { console.log(c.green("Pushed to the user's devices.")); return; }
2674
+ const noPush = { user_watching: "the user has the app open, so they see your reply; add --urgent to push anyway", rate_limited: "this session pushed less than a minute ago", unavailable: "this machine isn't signed in to a Bivy account" }[body.push];
2675
+ console.log(c.yellow(`No push sent: ${noPush ?? body.push}.`));
2716
2676
  }
2717
2677
 
2718
2678
  // `bivy ask "<question>" [--option …]` — the question card, for any agent.
@@ -6515,9 +6475,6 @@ An agent's own --help passes through, e.g. 'bivy run claude --help'.`);
6515
6475
  case "attach":
6516
6476
  await cmdAttach(args);
6517
6477
  break;
6518
- case "suggest":
6519
- await cmdSuggest(args);
6520
- break;
6521
6478
  case "title":
6522
6479
  await cmdTitle(args);
6523
6480
  break;
@@ -44,14 +44,12 @@ export const COMMANDS = [
44
44
  { name: "context", group: "session", scope: "session", json: true, usage: "context [--json]", summary: "Where this agent is running: session, workspace, machine, apps, and what to run next",
45
45
  tools: [{ name: "bivy_context", description: "Where you are running inside Bivy: your session, agent, workspace and git branch, the machine, apps you have published, whether the user has the app open, and the commands that reach them.", input: {}, argv: ["context"] }] },
46
46
  { name: "attach", group: "session", scope: "session", json: true, usage: 'attach <file> [--caption "…"] [--artifact]', summary: "Show a local file or image to the user in the chat" },
47
- { name: "notify", group: "session", scope: "session", json: true, usage: 'notify "<message>" [--urgent]', summary: "Message the user: a card in the chat, and a push when they're away",
48
- tools: [{ name: "notify_user", description: "Send the user a message: a card in the chat, and a push to their phone when nobody has the app open (the push names the session, not the text). Use it when long work finishes, you are blocked, or something needs their attention. At most one push a minute per session.", input: { message: { type: "string", description: "What to tell the user.", required: true }, urgent: { type: "boolean", description: "Push even while the user has the app open." } }, argv: ["notify", { arg: "message" }, { when: "urgent", flag: "--urgent" }] }] },
47
+ { name: "notify", group: "session", scope: "session", json: true, usage: "notify [--urgent]", summary: "Push to the user's phone so they come back (nothing posted in the chat)",
48
+ tools: [{ name: "notify_user", description: "Push to the user's phone so they come back to this session, e.g. when you are blocked on them while still working. The push names the session only and nothing is posted in the chat, so say what you need in your reply. Not needed when you finish: Bivy already pushes when a turn ends while they are away. Sent only when nobody has the app open, at most one a minute per session.", input: { urgent: { type: "boolean", description: "Push even while the user has the app open." } }, argv: ["notify", { when: "urgent", flag: "--urgent" }] }] },
49
49
  { name: "ask", group: "session", scope: "session", json: true, usage: 'ask "<question>" [--option A --option B] [--async]', summary: "Ask the user a question and wait for the answer", subcommands: ["status", "wait"],
50
50
  tools: [{ name: "ask_user", description: "Ask the user a question in the chat and wait for their answer (their phone gets a push). Offer 2-8 options, or none for a free-text answer; they can always write their own. Returns {status, answer}: status is answered, dismissed (decide yourself) or expired.", input: { question: { type: "string", description: "The question.", required: true }, options: { type: "array", items: { type: "string" }, description: "Choices to pick from (2-8)." }, multiple: { type: "boolean", description: "Allow several choices." }, header: { type: "string", description: "A short label for the card." }, timeout_seconds: { type: "number", description: "How long to wait (default 600)." } }, argv: ["ask", { arg: "question" }, { flag: "--option", from: "options" }, { when: "multiple", flag: "--multi" }, { flag: "--header", from: "header" }, { flag: "--timeout", from: "timeout_seconds" }] }] },
51
51
  { name: "title", group: "session", scope: "session", json: true, usage: 'title "<title>"', summary: "Rename this session",
52
52
  tools: [{ name: "set_session_title", description: "Rename this session in the user's session list. Use a short title (under 60 characters) that says what the work is, when the first message made a poor title or the work changed direction.", input: { title: { type: "string", description: "The new title.", required: true } }, argv: ["title", { arg: "title" }] }] },
53
- { name: "suggest", group: "session", scope: "session", json: true, usage: 'suggest "<task>" [--title "label"] [--run here|subagents|new]', summary: "Propose a task the user can start in one tap",
54
- tools: [{ name: "suggest_task", description: "Propose a next step the user can start in one tap: in this session, through your sub-agents, or in a parallel session with its own copy of the project. Write the task as a complete instruction with paths relative to the project root. Post one per idea instead of listing them.", input: { task: { type: "string", description: "The complete instruction.", required: true }, title: { type: "string", description: "A short label for the card." }, run: { type: "string", enum: ["here", "subagents", "new"], description: "Where you recommend running it: here (builds on this conversation), subagents (independent tasks you can split across your own sub-agents; only if you have them), or new (bigger independent work in its own session). Defaults: here for one card, new for several." } }, argv: ["suggest", { arg: "task" }, { flag: "--title", from: "title" }, { flag: "--run", from: "run" }] }] },
55
53
  { name: "app", group: "session", scope: "session", json: true, usage: "app <publish|shot|present|share|run|…>", summary: "Live previews: publish a web/terminal/desktop app, screenshot it, present it", subcommands: ["publish", "list", "remove", "shot", "present", "share", "notes", "run", "click", "type", "key", "scroll", "drag", "move", "menu"],
56
54
  tools: [
57
55
  { name: "app_publish", description: "Give the user a live preview of something with a UI, from an app manifest file (web server port, static build, terminal, or desktop app). Run `bivy app --help` for the manifest format.", input: { manifest_path: { type: "string", description: "Path to the manifest JSON.", required: true } }, argv: ["app", "publish", { arg: "manifest_path" }] },
@@ -1,18 +1,24 @@
1
1
  # Talk to the user
2
2
 
3
- Summary: When to answer in chat, when to notify, when to ask and wait, and how to offer next steps.
3
+ Summary: When to answer in chat, when to notify, and when to ask and wait.
4
4
 
5
5
  Your chat reply is the default. Reach for these when the chat alone won't do.
6
6
 
7
7
  ## They may be away: notify
8
8
 
9
- bivy notify "Migration finished: 3 tables rewritten, all tests pass."
10
- bivy notify --urgent "Production is serving the old build."
9
+ bivy notify
10
+ bivy notify --urgent
11
11
 
12
- Posts a card in the chat. When nobody has the app open, their devices get a push
13
- naming this session (the text stays in the chat). `--urgent` pushes even while
14
- they are looking. One push a minute per session; don't send one per step.
15
- Good moments: long work finished, you are blocked, something needs a look.
12
+ Pushes to their devices so they come back to this session. The push names the
13
+ session only and nothing is posted in the chat, so say what you need in your
14
+ reply. It goes out only when nobody has the app open; `--urgent` pushes even
15
+ while they are looking. One push a minute per session.
16
+
17
+ You don't need it when you finish: Bivy already pushes when a turn ends while
18
+ they are away. Use it while you keep working and need them, for example when a
19
+ long run is blocked on something only they can do.
20
+
21
+ Next steps and options go in your reply as plain text.
16
22
 
17
23
  ## You need a decision: ask
18
24
 
@@ -24,23 +30,6 @@ dismissed it (use your judgment), exit 5 that `--timeout` (default 10m) passed.
24
30
  For long waits use `--async`, keep working, and later `bivy ask wait <id>`.
25
31
  Ask only when you can't reasonably decide yourself.
26
32
 
27
- ## Offer next steps: suggest
28
-
29
- bivy suggest "Add a GET /version endpoint that returns the package version." --title "Add /version"
30
-
31
- Each suggestion is a card the user can start in one tap: here, through your
32
- sub-agents, or in a parallel session with its own copy of the project. Write it as
33
- a complete instruction with paths relative to the project root. Post one per idea
34
- instead of a bulleted list.
35
-
36
- `--run` picks the card's main button; the others stay one tap away:
37
-
38
- | `--run` | When |
39
- |---|---|
40
- | `here` | It builds on this conversation, or it's small. Default for a single card. |
41
- | `subagents` | Independent tasks you can split across your own sub-agents and report back on. Only if you have sub-agents. |
42
- | `new` | Bigger independent work the user will want to follow, review or merge on its own. Default for several cards. |
43
-
44
33
  ## Name the session: title
45
34
 
46
35
  bivy title "Fix login redirect loop"
@@ -39,18 +39,12 @@ export const BIVY_AGENT_NOTE = [
39
39
  "can check your work; `bivy app present` tells the user a visible change is ready to look at, and " +
40
40
  "`.bivy/scenarios/*.json` files let them open it in the states that matter (errors, empty, slow) with `--try`. " +
41
41
  "`bivy app --help` has the details.",
42
- "- Proposing tasks the user could hand you (next steps, ideas, options): post each with " +
43
- '`bivy suggest "<complete instruction>" [--title "short label"] [--run here|subagents|new]` instead of only ' +
44
- "listing them. The user can start each in one tap, in this session, through your sub-agents, or in a parallel " +
45
- "one that works in its own copy of the project, so write it to stand on its own, with paths relative to the " +
46
- "project root. `--run` is the one you recommend: here when it builds on this conversation, subagents for " +
47
- "independent tasks you can split and supervise (only if you have sub-agents), new for bigger work worth its " +
48
- "own session.",
49
42
  '- If this session\'s title (taken from the first message) doesn\'t say what the work is, or the work changes ' +
50
43
  'direction: `bivy title "<short title>"`.',
51
- "- When you finish long work, get blocked, or need the user to look at something: " +
52
- '`bivy notify "<message>"` (a chat card, plus a push to their phone when they are away). To ask and wait for ' +
53
- 'an answer: `bivy ask "<question>" [--option A --option B]` prints their answer (or use your own ask-the-user tool).',
44
+ "- Your chat reply is what the user reads; when you finish while they are away, Bivy pushes to their phone for you. " +
45
+ 'To bring them back while you keep working: `bivy notify` (a push only; say what you need in the chat). To ask ' +
46
+ 'and wait for an answer: `bivy ask "<question>" [--option A --option B]` prints their answer (or use your own ' +
47
+ "ask-the-user tool).",
54
48
  "- Only when the user asks for another agent or another of their machines to take part: " +
55
49
  '`bivy delegate "<self-contained task>" --agent <id> [--machine <name>] --wait` runs it there and prints ' +
56
50
  "its answer and any branch/PR; `--to codex,grok@<machine>` asks several to compare, `bivy delegate machines` " +
@@ -6,7 +6,7 @@ import path from "node:path";
6
6
  import { defineAgentIntegration } from "../definition.js";
7
7
  import { PI_CAPABILITIES } from "./capabilities.js";
8
8
  import { bridgeInstalled, enableBridgeResolution } from "../../agent-bridges.mjs";
9
- export const PI_TESTED_VERSION = "0.99.2";
9
+ export const PI_TESTED_VERSION = "1.0.4";
10
10
  export function piAgentDir() {
11
11
  return process.env.PI_CODING_AGENT_DIR?.trim() || path.join(os.homedir(), ".pi", "agent");
12
12
  }
@@ -485,6 +485,12 @@ input, textarea, select { font-family: inherit; font-size: 16px; }
485
485
  }
486
486
  .transcript-turn { display: flex; flex-direction: column; gap: var(--space-4); min-width: 0; }
487
487
  .transcript-standalone { display: flex; justify-content: center; min-width: 0; }
488
+ /* A centered flex row lets a child grow past the column on one long unbroken
489
+ token (a path in an error); cap every standalone item to the column. */
490
+ .transcript-standalone > * { min-width: 0; max-width: 100%; }
491
+ /* Errors span the column so their edges line up with the turn content. */
492
+ .transcript-error { flex: 1; display: flex; flex-direction: column; gap: var(--space-2); overflow-wrap: anywhere; }
493
+ .transcript-error .settings-disclosure-body { margin: 0; padding: 0; white-space: pre-wrap; font-size: var(--text-xs); color: var(--muted); }
488
494
  .turn-response-body { display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; }
489
495
  .jump-latest {
490
496
  position: absolute; left: 50%; bottom: 16px; transform: translateX(-50%);
@@ -655,20 +661,6 @@ input, textarea, select { font-family: inherit; font-size: 16px; }
655
661
  .entry-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); font-style: normal; }
656
662
  .msg.system .entry-actions { justify-content: center; }
657
663
  .msg.thinking { align-self: flex-start; color: var(--muted); font-size: 13px; border-left: 2px solid var(--line-strong); padding-left: 10px; }
658
- .msg.error {
659
- align-self: stretch; display: flex; gap: 8px; align-items: flex-start;
660
- background: var(--danger-soft); border: 1px solid color-mix(in srgb, var(--danger) 45%, transparent);
661
- border-left: 3px solid var(--danger); border-radius: var(--radius-sm); padding: 10px 12px;
662
- }
663
- .msg-error-icon {
664
- flex: none; width: 18px; height: 18px; margin-top: 1px; border-radius: var(--radius-full);
665
- background: var(--danger); color: var(--danger-contrast); font-weight: 700; font-size: 12px;
666
- display: grid; place-items: center;
667
- }
668
- .msg-error-text {
669
- flex: 1; min-width: 0; font-size: 13px; line-height: 1.45; color: var(--ink);
670
- white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word;
671
- }
672
664
  .msg.streaming { position: relative; }
673
665
  .msg.streaming::after { content: "▍"; color: var(--accent); animation: blink 1s steps(2) infinite; }
674
666
  @keyframes blink { 50% { opacity: 0; } }
@@ -1477,10 +1469,8 @@ button.sheet-action:last-child { border-bottom: 0; }
1477
1469
  }
1478
1470
  /* Password-manager-style provider credential vault. Saved items are the primary
1479
1471
  list; the provider catalog appears only inside Add. */
1480
- /* Wide settings panels that read as a shallow "vault": the credential vault and
1481
- the isolated-machine-profiles panel share this container + header layout. */
1482
- .credential-vault, .machine-profiles { gap: 14px; max-width: 680px; width: 100%; }
1483
- .credential-vault { overflow-wrap: anywhere; }
1472
+ /* The credential vault: a wide settings panel that reads as a shallow "vault". */
1473
+ .credential-vault { gap: 14px; max-width: 680px; width: 100%; overflow-wrap: anywhere; }
1484
1474
  .vault-title-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; }
1485
1475
  .vault-title-row > div { min-width: 0; }
1486
1476
  /* The header action (e.g. "+ Add profile") never shrinks or wraps — the title
@@ -2617,15 +2607,6 @@ a.queue-item:hover { border-color: var(--line-strong); background: var(--surface
2617
2607
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
2618
2608
  }
2619
2609
  .connect-option:hover { border-color: var(--line-strong); box-shadow: var(--shadow-md); }
2620
- .connect-option-head { display: flex; align-items: flex-start; gap: 12px; }
2621
- .connect-option-badge {
2622
- flex: none; width: 36px; height: 36px; display: grid; place-items: center;
2623
- border-radius: var(--radius-md); color: var(--accent);
2624
- background: color-mix(in srgb, var(--accent) 12%, transparent);
2625
- }
2626
- .connect-option-copy { min-width: 0; }
2627
- .connect-option-copy h3 { margin: 1px 0 3px; font-size: 15px; }
2628
- .connect-option-copy p { margin: 0; color: var(--muted); font-size: 13px; line-height: 1.45; }
2629
2610
 
2630
2611
  .connect-command {
2631
2612
  display: flex; align-items: center; gap: 8px; padding: 4px 4px 4px 12px;
@@ -2645,9 +2626,8 @@ a.queue-item:hover { border-color: var(--line-strong); background: var(--surface
2645
2626
  .connect-option-links { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
2646
2627
  .connect-option-link { align-self: flex-start; color: var(--accent); font-size: 12.5px; font-weight: 600; text-decoration: none; }
2647
2628
  .connect-option-link:hover { text-decoration: underline; }
2648
- .connect-option-cta { align-self: flex-start; }
2649
2629
  /* A one-time "next step" card above the composer (automations, notifications). */
2650
- .next-step { margin: 0 var(--space-4) var(--space-3); display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); }
2630
+ .next-step { box-sizing: border-box; width: min(100% - 2 * var(--space-3), 800px - 2 * var(--space-3)); margin: 0 auto var(--space-3); display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); }
2651
2631
  .next-step p { margin: var(--space-1) 0 0; color: var(--muted); font-size: var(--text-sm); }
2652
2632
  .next-step .banner { margin-top: var(--space-2); }
2653
2633
  .next-step .next-step-ok { margin: 0; color: var(--ok); font-weight: var(--weight-medium); }
@@ -3000,7 +2980,7 @@ textarea.picker-search { resize: vertical; font-family: ui-monospace, monospace;
3000
2980
  .node-switcher-btn.locked { cursor: default; }
3001
2981
  .node-switcher-name { max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
3002
2982
  .node-switcher-caret { color: var(--muted); font-size: 9px; }
3003
- .node-menu { position: absolute; left: 0; top: calc(100% + 6px); min-width: 220px; z-index: var(--z-dropdown); }
2983
+ .node-menu { position: absolute; left: 0; top: calc(100% + 6px); min-width: 220px; width: max-content; max-width: calc(100vw - var(--space-6)); z-index: var(--z-dropdown); }
3004
2984
  /* "Reconnecting…" line at the top of the open switcher, under the node the
3005
2985
  button shows — the dropdown's spelled-out counterpart to the spinner dot. */
3006
2986
  .node-menu-status { display: flex; align-items: center; gap: 8px; padding: 10px 14px; font-size: 13px; color: var(--muted); border-bottom: 1px solid var(--line); }
@@ -3015,6 +2995,8 @@ textarea.picker-search { resize: vertical; font-family: ui-monospace, monospace;
3015
2995
  .node-menu-item.active { font-weight: 600; }
3016
2996
  .node-menu-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
3017
2997
  .node-menu-check { color: var(--accent); }
2998
+ /* Quiet trailing hint on a cloud row, e.g. "starts when you send". */
2999
+ .node-menu-meta { flex: none; color: var(--muted); font-size: var(--text-xs); }
3018
3000
  .node-menu-glyph { color: var(--accent); flex: none; }
3019
3001
  .eph-steps { margin: 4px 0; padding-left: 18px; color: var(--muted); font-size: 13px; display: grid; gap: 4px; }
3020
3002
  .code-snippet { margin: 6px 0 0; overflow: auto; border: 1px solid var(--line); border-radius: var(--radius-md); padding: 10px; background: var(--surface); font-size: 12px; }
@@ -3247,24 +3229,9 @@ p.setup-note .btn.link { min-height: 44px; margin-block: calc((1lh - 44px) / 2);
3247
3229
  .app-row-meta { color: var(--muted); font-size: var(--text-xs); line-height: var(--lh-xs); }
3248
3230
  .app-row-action { flex: none; display: flex; align-items: center; gap: var(--space-1); }
3249
3231
  .app-message { margin-block: var(--space-2); }
3250
- /* Suggested task (SuggestionCard.tsx): a task the agent proposed, one tap to
3251
- start in its own session. Shell from .card; layout only here. */
3252
- .suggestion-card { max-inline-size: 440px; display: flex; flex-direction: column; gap: var(--space-1); }
3253
3232
  /* The small label above a chat card's content, shared by every agent-posted card. */
3254
- .suggestion-eyebrow, .delegation-eyebrow, .notice-eyebrow { margin: 0; color: var(--muted); font-size: var(--text-xs); font-weight: var(--weight-semibold); }
3255
- .suggestion-title { margin: 0; font-size: var(--text-base); font-weight: var(--weight-semibold); overflow-wrap: anywhere; }
3256
- .suggestion-text { margin: 0; color: var(--muted); font-size: var(--text-sm); white-space: pre-wrap; overflow-wrap: anywhere; display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden; }
3257
- .suggestion-text[data-expanded="true"] { display: block; overflow: visible; }
3258
- .suggestion-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-2); }
3259
- .suggestion-bar, .suggestion-all { display: flex; flex-direction: column; gap: var(--space-1); margin-top: var(--space-2); }
3260
- .suggestion-all { padding-top: var(--space-2); border-top: 1px solid var(--line); }
3261
- .suggestion-bar .suggestion-actions, .suggestion-all .suggestion-actions { margin-top: 0; }
3262
- .suggestion-hint { margin: 0; color: var(--muted); font-size: var(--text-xs); }
3263
- /* In a set, the title is the checkbox's label, so the whole line is the tap target. */
3264
- .suggestion-pick { display: flex; align-items: flex-start; gap: var(--space-2); cursor: pointer; }
3265
- .suggestion-pick input { flex: none; inline-size: 18px; block-size: 18px; margin: 2px 0 0; accent-color: var(--accent); }
3266
- .suggestion-status { margin: var(--space-2) 0 0; display: flex; align-items: center; gap: var(--space-2); color: var(--ok); font-size: var(--text-sm); font-weight: var(--weight-medium); }
3267
- /* Notice (NoticeCard.tsx): a message the agent sent with `bivy notify`.
3233
+ .delegation-eyebrow, .notice-eyebrow { margin: 0; color: var(--muted); font-size: var(--text-xs); font-weight: var(--weight-semibold); }
3234
+ /* Notice (NoticeCard.tsx): a message from Bivy, e.g. applied automations.
3268
3235
  Shell and tone from .card, the urgent marker from .badge. */
3269
3236
  .notice-card { max-inline-size: 440px; display: flex; flex-direction: column; gap: var(--space-1); }
3270
3237
  .notice-eyebrow { display: flex; align-items: center; gap: var(--space-2); }
@@ -5,7 +5,7 @@ export const CERTIFICATION_MATRIX = {
5
5
  agents: [
6
6
  { id: "claude-code-sdk", status: "active", executionMode: "protocol", pinnedVersion: "0.3.286", capabilities: ["toolInterception", "modelSelection", "resume"] },
7
7
  { id: "codex-approvals", status: "active", executionMode: "protocol", pinnedVersion: "0.159.2", capabilities: ["toolInterception", "modelSelection", "resume"] },
8
- { id: "pi", status: "active", executionMode: "protocol", pinnedVersion: "0.99.2", capabilities: ["toolInterception", "modelSelection", "resume"] },
8
+ { id: "pi", status: "active", executionMode: "protocol", pinnedVersion: "1.0.4", capabilities: ["toolInterception", "modelSelection", "resume"] },
9
9
  { id: "opencode", status: "active", executionMode: "protocol", pinnedVersion: "1.18.33", capabilities: ["toolInterception", "modelSelection", "resume"] },
10
10
  { id: "grok", status: "active", executionMode: "protocol", pinnedVersion: "1.0.44", capabilities: ["toolInterception", "modelSelection", "resume"] },
11
11
  ]
@@ -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) {
package/dist/server.js CHANGED
@@ -80,9 +80,7 @@ import { exportProviderAuth, exportAccountApiKeys, exportAccountOAuthCredentials
80
80
  import { listProviders } from "./runtime/provider-catalog.js";
81
81
  import { exportLocalModels, importLocalModels } from "./runtime/local-model-store.js";
82
82
  import { sessionLikeFields } from "./session/start-like.js";
83
- import { MAX_SUGGESTION_TEXT, MAX_SUGGESTION_TITLE, SUGGESTION_RUNS, isTaskSuggestion } from "./session/suggestions.js";
84
83
  import { BIVY_AGENT_NOTE, mergeSyncedAgentInstructions, readAgentInstructions, sessionInstructions, writeAgentInstructions, MAX_AGENT_INSTRUCTIONS_BYTES } from "./agent-instructions.js";
85
- import { execEphemeralRequest } from "./ephemeral-exec.js";
86
84
  import { ApprovalManager } from "./approval.js";
87
85
  import { QuestionManager, validQuestions, isAskUserQuestionTool, formatQuestionResult } from "./question.js";
88
86
  import { NodeIdentity } from "./identity.js";
@@ -143,7 +141,7 @@ import { AgentQuestions, askQuestionsFrom } from "./session/agent-questions.js";
143
141
  import { AutomationProposals, describeChange } from "./session/automation-proposals.js";
144
142
  import { createSessionTokenCodec, isSessionToken, sessionTokenAllows } from "./session/session-tokens.js";
145
143
  import { setSessionTokenSigner } from "./runtime/session-env.js";
146
- import { MAX_NOTICE_TEXT, isAgentNotice, noticePush } from "./session/notices.js";
144
+ import { MAX_NOTICE_TEXT, noticePush } from "./session/notices.js";
147
145
  import { createForkCommands } from "./controllers/fork-commands.js";
148
146
  import { createGithubCommands } from "./controllers/github-commands.js";
149
147
  import { createCredentialCommands } from "./controllers/credential-commands.js";
@@ -3188,22 +3186,6 @@ const RELAY_COMMANDS = {
3188
3186
  return ctx.reply({ type: "session.promote.result", requestId, ok: false, error: "Promotion lost the epoch race" });
3189
3187
  ctx.reply({ type: "session.promote.result", requestId, ok: true, sessionId, epoch });
3190
3188
  },
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
3189
  // Subscription / OAuth login driven from a remote device. The node runs the
3208
3190
  // provider's device-code (or paste-back) flow and reports the verification
3209
3191
  // link + code over the relay; the phone never needs the node's localhost.
@@ -7643,6 +7625,7 @@ function evaluateEphemeralTeardown() {
7643
7625
  }
7644
7626
  await performSelfTeardown({
7645
7627
  provider: ephemeralTeardownCfg.provider,
7628
+ sleep: ephemeralTeardownCfg.sleep,
7646
7629
  signalSettled: signalSettledToControlPlane,
7647
7630
  shutdown: () => { try {
7648
7631
  spawnSync("shutdown", ["-h", "now"], { stdio: "ignore" });
@@ -7657,7 +7640,7 @@ if (ephemeralTeardownCfg.enabled) {
7657
7640
  const ephemeralEvalMs = Math.max(2_000, Math.min(ephemeralTeardownCfg.finishGraceMs, 15_000));
7658
7641
  const ephemeralTeardownTimer = setInterval(() => evaluateEphemeralTeardown(), ephemeralEvalMs);
7659
7642
  ephemeralTeardownTimer.unref?.();
7660
- console.log(`[ephemeral-teardown] armed: provider=${ephemeralTeardownCfg.provider} onFinish=${ephemeralTeardownCfg.onFinish} ttl=${ephemeralTeardownCfg.ttlMin}m`);
7643
+ console.log(`[ephemeral-teardown] armed: provider=${ephemeralTeardownCfg.provider} onFinish=${ephemeralTeardownCfg.onFinish} sleep=${ephemeralTeardownCfg.sleep} ttl=${ephemeralTeardownCfg.ttlMin}m`);
7661
7644
  }
7662
7645
  setTimeout(() => void sweepDiskGuardrails(), 30_000).unref?.();
7663
7646
  // One sweep shortly after boot clears ghosts left by a previous run before any
@@ -11741,10 +11724,9 @@ app.get("/api/session/:id/context", (req, res) => {
11741
11724
  previewAvailable,
11742
11725
  }));
11743
11726
  });
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.
11727
+ // `bivy notify`: a push naming the session (never any text) when nobody has the
11728
+ // app open, or when it's urgent. At most one push a minute per session. It adds
11729
+ // nothing to the chat: what the agent has to say belongs in its reply there.
11748
11730
  const lastNotifyPush = new Map();
11749
11731
  app.post("/api/session/:id/notify", (req, res) => {
11750
11732
  const record = openSessions.get(String(req.params.id));
@@ -11752,16 +11734,9 @@ app.post("/api/session/:id/notify", (req, res) => {
11752
11734
  const sessionId = record?.id ?? run?.sessionId;
11753
11735
  if (!sessionId)
11754
11736
  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
- }
11737
+ const urgent = req.body?.urgent === true;
11763
11738
  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) });
11739
+ const push = noticePush({ urgent, userWatching: watching, lastPushAt: lastNotifyPush.get(sessionId), now: Date.now(), pushConfigured: Boolean(sessionAdvertiseTarget) });
11765
11740
  if (push === "sent") {
11766
11741
  lastNotifyPush.set(sessionId, Date.now());
11767
11742
  void sendNotificationHint({
@@ -11769,10 +11744,10 @@ app.post("/api/session/:id/notify", (req, res) => {
11769
11744
  sessionId,
11770
11745
  targetSessionId: sessionId,
11771
11746
  title: sessionNotifyLabel(record, run?.name || "A terminal session"),
11772
- body: "Has a message for you — tap to read it.",
11747
+ body: "Wants you to take a look — tap to open the session.",
11773
11748
  });
11774
11749
  }
11775
- res.json({ ok: true, id: notice.id, posted: Boolean(record), push, userWatching: watching });
11750
+ res.json({ ok: true, push, userWatching: watching });
11776
11751
  });
11777
11752
  // `bivy ask`: the question card and "needs your input" push, for any agent.
11778
11753
  // Returns at once with an id; `/wait` blocks up to 240s per call (under common
@@ -11830,23 +11805,6 @@ app.post("/api/session/:id/title", (req, res) => {
11830
11805
  sessionNamer.setSessionName(record, title);
11831
11806
  res.json({ ok: true, title });
11832
11807
  });
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
11808
  // Explicit child Run API for first-party/user-directed workflows. These routes
11851
11809
  // are intentionally not advertised to agents as tools; the service still
11852
11810
  // authorizes every status lookup against this parent Session's provenance.