@chenacc/agentbridge 0.2.6 → 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.6",
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
@@ -347,7 +367,16 @@ function collapsedEntry(event, label, summary, text, className, options) {
347
367
  }
348
368
 
349
369
  /** Turn a session's events into the list of transcript entries to display. */
350
- 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
+ });
351
380
  const statusLabel = options.statusLabel ?? ((kind) => DEFAULT_STATUS_LABELS[kind] ?? kind);
352
381
  const settings = { cache: entryCache, formatMessageTime: options.formatMessageTime, math: options.math };
353
382
  const { textByTurn, completedTurns } = foldDeltas(relevant);