@bivy/bivy 0.20.9-staging.9 → 0.21.0

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 (168) hide show
  1. package/README.md +1 -2
  2. package/dist/agent-instructions.js +14 -4
  3. package/dist/apps/remote-preview.js +7 -0
  4. package/dist/apps/service.js +6 -1
  5. package/dist/apps/styles.css +56 -70
  6. package/dist/automation-reseal.js +30 -0
  7. package/dist/config-cli.js +2 -0
  8. package/dist/controllers/credential-commands.js +3 -27
  9. package/dist/credentials/api.js +2 -43
  10. package/dist/credentials/document.js +6 -14
  11. package/dist/credentials/store.js +5 -6
  12. package/dist/device-registry.js +27 -0
  13. package/dist/harness/checkpoint.js +0 -0
  14. package/dist/node-config.js +4 -3
  15. package/dist/runtime/model-catalog.js +16 -0
  16. package/dist/runtime/tool-call-map.js +1 -1
  17. package/dist/runtime/tool-trace.js +1 -1
  18. package/dist/server.js +136 -244
  19. package/dist/session/event-log.js +136 -29
  20. package/dist/session/inline-image-fetch.js +24 -33
  21. package/dist/session/message-components.js +429 -0
  22. package/dist/session/run-terminal.js +9 -7
  23. package/dist/session/transcript-persistence.js +135 -10
  24. package/dist/work-instructions.js +50 -0
  25. package/package.json +2 -2
  26. package/web/assets/AutomationsView-HnUfeti_.js +51 -0
  27. package/web/assets/ChangesCard-Dnkguen5.js +4 -0
  28. package/web/assets/LibraryView-CSKKV8TI.js +2 -0
  29. package/web/assets/{ProviderConnect-BPwnaFGk.js → ProviderConnect-DNgxGmP9.js} +1 -1
  30. package/web/assets/SessionList-CYU9IGnd.js +1 -0
  31. package/web/assets/Settings-HQZtxiMV.js +10 -0
  32. package/web/assets/{Sheet-DOOJosQY.js → Sheet-DoPtOHBC.js} +1 -1
  33. package/web/assets/{StatusDot-dpbztK53.js → Spinner-CPdRfDcT.js} +1 -1
  34. package/web/assets/{Terminal-DVGXS6Yd.js → Terminal-DY6UB5U-.js} +1 -1
  35. package/web/assets/{VoiceSettings-DX-n0SFl.js → VoiceSettings-BoQ_Mhpm.js} +1 -1
  36. package/web/assets/{abnfDiagram-VCTEODGH-CnJ4astd.js → abnfDiagram-VCTEODGH-n-P11_L4.js} +1 -1
  37. package/web/assets/arc-BNLueWOY.js +1 -0
  38. package/web/assets/architecture-7GRP2DOG-DyG-wgaH.js +1 -0
  39. package/web/assets/{architectureDiagram-5GKGNRK7-B26-V_Xd.js → architectureDiagram-5GKGNRK7-C0b9IP5U.js} +1 -1
  40. package/web/assets/{blockDiagram-I7D4REHJ-DuoZkDHs.js → blockDiagram-I7D4REHJ-iVx1FQBD.js} +1 -1
  41. package/web/assets/{c4Diagram-7LVT6UL2-B81PZ-Vw.js → c4Diagram-7LVT6UL2-DIwEaAvJ.js} +1 -1
  42. package/web/assets/channel-HmCJmS4T.js +1 -0
  43. package/web/assets/{chunk-4HAMMTFA-DmSrqg9X.js → chunk-4HAMMTFA-DSsyukSz.js} +1 -1
  44. package/web/assets/chunk-75Z2AOVW-zwLYo-XL.js +2 -0
  45. package/web/assets/{chunk-DU6HZSFF-jMBIr1yK.js → chunk-DU6HZSFF-PGuLkqeP.js} +15 -15
  46. package/web/assets/chunk-F27PBJKO-CpkyeGC9.js +1 -0
  47. package/web/assets/{chunk-GMAD6QVW-C5YdBKWY.js → chunk-GMAD6QVW-CZSmK3Ms.js} +1 -1
  48. package/web/assets/{chunk-GVQU2GXP-DOOAmJLJ.js → chunk-GVQU2GXP-C2XMkY6v.js} +1 -1
  49. package/web/assets/{chunk-IMKFNOWR-CISXYXGE.js → chunk-IMKFNOWR-DAIVz3j5.js} +1 -1
  50. package/web/assets/{chunk-L3NEJ4N5-CvWtiEy-.js → chunk-L3NEJ4N5-C0geNTDK.js} +1 -1
  51. package/web/assets/{chunk-OSK3NFVY-yVAn5yky.js → chunk-OSK3NFVY-D-WN2fBr.js} +2 -2
  52. package/web/assets/{chunk-P2QGCYS3-D-VGoELb.js → chunk-P2QGCYS3-DYGhprKz.js} +1 -1
  53. package/web/assets/{chunk-POPQ4Y6H-0Mj4Bp-N.js → chunk-POPQ4Y6H-Dlsu2a2Q.js} +1 -1
  54. package/web/assets/{chunk-PWAF6VOD-Dvvdg6s1.js → chunk-PWAF6VOD-DK0QWYdc.js} +1 -1
  55. package/web/assets/{chunk-SHT3W25Y-DcllnbDq.js → chunk-SHT3W25Y-BpQHIdUi.js} +1 -1
  56. package/web/assets/{chunk-SVP7TREG-DnuOGeio.js → chunk-SVP7TREG-Cs5YWxpG.js} +1 -1
  57. package/web/assets/{chunk-TICWLB2K-Dr76NyiT.js → chunk-TICWLB2K-BRsWLPXA.js} +1 -1
  58. package/web/assets/chunk-XXDRQBXY-CjF14Z7v.js +1 -0
  59. package/web/assets/classDiagram-ZZMXUADV-BmdAoNdO.js +1 -0
  60. package/web/assets/classDiagram-v2-VYDZK3BY-BmdAoNdO.js +1 -0
  61. package/web/assets/colors-Cg5Vh3br.js +1 -0
  62. package/web/assets/{cose-bilkent-JH36ORCC-BIqBF2fY.js → cose-bilkent-JH36ORCC-CEUD3V3W.js} +1 -1
  63. package/web/assets/{cynefin-OW5HDTMX-DoAt_4E6.js → cynefin-OW5HDTMX-DoHWnu_-.js} +1 -1
  64. package/web/assets/cynefinDiagram-5FMLGOSQ-CgPeHmV5.js +62 -0
  65. package/web/assets/{dagre-GXQ25YYZ-DIAXxVMd.js → dagre-GXQ25YYZ-CrIlgwM7.js} +1 -1
  66. package/web/assets/{defaultLocale-BFoDCU3G.js → defaultLocale-CNPGWpMu.js} +1 -1
  67. package/web/assets/{diagram-S7CK7UJ4-DA78JGp6.js → diagram-S7CK7UJ4-BM_l51zV.js} +1 -1
  68. package/web/assets/diagram-UQ7AKVKN-CfkxqgPI.js +41 -0
  69. package/web/assets/{diagram-VSXAHHWV-DCkvR_pJ.js → diagram-VSXAHHWV-0y5vCI9g.js} +1 -1
  70. package/web/assets/diagram-VX7I27RA-BMkMy7jn.js +24 -0
  71. package/web/assets/diagram-Z3DM3KII--0fxfDqG.js +24 -0
  72. package/web/assets/dist-kYfyPZP9.js +1 -0
  73. package/web/assets/{ebnfDiagram-PWID7BFC-CSCD0PAi.js → ebnfDiagram-PWID7BFC-6tPNzbJl.js} +1 -1
  74. package/web/assets/embed-BXV41wg7.js +201 -0
  75. package/web/assets/{erDiagram-RLTQ6QDP-M512qtMu.js → erDiagram-RLTQ6QDP-Bjqm_o3R.js} +1 -1
  76. package/web/assets/eventmodeling-NTZA5JFV-CoCZxN1n.js +1 -0
  77. package/web/assets/flowDiagram-HODETNUW-CznfWzEq.js +1 -0
  78. package/web/assets/ganttDiagram-EL5Y4UJY-92X7KOOI.js +292 -0
  79. package/web/assets/{gitGraph-4MIJSDKK-CwjwFpZb.js → gitGraph-4MIJSDKK-B5F-u7Vq.js} +1 -1
  80. package/web/assets/gitGraphDiagram-WWUBYQGX-Ct2hVG0b.js +106 -0
  81. package/web/assets/index-BKSbod42.css +1 -0
  82. package/web/assets/{index-CNs5i7tU.js → index-CnjJQpgZ.js} +2 -2
  83. package/web/assets/{info-A6RAGUB7-D6nXu5V2.js → info-A6RAGUB7-D3xmYWyC.js} +1 -1
  84. package/web/assets/infoDiagram-27XIBGKW-lzqZ7xaA.js +2 -0
  85. package/web/assets/init-ZlXS9mIS.js +1 -0
  86. package/web/assets/{ishikawaDiagram-5VMMS53U-4bsiyG2x.js → ishikawaDiagram-5VMMS53U-CsVZHXR0.js} +1 -1
  87. package/web/assets/{journeyDiagram-3NMN7TZE-o3U3L2rt.js → journeyDiagram-3NMN7TZE-DN9AdyVy.js} +1 -1
  88. package/web/assets/{kanban-definition-UXKFOSKX-CQOPj0NZ.js → kanban-definition-UXKFOSKX-gvkBaOFe.js} +1 -1
  89. package/web/assets/linear-CeYXFQza.js +1 -0
  90. package/web/assets/{mermaid-parser.core-Ba2-DGvb.js → mermaid-parser.core-CPwOm7-a.js} +3 -3
  91. package/web/assets/{mermaid.core-BxdMVbm9.js → mermaid.core-F09Wr40d.js} +6 -6
  92. package/web/assets/{mindmap-definition-YA3MSWOX-1Eu4-zLM.js → mindmap-definition-YA3MSWOX-Qzc2J9Pm.js} +1 -1
  93. package/web/assets/mount-Bvl-yslT.js +36 -0
  94. package/web/assets/ordinal-BdBDtYTt.js +1 -0
  95. package/web/assets/{packet-AYTQ26CC-BTZ7CbVd.js → packet-AYTQ26CC-Unxh5mP2.js} +1 -1
  96. package/web/assets/{pegDiagram-XKGWAZYB-DNdK3axo.js → pegDiagram-XKGWAZYB-BIqcRp3m.js} +1 -1
  97. package/web/assets/{pie-WAS4IAKB-DidjfqgW.js → pie-WAS4IAKB-BipHZKbc.js} +1 -1
  98. package/web/assets/{pieDiagram-E7YTZNPT-Dme68TUA.js → pieDiagram-E7YTZNPT-gaTNuzPD.js} +2 -2
  99. package/web/assets/{quadrantDiagram-AXDQQJYC-z0xeSJSB.js → quadrantDiagram-AXDQQJYC-3BEmlbuE.js} +1 -1
  100. package/web/assets/{radar-RG4KPBEZ-BV50NO32.js → radar-RG4KPBEZ-Cm1_Q0XJ.js} +1 -1
  101. package/web/assets/{railroad-74A4TZTK-BOkd4wGS.js → railroad-74A4TZTK-CJ4K8tjT.js} +1 -1
  102. package/web/assets/railroad-abnf-HS5TGJTU-BVKI4cmM.js +1 -0
  103. package/web/assets/railroad-ebnf-LZEXJU2U-UMcDSelb.js +1 -0
  104. package/web/assets/railroad-peg-WCYAUIDC-C00zpjQU.js +1 -0
  105. package/web/assets/{railroadDiagram-O6MQD6OU-2eK1mPm_.js → railroadDiagram-O6MQD6OU-Nzyn7Xx6.js} +1 -1
  106. package/web/assets/range-EplFIxc7.js +1 -0
  107. package/web/assets/{requirementDiagram-BXWQKSXE-DvlVp433.js → requirementDiagram-BXWQKSXE-9zXuiai_.js} +1 -1
  108. package/web/assets/{sankeyDiagram-P5KCCOFB-C6CsEyCg.js → sankeyDiagram-P5KCCOFB-Cnw1rQun.js} +1 -1
  109. package/web/assets/{sequenceDiagram-WJ2MYXX4-1wwxxN6P.js → sequenceDiagram-WJ2MYXX4-BSLLRV-U.js} +3 -3
  110. package/web/assets/{settingsRoute-Dxrvnqih.js → settingsRoute-C2Up46o6.js} +1 -1
  111. package/web/assets/src-CFHfg5gg.js +1 -0
  112. package/web/assets/stateDiagram-D77RDMKH-D07b4cJB.js +1 -0
  113. package/web/assets/stateDiagram-v2-MP3YSRHH-DaG9Yzhk.js +1 -0
  114. package/web/assets/step-BDqHAzds.js +1 -0
  115. package/web/assets/{swimlanes-42K2YHIH-XLThGmTf.js → swimlanes-42K2YHIH-RpDl0CG9.js} +1 -1
  116. package/web/assets/swimlanesDiagram-VR7AAH4N-7003ldWX.js +8 -0
  117. package/web/assets/time-Dzw-YSCF.js +1 -0
  118. package/web/assets/{timeline-definition-24CTP7MA-BEOV0lDT.js → timeline-definition-24CTP7MA-BgQOP12r.js} +1 -1
  119. package/web/assets/{treeView-Q6P3EWNA-Dm4FCZVW.js → treeView-Q6P3EWNA-BiSBsrlo.js} +1 -1
  120. package/web/assets/treemap-BfjIY-m6.js +1 -0
  121. package/web/assets/{treemap-WGGIJYW6-DEZGmkE8.js → treemap-WGGIJYW6-CNdjKqUG.js} +1 -1
  122. package/web/assets/useStore-CxjHbFPA.js +16 -0
  123. package/web/assets/{vennDiagram-4TSXK5OY-CDRUEJ_c.js → vennDiagram-4TSXK5OY-DLzOr3Yv.js} +1 -1
  124. package/web/assets/{wardley-WFR3VGLG-CjZ5JazT.js → wardley-WFR3VGLG-LCC8FWVb.js} +1 -1
  125. package/web/assets/{wardleyDiagram-VM6X3IG4-COsxLnU6.js → wardleyDiagram-VM6X3IG4-WrwzM3YW.js} +1 -1
  126. package/web/assets/{xychartDiagram-S5SC5T6Z-BfwzNIP-.js → xychartDiagram-S5SC5T6Z-B_HuoAnl.js} +3 -3
  127. package/web/index.html +2 -2
  128. package/web/sw.js +1 -1
  129. package/dist/ephemeral-exec.js +0 -108
  130. package/dist/hosted-custody.js +0 -30
  131. package/web/assets/AutomationsView-Dk1aHviY.js +0 -51
  132. package/web/assets/ChangesCard-Bn37dRew.js +0 -4
  133. package/web/assets/LibraryView-CGkpNOgR.js +0 -2
  134. package/web/assets/Settings-DJjEVwmu.js +0 -10
  135. package/web/assets/arc-Pry-7G1_.js +0 -1
  136. package/web/assets/architecture-7GRP2DOG-kbFZ_7eU.js +0 -1
  137. package/web/assets/array-BifhSqXX.js +0 -1
  138. package/web/assets/channel-CpJ3OYta.js +0 -1
  139. package/web/assets/chunk-75Z2AOVW-CGlA2AB-.js +0 -2
  140. package/web/assets/chunk-F27PBJKO-Cto7X88Z.js +0 -1
  141. package/web/assets/chunk-XXDRQBXY-DqAYeljN.js +0 -1
  142. package/web/assets/classDiagram-ZZMXUADV-BSqiC-6r.js +0 -1
  143. package/web/assets/classDiagram-v2-VYDZK3BY-BSqiC-6r.js +0 -1
  144. package/web/assets/cynefinDiagram-5FMLGOSQ-B2jZZ_es.js +0 -62
  145. package/web/assets/diagram-UQ7AKVKN-D7BIW4QU.js +0 -41
  146. package/web/assets/diagram-VX7I27RA-qmkf9rFQ.js +0 -24
  147. package/web/assets/diagram-Z3DM3KII-BZf2h-T8.js +0 -24
  148. package/web/assets/dist-v5Q1xZ2K.js +0 -1
  149. package/web/assets/eventmodeling-NTZA5JFV-DHYpZdDY.js +0 -1
  150. package/web/assets/flowDiagram-HODETNUW-BdNS-1c7.js +0 -1
  151. package/web/assets/ganttDiagram-EL5Y4UJY-DGMQzJ8U.js +0 -292
  152. package/web/assets/gitGraphDiagram-WWUBYQGX-DNfbdhPa.js +0 -106
  153. package/web/assets/index-kWXUrhG8.css +0 -1
  154. package/web/assets/infoDiagram-27XIBGKW-QcpEFiaQ.js +0 -2
  155. package/web/assets/init-C-OQMol4.js +0 -1
  156. package/web/assets/line-Cq9nN3X2.js +0 -1
  157. package/web/assets/linear-SzVmGSw4.js +0 -1
  158. package/web/assets/mount-C9EzYXqA.js +0 -31
  159. package/web/assets/ordinal-BDEzSJ7C.js +0 -1
  160. package/web/assets/path-fybaL0A-.js +0 -1
  161. package/web/assets/railroad-abnf-HS5TGJTU-DiQLjR_J.js +0 -1
  162. package/web/assets/railroad-ebnf-LZEXJU2U-D4bSTzXb.js +0 -1
  163. package/web/assets/railroad-peg-WCYAUIDC-DDhl4AMh.js +0 -1
  164. package/web/assets/src-B6xuSHsQ.js +0 -1
  165. package/web/assets/stateDiagram-D77RDMKH-_fkXrLZe.js +0 -1
  166. package/web/assets/stateDiagram-v2-MP3YSRHH-Cuo-j5c1.js +0 -1
  167. package/web/assets/swimlanesDiagram-VR7AAH4N-DOpCt5KW.js +0 -8
  168. package/web/assets/useStore-C1E8iwAQ.js +0 -29
@@ -17,10 +17,12 @@
17
17
  // text). The base is a SNAPSHOT that is overwritten each turn and SHRINKS on
18
18
  // compaction, so it cannot be appended naively. Instead each snapshot is stored
19
19
  // as a bounded DELTA against the previous one: a `reset:false` record carrying
20
- // only the new tail when the snapshot merely extends the last (the common case),
21
- // or a `reset:true` record carrying the full snapshot when it shrinks or a
22
- // prefix message changed (compaction / in-place mutation). `baseReplay` folds
23
- // these back — reset replaces, non-reset extends — reproducing the last snapshot
20
+ // only the new tail when the snapshot merely extends the last (the common case)
21
+ // or changes after a shared prefix (`keep` = how much of the prefix survives;
22
+ // a retried turn replacing its trailing error), or a `reset:true` record
23
+ // carrying the full snapshot when its first message changed (compaction).
24
+ // `baseReplay` folds these back — reset replaces, non-reset truncates to `keep`
25
+ // and extends — reproducing the last snapshot
24
26
  // exactly, so the fold is behaviour-preserving while keeping the file bounded
25
27
  // (~O(total messages)) instead of O(turns × transcript size).
26
28
  //
@@ -234,7 +236,46 @@ export function mergeBases(logged, runtime) {
234
236
  if (add(m))
235
237
  sequence.push({ fresh: m }); // not in the logged base
236
238
  }
237
- return placeFresh(merged, sequence);
239
+ return placeLoggedOnly(placeFresh(merged, sequence), sequence);
240
+ }
241
+ function timeOf(m) {
242
+ const t = m.timestamp;
243
+ return typeof t === "number" && Number.isFinite(t) ? t : undefined;
244
+ }
245
+ /**
246
+ * Put runs of messages only the log has back where they happened. A reload can
247
+ * drop messages the log stored: Claude's reloaded transcript has no subagent
248
+ * sidechain. Such a run kept its spot after its last shared message, so every
249
+ * later turn went in ahead of it and the old subagent work sat below the newest
250
+ * reply. A run that is older than the shared message before it moves to just
251
+ * before the first shared message that came later.
252
+ */
253
+ function placeLoggedOnly(list, sequence) {
254
+ const shared = new Set(sequence.map((item) => ("anchor" in item ? item.anchor : item.fresh)));
255
+ const out = [];
256
+ let previous; // last shared message already in `out`
257
+ for (let i = 0; i < list.length;) {
258
+ if (shared.has(list[i])) {
259
+ previous = list[i];
260
+ out.push(previous);
261
+ i++;
262
+ continue;
263
+ }
264
+ let end = i;
265
+ while (end < list.length && !shared.has(list[end]))
266
+ end++;
267
+ const run = list.slice(i, end);
268
+ i = end;
269
+ const t = timeOf(run[0]);
270
+ const before = previous && timeOf(previous);
271
+ if (t === undefined || before === undefined || before <= t) {
272
+ out.push(...run);
273
+ continue;
274
+ }
275
+ const at = out.findIndex((m) => shared.has(m) && (timeOf(m) ?? -Infinity) > t);
276
+ out.splice(at < 0 ? out.length : at, 0, ...run);
277
+ }
278
+ return out;
238
279
  }
239
280
  /**
240
281
  * Put messages only the runtime had where they happened, not at the end. A
@@ -249,10 +290,6 @@ function placeFresh(merged, sequence) {
249
290
  const freshSet = new Set(sequence.flatMap((item) => ("fresh" in item ? [item.fresh] : [])));
250
291
  if (!freshSet.size)
251
292
  return merged;
252
- const timeOf = (m) => {
253
- const t = m.timestamp;
254
- return typeof t === "number" && Number.isFinite(t) ? t : undefined;
255
- };
256
293
  const placed = merged.filter((m) => !freshSet.has(m));
257
294
  let previous; // last anchor seen, or last fresh placed after it
258
295
  for (let i = 0; i < sequence.length; i++) {
@@ -291,7 +328,9 @@ function isBase(value) {
291
328
  if (!value || typeof value !== "object")
292
329
  return false;
293
330
  const record = value;
294
- return record.bivyKind === "base" && Array.isArray(record.messages) && typeof record.reset === "boolean";
331
+ const keep = value.keep;
332
+ return record.bivyKind === "base" && Array.isArray(record.messages) && typeof record.reset === "boolean"
333
+ && (keep === undefined || (Number.isInteger(keep) && keep >= 0));
295
334
  }
296
335
  function isForkDisplay(value) {
297
336
  if (!value || typeof value !== "object")
@@ -544,34 +583,70 @@ export function baseReplay(entries) {
544
583
  if (entry.reset)
545
584
  base = [...entry.messages];
546
585
  else
547
- base = base.concat(entry.messages);
586
+ base = (entry.keep === undefined ? base : base.slice(0, entry.keep)).concat(entry.messages);
548
587
  }
549
588
  return base;
550
589
  }
551
- function parseLogDetailed(body, keep = () => true) {
552
- const records = [];
590
+ function parseLogDetailed(lines, keep = () => true) {
591
+ let records = [];
553
592
  let malformedLines = 0;
554
- for (const line of body.split("\n")) {
593
+ let superseded = 0;
594
+ for (const line of lines) {
555
595
  const trimmed = line.trim();
556
596
  if (!trimmed || !keep(trimmed))
557
597
  continue;
558
598
  try {
559
599
  const value = JSON.parse(trimmed);
560
- if (isRecord(value))
561
- records.push(value);
562
- else
600
+ if (!isRecord(value)) {
563
601
  malformedLines += 1;
602
+ continue;
603
+ }
604
+ // A full reset replaces every earlier base record (baseReplay discards
605
+ // them), so don't hold old snapshots and their images in memory.
606
+ if (isBase(value) && value.reset) {
607
+ const before = records.length;
608
+ records = records.filter((record) => record.bivyKind !== "base");
609
+ superseded += before - records.length;
610
+ }
611
+ records.push(value);
564
612
  }
565
613
  catch {
566
614
  malformedLines += 1;
567
615
  }
568
616
  }
569
- return { records, malformedLines };
617
+ return { records, malformedLines, superseded };
618
+ }
619
+ /** A log this large that carries replaced base snapshots is rewritten without
620
+ * them when it's next read. */
621
+ const COMPACT_LOG_BYTES = 32 * 1024 * 1024;
622
+ /** A file's lines, read in chunks: a log can outgrow the longest string V8
623
+ * allows (~512 MB), which made the whole session unreadable. */
624
+ function* fileLines(file) {
625
+ const fd = fs.openSync(file, "r");
626
+ try {
627
+ const chunk = Buffer.allocUnsafe(4 * 1024 * 1024);
628
+ let partial = [];
629
+ for (let n = fs.readSync(fd, chunk); n > 0; n = fs.readSync(fd, chunk)) {
630
+ let start = 0;
631
+ for (let nl = chunk.indexOf(10, start); nl !== -1 && nl < n; nl = chunk.indexOf(10, start)) {
632
+ yield Buffer.concat([...partial, chunk.subarray(start, nl)]).toString("utf8");
633
+ partial = [];
634
+ start = nl + 1;
635
+ }
636
+ if (start < n)
637
+ partial.push(Buffer.from(chunk.subarray(start, n)));
638
+ }
639
+ if (partial.length)
640
+ yield Buffer.concat(partial).toString("utf8");
641
+ }
642
+ finally {
643
+ fs.closeSync(fd);
644
+ }
570
645
  }
571
646
  /** Parse a JSONL log body into valid records. Callers that need corruption
572
647
  * diagnostics use EventLog.load, which reports malformed lines via onIssue. */
573
648
  export function parseLog(body) {
574
- return parseLogDetailed(body).records;
649
+ return parseLogDetailed(body.split("\n")).records;
575
650
  }
576
651
  /**
577
652
  * Append-only, coalesced, throttled JSONL log store — the write+read companion to
@@ -655,11 +730,14 @@ export class EventLog {
655
730
  /** Read + parse a session's log file (lines passing `keep`), reporting problems. */
656
731
  readLog(id, keep) {
657
732
  try {
658
- const body = fs.readFileSync(this.pathFor(id), "utf8");
659
- const parsed = parseLogDetailed(body, keep);
733
+ const file = this.pathFor(id);
734
+ const bytes = fs.statSync(file).size;
735
+ const parsed = parseLogDetailed(fileLines(file), keep);
660
736
  if (parsed.malformedLines > 0)
661
737
  this.report(id, "parse", new Error(`${parsed.malformedLines} malformed record(s); valid history was recovered`));
662
- return { records: parsed.records, bytes: body.length };
738
+ if (!keep && parsed.superseded > 0 && bytes > COMPACT_LOG_BYTES)
739
+ return { records: parsed.records, bytes: this.compact(id, parsed.records) ?? bytes };
740
+ return { records: parsed.records, bytes };
663
741
  }
664
742
  catch (error) {
665
743
  if (error?.code !== "ENOENT")
@@ -667,6 +745,31 @@ export class EventLog {
667
745
  return { records: [], bytes: 0 };
668
746
  }
669
747
  }
748
+ /** Rewrite a log without the base snapshots a later reset replaced (logs
749
+ * written before truncate-and-extend records repeated the whole transcript on
750
+ * every retry). Atomic; returns the new size, or undefined if it failed. */
751
+ compact(id, records) {
752
+ const file = this.pathFor(id);
753
+ const tmp = `${file}.compact-${process.pid}`;
754
+ try {
755
+ const fd = fs.openSync(tmp, "w");
756
+ let bytes = 0;
757
+ try {
758
+ for (const record of records)
759
+ bytes += fs.writeSync(fd, `${JSON.stringify(record)}\n`);
760
+ }
761
+ finally {
762
+ fs.closeSync(fd);
763
+ }
764
+ fs.renameSync(tmp, file);
765
+ return bytes;
766
+ }
767
+ catch (error) {
768
+ fs.rmSync(tmp, { force: true });
769
+ this.report(id, "rewrite", error);
770
+ return undefined;
771
+ }
772
+ }
670
773
  load(id) {
671
774
  const cached = this.disk.get(id);
672
775
  if (cached) {
@@ -762,15 +865,19 @@ export class EventLog {
762
865
  // Identical snapshot → nothing to record.
763
866
  if (nextKeys.length === prevKeys.length && prevKeys.every((k, i) => k === nextKeys[i]))
764
867
  return;
765
- // A prefix-EXTEND (strictly longer, every prior message unchanged) appends only
766
- // the new tail; anything else — a shrink (compaction), a changed prefix message
767
- // (in-place mutation), or the first snapshot (empty prev, nothing to extend) —
868
+ // Record only what follows the prefix both snapshots share: a plain extend, or
869
+ // a truncate-and-extend (`keep`) when a later message changed — a retried turn
870
+ // replacing its trailing error rewrote the whole transcript, images and all,
871
+ // on every retry. A changed first message (compaction) or the first snapshot
768
872
  // appends a full reset. Rebuild the recorded messages from the serialized form:
769
873
  // a free deep copy that also matches exactly what will land on disk.
770
- const extend = prevKeys.length > 0 && nextKeys.length > prevKeys.length && prevKeys.every((k, i) => k === nextKeys[i]);
771
- const record = extend
772
- ? { bivyKind: "base", reset: false, createdAt: Date.now(), messages: nextKeys.slice(prevKeys.length).map((s) => JSON.parse(s)) }
773
- : { bivyKind: "base", reset: true, createdAt: Date.now(), messages: nextKeys.map((s) => JSON.parse(s)) };
874
+ let shared = 0;
875
+ while (shared < prevKeys.length && shared < nextKeys.length && prevKeys[shared] === nextKeys[shared])
876
+ shared++;
877
+ const tail = nextKeys.slice(shared).map((s) => JSON.parse(s));
878
+ const record = shared === 0
879
+ ? { bivyKind: "base", reset: true, createdAt: Date.now(), messages: tail }
880
+ : { bivyKind: "base", reset: false, createdAt: Date.now(), messages: tail, ...(shared < prevKeys.length ? { keep: shared } : {}) };
774
881
  this.baseKeys.set(id, nextKeys);
775
882
  this.enqueue(id, this.syntheticKey(id), record);
776
883
  }
@@ -10,27 +10,21 @@
10
10
  // otherwise get the *viewer's* browser/IP to hit an arbitrary URL by embedding
11
11
  // it in a reply).
12
12
  //
13
- // This is the PURE-ish, testable half — URL extraction, host/SSRF validation,
14
- // and the guarded fetch itself — all dependency-injectable so tests never hit
15
- // the real network or DNS. The server half (src/server.ts) owns the AttachmentStore
13
+ // This is the PURE-ish, testable half — host/SSRF validation and the guarded
14
+ // fetch itself — all dependency-injectable so tests never hit the real network
15
+ // or DNS. Which references a message makes at all is the renderer's and this
16
+ // module's shared concern, so it lives in ./message-components.js (the same file
17
+ // @bivy/core compiles). The server half (src/server.ts) owns the AttachmentStore
16
18
  // write, the durable event-log ref, and the live broadcast; see resolveInlineImages.
17
19
  import dns from "node:dns/promises";
18
20
  import { hostnameIsLocal } from "../auth.js";
19
21
  import { sanitizeAttachmentName, sniffMime } from "./attach-to-chat.js";
20
- /**
21
- * The image-markdown pattern, `![alt](https://…)` — MUST match the image regex
22
- * in `inline()` in packages/core/src/markdown.ts exactly. Not a shared import: the
23
- * node (src/) intentionally does not depend on @bivy/core (a browser/client
24
- * package — see packages/core's own description). Kept in lock-step by comment
25
- * instead, the same convention EPHEMERAL_ALLOWED_HOSTS uses in
26
- * src/ephemeral-exec.ts for its cross-copy host allowlist. If you change one,
27
- * change the other.
28
- */
29
- const INLINE_IMAGE_MD_RE = /!\[[^\]]*\]\((https:\/\/[^)\s]+)\)/g;
30
- /** Bound how many distinct remote images a single message can trigger a fetch
31
- * for — a pathological/malicious message can't fan out into an unbounded
32
- * number of outbound requests. */
33
- export const MAX_INLINE_IMAGES_PER_MESSAGE = 6;
22
+ import { extractImageReferences, extractMessageReferences, MAX_COMPONENTS_PER_MESSAGE } from "./message-components.js";
23
+ /** Bound how many distinct images a single message can trigger a resolve for —
24
+ * a pathological/malicious message can't fan out into an unbounded number of
25
+ * outbound requests or file reads. Re-exported from the shared grammar, which
26
+ * the renderer applies the same cap from. */
27
+ export const MAX_INLINE_IMAGES_PER_MESSAGE = MAX_COMPONENTS_PER_MESSAGE;
34
28
  /** Ceiling for a single fetched inline image. Smaller than
35
29
  * MAX_AGENT_ATTACHMENT_BYTES (attach-to-chat.ts) because these bytes come from
36
30
  * an arbitrary, untrusted remote origin rather than the local workspace —
@@ -40,23 +34,20 @@ export const MAX_INLINE_IMAGE_BYTES = 8 * 1024 * 1024;
40
34
  const FETCH_TIMEOUT_MS = 10_000;
41
35
  /** Hard cap on redirect hops, mirroring execEphemeralRequest's guard. */
42
36
  const MAX_REDIRECTS = 5;
43
- /** Extract the distinct `https://` URLs a message's raw markdown references via
44
- * `![alt](url)`, in first-seen order, capped at MAX_INLINE_IMAGES_PER_MESSAGE. */
37
+ /** The distinct `https://` URLs a message's raw markdown references via
38
+ * `![alt](url)`, in first-seen order, capped at MAX_INLINE_IMAGES_PER_MESSAGE.
39
+ * Workspace-relative references are a different resolver's job (they are read,
40
+ * not fetched) — see resolveWorkspaceImageRefs in transcript-persistence.ts. */
45
41
  export function extractInlineImageUrls(text) {
46
- if (!text)
47
- return [];
48
- const out = [];
49
- const seen = new Set();
50
- for (const match of text.matchAll(INLINE_IMAGE_MD_RE)) {
51
- const url = match[1];
52
- if (!url || seen.has(url))
53
- continue;
54
- seen.add(url);
55
- out.push(url);
56
- if (out.length >= MAX_INLINE_IMAGES_PER_MESSAGE)
57
- break;
58
- }
59
- return out;
42
+ return extractImageReferences(text).filter((i) => i.origin === "remote").map((i) => i.ref);
43
+ }
44
+ /** The distinct workspace references a message makes — from image syntax or a
45
+ * `::view{src=…}` directive — in first-seen order, under the same per-message
46
+ * cap. `syntax` tells the resolver whether non-image bytes are acceptable. */
47
+ export function extractWorkspaceRefs(text) {
48
+ return extractMessageReferences(text)
49
+ .filter((r) => r.origin === "workspace")
50
+ .map(({ ref, syntax }) => ({ ref, syntax }));
60
51
  }
61
52
  /** Best-effort plain text for an assistant RuntimeMessage's `content`, which is
62
53
  * either a plain string or an array of typed blocks (`{type:"text", text}`