@mirrowel/opencode-openshare 0.1.1 → 0.2.0-dev.2

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/ci/openshare.js CHANGED
@@ -345,7 +345,226 @@ function resolveShareMode(cfg, input) {
345
345
 
346
346
  // src/core.ts
347
347
  import { appendFileSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
348
+ import { createHash } from "node:crypto";
348
349
  import { join as join2 } from "node:path";
350
+
351
+ // src/contract.ts
352
+ function messageTombstone(sessionID, messageID) {
353
+ return { type: "message", del: true, sessionID, data: { id: messageID } };
354
+ }
355
+ function isV2NativeMessage(x) {
356
+ if (!x || typeof x !== "object")
357
+ return false;
358
+ const r = x;
359
+ return typeof r.type === "string" && r.type !== "" && r.role === undefined && typeof r.id === "string";
360
+ }
361
+ var TOOL_NAME_MAP = {
362
+ bash: "shell",
363
+ task: "subagent"
364
+ };
365
+ var INPUT_KEY_MAP = {
366
+ filePath: "path",
367
+ subagent_type: "agent"
368
+ };
369
+ function normalizeToolInput(input, title) {
370
+ const out = {};
371
+ for (const [k, v] of Object.entries(input ?? {}))
372
+ out[INPUT_KEY_MAP[k] ?? k] = v;
373
+ if (title && out.description === undefined)
374
+ out.description = title;
375
+ return out;
376
+ }
377
+ function normalizeToolMetadata(meta) {
378
+ const out = {};
379
+ for (const [k, v] of Object.entries(meta ?? {})) {
380
+ if (k === "filediff")
381
+ out.files = [v];
382
+ else if (k === "sessionId")
383
+ out.sessionID = v;
384
+ else
385
+ out[k] = v;
386
+ }
387
+ return out;
388
+ }
389
+ function toolContent(output) {
390
+ const text = typeof output === "string" ? output : "";
391
+ return [{ type: "text", text: text || " " }];
392
+ }
393
+ function normalizeToolState(state) {
394
+ const status = String(state?.status ?? "pending");
395
+ if (status === "pending") {
396
+ return { status: "streaming", input: typeof state.raw === "string" ? state.raw : JSON.stringify(state.input ?? {}) };
397
+ }
398
+ if (status === "running") {
399
+ return { status: "running", input: normalizeToolInput(state.input ?? {}, state.title), metadata: normalizeToolMetadata(state.metadata) };
400
+ }
401
+ if (status === "completed") {
402
+ return { status: "completed", input: normalizeToolInput(state.input ?? {}, state.title), content: toolContent(state.output), metadata: normalizeToolMetadata(state.metadata) };
403
+ }
404
+ return {
405
+ status: "error",
406
+ input: normalizeToolInput(state.input ?? {}),
407
+ error: { type: "unknown", message: typeof state.error === "string" ? state.error : "tool failed" },
408
+ metadata: normalizeToolMetadata(state.metadata)
409
+ };
410
+ }
411
+ function normalizeFilePart(p) {
412
+ const url = typeof p.url === "string" ? p.url : "";
413
+ if (!url)
414
+ return;
415
+ const inline = url.startsWith("data:");
416
+ const comma = url.indexOf(",");
417
+ return {
418
+ data: inline ? url.slice(comma + 1) : url,
419
+ mime: typeof p.mime === "string" ? p.mime : "application/octet-stream",
420
+ source: inline ? { type: "inline" } : { type: "uri", uri: url },
421
+ ...typeof p.filename === "string" ? { name: p.filename } : {}
422
+ };
423
+ }
424
+ function normalizeV1Message(info, parts) {
425
+ if (!info || typeof info.id !== "string")
426
+ return;
427
+ const id = info.id;
428
+ const created = info.time?.created ?? 0;
429
+ if (info.role === "user") {
430
+ const compaction = parts.find((p) => p?.type === "compaction");
431
+ if (compaction) {
432
+ const message = {
433
+ id,
434
+ type: "compaction",
435
+ status: "completed",
436
+ reason: compaction.auto === false ? "manual" : "auto",
437
+ summary: "",
438
+ recent: "",
439
+ time: { created }
440
+ };
441
+ return { message };
442
+ }
443
+ const text = parts.filter((p) => p?.type === "text" && typeof p.text === "string").map((p) => p.text).join(`
444
+ `);
445
+ const files = parts.filter((p) => p?.type === "file").map(normalizeFilePart).filter((f) => f !== undefined);
446
+ const agents = parts.filter((p) => p?.type === "agent").map((p) => ({ name: String(p.name ?? "") })).filter((a) => a.name);
447
+ const model = info.model;
448
+ const message = {
449
+ id,
450
+ type: "user",
451
+ time: { created },
452
+ text,
453
+ ...files.length ? { files } : {},
454
+ ...agents.length ? { agents } : {},
455
+ metadata: {
456
+ ...info.agent !== undefined ? { agent: String(info.agent) } : {},
457
+ ...model ? { model: { id: String(model.modelID ?? ""), providerID: String(model.providerID ?? "") } } : {}
458
+ }
459
+ };
460
+ return { message };
461
+ }
462
+ if (info.role === "assistant") {
463
+ const time = info.time;
464
+ if (info.summary === true && info.mode === "compaction") {
465
+ const summary = parts.filter((p) => p?.type === "text" && typeof p.text === "string").map((p) => p.text).join(`
466
+ `);
467
+ return { refill: String(info.parentID ?? ""), summary };
468
+ }
469
+ const content = [];
470
+ let cost = typeof info.cost === "number" ? info.cost : 0;
471
+ let tokens;
472
+ let retry;
473
+ for (const p of parts) {
474
+ switch (p?.type) {
475
+ case "text":
476
+ content.push({ type: "text", text: String(p.text ?? "") });
477
+ break;
478
+ case "reasoning": {
479
+ const rt = p.time;
480
+ content.push({ type: "reasoning", text: String(p.text ?? ""), time: { created: rt?.start ?? undefined, completed: rt?.end ?? undefined } });
481
+ break;
482
+ }
483
+ case "tool": {
484
+ const st = p.state;
485
+ const tt = st?.time ?? {};
486
+ content.push({
487
+ type: "tool",
488
+ id: String(p.id ?? ""),
489
+ name: TOOL_NAME_MAP[String(p.tool ?? "unknown")] ?? String(p.tool ?? "unknown"),
490
+ state: normalizeToolState(st ?? {}),
491
+ time: { created: tt.start ?? undefined, completed: tt.end ?? undefined }
492
+ });
493
+ break;
494
+ }
495
+ case "retry": {
496
+ const err = p.error;
497
+ retry = {
498
+ attempt: Number(p.attempt ?? 0),
499
+ at: p.time?.created ?? 0,
500
+ error: { type: "unknown", message: String(err?.message ?? err?.name ?? "retry") }
501
+ };
502
+ break;
503
+ }
504
+ case "step-finish": {
505
+ if (typeof p.cost === "number")
506
+ cost += p.cost;
507
+ if (p.tokens && typeof p.tokens === "object")
508
+ tokens = p.tokens;
509
+ break;
510
+ }
511
+ }
512
+ }
513
+ const finish = typeof info.finish === "string" ? info.finish : undefined;
514
+ const errObj = info.error;
515
+ const message = {
516
+ id,
517
+ type: "assistant",
518
+ agent: String(info.agent ?? "build"),
519
+ model: { id: String(info.modelID ?? ""), providerID: String(info.providerID ?? "") },
520
+ time: { created, streamed: time?.started ?? undefined, completed: time?.completed ?? undefined },
521
+ content,
522
+ cost,
523
+ ...tokens ? { tokens } : {},
524
+ ...finish ? { finish } : {},
525
+ ...errObj ? { error: { type: String(errObj.name ?? "UnknownError"), message: String(errObj.message ?? "turn failed") } } : {},
526
+ ...retry ? { retry } : {}
527
+ };
528
+ return { message };
529
+ }
530
+ return;
531
+ }
532
+ function normalizeV1Diff(diff) {
533
+ if (!Array.isArray(diff))
534
+ return diff;
535
+ return diff.map((d) => {
536
+ if (!d || typeof d !== "object")
537
+ return d;
538
+ const r = d;
539
+ if (typeof r.additions === "number" && typeof r.deletions === "number")
540
+ return r;
541
+ const patch = typeof r.patch === "string" ? r.patch : "";
542
+ let additions = 0;
543
+ let deletions = 0;
544
+ for (const line of patch.split(`
545
+ `)) {
546
+ if (line.startsWith("+") && !line.startsWith("+++"))
547
+ additions++;
548
+ else if (line.startsWith("-") && !line.startsWith("---"))
549
+ deletions++;
550
+ }
551
+ return { ...r, additions: typeof r.additions === "number" ? r.additions : additions, deletions: typeof r.deletions === "number" ? r.deletions : deletions };
552
+ });
553
+ }
554
+ function cursorFor(id, time) {
555
+ return Buffer.from(JSON.stringify({ id, time })).toString("base64url");
556
+ }
557
+
558
+ // src/binding-queue.ts
559
+ var queues = new Map;
560
+ function queueBindingWrite(sid, fn) {
561
+ const prev = queues.get(sid) ?? Promise.resolve();
562
+ const run = prev.then(fn, fn);
563
+ queues.set(sid, run.catch(() => {}));
564
+ return run;
565
+ }
566
+
567
+ // src/core.ts
349
568
  var DEFAULT_POLICY = {
350
569
  mode: "persistent",
351
570
  turnGraceMs: 120000,
@@ -381,11 +600,9 @@ function itemFor(type, data, sessionID) {
381
600
  if (type === "prefs")
382
601
  return { key: "prefs", item: { type, data } };
383
602
  if (type === "session")
384
- return { key: `session/${data?.id ?? ""}`, item: { type, data } };
603
+ return { key: `session/${data?.id ?? ""}`, item: { type, sessionID, data } };
385
604
  if (type === "message")
386
- return { key: `message/${data?.id ?? ""}`, item: { type, data } };
387
- if (type === "part")
388
- return { key: `part/${data?.messageID ?? ""}/${data?.id ?? ""}`, item: { type, data } };
605
+ return { key: `message/${data?.id ?? ""}`, item: { type, sessionID, data } };
389
606
  if (type === "session_diff")
390
607
  return { key: `session_diff/${sessionID}`, item: { type, sessionID, data } };
391
608
  return;
@@ -542,6 +759,8 @@ function createEngine(deps) {
542
759
  initTimer: null,
543
760
  initWasAttach: false,
544
761
  backoff: 1000,
762
+ backoffResetTimer: null,
763
+ spoolSaveTimer: null,
545
764
  reconnectTimer: null,
546
765
  intentionalClose: false,
547
766
  connectQueued: false,
@@ -599,10 +818,14 @@ function createEngine(deps) {
599
818
  if (b && typeof b === "object" && b.policy) {
600
819
  applyPolicy(b.policy, url);
601
820
  }
821
+ if (b && typeof b === "object" && b.frameAuth) {
822
+ frameAuthUrls.add(url);
823
+ }
602
824
  }).catch(() => {
603
825
  policyAsked.delete(url);
604
826
  });
605
827
  };
828
+ const frameAuthUrls = new Set;
606
829
  const wsUrl = (url) => url.replace(/^http/, "ws");
607
830
  let spoolRoot = spoolDirFor(cfg);
608
831
  try {
@@ -637,7 +860,9 @@ function createEngine(deps) {
637
860
  opts: undefined,
638
861
  activated: false,
639
862
  wroteBinding: false,
640
- everConnected: false
863
+ everConnected: false,
864
+ merged: new Map,
865
+ sentHash: new Map
641
866
  });
642
867
  const refreshConfig = () => {
643
868
  const next = loadConfig(deps.pluginDir, deps.options);
@@ -670,6 +895,7 @@ function createEngine(deps) {
670
895
  }
671
896
  st.activated = true;
672
897
  st.opts = opts;
898
+ st.sentHash.clear();
673
899
  const gated = mode === "manual" || opts2?.gated === true;
674
900
  if (gated) {
675
901
  chain = chain.then(() => seedManualStatus(st)).then(() => backfill(st, info)).then(() => queueConnectAll(st)).catch((e) => logError("activation error", e?.message || e));
@@ -721,24 +947,26 @@ function createEngine(deps) {
721
947
  async function writeMetadata(st, value) {
722
948
  if (!st.client)
723
949
  return;
724
- try {
725
- const info = unwrap(await st.client.session.get({
726
- path: { id: st.sid },
727
- ...deps.directory ? { query: { directory: deps.directory } } : {}
728
- }));
729
- const metadata = { ...info?.metadata ?? {} };
730
- if (value === null)
731
- delete metadata[METADATA_KEY];
732
- else
733
- metadata[METADATA_KEY] = value;
734
- unwrap(await st.client.session.update({
735
- path: { id: st.sid },
736
- body: { metadata },
737
- ...deps.directory ? { query: { directory: deps.directory } } : {}
738
- }));
739
- } catch (e) {
740
- log("metadata write failed (spool still covers reconnects):", e?.message || e);
741
- }
950
+ await queueBindingWrite(st.sid, async () => {
951
+ try {
952
+ const info = unwrap(await st.client.session.get({
953
+ path: { id: st.sid },
954
+ ...deps.directory ? { query: { directory: deps.directory } } : {}
955
+ }));
956
+ const metadata = { ...info?.metadata ?? {} };
957
+ if (value === null)
958
+ delete metadata[METADATA_KEY];
959
+ else
960
+ metadata[METADATA_KEY] = value;
961
+ unwrap(await st.client.session.update({
962
+ path: { id: st.sid },
963
+ body: { metadata },
964
+ ...deps.directory ? { query: { directory: deps.directory } } : {}
965
+ }));
966
+ } catch (e) {
967
+ log("metadata write failed (spool still covers reconnects):", e?.message || e);
968
+ }
969
+ });
742
970
  }
743
971
  const parseBinding = (record) => {
744
972
  const out = new Map;
@@ -842,13 +1070,33 @@ function createEngine(deps) {
842
1070
  const spoolSave = (st, r) => {
843
1071
  try {
844
1072
  writeFileSync2(metaPath(st, r.url), JSON.stringify({ shareId: r.shareId, secret: r.secret, ackedRev: r.ackedRev, sid: st.sid, url: r.url }));
845
- writeFileSync2(queuePath(st, r.url), r.pending.map((f) => JSON.stringify(f)).join(`
1073
+ writeFileSync2(queuePath(st, r.url), r.pending.map((f) => f.wire ?? JSON.stringify(f)).join(`
846
1074
  `) + (r.pending.length ? `
847
1075
  ` : ""));
1076
+ rmSync(legacyMetaPath(st), { force: true });
1077
+ rmSync(legacyQueuePath(st), { force: true });
848
1078
  } catch (e) {
849
1079
  logError("spool save failed", e?.message || e);
850
1080
  }
851
1081
  };
1082
+ const spoolAppend = (st, r, f) => {
1083
+ try {
1084
+ appendFileSync(queuePath(st, r.url), (f.wire ?? JSON.stringify(f)) + `
1085
+ `);
1086
+ } catch (e) {
1087
+ r.spoolAppendFailed = true;
1088
+ logError("spool append failed", e?.message || e);
1089
+ }
1090
+ };
1091
+ const scheduleSpoolSave = (st, r) => {
1092
+ if (r.spoolSaveTimer)
1093
+ return;
1094
+ r.spoolSaveTimer = setTimeout(() => {
1095
+ r.spoolSaveTimer = null;
1096
+ if (!st.closed)
1097
+ spoolSave(st, r);
1098
+ }, 300);
1099
+ };
852
1100
  const spoolLoad = (st, r) => {
853
1101
  try {
854
1102
  const meta = JSON.parse(readFileSync2(metaPath(st, r.url), "utf8"));
@@ -857,13 +1105,33 @@ function createEngine(deps) {
857
1105
  r.secret = meta.secret;
858
1106
  r.ackedRev = meta.ackedRev ?? 0;
859
1107
  }
1108
+ } catch {}
1109
+ try {
860
1110
  if (existsSync2(queuePath(st, r.url))) {
861
- r.pending = readFileSync2(queuePath(st, r.url), "utf8").split(`
862
- `).filter((l) => l.trim()).map((l) => JSON.parse(l));
1111
+ const disk = [];
1112
+ for (const line of readFileSync2(queuePath(st, r.url), "utf8").split(`
1113
+ `)) {
1114
+ if (!line.trim())
1115
+ continue;
1116
+ try {
1117
+ disk.push(JSON.parse(line));
1118
+ } catch {
1119
+ log(`spool load: dropped torn tail line for ${st.sid.slice(0, 18)}`);
1120
+ break;
1121
+ }
1122
+ }
1123
+ for (const f of disk) {
1124
+ if (typeof f.seq !== "number")
1125
+ f.seq = ++nextSeq;
1126
+ else if (f.seq >= nextSeq)
1127
+ nextSeq = f.seq + 1;
1128
+ }
1129
+ if (!r.spoolAppendFailed)
1130
+ r.pending = disk;
1131
+ else
1132
+ r.spoolAppendFailed = false;
863
1133
  }
864
- } catch {
865
- r.pending = [];
866
- }
1134
+ } catch {}
867
1135
  if (!r.shareId && targets[0]?.url === r.url) {
868
1136
  try {
869
1137
  const meta = JSON.parse(readFileSync2(legacyMetaPath(st), "utf8"));
@@ -882,10 +1150,17 @@ function createEngine(deps) {
882
1150
  };
883
1151
  const spoolClear = (st, r) => {
884
1152
  try {
1153
+ if (r.spoolSaveTimer) {
1154
+ clearTimeout(r.spoolSaveTimer);
1155
+ r.spoolSaveTimer = null;
1156
+ }
885
1157
  rmSync(metaPath(st, r.url), { force: true });
886
1158
  rmSync(queuePath(st, r.url), { force: true });
1159
+ rmSync(legacyMetaPath(st), { force: true });
1160
+ rmSync(legacyQueuePath(st), { force: true });
887
1161
  } catch {}
888
1162
  };
1163
+ let nextSeq = 0;
889
1164
  const wsSend = (r, obj) => {
890
1165
  if (debugEvents)
891
1166
  log(`ws ${r.url.slice(0, 24)} #${r.socketSeq} send ${obj?.op} (ready=${r.wsReady})`);
@@ -896,13 +1171,50 @@ function createEngine(deps) {
896
1171
  }
897
1172
  return true;
898
1173
  };
899
- const SYNC_CHUNK = 200;
1174
+ const chunkedFrames = (items, maxItems, maxBytes) => {
1175
+ const chunks = [];
1176
+ let cur = [];
1177
+ let curJson = [];
1178
+ let curBytes = 0;
1179
+ for (const it of items) {
1180
+ const j = JSON.stringify(it) ?? "null";
1181
+ if (cur.length && (cur.length >= maxItems || curBytes + j.length > maxBytes)) {
1182
+ chunks.push({ items: cur, json: "[" + curJson.join(",") + "]" });
1183
+ cur = [];
1184
+ curJson = [];
1185
+ curBytes = 0;
1186
+ }
1187
+ cur.push(it);
1188
+ curJson.push(j);
1189
+ curBytes += j.length;
1190
+ }
1191
+ if (cur.length)
1192
+ chunks.push({ items: cur, json: "[" + curJson.join(",") + "]" });
1193
+ return chunks;
1194
+ };
1195
+ const chunkItems = (items, maxItems, maxBytes) => chunkedFrames(items, maxItems, maxBytes).map((c) => c.items);
1196
+ const syncFrameStr = (itemsJson, status, seq) => `{"op":"sync","items":${itemsJson},"status":${JSON.stringify(status)}${seq !== undefined ? `,"seq":${seq}` : ""}}`;
1197
+ const wsSendRaw = (r, s) => {
1198
+ if (debugEvents)
1199
+ log(`ws ${r.url.slice(0, 24)} #${r.socketSeq} send raw (${Math.round(s.length / 1024)}KB, ready=${r.wsReady})`);
1200
+ try {
1201
+ r.ws?.send(s);
1202
+ } catch {
1203
+ return false;
1204
+ }
1205
+ return true;
1206
+ };
900
1207
  const sendSync = (r, f) => {
901
1208
  if (!f.items.length)
902
- return wsSend(r, { op: "sync", items: [], status: f.status });
903
- for (let i = 0;i < f.items.length; i += SYNC_CHUNK) {
904
- wsSend(r, { op: "sync", items: f.items.slice(i, i + SYNC_CHUNK), status: f.status });
905
- }
1209
+ return wsSend(r, { op: "sync", items: [], status: f.status, seq: f.seq });
1210
+ if (f.wire)
1211
+ return wsSendRaw(r, f.wire);
1212
+ const itemsJson = JSON.stringify(f.items);
1213
+ if (f.items.length <= 200 && itemsJson.length <= 512 * 1024) {
1214
+ return wsSendRaw(r, syncFrameStr(itemsJson, f.status, f.seq));
1215
+ }
1216
+ for (const c of chunkedFrames(f.items, 200, 512 * 1024))
1217
+ wsSendRaw(r, syncFrameStr(c.json, f.status, f.seq));
906
1218
  return true;
907
1219
  };
908
1220
  const announceUrl = (st, r) => {
@@ -1044,7 +1356,8 @@ function createEngine(deps) {
1044
1356
  logError(`sharing to ${r.url} disabled (no credential): ${cred.error}`);
1045
1357
  return;
1046
1358
  }
1047
- const ws = new WebSocket(`${wsUrl(r.url)}/api/ws?token=${encodeURIComponent(token ?? "")}`);
1359
+ const useFrameAuth = !!token && frameAuthUrls.has(r.url);
1360
+ const ws = new WebSocket(`${wsUrl(r.url)}/api/ws${useFrameAuth ? "" : `?token=${encodeURIComponent(token ?? "")}`}`);
1048
1361
  r.ws = ws;
1049
1362
  await new Promise((resolve, reject) => {
1050
1363
  const t = setTimeout(() => reject(new Error("ws open timeout")), 15000);
@@ -1080,6 +1393,27 @@ function createEngine(deps) {
1080
1393
  if (!st.closed)
1081
1394
  scheduleReconnect(st, r);
1082
1395
  });
1396
+ if (useFrameAuth) {
1397
+ wsSend(r, { op: "auth", token });
1398
+ const authed = await new Promise((resolve) => {
1399
+ const t = setTimeout(() => resolve(false), 3000);
1400
+ r.authWaiter = () => {
1401
+ clearTimeout(t);
1402
+ resolve(true);
1403
+ };
1404
+ });
1405
+ r.authWaiter = undefined;
1406
+ if (!authed) {
1407
+ logError(`ws ${r.url.slice(0, 24)}: frame-auth ack timeout - reconnecting with query token`);
1408
+ frameAuthUrls.delete(r.url);
1409
+ try {
1410
+ ws.close(4000, "auth timeout");
1411
+ } catch {}
1412
+ if (!st.closed)
1413
+ scheduleReconnect(st, r);
1414
+ return;
1415
+ }
1416
+ }
1083
1417
  if (r.initWasAttach) {
1084
1418
  wsSend(r, { op: "init", attach: { id: r.shareId, secret: r.secret, ...authFrame(st) } });
1085
1419
  } else {
@@ -1108,7 +1442,7 @@ function createEngine(deps) {
1108
1442
  const scheduleReconnect = (st, r) => {
1109
1443
  if (st.closed || r.reconnectTimer)
1110
1444
  return;
1111
- const wait = r.backoff;
1445
+ const wait = Math.max(250, Math.round(r.backoff * (0.8 + Math.random() * 0.4)));
1112
1446
  r.backoff = Math.min(r.backoff * 2, 60000);
1113
1447
  r.reconnectTimer = setTimeout(() => {
1114
1448
  r.reconnectTimer = null;
@@ -1190,6 +1524,11 @@ function createEngine(deps) {
1190
1524
  }
1191
1525
  };
1192
1526
  const handleFrame = async (st, r, frame) => {
1527
+ if (frame.op === "authed") {
1528
+ r.authWaiter?.();
1529
+ r.authWaiter = undefined;
1530
+ return;
1531
+ }
1193
1532
  if (frame.op === "init") {
1194
1533
  if (!frame.ok) {
1195
1534
  const err = String(frame.error ?? "");
@@ -1228,6 +1567,16 @@ function createEngine(deps) {
1228
1567
  queueConnect(st, r);
1229
1568
  return;
1230
1569
  }
1570
+ if (/limit|too many|rate/i.test(err)) {
1571
+ logError(`${r.url}: create refused:`, frame.error, "- retrying with backoff");
1572
+ r.connecting = false;
1573
+ try {
1574
+ r.ws?.close(4000, "rate limited");
1575
+ } catch {}
1576
+ if (!st.closed)
1577
+ scheduleReconnect(st, r);
1578
+ return;
1579
+ }
1231
1580
  logError(`${r.url}: init refused:`, frame.error, "- creating fresh share");
1232
1581
  deleteAbandoned(r);
1233
1582
  r.shareId = null;
@@ -1256,11 +1605,19 @@ function createEngine(deps) {
1256
1605
  r.connecting = false;
1257
1606
  r.initWasAttach = false;
1258
1607
  r.collided = null;
1608
+ if (r.frozenRemote && !frame.error)
1609
+ r.frozenRemote = false;
1259
1610
  if (r.initTimer) {
1260
1611
  clearTimeout(r.initTimer);
1261
1612
  r.initTimer = null;
1262
1613
  }
1263
- r.backoff = 1000;
1614
+ if (r.backoffResetTimer)
1615
+ clearTimeout(r.backoffResetTimer);
1616
+ r.backoffResetTimer = setTimeout(() => {
1617
+ r.backoffResetTimer = null;
1618
+ if (r.wsReady && !r.reconnectTimer)
1619
+ r.backoff = 1000;
1620
+ }, 30000);
1264
1621
  st.everConnected = true;
1265
1622
  if (frame.policy)
1266
1623
  applyPolicy(frame.policy, r.url);
@@ -1280,18 +1637,49 @@ function createEngine(deps) {
1280
1637
  flush(st);
1281
1638
  }
1282
1639
  if (wasFanoutCreate) {
1640
+ st.sentHash.clear();
1283
1641
  chain = chain.then(() => backfill(st)).catch((e) => logError("fan-out backfill error", e?.message || e));
1284
1642
  }
1285
1643
  return;
1286
1644
  }
1287
1645
  if (frame.op === "ack") {
1288
1646
  r.ackedRev = frame.rev ?? r.ackedRev;
1289
- r.pending = [];
1290
- spoolSave(st, r);
1647
+ if (r.backoffResetTimer) {
1648
+ clearTimeout(r.backoffResetTimer);
1649
+ r.backoffResetTimer = null;
1650
+ r.backoff = 1000;
1651
+ }
1652
+ if (typeof frame.seq === "number") {
1653
+ r.pending = r.pending.filter((p) => p.seq !== frame.seq);
1654
+ } else {
1655
+ r.pending = [];
1656
+ }
1657
+ scheduleSpoolSave(st, r);
1291
1658
  scheduleBindingWrite(st);
1292
1659
  return;
1293
1660
  }
1294
1661
  if (frame.op === "nack" || frame.op === "error") {
1662
+ const errText = String(frame.error ?? "");
1663
+ if (frame.op === "nack") {
1664
+ if (/not found|gone/i.test(errText)) {
1665
+ logError(`${r.url}: share vanished server-side (nack: ${errText}) - recreating`);
1666
+ deleteAbandoned(r);
1667
+ r.shareId = null;
1668
+ r.secret = null;
1669
+ r.ackedRev = 0;
1670
+ try {
1671
+ r.ws?.close(4000, "share gone");
1672
+ } catch {}
1673
+ if (!st.closed)
1674
+ queueConnect(st, r);
1675
+ return;
1676
+ }
1677
+ if (/frozen/i.test(errText)) {
1678
+ r.frozenRemote = true;
1679
+ logError(`${r.url}: share frozen by the hoster - edits refused; contact the server admin`);
1680
+ return;
1681
+ }
1682
+ }
1295
1683
  logError(`${r.url}: worker refused frame:`, JSON.stringify(frame).slice(0, 200));
1296
1684
  if (frame.op === "error" && r.wsReady && !st.closed) {
1297
1685
  r.wsReady = false;
@@ -1314,6 +1702,7 @@ function createEngine(deps) {
1314
1702
  queueConnect(st, r);
1315
1703
  };
1316
1704
  const frameFrom = (st) => {
1705
+ stampsPass(st);
1317
1706
  const items = Array.from(st.queue.values());
1318
1707
  st.queue.clear();
1319
1708
  const statusChanged = st.status !== st.lastSentStatus;
@@ -1333,14 +1722,32 @@ function createEngine(deps) {
1333
1722
  const f = frameFrom(st);
1334
1723
  if (!f)
1335
1724
  return;
1725
+ const chunks = f.items.length ? chunkedFrames(f.items, 200, 512 * 1024) : [];
1336
1726
  let httpWork;
1337
1727
  for (const r of st.remotes.values()) {
1338
- r.pending.push(f);
1339
- spoolSave(st, r);
1728
+ if (r.frozenRemote)
1729
+ continue;
1730
+ const fresh = [];
1731
+ if (chunks.length) {
1732
+ for (const c of chunks) {
1733
+ const seq = ++nextSeq;
1734
+ const cf = { items: c.items, status: f.status, seq, wire: syncFrameStr(c.json, f.status, seq) };
1735
+ fresh.push(cf);
1736
+ r.pending.push(cf);
1737
+ spoolAppend(st, r, cf);
1738
+ }
1739
+ } else {
1740
+ const seq = ++nextSeq;
1741
+ const cf = { items: [], status: f.status, seq };
1742
+ fresh.push(cf);
1743
+ r.pending.push(cf);
1744
+ spoolAppend(st, r, cf);
1745
+ }
1340
1746
  if (r.ws && !r.wsReady)
1341
1747
  continue;
1342
1748
  if (r.wsReady) {
1343
- sendSync(r, f);
1749
+ for (const cf of fresh)
1750
+ sendSync(r, cf);
1344
1751
  continue;
1345
1752
  }
1346
1753
  const viaHttp = flushViaHttp(st, r);
@@ -1349,32 +1756,68 @@ function createEngine(deps) {
1349
1756
  }
1350
1757
  if (st.remotes.size)
1351
1758
  st.lastFlushAt = Date.now();
1759
+ if (httpWork)
1760
+ httpWork.catch((e) => logError("http flush error", e?.message || e));
1352
1761
  return httpWork;
1353
1762
  };
1763
+ const httpDedupeKey = (it) => {
1764
+ if (!it || typeof it !== "object")
1765
+ return null;
1766
+ const o = it;
1767
+ switch (o.type) {
1768
+ case "session":
1769
+ return "session/" + (o.data?.id ?? "");
1770
+ case "session_diff":
1771
+ return "session_diff/" + (o.sessionID ?? o.data?.sessionID ?? "");
1772
+ case "message":
1773
+ return "message/" + (o.data?.id ?? "");
1774
+ case "part":
1775
+ return "part/" + (o.data?.messageID ?? "") + "/" + (o.data?.id ?? "");
1776
+ case "model":
1777
+ return "model/" + (o.sessionID ?? o.data?.sessionID ?? "");
1778
+ case "prefs":
1779
+ return "prefs";
1780
+ default:
1781
+ return null;
1782
+ }
1783
+ };
1354
1784
  const flushViaHttp = async (st, r) => {
1355
1785
  if (!r.shareId || !r.pending.length)
1356
1786
  return;
1357
- const items = [];
1787
+ const byKey = new Map;
1788
+ const unkeyed = [];
1358
1789
  let status;
1790
+ const sentSeqs = new Set;
1359
1791
  for (const f of r.pending) {
1360
- for (const it of f.items)
1361
- items.push(it);
1792
+ for (const it of f.items) {
1793
+ const k = httpDedupeKey(it);
1794
+ if (k)
1795
+ byKey.set(k, it);
1796
+ else
1797
+ unkeyed.push(it);
1798
+ }
1362
1799
  if (f.status)
1363
1800
  status = f.status;
1801
+ if (typeof f.seq === "number")
1802
+ sentSeqs.add(f.seq);
1364
1803
  }
1804
+ const items = [...byKey.values(), ...unkeyed];
1365
1805
  const cred = credOf(r.url);
1366
- const res = await fetch(`${r.url}/api/share/${r.shareId}/sync`, {
1367
- method: "POST",
1368
- headers: { "content-type": "application/json", ...cred.credential ? { authorization: `Bearer ${cred.credential}` } : {} },
1369
- body: JSON.stringify({ secret: r.secret ?? "", data: items, status }),
1370
- signal: AbortSignal.timeout(15000)
1371
- }).catch(() => null);
1372
- if (res && res.ok) {
1806
+ const chunks = items.length ? chunkItems(items, 500, 4 * 1024 * 1024) : [[]];
1807
+ for (const chunk of chunks) {
1808
+ const res = await fetch(`${r.url}/api/share/${r.shareId}/sync`, {
1809
+ method: "POST",
1810
+ headers: { "content-type": "application/json", ...cred.credential ? { authorization: `Bearer ${cred.credential}` } : {} },
1811
+ body: JSON.stringify({ secret: r.secret ?? "", data: chunk, status }),
1812
+ signal: AbortSignal.timeout(15000)
1813
+ }).catch(() => null);
1814
+ if (!res || !res.ok)
1815
+ return;
1373
1816
  const body = await res.json().catch(() => ({}));
1374
1817
  r.ackedRev = body.rev ?? r.ackedRev;
1375
- r.pending = [];
1376
- spoolSave(st, r);
1377
1818
  }
1819
+ r.pending = r.pending.filter((p) => !sentSeqs.has(p.seq));
1820
+ scheduleSpoolSave(st, r);
1378
1821
  };
1379
1822
  const scheduleFlush = (st) => {
1380
1823
  if (st.timer)
@@ -1385,19 +1828,316 @@ function createEngine(deps) {
1385
1828
  st.timer = null;
1386
1829
  if (debugEvents)
1387
1830
  log(`flush timer fired sid=${st.sid.slice(0, 18)} queue=${st.queue.size} remotes=${st.remotes.size}`);
1388
- chain = chain.then(() => flush(st)).catch((e) => logError("flush error", e?.message || e));
1831
+ chain = chain.then(() => {
1832
+ flush(st);
1833
+ }).catch((e) => logError("flush error", e?.message || e));
1389
1834
  }, wait);
1390
1835
  };
1391
1836
  const enqueue = (st, entry) => {
1392
1837
  if (!st || !entry)
1393
1838
  return;
1839
+ let h;
1840
+ try {
1841
+ h = createHash("sha256").update(JSON.stringify(entry.item)).digest("hex");
1842
+ } catch {
1843
+ h = "";
1844
+ }
1845
+ if (h && st.sentHash.get(entry.key) === h) {
1846
+ if (debugEvents)
1847
+ log(`enqueue dedupe ${entry.item.type} ${entry.key.slice(0, 40)}`);
1848
+ return;
1849
+ }
1850
+ if (h)
1851
+ st.sentHash.set(entry.key, h);
1394
1852
  st.queue.set(entry.key, entry.item);
1853
+ indexMessage(entry);
1395
1854
  if (debugEvents)
1396
1855
  log(`enqueue sid=${st.sid.slice(0, 18)} ${entry.item.type} queue=${st.queue.size}`);
1397
1856
  touch(st);
1398
1857
  ensureConnected(st);
1399
1858
  scheduleFlush(st);
1400
1859
  };
1860
+ const msgIndex = new Map;
1861
+ const tombstoned = new Set;
1862
+ const indexMessage = (entry) => {
1863
+ if (entry.item.type !== "message")
1864
+ return;
1865
+ const d = entry.item.data;
1866
+ if (!d || typeof d.id !== "string")
1867
+ return;
1868
+ const role = String(d.type ?? "");
1869
+ if (role !== "user" && role !== "assistant")
1870
+ return;
1871
+ const key = `${entry.item.sessionID}:${d.id}`;
1872
+ const prev = msgIndex.get(key);
1873
+ msgIndex.set(key, {
1874
+ sid: String(entry.item.sessionID ?? ""),
1875
+ role,
1876
+ agent: role === "assistant" ? typeof d.agent === "string" ? d.agent : prev?.agent : typeof d.metadata?.agent === "string" ? d.metadata.agent : prev?.agent,
1877
+ created: Number(d.time?.created ?? prev?.created ?? 0),
1878
+ data: d
1879
+ });
1880
+ };
1881
+ const sessionStateOf = (sid) => sessions.get(childToParent.get(sid) ?? sid);
1882
+ const reEnqueueMessage = (m) => {
1883
+ if (tombstoned.has(`${m.sid}:${m.data.id}`))
1884
+ return;
1885
+ const st = sessionStateOf(m.sid);
1886
+ if (st)
1887
+ enqueue(st, itemFor("message", m.data, m.sid));
1888
+ };
1889
+ const segmentAgent = (ordered, at) => {
1890
+ const own = ordered[at]?.agent;
1891
+ if (own)
1892
+ return own;
1893
+ for (let i = at + 1;i < ordered.length; i++) {
1894
+ if (ordered[i].role === "assistant" && ordered[i].agent)
1895
+ return ordered[i].agent;
1896
+ if (ordered[i].role === "user")
1897
+ break;
1898
+ }
1899
+ return;
1900
+ };
1901
+ const stampAgentSwitches = (bySession) => {
1902
+ for (const [, list] of bySession) {
1903
+ list.sort((a, b) => a.created - b.created);
1904
+ const users = list.map((m, i) => m.role === "user" ? i : -1).filter((i) => i >= 0);
1905
+ let prev;
1906
+ for (let k = 0;k < users.length; k++) {
1907
+ const agent = segmentAgent(list, users[k]);
1908
+ const d = list[users[k]].data;
1909
+ const want = k > 0 && agent && prev && agent !== prev ? agent : undefined;
1910
+ if (want !== d.metadata?.agentSwitch) {
1911
+ if (want) {
1912
+ const meta = d.metadata ??= {};
1913
+ meta.agentSwitch = want;
1914
+ } else if (d.metadata) {
1915
+ delete d.metadata.agentSwitch;
1916
+ }
1917
+ reEnqueueMessage(list[users[k]]);
1918
+ }
1919
+ if (agent)
1920
+ prev = agent;
1921
+ }
1922
+ }
1923
+ };
1924
+ const stampTaskAgents = (bySession) => {
1925
+ for (const list of bySession.values())
1926
+ list.sort((a, b) => a.created - b.created);
1927
+ for (const m of msgIndex.values()) {
1928
+ if (m.role !== "assistant")
1929
+ continue;
1930
+ const content = m.data.content;
1931
+ if (!Array.isArray(content))
1932
+ continue;
1933
+ let mutated = false;
1934
+ for (const part of content) {
1935
+ if (!part || part.type !== "tool")
1936
+ continue;
1937
+ if (part.name !== "task" && part.name !== "subagent")
1938
+ continue;
1939
+ const state = part.state;
1940
+ const meta = state?.metadata;
1941
+ const childSid = typeof meta?.sessionID === "string" ? meta.sessionID : undefined;
1942
+ if (!childSid || typeof meta.agent === "string")
1943
+ continue;
1944
+ const partStart = Number(part.time?.created ?? 0);
1945
+ const child = bySession.get(childSid);
1946
+ if (!child)
1947
+ continue;
1948
+ let agent;
1949
+ for (const c of child) {
1950
+ if (c.created < partStart)
1951
+ continue;
1952
+ if (c.role === "user" && c.agent) {
1953
+ agent = c.agent;
1954
+ break;
1955
+ }
1956
+ }
1957
+ if (!agent) {
1958
+ for (const c of child) {
1959
+ if (c.created < partStart)
1960
+ continue;
1961
+ if (c.role === "assistant" && c.agent) {
1962
+ agent = c.agent;
1963
+ break;
1964
+ }
1965
+ }
1966
+ }
1967
+ if (agent) {
1968
+ meta.agent = agent;
1969
+ mutated = true;
1970
+ }
1971
+ }
1972
+ if (mutated)
1973
+ reEnqueueMessage(m);
1974
+ }
1975
+ };
1976
+ const LANG_BY_EXT = {
1977
+ ts: "typescript",
1978
+ tsx: "tsx",
1979
+ mts: "typescript",
1980
+ cts: "typescript",
1981
+ js: "javascript",
1982
+ jsx: "jsx",
1983
+ mjs: "javascript",
1984
+ cjs: "javascript",
1985
+ json: "json",
1986
+ jsonc: "json",
1987
+ md: "markdown",
1988
+ mdx: "mdx",
1989
+ py: "python",
1990
+ rs: "rust",
1991
+ go: "go",
1992
+ java: "java",
1993
+ rb: "ruby",
1994
+ css: "css",
1995
+ scss: "scss",
1996
+ html: "html",
1997
+ xml: "xml",
1998
+ yaml: "yaml",
1999
+ yml: "yaml",
2000
+ toml: "toml",
2001
+ sh: "shellscript",
2002
+ bash: "shellscript",
2003
+ ps1: "powershell",
2004
+ sql: "sql",
2005
+ c: "c",
2006
+ h: "c",
2007
+ cpp: "cpp",
2008
+ hpp: "cpp",
2009
+ cs: "csharp",
2010
+ swift: "swift",
2011
+ kt: "kotlin",
2012
+ php: "php",
2013
+ lua: "lua",
2014
+ vue: "vue",
2015
+ svelte: "svelte"
2016
+ };
2017
+ const FILE_TOOLS = new Set(["read", "write", "edit", "patch"]);
2018
+ const stampFileLanguages = () => {
2019
+ for (const m of msgIndex.values()) {
2020
+ if (m.role !== "assistant")
2021
+ continue;
2022
+ const content = m.data.content;
2023
+ if (!Array.isArray(content))
2024
+ continue;
2025
+ let mutated = false;
2026
+ for (const part of content) {
2027
+ if (!part || part.type !== "tool")
2028
+ continue;
2029
+ if (!FILE_TOOLS.has(part.name))
2030
+ continue;
2031
+ const state = part.state;
2032
+ const meta = state?.metadata;
2033
+ if (!meta)
2034
+ continue;
2035
+ const raw = state.input?.path ?? state.input?.filePath;
2036
+ if (typeof meta.language !== "string" && typeof raw === "string") {
2037
+ const ext = raw.split(".").pop()?.toLowerCase() ?? "";
2038
+ const lang = LANG_BY_EXT[ext];
2039
+ if (lang) {
2040
+ meta.language = lang;
2041
+ mutated = true;
2042
+ }
2043
+ }
2044
+ if (typeof meta.lineStart !== "number" && part.name === "read") {
2045
+ const off = state.input?.offset;
2046
+ if (typeof off === "number" && off >= 0 && off > 0) {
2047
+ meta.lineStart = off + 1;
2048
+ mutated = true;
2049
+ }
2050
+ }
2051
+ }
2052
+ if (mutated)
2053
+ reEnqueueMessage(m);
2054
+ }
2055
+ };
2056
+ const stampsPass = (st) => {
2057
+ let any = false;
2058
+ for (const it of st.queue.values()) {
2059
+ if (it.type === "message") {
2060
+ any = true;
2061
+ break;
2062
+ }
2063
+ }
2064
+ if (!any)
2065
+ return;
2066
+ const bySession = new Map;
2067
+ for (const m of msgIndex.values()) {
2068
+ const list = bySession.get(m.sid);
2069
+ if (list)
2070
+ list.push(m);
2071
+ else
2072
+ bySession.set(m.sid, [m]);
2073
+ }
2074
+ for (const list of bySession.values())
2075
+ list.sort((a, b) => a.created - b.created);
2076
+ stampAgentSwitches(bySession);
2077
+ stampTaskAgents(bySession);
2078
+ stampFileLanguages();
2079
+ };
2080
+ const mergeEntry = (st, messageID) => {
2081
+ let e = st.merged.get(messageID);
2082
+ if (!e) {
2083
+ e = { parts: new Map };
2084
+ st.merged.set(messageID, e);
2085
+ }
2086
+ return e;
2087
+ };
2088
+ const emitMerged = (st, sid, messageID) => {
2089
+ const e = st.merged.get(messageID);
2090
+ if (!e?.info)
2091
+ return;
2092
+ const res = normalizeV1Message(e.info, [...e.parts.values()]);
2093
+ if (!res)
2094
+ return;
2095
+ if ("refill" in res) {
2096
+ const parent = st.merged.get(res.refill);
2097
+ if (parent?.info) {
2098
+ const compaction = normalizeV1Message(parent.info, [...parent.parts.values()]);
2099
+ if (compaction && "message" in compaction) {
2100
+ const msg = compaction.message;
2101
+ msg.summary = res.summary;
2102
+ enqueue(st, itemFor("message", msg, sid));
2103
+ }
2104
+ }
2105
+ return;
2106
+ }
2107
+ enqueue(st, itemFor("message", res.message, sid));
2108
+ };
2109
+ const ingestMessageInfo = (st, sid, info) => {
2110
+ if (isV2NativeMessage(info)) {
2111
+ enqueue(st, itemFor("message", info, sid));
2112
+ return;
2113
+ }
2114
+ if (typeof info?.id !== "string")
2115
+ return;
2116
+ const e = mergeEntry(st, info.id);
2117
+ e.info = info;
2118
+ emitMerged(st, sid, info.id);
2119
+ };
2120
+ const ingestPart = (st, sid, part) => {
2121
+ if (typeof part?.id !== "string" || typeof part.messageID !== "string")
2122
+ return;
2123
+ const e = mergeEntry(st, part.messageID);
2124
+ e.parts.set(part.id, part);
2125
+ emitMerged(st, sid, part.messageID);
2126
+ };
2127
+ const dropMergedMessage = (st, messageID, evSid) => {
2128
+ const sid = evSid ?? st.sid;
2129
+ st.merged.delete(messageID);
2130
+ msgIndex.delete(`${sid}:${messageID}`);
2131
+ tombstoned.add(`${sid}:${messageID}`);
2132
+ };
2133
+ const dropMergedPart = (st, sid, messageID, partID) => {
2134
+ const e = st.merged.get(messageID);
2135
+ if (!e)
2136
+ return;
2137
+ e.parts.delete(partID);
2138
+ if (e.info)
2139
+ emitMerged(st, sid, messageID);
2140
+ };
1401
2141
  const remove = (st) => {
1402
2142
  st.closed = true;
1403
2143
  st.activated = false;
@@ -1416,6 +2156,10 @@ function createEngine(deps) {
1416
2156
  clearTimeout(r.initTimer);
1417
2157
  r.initTimer = null;
1418
2158
  }
2159
+ if (r.backoffResetTimer) {
2160
+ clearTimeout(r.backoffResetTimer);
2161
+ r.backoffResetTimer = null;
2162
+ }
1419
2163
  if (r.reconnectTimer) {
1420
2164
  clearTimeout(r.reconnectTimer);
1421
2165
  r.reconnectTimer = null;
@@ -1425,9 +2169,21 @@ function createEngine(deps) {
1425
2169
  clearTimeout(r.idleDropTimer);
1426
2170
  r.idleDropTimer = null;
1427
2171
  }
1428
- if (r.wsReady)
2172
+ if (r.wsReady && r.shareId) {
1429
2173
  wsSend(r, { op: "delete" });
1430
- else if (r.shareId && r.secret) {
2174
+ if (r.shareId && r.secret) {
2175
+ const sid = r.shareId;
2176
+ const sec = r.secret;
2177
+ setTimeout(() => {
2178
+ fetch(`${r.url}/api/share/${sid}`, {
2179
+ method: "DELETE",
2180
+ headers: { "content-type": "application/json" },
2181
+ body: JSON.stringify({ secret: sec }),
2182
+ signal: AbortSignal.timeout(8000)
2183
+ }).catch(() => {});
2184
+ }, 2000);
2185
+ }
2186
+ } else if (r.shareId && r.secret) {
1431
2187
  fetch(`${r.url}/api/share/${r.shareId}`, {
1432
2188
  method: "DELETE",
1433
2189
  headers: { "content-type": "application/json" },
@@ -1445,6 +2201,17 @@ function createEngine(deps) {
1445
2201
  }, 1000);
1446
2202
  for (const r of st.remotes.values())
1447
2203
  spoolClear(st, r);
2204
+ const stSid = st.sid;
2205
+ const pfx = `${stSid}:`;
2206
+ for (const k of msgIndex.keys())
2207
+ if (k.startsWith(pfx))
2208
+ msgIndex.delete(k);
2209
+ for (const k of tombstoned)
2210
+ if (k.startsWith(pfx))
2211
+ tombstoned.delete(k);
2212
+ for (const [child, parent] of childToParent)
2213
+ if (parent === stSid || child === stSid)
2214
+ childToParent.delete(child);
1448
2215
  sessions.delete(st.sid);
1449
2216
  try {
1450
2217
  chain = chain.then(async () => {
@@ -1470,7 +2237,7 @@ function createEngine(deps) {
1470
2237
  const PAGE_SIZE = 100;
1471
2238
  const MAX_PAGES = 50;
1472
2239
  const query = () => deps.directory ? { directory: deps.directory } : {};
1473
- async function fetchMessages(client, sid) {
2240
+ async function fetchMessages(client, sid, onPage) {
1474
2241
  if (typeof client.session.messages !== "function")
1475
2242
  return [];
1476
2243
  const out = [];
@@ -1486,41 +2253,58 @@ function createEngine(deps) {
1486
2253
  if (!Array.isArray(result) || result.length === 0)
1487
2254
  break;
1488
2255
  out.push(...result);
2256
+ if (onPage) {
2257
+ try {
2258
+ onPage(result);
2259
+ } catch {}
2260
+ }
1489
2261
  if (result.length < PAGE_SIZE)
1490
2262
  break;
1491
2263
  const last = result[result.length - 1];
1492
- const nextBefore = typeof last?.info?.id === "string" ? last.info.id : typeof last?.id === "string" ? last.id : undefined;
1493
- if (!nextBefore || nextBefore === before)
2264
+ const lastInfo = last?.info ?? last;
2265
+ const lastID = typeof lastInfo?.id === "string" ? lastInfo.id : undefined;
2266
+ const lastTime = lastInfo?.time?.created;
2267
+ if (!lastID || typeof lastTime !== "number")
2268
+ break;
2269
+ const nextBefore = cursorFor(lastID, lastTime);
2270
+ if (nextBefore === before)
1494
2271
  break;
1495
2272
  before = nextBefore;
1496
2273
  }
2274
+ if (out.length >= MAX_PAGES * PAGE_SIZE) {
2275
+ logError(`backfill truncated at ${out.length} messages (page cap) for ${sid.slice(0, 18)} — oldest history not shared`);
2276
+ }
1497
2277
  return out;
1498
2278
  }
1499
2279
  const backfillSession = async (st, client, sid, info) => {
1500
2280
  const sessionInfoValue = info ?? await sessionInfo(client, sid);
1501
2281
  if (sessionInfoValue)
1502
2282
  enqueue(st, itemFor("session", redact(sessionInfoValue), sid));
1503
- const messages = await fetchMessages(client, sid);
1504
2283
  let modelSeen = false;
1505
- for (const message of messages) {
1506
- const m = message.info ?? message;
1507
- if (!m || typeof m.id !== "string")
1508
- continue;
1509
- enqueue(st, itemFor("message", m, sid));
1510
- if (m.role === "user" && m.model) {
1511
- modelSeen = true;
1512
- const label = formatLabel(st.opts?.modelLabel ?? cfg.modelLabel, m.model);
1513
- enqueue(st, itemFor("model", [{ id: m.model.modelID, providerID: m.model.providerID, name: label }], sid));
1514
- }
1515
- for (const part of message.parts ?? []) {
1516
- const p = part;
1517
- if (!p || typeof p.id !== "string" || typeof p.messageID !== "string")
2284
+ const consume = (messages) => {
2285
+ for (const message of messages) {
2286
+ const m = message.info ?? message;
2287
+ if (!m || typeof m.id !== "string")
1518
2288
  continue;
1519
- if (p.state?.status === "error")
1520
- st.badSignal = true;
1521
- enqueue(st, itemFor("part", p, sid));
2289
+ const e = mergeEntry(st, m.id);
2290
+ e.info = m;
2291
+ for (const rawPart of message.parts ?? []) {
2292
+ const p = rawPart;
2293
+ if (!p || typeof p.id !== "string" || typeof p.messageID !== "string")
2294
+ continue;
2295
+ if (p.state?.status === "error")
2296
+ st.badSignal = true;
2297
+ e.parts.set(p.id, p);
2298
+ }
2299
+ emitMerged(st, sid, m.id);
2300
+ if (m.role === "user" && m.model) {
2301
+ modelSeen = true;
2302
+ const label = formatLabel(st.opts?.modelLabel ?? cfg.modelLabel, m.model);
2303
+ enqueue(st, itemFor("model", [{ id: m.model.modelID, providerID: m.model.providerID, name: label }], sid));
2304
+ }
1522
2305
  }
1523
- }
2306
+ };
2307
+ const messages = await fetchMessages(client, sid, (page) => consume(page));
1524
2308
  if (!modelSeen && sessionInfoValue?.model) {
1525
2309
  const label = formatLabel(st.opts?.modelLabel ?? cfg.modelLabel, sessionInfoValue.model);
1526
2310
  enqueue(st, itemFor("model", [{ id: sessionInfoValue.model.modelID, providerID: sessionInfoValue.model.providerID, name: label }], sid));
@@ -1734,7 +2518,9 @@ function createEngine(deps) {
1734
2518
  if (st.timer)
1735
2519
  clearTimeout(st.timer);
1736
2520
  st.timer = null;
1737
- chain = chain.then(() => flush(st)).catch((e) => logError("idle flush error", e?.message || e));
2521
+ chain = chain.then(() => {
2522
+ flush(st);
2523
+ }).catch((e) => logError("idle flush error", e?.message || e));
1738
2524
  const epoch = st.statusEpoch;
1739
2525
  const settle = () => {
1740
2526
  st.status = st.badSignal ? "error" : "completed";
@@ -1742,7 +2528,9 @@ function createEngine(deps) {
1742
2528
  if (st.timer)
1743
2529
  clearTimeout(st.timer);
1744
2530
  st.timer = null;
1745
- chain = chain.then(() => flush(st)).then(() => {
2531
+ chain = chain.then(() => {
2532
+ flush(st);
2533
+ }).then(() => {
1746
2534
  armSettleDrop(st);
1747
2535
  }).catch((e) => logError("settle flush error", e?.message || e));
1748
2536
  };
@@ -1773,12 +2561,13 @@ function createEngine(deps) {
1773
2561
  if (!messageID)
1774
2562
  return;
1775
2563
  if (t === "message.removed") {
1776
- enqueue(st, { key: `message/${messageID}`, item: { type: "message", del: true, data: { id: messageID } } });
2564
+ dropMergedMessage(st, messageID, sid);
2565
+ enqueue(st, { key: `message/${messageID}`, item: messageTombstone(sid, messageID) });
1777
2566
  } else {
1778
2567
  const partID = p.partID;
1779
2568
  if (!partID)
1780
2569
  return;
1781
- enqueue(st, { key: `part/${messageID}/${partID}`, item: { type: "part", del: true, data: { id: partID, messageID } } });
2570
+ dropMergedPart(st, sid, messageID, partID);
1782
2571
  }
1783
2572
  } else if (t === "message.updated") {
1784
2573
  const main = childToParent.get(sid) ?? sid;
@@ -1819,13 +2608,13 @@ function createEngine(deps) {
1819
2608
  }
1820
2609
  if (!st || !st.activated || st.closed)
1821
2610
  return;
1822
- enqueue(st, itemFor("message", p.info, sid));
2611
+ ingestMessageInfo(st, sid, p.info);
1823
2612
  const info = p.info;
1824
2613
  if (info?.role === "user" && info?.model) {
1825
2614
  const label = formatLabel(st.opts?.modelLabel ?? cfg.modelLabel, info.model);
1826
2615
  enqueue(st, itemFor("model", [{ id: info.model.modelID, providerID: info.model.providerID, name: label }], sid));
1827
2616
  }
1828
- if (info?.role === "assistant" && info.finish === "error") {
2617
+ if (info?.finish === "error") {
1829
2618
  st.badSignal = true;
1830
2619
  }
1831
2620
  } else if (t === "message.part.updated") {
@@ -1835,12 +2624,12 @@ function createEngine(deps) {
1835
2624
  const part = p.part;
1836
2625
  if (part?.state?.status === "error")
1837
2626
  st.badSignal = true;
1838
- enqueue(st, itemFor("part", part, sid));
2627
+ ingestPart(st, sid, part);
1839
2628
  } else if (t === "session.diff") {
1840
2629
  const st = stFor(sid);
1841
2630
  if (!st || !st.activated || st.closed)
1842
2631
  return;
1843
- enqueue(st, itemFor("session_diff", p.diff, sid));
2632
+ enqueue(st, itemFor("session_diff", normalizeV1Diff(p.diff), sid));
1844
2633
  } else if (t === "session.deleted") {
1845
2634
  const isChild = childToParent.has(sid);
1846
2635
  if (isChild) {
@@ -1884,6 +2673,13 @@ function createEngine(deps) {
1884
2673
  await flushViaHttp(st, r).catch(() => {});
1885
2674
  }
1886
2675
  }
2676
+ for (const r of st.remotes.values()) {
2677
+ if (r.spoolSaveTimer) {
2678
+ clearTimeout(r.spoolSaveTimer);
2679
+ r.spoolSaveTimer = null;
2680
+ spoolSave(st, r);
2681
+ }
2682
+ }
1887
2683
  if (st.metaTimer)
1888
2684
  clearTimeout(st.metaTimer);
1889
2685
  st.metaTimer = null;
@@ -1904,116 +2700,6 @@ function createEngine(deps) {
1904
2700
  // src/v2-server.ts
1905
2701
  import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
1906
2702
  import { join as join3 } from "node:path";
1907
- function toolContentText(content) {
1908
- if (typeof content === "string")
1909
- return content;
1910
- if (Array.isArray(content)) {
1911
- return content.map((item) => {
1912
- if (typeof item === "string")
1913
- return item;
1914
- if (item && typeof item === "object") {
1915
- const record = item;
1916
- if (typeof record.text === "string")
1917
- return record.text;
1918
- }
1919
- return "";
1920
- }).join("");
1921
- }
1922
- return "";
1923
- }
1924
- function convertToolState(state) {
1925
- if (!state || typeof state !== "object")
1926
- return { status: "pending" };
1927
- const record = state;
1928
- const status = String(record.status ?? "pending");
1929
- const mapped = {};
1930
- if (status === "streaming" || status === "running")
1931
- mapped.status = "running";
1932
- else if (status === "completed")
1933
- mapped.status = "completed";
1934
- else if (status === "failed" || status === "error")
1935
- mapped.status = "error";
1936
- else
1937
- mapped.status = "pending";
1938
- if (record.input !== undefined)
1939
- mapped.input = record.input;
1940
- if (record.output !== undefined)
1941
- mapped.output = toolContentText(record.output);
1942
- if (record.error !== undefined)
1943
- mapped.error = record.error;
1944
- if (record.time !== undefined)
1945
- mapped.time = record.time;
1946
- return mapped;
1947
- }
1948
- function convertV2Message(message, sessionID) {
1949
- if (!message || typeof message.id !== "string")
1950
- return;
1951
- const base = {
1952
- id: message.id,
1953
- sessionID
1954
- };
1955
- if (message.type === "user") {
1956
- return {
1957
- info: {
1958
- ...base,
1959
- role: "user",
1960
- time: { created: message.time?.created ?? 0 },
1961
- text: message.text ?? ""
1962
- },
1963
- parts: []
1964
- };
1965
- }
1966
- if (message.type === "assistant") {
1967
- const parts = [];
1968
- const content = Array.isArray(message.content) ? message.content : [];
1969
- content.forEach((block, index) => {
1970
- const record = block;
1971
- const kind = String(record.type ?? "");
1972
- if (kind === "text") {
1973
- parts.push({
1974
- id: `${message.id}_t${index}`,
1975
- messageID: message.id,
1976
- sessionID,
1977
- type: "text",
1978
- text: String(record.text ?? ""),
1979
- synthetic: false,
1980
- time: { start: message.time?.created ?? 0, end: message.time?.completed ?? 0 }
1981
- });
1982
- } else if (kind === "reasoning") {
1983
- parts.push({
1984
- id: `${message.id}_r${index}`,
1985
- messageID: message.id,
1986
- sessionID,
1987
- type: "reasoning",
1988
- text: String(record.text ?? ""),
1989
- synthetic: false,
1990
- time: { start: message.time?.created ?? 0, end: message.time?.completed ?? 0 }
1991
- });
1992
- } else if (kind === "tool") {
1993
- parts.push({
1994
- id: String(record.id ?? `${message.id}_tool${index}`),
1995
- messageID: message.id,
1996
- sessionID,
1997
- type: "tool",
1998
- tool: String(record.name ?? "unknown"),
1999
- state: convertToolState(record.state),
2000
- synthetic: false
2001
- });
2002
- }
2003
- });
2004
- return {
2005
- info: {
2006
- ...base,
2007
- role: "assistant",
2008
- agent: message.agent,
2009
- model: message.model ? { providerID: message.model.providerID, modelID: message.model.modelID ?? message.model.id } : undefined,
2010
- time: { created: message.time?.created ?? 0, completed: message.time?.completed ?? 0 }
2011
- },
2012
- parts
2013
- };
2014
- }
2015
- return;
2016
- }
2017
2703
  function adaptSessionClient(session, message, directory) {
2018
2704
  if (!session)
2019
2705
  return;
@@ -2044,9 +2730,9 @@ function adaptSessionClient(session, message, directory) {
2044
2730
  if (!Array.isArray(data))
2045
2731
  break;
2046
2732
  for (const raw of data) {
2047
- const converted = convertV2Message(raw, sessionID);
2048
- if (converted)
2049
- out.push(converted);
2733
+ if (raw && typeof raw === "object" && typeof raw.id === "string") {
2734
+ out.push({ info: raw, parts: [] });
2735
+ }
2050
2736
  }
2051
2737
  const next = res?.cursor?.next ?? undefined;
2052
2738
  if (!next || data.length === 0)
@@ -2146,6 +2832,7 @@ function createV2ServerSetup() {
2146
2832
  return () => {};
2147
2833
  }
2148
2834
  const pendingRefresh = new Map;
2835
+ const queuedInbox = new Map;
2149
2836
  const refresh = (sessionID) => {
2150
2837
  let timer = pendingRefresh.get(sessionID);
2151
2838
  if (timer)
@@ -2160,7 +2847,10 @@ function createV2ServerSetup() {
2160
2847
  });
2161
2848
  const messages = await client.session.messages?.({ path: { id: sessionID } });
2162
2849
  const list = messages?.data ?? [];
2850
+ const held = queuedInbox.get(sessionID);
2163
2851
  for (const message of list) {
2852
+ if (held?.has(message.info.id))
2853
+ continue;
2164
2854
  await engine.hooks.event({
2165
2855
  event: { type: "message.updated", properties: { sessionID, info: message.info } }
2166
2856
  });
@@ -2193,6 +2883,33 @@ function createV2ServerSetup() {
2193
2883
  await engine.hooks.event({ event: { type, properties: { sessionID: sid } } });
2194
2884
  continue;
2195
2885
  }
2886
+ if (type.startsWith("session.inbox.")) {
2887
+ const data = event.data ?? {};
2888
+ const inboxID = data.inboxID;
2889
+ if (inboxID) {
2890
+ let set = queuedInbox.get(sid);
2891
+ if (!set) {
2892
+ set = new Set;
2893
+ queuedInbox.set(sid, set);
2894
+ }
2895
+ if (type === "session.inbox.enqueued" && data.item?.delivery === "queue") {
2896
+ set.add(inboxID);
2897
+ } else if (type === "session.inbox.delivered") {
2898
+ set.delete(inboxID);
2899
+ refresh(sid);
2900
+ } else if (type === "session.inbox.cancelled") {
2901
+ set.add(inboxID);
2902
+ } else if (type === "session.inbox.delivery.changed") {
2903
+ if (data.item?.delivery === "queue")
2904
+ set.add(inboxID);
2905
+ else {
2906
+ set.delete(inboxID);
2907
+ refresh(sid);
2908
+ }
2909
+ }
2910
+ }
2911
+ continue;
2912
+ }
2196
2913
  if (type === "message.removed" || type === "message.part.removed") {
2197
2914
  const data = event.data ?? {};
2198
2915
  await engine.hooks.event({