@chenacc/agentbridge 0.2.5 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chenacc/agentbridge",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "Control local coding agents from a phone through a secure WeChat QR web console.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -28,14 +28,35 @@ export function indexDeltas(events, index = new Map()) {
28
28
  return index;
29
29
  }
30
30
 
31
+ /**
32
+ * Keep the buffer in sequence order, whatever order events arrive in.
33
+ *
34
+ * Appending blindly was wrong in a way that only shows up on a real phone: when the
35
+ * event stream reconnects it replays from the last id it saw, so a batch of *older*
36
+ * events lands after the newest ones already on screen, and the transcript's clock
37
+ * visibly jumps backwards (11:37 above 10:07). Appending is still the common case —
38
+ * the scan almost always stops on the first comparison.
39
+ */
40
+ function insertInOrder(events, event) {
41
+ const seq = Number.isFinite(event.seq) ? event.seq : Infinity;
42
+ let index = events.length;
43
+ while (index > 0) {
44
+ const previous = Number.isFinite(events[index - 1].seq) ? events[index - 1].seq : -Infinity;
45
+ if (previous <= seq) break;
46
+ index -= 1;
47
+ }
48
+ if (index === events.length) events.push(event);
49
+ else events.splice(index, 0, event);
50
+ return event;
51
+ }
52
+
31
53
  // Token deltas are folded into a single cumulative entry per turn, matching the
32
54
  // bridge. Without this a long turn's tokens fill the whole buffer and push the
33
55
  // actual conversation out of it, which is why replies used to arrive missing or
34
56
  // half-rendered.
35
57
  export function absorbEvent(events, deltaIndex, event) {
36
58
  if (event.kind !== "message.delta") {
37
- events.push(event);
38
- return event;
59
+ return insertInOrder(events, event);
39
60
  }
40
61
  const text = String(event.payload?.text ?? "");
41
62
  const existing = deltaIndex.get(deltaKeyOf(event));
@@ -51,8 +72,7 @@ export function absorbEvent(events, deltaIndex, event) {
51
72
  }
52
73
  const entry = { ...event, payload: { ...event.payload, offset: 0 } };
53
74
  deltaIndex.set(deltaKeyOf(event), entry);
54
- events.push(entry);
55
- return entry;
75
+ return insertInOrder(events, entry);
56
76
  }
57
77
 
58
78
  // Trimming normally drops the oldest entries, which is exactly what a freshly
@@ -332,7 +352,10 @@ function inputEntry(event, options, settledOutcome) {
332
352
  * full turned one request into a wall of overlapping drafts on a phone screen.
333
353
  */
334
354
  function collapsedEntry(event, label, summary, text, className, options) {
335
- const cached = cachedEntry(options.cache, event);
355
+ // A streaming row changes on every chunk, so it must never be cached — caching it
356
+ // froze the collapsed text at the first batch while the answer kept growing.
357
+ const stable = event.kind !== "message.delta";
358
+ const cached = stable ? cachedEntry(options.cache, event) : null;
336
359
  if (cached) return cached;
337
360
  const content = renderMessageMarkdown(String(text ?? ""), options.math ? { math: options.math } : undefined);
338
361
  const entry = {
@@ -340,11 +363,20 @@ function collapsedEntry(event, label, summary, text, className, options) {
340
363
  className,
341
364
  html: `<time class="event-time">${formatTime(event.time, options.formatMessageTime)}</time><div class="event-body"><strong>${escapeHtml(label)}</strong><details class="step-detail"><summary>${escapeHtml(summary)}</summary><div class="message-markdown">${content}</div></details></div>`,
342
365
  };
343
- return rememberEntry(options.cache, event, entry);
366
+ return stable ? rememberEntry(options.cache, event, entry) : entry;
344
367
  }
345
368
 
346
369
  /** Turn a session's events into the list of transcript entries to display. */
347
- export function buildEntries(relevant, options = {}) {
370
+ export function buildEntries(incomingEvents, options = {}) {
371
+ // The buffer should already be in sequence order; sorting here means a page that
372
+ // was assembled before that was true (or by an older client) still renders in
373
+ // chronological order instead of jumping backwards mid-conversation.
374
+ const relevant = [...incomingEvents].sort((left, right) => {
375
+ const a = Number.isFinite(left.seq) ? left.seq : Infinity;
376
+ const b = Number.isFinite(right.seq) ? right.seq : Infinity;
377
+ if (a !== b) return a - b;
378
+ return (Date.parse(left.time) || 0) - (Date.parse(right.time) || 0);
379
+ });
348
380
  const statusLabel = options.statusLabel ?? ((kind) => DEFAULT_STATUS_LABELS[kind] ?? kind);
349
381
  const settings = { cache: entryCache, formatMessageTime: options.formatMessageTime, math: options.math };
350
382
  const { textByTurn, completedTurns } = foldDeltas(relevant);