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

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
@@ -346,6 +346,215 @@ function resolveShareMode(cfg, input) {
346
346
  // src/core.ts
347
347
  import { appendFileSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
348
348
  import { join as join2 } from "node:path";
349
+
350
+ // src/contract.ts
351
+ function messageTombstone(sessionID, messageID) {
352
+ return { type: "message", del: true, sessionID, data: { id: messageID } };
353
+ }
354
+ function isV2NativeMessage(x) {
355
+ if (!x || typeof x !== "object")
356
+ return false;
357
+ const r = x;
358
+ return typeof r.type === "string" && r.type !== "" && r.role === undefined && typeof r.id === "string";
359
+ }
360
+ var TOOL_NAME_MAP = {
361
+ bash: "shell",
362
+ task: "subagent"
363
+ };
364
+ var INPUT_KEY_MAP = {
365
+ filePath: "path",
366
+ subagent_type: "agent"
367
+ };
368
+ function normalizeToolInput(input, title) {
369
+ const out = {};
370
+ for (const [k, v] of Object.entries(input ?? {}))
371
+ out[INPUT_KEY_MAP[k] ?? k] = v;
372
+ if (title && out.description === undefined)
373
+ out.description = title;
374
+ return out;
375
+ }
376
+ function normalizeToolMetadata(meta) {
377
+ const out = {};
378
+ for (const [k, v] of Object.entries(meta ?? {})) {
379
+ if (k === "filediff")
380
+ out.files = [v];
381
+ else if (k === "sessionId")
382
+ out.sessionID = v;
383
+ else
384
+ out[k] = v;
385
+ }
386
+ return out;
387
+ }
388
+ function toolContent(output) {
389
+ const text = typeof output === "string" ? output : "";
390
+ return [{ type: "text", text: text || " " }];
391
+ }
392
+ function normalizeToolState(state) {
393
+ const status = String(state?.status ?? "pending");
394
+ if (status === "pending") {
395
+ return { status: "streaming", input: typeof state.raw === "string" ? state.raw : JSON.stringify(state.input ?? {}) };
396
+ }
397
+ if (status === "running") {
398
+ return { status: "running", input: normalizeToolInput(state.input ?? {}, state.title), metadata: normalizeToolMetadata(state.metadata) };
399
+ }
400
+ if (status === "completed") {
401
+ return { status: "completed", input: normalizeToolInput(state.input ?? {}, state.title), content: toolContent(state.output), metadata: normalizeToolMetadata(state.metadata) };
402
+ }
403
+ return {
404
+ status: "error",
405
+ input: normalizeToolInput(state.input ?? {}),
406
+ error: { type: "unknown", message: typeof state.error === "string" ? state.error : "tool failed" },
407
+ metadata: normalizeToolMetadata(state.metadata)
408
+ };
409
+ }
410
+ function normalizeFilePart(p) {
411
+ const url = typeof p.url === "string" ? p.url : "";
412
+ if (!url)
413
+ return;
414
+ const inline = url.startsWith("data:");
415
+ const comma = url.indexOf(",");
416
+ return {
417
+ data: inline ? url.slice(comma + 1) : url,
418
+ mime: typeof p.mime === "string" ? p.mime : "application/octet-stream",
419
+ source: inline ? { type: "inline" } : { type: "uri", uri: url },
420
+ ...typeof p.filename === "string" ? { name: p.filename } : {}
421
+ };
422
+ }
423
+ function normalizeV1Message(info, parts) {
424
+ if (!info || typeof info.id !== "string")
425
+ return;
426
+ const id = info.id;
427
+ const created = info.time?.created ?? 0;
428
+ if (info.role === "user") {
429
+ const compaction = parts.find((p) => p?.type === "compaction");
430
+ if (compaction) {
431
+ const message = {
432
+ id,
433
+ type: "compaction",
434
+ status: "completed",
435
+ reason: compaction.auto === false ? "manual" : "auto",
436
+ summary: "",
437
+ recent: "",
438
+ time: { created }
439
+ };
440
+ return { message };
441
+ }
442
+ const text = parts.filter((p) => p?.type === "text" && typeof p.text === "string").map((p) => p.text).join(`
443
+ `);
444
+ const files = parts.filter((p) => p?.type === "file").map(normalizeFilePart).filter((f) => f !== undefined);
445
+ const agents = parts.filter((p) => p?.type === "agent").map((p) => ({ name: String(p.name ?? "") })).filter((a) => a.name);
446
+ const model = info.model;
447
+ const message = {
448
+ id,
449
+ type: "user",
450
+ time: { created },
451
+ text,
452
+ ...files.length ? { files } : {},
453
+ ...agents.length ? { agents } : {},
454
+ metadata: {
455
+ ...info.agent !== undefined ? { agent: String(info.agent) } : {},
456
+ ...model ? { model: { id: String(model.modelID ?? ""), providerID: String(model.providerID ?? "") } } : {}
457
+ }
458
+ };
459
+ return { message };
460
+ }
461
+ if (info.role === "assistant") {
462
+ const time = info.time;
463
+ if (info.summary === true && info.mode === "compaction") {
464
+ const summary = parts.filter((p) => p?.type === "text" && typeof p.text === "string").map((p) => p.text).join(`
465
+ `);
466
+ return { refill: String(info.parentID ?? ""), summary };
467
+ }
468
+ const content = [];
469
+ let cost = typeof info.cost === "number" ? info.cost : 0;
470
+ let tokens;
471
+ let retry;
472
+ for (const p of parts) {
473
+ switch (p?.type) {
474
+ case "text":
475
+ content.push({ type: "text", text: String(p.text ?? "") });
476
+ break;
477
+ case "reasoning": {
478
+ const rt = p.time;
479
+ content.push({ type: "reasoning", text: String(p.text ?? ""), time: { created: rt?.start ?? undefined, completed: rt?.end ?? undefined } });
480
+ break;
481
+ }
482
+ case "tool": {
483
+ const st = p.state;
484
+ const tt = st?.time ?? {};
485
+ content.push({
486
+ type: "tool",
487
+ id: String(p.id ?? ""),
488
+ name: TOOL_NAME_MAP[String(p.tool ?? "unknown")] ?? String(p.tool ?? "unknown"),
489
+ state: normalizeToolState(st ?? {}),
490
+ time: { created: tt.start ?? undefined, completed: tt.end ?? undefined }
491
+ });
492
+ break;
493
+ }
494
+ case "retry": {
495
+ const err = p.error;
496
+ retry = {
497
+ attempt: Number(p.attempt ?? 0),
498
+ at: p.time?.created ?? 0,
499
+ error: { type: "unknown", message: String(err?.message ?? err?.name ?? "retry") }
500
+ };
501
+ break;
502
+ }
503
+ case "step-finish": {
504
+ if (typeof p.cost === "number")
505
+ cost += p.cost;
506
+ if (p.tokens && typeof p.tokens === "object")
507
+ tokens = p.tokens;
508
+ break;
509
+ }
510
+ }
511
+ }
512
+ const finish = typeof info.finish === "string" ? info.finish : undefined;
513
+ const errObj = info.error;
514
+ const message = {
515
+ id,
516
+ type: "assistant",
517
+ agent: String(info.agent ?? "build"),
518
+ model: { id: String(info.modelID ?? ""), providerID: String(info.providerID ?? "") },
519
+ time: { created, streamed: time?.started ?? undefined, completed: time?.completed ?? undefined },
520
+ content,
521
+ cost,
522
+ ...tokens ? { tokens } : {},
523
+ ...finish ? { finish } : {},
524
+ ...errObj ? { error: { type: String(errObj.name ?? "UnknownError"), message: String(errObj.message ?? "turn failed") } } : {},
525
+ ...retry ? { retry } : {}
526
+ };
527
+ return { message };
528
+ }
529
+ return;
530
+ }
531
+ function normalizeV1Diff(diff) {
532
+ if (!Array.isArray(diff))
533
+ return diff;
534
+ return diff.map((d) => {
535
+ if (!d || typeof d !== "object")
536
+ return d;
537
+ const r = d;
538
+ if (typeof r.additions === "number" && typeof r.deletions === "number")
539
+ return r;
540
+ const patch = typeof r.patch === "string" ? r.patch : "";
541
+ let additions = 0;
542
+ let deletions = 0;
543
+ for (const line of patch.split(`
544
+ `)) {
545
+ if (line.startsWith("+") && !line.startsWith("+++"))
546
+ additions++;
547
+ else if (line.startsWith("-") && !line.startsWith("---"))
548
+ deletions++;
549
+ }
550
+ return { ...r, additions: typeof r.additions === "number" ? r.additions : additions, deletions: typeof r.deletions === "number" ? r.deletions : deletions };
551
+ });
552
+ }
553
+ function cursorFor(id, time) {
554
+ return Buffer.from(JSON.stringify({ id, time })).toString("base64url");
555
+ }
556
+
557
+ // src/core.ts
349
558
  var DEFAULT_POLICY = {
350
559
  mode: "persistent",
351
560
  turnGraceMs: 120000,
@@ -381,11 +590,9 @@ function itemFor(type, data, sessionID) {
381
590
  if (type === "prefs")
382
591
  return { key: "prefs", item: { type, data } };
383
592
  if (type === "session")
384
- return { key: `session/${data?.id ?? ""}`, item: { type, data } };
593
+ return { key: `session/${data?.id ?? ""}`, item: { type, sessionID, data } };
385
594
  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 } };
595
+ return { key: `message/${data?.id ?? ""}`, item: { type, sessionID, data } };
389
596
  if (type === "session_diff")
390
597
  return { key: `session_diff/${sessionID}`, item: { type, sessionID, data } };
391
598
  return;
@@ -637,7 +844,8 @@ function createEngine(deps) {
637
844
  opts: undefined,
638
845
  activated: false,
639
846
  wroteBinding: false,
640
- everConnected: false
847
+ everConnected: false,
848
+ merged: new Map
641
849
  });
642
850
  const refreshConfig = () => {
643
851
  const next = loadConfig(deps.pluginDir, deps.options);
@@ -849,6 +1057,14 @@ function createEngine(deps) {
849
1057
  logError("spool save failed", e?.message || e);
850
1058
  }
851
1059
  };
1060
+ const spoolAppend = (st, r, f) => {
1061
+ try {
1062
+ appendFileSync(queuePath(st, r.url), JSON.stringify(f) + `
1063
+ `);
1064
+ } catch (e) {
1065
+ logError("spool append failed", e?.message || e);
1066
+ }
1067
+ };
852
1068
  const spoolLoad = (st, r) => {
853
1069
  try {
854
1070
  const meta = JSON.parse(readFileSync2(metaPath(st, r.url), "utf8"));
@@ -858,8 +1074,18 @@ function createEngine(deps) {
858
1074
  r.ackedRev = meta.ackedRev ?? 0;
859
1075
  }
860
1076
  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));
1077
+ r.pending = [];
1078
+ for (const line of readFileSync2(queuePath(st, r.url), "utf8").split(`
1079
+ `)) {
1080
+ if (!line.trim())
1081
+ continue;
1082
+ try {
1083
+ r.pending.push(JSON.parse(line));
1084
+ } catch {
1085
+ log(`spool load: dropped torn tail line for ${st.sid.slice(0, 18)}`);
1086
+ break;
1087
+ }
1088
+ }
863
1089
  }
864
1090
  } catch {
865
1091
  r.pending = [];
@@ -896,12 +1122,29 @@ function createEngine(deps) {
896
1122
  }
897
1123
  return true;
898
1124
  };
899
- const SYNC_CHUNK = 200;
1125
+ const chunkItems = (items, maxItems, maxBytes) => {
1126
+ const chunks = [];
1127
+ let cur = [];
1128
+ let curBytes = 0;
1129
+ for (const it of items) {
1130
+ const n = (JSON.stringify(it) ?? "null").length;
1131
+ if (cur.length && (cur.length >= maxItems || curBytes + n > maxBytes)) {
1132
+ chunks.push(cur);
1133
+ cur = [];
1134
+ curBytes = 0;
1135
+ }
1136
+ cur.push(it);
1137
+ curBytes += n;
1138
+ }
1139
+ if (cur.length)
1140
+ chunks.push(cur);
1141
+ return chunks;
1142
+ };
900
1143
  const sendSync = (r, f) => {
901
1144
  if (!f.items.length)
902
1145
  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 });
1146
+ for (const chunk of chunkItems(f.items, 200, 512 * 1024)) {
1147
+ wsSend(r, { op: "sync", items: chunk, status: f.status });
905
1148
  }
906
1149
  return true;
907
1150
  };
@@ -1314,6 +1557,7 @@ function createEngine(deps) {
1314
1557
  queueConnect(st, r);
1315
1558
  };
1316
1559
  const frameFrom = (st) => {
1560
+ stampsPass();
1317
1561
  const items = Array.from(st.queue.values());
1318
1562
  st.queue.clear();
1319
1563
  const statusChanged = st.status !== st.lastSentStatus;
@@ -1336,7 +1580,7 @@ function createEngine(deps) {
1336
1580
  let httpWork;
1337
1581
  for (const r of st.remotes.values()) {
1338
1582
  r.pending.push(f);
1339
- spoolSave(st, r);
1583
+ spoolAppend(st, r, f);
1340
1584
  if (r.ws && !r.wsReady)
1341
1585
  continue;
1342
1586
  if (r.wsReady) {
@@ -1363,18 +1607,20 @@ function createEngine(deps) {
1363
1607
  status = f.status;
1364
1608
  }
1365
1609
  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) {
1610
+ for (const chunk of chunkItems(items, 500, 4 * 1024 * 1024)) {
1611
+ const res = await fetch(`${r.url}/api/share/${r.shareId}/sync`, {
1612
+ method: "POST",
1613
+ headers: { "content-type": "application/json", ...cred.credential ? { authorization: `Bearer ${cred.credential}` } : {} },
1614
+ body: JSON.stringify({ secret: r.secret ?? "", data: chunk, status }),
1615
+ signal: AbortSignal.timeout(15000)
1616
+ }).catch(() => null);
1617
+ if (!res || !res.ok)
1618
+ return;
1373
1619
  const body = await res.json().catch(() => ({}));
1374
1620
  r.ackedRev = body.rev ?? r.ackedRev;
1375
- r.pending = [];
1376
- spoolSave(st, r);
1377
1621
  }
1622
+ r.pending = [];
1623
+ spoolSave(st, r);
1378
1624
  };
1379
1625
  const scheduleFlush = (st) => {
1380
1626
  if (st.timer)
@@ -1392,12 +1638,284 @@ function createEngine(deps) {
1392
1638
  if (!st || !entry)
1393
1639
  return;
1394
1640
  st.queue.set(entry.key, entry.item);
1641
+ indexMessage(entry);
1395
1642
  if (debugEvents)
1396
1643
  log(`enqueue sid=${st.sid.slice(0, 18)} ${entry.item.type} queue=${st.queue.size}`);
1397
1644
  touch(st);
1398
1645
  ensureConnected(st);
1399
1646
  scheduleFlush(st);
1400
1647
  };
1648
+ const msgIndex = new Map;
1649
+ const indexMessage = (entry) => {
1650
+ if (entry.item.type !== "message")
1651
+ return;
1652
+ const d = entry.item.data;
1653
+ if (!d || typeof d.id !== "string")
1654
+ return;
1655
+ const role = String(d.type ?? "");
1656
+ if (role !== "user" && role !== "assistant")
1657
+ return;
1658
+ const key = `${entry.item.sessionID}:${d.id}`;
1659
+ const prev = msgIndex.get(key);
1660
+ msgIndex.set(key, {
1661
+ sid: String(entry.item.sessionID ?? ""),
1662
+ role,
1663
+ agent: role === "assistant" ? typeof d.agent === "string" ? d.agent : prev?.agent : typeof d.metadata?.agent === "string" ? d.metadata.agent : prev?.agent,
1664
+ created: Number(d.time?.created ?? prev?.created ?? 0),
1665
+ data: d
1666
+ });
1667
+ };
1668
+ const sessionStateOf = (sid) => sessions.get(childToParent.get(sid) ?? sid);
1669
+ const reEnqueueMessage = (m) => {
1670
+ const st = sessionStateOf(m.sid);
1671
+ if (st)
1672
+ enqueue(st, itemFor("message", m.data, m.sid));
1673
+ };
1674
+ const segmentAgent = (ordered, at) => {
1675
+ const own = ordered[at]?.agent;
1676
+ if (own)
1677
+ return own;
1678
+ for (let i = at + 1;i < ordered.length; i++) {
1679
+ if (ordered[i].role === "assistant" && ordered[i].agent)
1680
+ return ordered[i].agent;
1681
+ if (ordered[i].role === "user")
1682
+ break;
1683
+ }
1684
+ return;
1685
+ };
1686
+ const stampAgentSwitches = () => {
1687
+ const bySession = new Map;
1688
+ for (const m of msgIndex.values()) {
1689
+ const list = bySession.get(m.sid);
1690
+ if (list)
1691
+ list.push(m);
1692
+ else
1693
+ bySession.set(m.sid, [m]);
1694
+ }
1695
+ for (const [, list] of bySession) {
1696
+ list.sort((a, b) => a.created - b.created);
1697
+ const users = list.map((m, i) => m.role === "user" ? i : -1).filter((i) => i >= 0);
1698
+ let prev;
1699
+ for (let k = 0;k < users.length; k++) {
1700
+ const agent = segmentAgent(list, users[k]);
1701
+ const d = list[users[k]].data;
1702
+ const meta = d.metadata ??= {};
1703
+ const want = k > 0 && agent && prev && agent !== prev ? agent : undefined;
1704
+ if (want !== meta.agentSwitch) {
1705
+ if (want)
1706
+ meta.agentSwitch = want;
1707
+ else
1708
+ delete meta.agentSwitch;
1709
+ reEnqueueMessage(list[users[k]]);
1710
+ }
1711
+ if (agent)
1712
+ prev = agent;
1713
+ }
1714
+ }
1715
+ };
1716
+ const stampTaskAgents = () => {
1717
+ const bySession = new Map;
1718
+ for (const m of msgIndex.values()) {
1719
+ const list = bySession.get(m.sid);
1720
+ if (list)
1721
+ list.push(m);
1722
+ else
1723
+ bySession.set(m.sid, [m]);
1724
+ }
1725
+ for (const list of bySession.values())
1726
+ list.sort((a, b) => a.created - b.created);
1727
+ for (const m of msgIndex.values()) {
1728
+ if (m.role !== "assistant")
1729
+ continue;
1730
+ const content = m.data.content;
1731
+ if (!Array.isArray(content))
1732
+ continue;
1733
+ let mutated = false;
1734
+ for (const part of content) {
1735
+ if (!part || part.type !== "tool")
1736
+ continue;
1737
+ if (part.name !== "task" && part.name !== "subagent")
1738
+ continue;
1739
+ const state = part.state;
1740
+ const meta = state?.metadata;
1741
+ const childSid = typeof meta?.sessionID === "string" ? meta.sessionID : undefined;
1742
+ if (!childSid || typeof meta.agent === "string")
1743
+ continue;
1744
+ const partStart = Number(part.time?.created ?? 0);
1745
+ const child = bySession.get(childSid);
1746
+ if (!child)
1747
+ continue;
1748
+ let agent;
1749
+ for (const c of child) {
1750
+ if (c.created < partStart)
1751
+ continue;
1752
+ if (c.role === "user" && c.agent) {
1753
+ agent = c.agent;
1754
+ break;
1755
+ }
1756
+ }
1757
+ if (!agent) {
1758
+ for (const c of child) {
1759
+ if (c.created < partStart)
1760
+ continue;
1761
+ if (c.role === "assistant" && c.agent) {
1762
+ agent = c.agent;
1763
+ break;
1764
+ }
1765
+ }
1766
+ }
1767
+ if (agent) {
1768
+ meta.agent = agent;
1769
+ mutated = true;
1770
+ }
1771
+ }
1772
+ if (mutated)
1773
+ reEnqueueMessage(m);
1774
+ }
1775
+ };
1776
+ const LANG_BY_EXT = {
1777
+ ts: "typescript",
1778
+ tsx: "tsx",
1779
+ mts: "typescript",
1780
+ cts: "typescript",
1781
+ js: "javascript",
1782
+ jsx: "jsx",
1783
+ mjs: "javascript",
1784
+ cjs: "javascript",
1785
+ json: "json",
1786
+ jsonc: "json",
1787
+ md: "markdown",
1788
+ mdx: "mdx",
1789
+ py: "python",
1790
+ rs: "rust",
1791
+ go: "go",
1792
+ java: "java",
1793
+ rb: "ruby",
1794
+ css: "css",
1795
+ scss: "scss",
1796
+ html: "html",
1797
+ xml: "xml",
1798
+ yaml: "yaml",
1799
+ yml: "yaml",
1800
+ toml: "toml",
1801
+ sh: "shellscript",
1802
+ bash: "shellscript",
1803
+ ps1: "powershell",
1804
+ sql: "sql",
1805
+ c: "c",
1806
+ h: "c",
1807
+ cpp: "cpp",
1808
+ hpp: "cpp",
1809
+ cs: "csharp",
1810
+ swift: "swift",
1811
+ kt: "kotlin",
1812
+ php: "php",
1813
+ lua: "lua",
1814
+ vue: "vue",
1815
+ svelte: "svelte"
1816
+ };
1817
+ const FILE_TOOLS = new Set(["read", "write", "edit", "patch"]);
1818
+ const stampFileLanguages = () => {
1819
+ for (const m of msgIndex.values()) {
1820
+ if (m.role !== "assistant")
1821
+ continue;
1822
+ const content = m.data.content;
1823
+ if (!Array.isArray(content))
1824
+ continue;
1825
+ let mutated = false;
1826
+ for (const part of content) {
1827
+ if (!part || part.type !== "tool")
1828
+ continue;
1829
+ if (!FILE_TOOLS.has(part.name))
1830
+ continue;
1831
+ const state = part.state;
1832
+ const meta = state?.metadata;
1833
+ if (!meta)
1834
+ continue;
1835
+ const raw = state.input?.path ?? state.input?.filePath;
1836
+ if (typeof meta.language !== "string" && typeof raw === "string") {
1837
+ const ext = raw.split(".").pop()?.toLowerCase() ?? "";
1838
+ const lang = LANG_BY_EXT[ext];
1839
+ if (lang) {
1840
+ meta.language = lang;
1841
+ mutated = true;
1842
+ }
1843
+ }
1844
+ if (typeof meta.lineStart !== "number" && part.name === "read") {
1845
+ const off = state.input?.offset;
1846
+ if (typeof off === "number" && off >= 0 && off > 0) {
1847
+ meta.lineStart = off + 1;
1848
+ mutated = true;
1849
+ }
1850
+ }
1851
+ }
1852
+ if (mutated)
1853
+ reEnqueueMessage(m);
1854
+ }
1855
+ };
1856
+ const stampsPass = () => {
1857
+ stampAgentSwitches();
1858
+ stampTaskAgents();
1859
+ stampFileLanguages();
1860
+ };
1861
+ const mergeEntry = (st, messageID) => {
1862
+ let e = st.merged.get(messageID);
1863
+ if (!e) {
1864
+ e = { parts: new Map };
1865
+ st.merged.set(messageID, e);
1866
+ }
1867
+ return e;
1868
+ };
1869
+ const emitMerged = (st, sid, messageID) => {
1870
+ const e = st.merged.get(messageID);
1871
+ if (!e?.info)
1872
+ return;
1873
+ const res = normalizeV1Message(e.info, [...e.parts.values()]);
1874
+ if (!res)
1875
+ return;
1876
+ if ("refill" in res) {
1877
+ const parent = st.merged.get(res.refill);
1878
+ if (parent?.info) {
1879
+ const compaction = normalizeV1Message(parent.info, [...parent.parts.values()]);
1880
+ if (compaction && "message" in compaction) {
1881
+ const msg = compaction.message;
1882
+ msg.summary = res.summary;
1883
+ enqueue(st, itemFor("message", msg, sid));
1884
+ }
1885
+ }
1886
+ return;
1887
+ }
1888
+ enqueue(st, itemFor("message", res.message, sid));
1889
+ };
1890
+ const ingestMessageInfo = (st, sid, info) => {
1891
+ if (isV2NativeMessage(info)) {
1892
+ enqueue(st, itemFor("message", info, sid));
1893
+ return;
1894
+ }
1895
+ if (typeof info?.id !== "string")
1896
+ return;
1897
+ const e = mergeEntry(st, info.id);
1898
+ e.info = info;
1899
+ emitMerged(st, sid, info.id);
1900
+ };
1901
+ const ingestPart = (st, sid, part) => {
1902
+ if (typeof part?.id !== "string" || typeof part.messageID !== "string")
1903
+ return;
1904
+ const e = mergeEntry(st, part.messageID);
1905
+ e.parts.set(part.id, part);
1906
+ emitMerged(st, sid, part.messageID);
1907
+ };
1908
+ const dropMergedMessage = (st, messageID) => {
1909
+ st.merged.delete(messageID);
1910
+ };
1911
+ const dropMergedPart = (st, sid, messageID, partID) => {
1912
+ const e = st.merged.get(messageID);
1913
+ if (!e)
1914
+ return;
1915
+ e.parts.delete(partID);
1916
+ if (e.info)
1917
+ emitMerged(st, sid, messageID);
1918
+ };
1401
1919
  const remove = (st) => {
1402
1920
  st.closed = true;
1403
1921
  st.activated = false;
@@ -1470,7 +1988,7 @@ function createEngine(deps) {
1470
1988
  const PAGE_SIZE = 100;
1471
1989
  const MAX_PAGES = 50;
1472
1990
  const query = () => deps.directory ? { directory: deps.directory } : {};
1473
- async function fetchMessages(client, sid) {
1991
+ async function fetchMessages(client, sid, onPage) {
1474
1992
  if (typeof client.session.messages !== "function")
1475
1993
  return [];
1476
1994
  const out = [];
@@ -1486,11 +2004,21 @@ function createEngine(deps) {
1486
2004
  if (!Array.isArray(result) || result.length === 0)
1487
2005
  break;
1488
2006
  out.push(...result);
2007
+ if (onPage) {
2008
+ try {
2009
+ onPage(result);
2010
+ } catch {}
2011
+ }
1489
2012
  if (result.length < PAGE_SIZE)
1490
2013
  break;
1491
2014
  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)
2015
+ const lastInfo = last?.info ?? last;
2016
+ const lastID = typeof lastInfo?.id === "string" ? lastInfo.id : undefined;
2017
+ const lastTime = lastInfo?.time?.created;
2018
+ if (!lastID || typeof lastTime !== "number")
2019
+ break;
2020
+ const nextBefore = cursorFor(lastID, lastTime);
2021
+ if (nextBefore === before)
1494
2022
  break;
1495
2023
  before = nextBefore;
1496
2024
  }
@@ -1500,27 +2028,31 @@ function createEngine(deps) {
1500
2028
  const sessionInfoValue = info ?? await sessionInfo(client, sid);
1501
2029
  if (sessionInfoValue)
1502
2030
  enqueue(st, itemFor("session", redact(sessionInfoValue), sid));
1503
- const messages = await fetchMessages(client, sid);
1504
2031
  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")
2032
+ const consume = (messages) => {
2033
+ for (const message of messages) {
2034
+ const m = message.info ?? message;
2035
+ if (!m || typeof m.id !== "string")
1518
2036
  continue;
1519
- if (p.state?.status === "error")
1520
- st.badSignal = true;
1521
- enqueue(st, itemFor("part", p, sid));
2037
+ const e = mergeEntry(st, m.id);
2038
+ e.info = m;
2039
+ for (const rawPart of message.parts ?? []) {
2040
+ const p = rawPart;
2041
+ if (!p || typeof p.id !== "string" || typeof p.messageID !== "string")
2042
+ continue;
2043
+ if (p.state?.status === "error")
2044
+ st.badSignal = true;
2045
+ e.parts.set(p.id, p);
2046
+ }
2047
+ emitMerged(st, sid, m.id);
2048
+ if (m.role === "user" && m.model) {
2049
+ modelSeen = true;
2050
+ const label = formatLabel(st.opts?.modelLabel ?? cfg.modelLabel, m.model);
2051
+ enqueue(st, itemFor("model", [{ id: m.model.modelID, providerID: m.model.providerID, name: label }], sid));
2052
+ }
1522
2053
  }
1523
- }
2054
+ };
2055
+ const messages = await fetchMessages(client, sid, (page) => consume(page));
1524
2056
  if (!modelSeen && sessionInfoValue?.model) {
1525
2057
  const label = formatLabel(st.opts?.modelLabel ?? cfg.modelLabel, sessionInfoValue.model);
1526
2058
  enqueue(st, itemFor("model", [{ id: sessionInfoValue.model.modelID, providerID: sessionInfoValue.model.providerID, name: label }], sid));
@@ -1773,12 +2305,13 @@ function createEngine(deps) {
1773
2305
  if (!messageID)
1774
2306
  return;
1775
2307
  if (t === "message.removed") {
1776
- enqueue(st, { key: `message/${messageID}`, item: { type: "message", del: true, data: { id: messageID } } });
2308
+ dropMergedMessage(st, messageID);
2309
+ enqueue(st, { key: `message/${messageID}`, item: messageTombstone(st.sid, messageID) });
1777
2310
  } else {
1778
2311
  const partID = p.partID;
1779
2312
  if (!partID)
1780
2313
  return;
1781
- enqueue(st, { key: `part/${messageID}/${partID}`, item: { type: "part", del: true, data: { id: partID, messageID } } });
2314
+ dropMergedPart(st, sid, messageID, partID);
1782
2315
  }
1783
2316
  } else if (t === "message.updated") {
1784
2317
  const main = childToParent.get(sid) ?? sid;
@@ -1819,13 +2352,13 @@ function createEngine(deps) {
1819
2352
  }
1820
2353
  if (!st || !st.activated || st.closed)
1821
2354
  return;
1822
- enqueue(st, itemFor("message", p.info, sid));
2355
+ ingestMessageInfo(st, sid, p.info);
1823
2356
  const info = p.info;
1824
2357
  if (info?.role === "user" && info?.model) {
1825
2358
  const label = formatLabel(st.opts?.modelLabel ?? cfg.modelLabel, info.model);
1826
2359
  enqueue(st, itemFor("model", [{ id: info.model.modelID, providerID: info.model.providerID, name: label }], sid));
1827
2360
  }
1828
- if (info?.role === "assistant" && info.finish === "error") {
2361
+ if (info?.finish === "error") {
1829
2362
  st.badSignal = true;
1830
2363
  }
1831
2364
  } else if (t === "message.part.updated") {
@@ -1835,12 +2368,12 @@ function createEngine(deps) {
1835
2368
  const part = p.part;
1836
2369
  if (part?.state?.status === "error")
1837
2370
  st.badSignal = true;
1838
- enqueue(st, itemFor("part", part, sid));
2371
+ ingestPart(st, sid, part);
1839
2372
  } else if (t === "session.diff") {
1840
2373
  const st = stFor(sid);
1841
2374
  if (!st || !st.activated || st.closed)
1842
2375
  return;
1843
- enqueue(st, itemFor("session_diff", p.diff, sid));
2376
+ enqueue(st, itemFor("session_diff", normalizeV1Diff(p.diff), sid));
1844
2377
  } else if (t === "session.deleted") {
1845
2378
  const isChild = childToParent.has(sid);
1846
2379
  if (isChild) {
@@ -1904,116 +2437,6 @@ function createEngine(deps) {
1904
2437
  // src/v2-server.ts
1905
2438
  import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
1906
2439
  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
2440
  function adaptSessionClient(session, message, directory) {
2018
2441
  if (!session)
2019
2442
  return;
@@ -2044,9 +2467,9 @@ function adaptSessionClient(session, message, directory) {
2044
2467
  if (!Array.isArray(data))
2045
2468
  break;
2046
2469
  for (const raw of data) {
2047
- const converted = convertV2Message(raw, sessionID);
2048
- if (converted)
2049
- out.push(converted);
2470
+ if (raw && typeof raw === "object" && typeof raw.id === "string") {
2471
+ out.push({ info: raw, parts: [] });
2472
+ }
2050
2473
  }
2051
2474
  const next = res?.cursor?.next ?? undefined;
2052
2475
  if (!next || data.length === 0)