@mirrowel/opencode-openshare 0.1.0 → 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
@@ -13,12 +13,15 @@
13
13
  // lands in the log and optionally in `urlFile`).
14
14
  // - Interactive opt-in: install the package's TUI part (tui.json) too —
15
15
  // the server detects its presence and switches to manual mode
16
- // (`/share`, `/unshare` in the TUI).
16
+ // (`/share` + `/openshare` in the TUI).
17
17
  //
18
18
  // Config: env (OPENSHARE_*) > openshare.config.json (next to this file)
19
- // > code defaults. Keys: workerUrl, createToken, debounceMs, modelLabel,
20
- // ttlDays, openTasksInNewTab, authLogin, authPassword, spoolDir, urlFile,
21
- // shareMode ("all" | "manual"; default = TUI presence).
19
+ // > code defaults. Keys: servers (multi-server: url -> {token, code,
20
+ // label, active, adminToken}), workerUrl, createToken (legacy pair),
21
+ // shareMode ("all" | "manual"; default = TUI presence), debounceMs,
22
+ // completedDelayMs, modelLabel, ttlDays, openTasksInNewTab,
23
+ // authLogin, authPassword, spoolDir, urlFile ("none" = off),
24
+ // announceStderr.
22
25
  //
23
26
  // src/config.ts
24
27
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
@@ -117,8 +120,8 @@ function resolveServer(cfg) {
117
120
  if (cfg.workerUrl)
118
121
  servers[cfg.workerUrl] = { token: cfg.createToken || "", adminToken: cfg.createToken || "", active: true };
119
122
  if (cfg.servers)
120
- for (const [url2, entry] of Object.entries(cfg.servers))
121
- servers[url2] = entry;
123
+ for (const [url, entry] of Object.entries(cfg.servers))
124
+ servers[url] = entry;
122
125
  const urls = Object.keys(servers);
123
126
  if (!urls.length)
124
127
  return;
@@ -245,7 +248,9 @@ function specPath(spec) {
245
248
  function isOpenShareSpec(spec) {
246
249
  if (typeof spec !== "string" || spec.length === 0)
247
250
  return false;
248
- if (spec === NPM_NAME || spec.startsWith(`${NPM_NAME}@`))
251
+ if (spec === NPM_NAME)
252
+ return true;
253
+ if (spec.startsWith(`${NPM_NAME}@`) || spec.startsWith(`${NPM_NAME}/`))
249
254
  return true;
250
255
  const path = specPath(spec);
251
256
  if (path) {
@@ -341,6 +346,215 @@ function resolveShareMode(cfg, input) {
341
346
  // src/core.ts
342
347
  import { appendFileSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
343
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
344
558
  var DEFAULT_POLICY = {
345
559
  mode: "persistent",
346
560
  turnGraceMs: 120000,
@@ -376,11 +590,9 @@ function itemFor(type, data, sessionID) {
376
590
  if (type === "prefs")
377
591
  return { key: "prefs", item: { type, data } };
378
592
  if (type === "session")
379
- return { key: `session/${data?.id ?? ""}`, item: { type, data } };
593
+ return { key: `session/${data?.id ?? ""}`, item: { type, sessionID, data } };
380
594
  if (type === "message")
381
- return { key: `message/${data?.id ?? ""}`, item: { type, data } };
382
- if (type === "part")
383
- return { key: `part/${data?.messageID ?? ""}/${data?.id ?? ""}`, item: { type, data } };
595
+ return { key: `message/${data?.id ?? ""}`, item: { type, sessionID, data } };
384
596
  if (type === "session_diff")
385
597
  return { key: `session_diff/${sessionID}`, item: { type, sessionID, data } };
386
598
  return;
@@ -568,19 +780,19 @@ function createEngine(deps) {
568
780
  if (!raw || typeof raw !== "object")
569
781
  return;
570
782
  const p = raw;
571
- const num2 = (v, d) => {
783
+ const num = (v, d) => {
572
784
  const n = Number(v);
573
785
  return Number.isFinite(n) && n >= 0 ? n : d;
574
786
  };
575
787
  const prev = policyOf(url);
576
788
  policyMap.set(url, {
577
789
  mode: p.mode === "turn" ? "turn" : "persistent",
578
- turnGraceMs: num2(p.turnGraceMs, prev.turnGraceMs),
579
- idleDropMs: num2(p.idleDropMs, prev.idleDropMs),
580
- finalizeAfterMs: num2(p.finalizeAfterMs, prev.finalizeAfterMs),
581
- debounceMinMs: num2(p.debounceMinMs, 0),
582
- debounceMaxMs: Math.max(num2(p.debounceMaxMs, Infinity), num2(p.debounceMinMs, 0)),
583
- completedDelayMaxMs: num2(p.completedDelayMaxMs, Infinity)
790
+ turnGraceMs: num(p.turnGraceMs, prev.turnGraceMs),
791
+ idleDropMs: num(p.idleDropMs, prev.idleDropMs),
792
+ finalizeAfterMs: num(p.finalizeAfterMs, prev.finalizeAfterMs),
793
+ debounceMinMs: num(p.debounceMinMs, 0),
794
+ debounceMaxMs: Math.max(num(p.debounceMaxMs, Infinity), num(p.debounceMinMs, 0)),
795
+ completedDelayMaxMs: num(p.completedDelayMaxMs, Infinity)
584
796
  });
585
797
  clampToPolicy();
586
798
  if (debugEvents)
@@ -632,7 +844,8 @@ function createEngine(deps) {
632
844
  opts: undefined,
633
845
  activated: false,
634
846
  wroteBinding: false,
635
- everConnected: false
847
+ everConnected: false,
848
+ merged: new Map
636
849
  });
637
850
  const refreshConfig = () => {
638
851
  const next = loadConfig(deps.pluginDir, deps.options);
@@ -844,6 +1057,14 @@ function createEngine(deps) {
844
1057
  logError("spool save failed", e?.message || e);
845
1058
  }
846
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
+ };
847
1068
  const spoolLoad = (st, r) => {
848
1069
  try {
849
1070
  const meta = JSON.parse(readFileSync2(metaPath(st, r.url), "utf8"));
@@ -853,8 +1074,18 @@ function createEngine(deps) {
853
1074
  r.ackedRev = meta.ackedRev ?? 0;
854
1075
  }
855
1076
  if (existsSync2(queuePath(st, r.url))) {
856
- r.pending = readFileSync2(queuePath(st, r.url), "utf8").split(`
857
- `).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
+ }
858
1089
  }
859
1090
  } catch {
860
1091
  r.pending = [];
@@ -891,12 +1122,29 @@ function createEngine(deps) {
891
1122
  }
892
1123
  return true;
893
1124
  };
894
- 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
+ };
895
1143
  const sendSync = (r, f) => {
896
1144
  if (!f.items.length)
897
1145
  return wsSend(r, { op: "sync", items: [], status: f.status });
898
- for (let i = 0;i < f.items.length; i += SYNC_CHUNK) {
899
- 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 });
900
1148
  }
901
1149
  return true;
902
1150
  };
@@ -1309,6 +1557,7 @@ function createEngine(deps) {
1309
1557
  queueConnect(st, r);
1310
1558
  };
1311
1559
  const frameFrom = (st) => {
1560
+ stampsPass();
1312
1561
  const items = Array.from(st.queue.values());
1313
1562
  st.queue.clear();
1314
1563
  const statusChanged = st.status !== st.lastSentStatus;
@@ -1331,7 +1580,7 @@ function createEngine(deps) {
1331
1580
  let httpWork;
1332
1581
  for (const r of st.remotes.values()) {
1333
1582
  r.pending.push(f);
1334
- spoolSave(st, r);
1583
+ spoolAppend(st, r, f);
1335
1584
  if (r.ws && !r.wsReady)
1336
1585
  continue;
1337
1586
  if (r.wsReady) {
@@ -1358,18 +1607,20 @@ function createEngine(deps) {
1358
1607
  status = f.status;
1359
1608
  }
1360
1609
  const cred = credOf(r.url);
1361
- const res = await fetch(`${r.url}/api/share/${r.shareId}/sync`, {
1362
- method: "POST",
1363
- headers: { "content-type": "application/json", ...cred.credential ? { authorization: `Bearer ${cred.credential}` } : {} },
1364
- body: JSON.stringify({ secret: r.secret ?? "", data: items, status }),
1365
- signal: AbortSignal.timeout(15000)
1366
- }).catch(() => null);
1367
- 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;
1368
1619
  const body = await res.json().catch(() => ({}));
1369
1620
  r.ackedRev = body.rev ?? r.ackedRev;
1370
- r.pending = [];
1371
- spoolSave(st, r);
1372
1621
  }
1622
+ r.pending = [];
1623
+ spoolSave(st, r);
1373
1624
  };
1374
1625
  const scheduleFlush = (st) => {
1375
1626
  if (st.timer)
@@ -1387,12 +1638,284 @@ function createEngine(deps) {
1387
1638
  if (!st || !entry)
1388
1639
  return;
1389
1640
  st.queue.set(entry.key, entry.item);
1641
+ indexMessage(entry);
1390
1642
  if (debugEvents)
1391
1643
  log(`enqueue sid=${st.sid.slice(0, 18)} ${entry.item.type} queue=${st.queue.size}`);
1392
1644
  touch(st);
1393
1645
  ensureConnected(st);
1394
1646
  scheduleFlush(st);
1395
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
+ };
1396
1919
  const remove = (st) => {
1397
1920
  st.closed = true;
1398
1921
  st.activated = false;
@@ -1465,7 +1988,7 @@ function createEngine(deps) {
1465
1988
  const PAGE_SIZE = 100;
1466
1989
  const MAX_PAGES = 50;
1467
1990
  const query = () => deps.directory ? { directory: deps.directory } : {};
1468
- async function fetchMessages(client, sid) {
1991
+ async function fetchMessages(client, sid, onPage) {
1469
1992
  if (typeof client.session.messages !== "function")
1470
1993
  return [];
1471
1994
  const out = [];
@@ -1481,11 +2004,21 @@ function createEngine(deps) {
1481
2004
  if (!Array.isArray(result) || result.length === 0)
1482
2005
  break;
1483
2006
  out.push(...result);
2007
+ if (onPage) {
2008
+ try {
2009
+ onPage(result);
2010
+ } catch {}
2011
+ }
1484
2012
  if (result.length < PAGE_SIZE)
1485
2013
  break;
1486
2014
  const last = result[result.length - 1];
1487
- const nextBefore = typeof last?.info?.id === "string" ? last.info.id : typeof last?.id === "string" ? last.id : undefined;
1488
- 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)
1489
2022
  break;
1490
2023
  before = nextBefore;
1491
2024
  }
@@ -1495,27 +2028,31 @@ function createEngine(deps) {
1495
2028
  const sessionInfoValue = info ?? await sessionInfo(client, sid);
1496
2029
  if (sessionInfoValue)
1497
2030
  enqueue(st, itemFor("session", redact(sessionInfoValue), sid));
1498
- const messages = await fetchMessages(client, sid);
1499
2031
  let modelSeen = false;
1500
- for (const message of messages) {
1501
- const m = message.info ?? message;
1502
- if (!m || typeof m.id !== "string")
1503
- continue;
1504
- enqueue(st, itemFor("message", m, sid));
1505
- if (m.role === "user" && m.model) {
1506
- modelSeen = true;
1507
- const label = formatLabel(st.opts?.modelLabel ?? cfg.modelLabel, m.model);
1508
- enqueue(st, itemFor("model", [{ id: m.model.modelID, providerID: m.model.providerID, name: label }], sid));
1509
- }
1510
- for (const part of message.parts ?? []) {
1511
- const p = part;
1512
- 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")
1513
2036
  continue;
1514
- if (p.state?.status === "error")
1515
- st.badSignal = true;
1516
- 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
+ }
1517
2053
  }
1518
- }
2054
+ };
2055
+ const messages = await fetchMessages(client, sid, (page) => consume(page));
1519
2056
  if (!modelSeen && sessionInfoValue?.model) {
1520
2057
  const label = formatLabel(st.opts?.modelLabel ?? cfg.modelLabel, sessionInfoValue.model);
1521
2058
  enqueue(st, itemFor("model", [{ id: sessionInfoValue.model.modelID, providerID: sessionInfoValue.model.providerID, name: label }], sid));
@@ -1605,31 +2142,31 @@ function createEngine(deps) {
1605
2142
  const parent = trackChild(sid, info);
1606
2143
  if (mode === "manual") {
1607
2144
  if (parent) {
1608
- const st3 = sessions.get(parent);
1609
- if (st3?.activated && !st3.closed) {
1610
- if (!st3.prefsSent) {
1611
- st3.prefsSent = true;
1612
- enqueue(st3, itemFor("prefs", { openTasksInNewTab: cfg.openTasksInNewTab !== false }, sid));
2145
+ const st = sessions.get(parent);
2146
+ if (st?.activated && !st.closed) {
2147
+ if (!st.prefsSent) {
2148
+ st.prefsSent = true;
2149
+ enqueue(st, itemFor("prefs", { openTasksInNewTab: cfg.openTasksInNewTab !== false }, sid));
1613
2150
  }
1614
2151
  if (info)
1615
- enqueue(st3, itemFor("session", redact(info), sid));
2152
+ enqueue(st, itemFor("session", redact(info), sid));
1616
2153
  }
1617
2154
  return;
1618
2155
  }
1619
2156
  const flag = shareFlag(info);
1620
- const st2 = sessions.get(sid);
2157
+ const st = sessions.get(sid);
1621
2158
  if (flag.enabled) {
1622
- if (st2?.activated && !st2.closed) {
2159
+ if (st?.activated && !st.closed) {
1623
2160
  if (flag.opts)
1624
- st2.opts = flag.opts;
1625
- if (!st2.prefsSent) {
1626
- st2.prefsSent = true;
1627
- enqueue(st2, itemFor("prefs", { openTasksInNewTab: cfg.openTasksInNewTab !== false }, sid));
2161
+ st.opts = flag.opts;
2162
+ if (!st.prefsSent) {
2163
+ st.prefsSent = true;
2164
+ enqueue(st, itemFor("prefs", { openTasksInNewTab: cfg.openTasksInNewTab !== false }, sid));
1628
2165
  }
1629
2166
  if (info)
1630
- enqueue(st2, itemFor("session", redact(info), sid));
2167
+ enqueue(st, itemFor("session", redact(info), sid));
1631
2168
  if (t === "session.updated")
1632
- chain = chain.then(() => refreshOptOuts(st2)).catch(() => {});
2169
+ chain = chain.then(() => refreshOptOuts(st)).catch(() => {});
1633
2170
  return;
1634
2171
  }
1635
2172
  activate(sid, flag.opts, info);
@@ -1640,9 +2177,9 @@ function createEngine(deps) {
1640
2177
  }
1641
2178
  return;
1642
2179
  }
1643
- if (st2?.activated && !st2.closed && t === "session.updated") {
2180
+ if (st?.activated && !st.closed && t === "session.updated") {
1644
2181
  log(`share disabled for ${sid} - removing`);
1645
- chain = chain.then(() => remove(st2)).catch((e) => logError("remove error", e?.message || e));
2182
+ chain = chain.then(() => remove(st)).catch((e) => logError("remove error", e?.message || e));
1646
2183
  return;
1647
2184
  }
1648
2185
  return;
@@ -1768,12 +2305,13 @@ function createEngine(deps) {
1768
2305
  if (!messageID)
1769
2306
  return;
1770
2307
  if (t === "message.removed") {
1771
- 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) });
1772
2310
  } else {
1773
2311
  const partID = p.partID;
1774
2312
  if (!partID)
1775
2313
  return;
1776
- enqueue(st, { key: `part/${messageID}/${partID}`, item: { type: "part", del: true, data: { id: partID, messageID } } });
2314
+ dropMergedPart(st, sid, messageID, partID);
1777
2315
  }
1778
2316
  } else if (t === "message.updated") {
1779
2317
  const main = childToParent.get(sid) ?? sid;
@@ -1788,16 +2326,16 @@ function createEngine(deps) {
1788
2326
  chain = chain.then(async () => {
1789
2327
  if (created.closed)
1790
2328
  return;
1791
- const info2 = await sessionInfo(created.client, main);
2329
+ const info = await sessionInfo(created.client, main);
1792
2330
  verifying.delete(main);
1793
2331
  if (created.closed)
1794
2332
  return;
1795
- const flag = shareFlag(info2);
1796
- const hasBinding = !!info2?.metadata?.[METADATA_KEY];
1797
- const createdMs = info2?.time?.created;
2333
+ const flag = shareFlag(info);
2334
+ const hasBinding = !!info?.metadata?.[METADATA_KEY];
2335
+ const createdMs = info?.time?.created;
1798
2336
  const isPreExisting = typeof createdMs === "number" && createdMs < bootTime;
1799
2337
  if (debugEvents) {
1800
- log(`verify ${main}: created=${createdMs} boot=${bootTime} pre=${isPreExisting} flag=${flag.enabled} binding=${hasBinding} infoMissing=${!info2}`);
2338
+ log(`verify ${main}: created=${createdMs} boot=${bootTime} pre=${isPreExisting} flag=${flag.enabled} binding=${hasBinding} infoMissing=${!info}`);
1801
2339
  }
1802
2340
  if (isPreExisting && !flag.enabled && !hasBinding) {
1803
2341
  preExistingRetired.add(main);
@@ -1814,13 +2352,13 @@ function createEngine(deps) {
1814
2352
  }
1815
2353
  if (!st || !st.activated || st.closed)
1816
2354
  return;
1817
- enqueue(st, itemFor("message", p.info, sid));
2355
+ ingestMessageInfo(st, sid, p.info);
1818
2356
  const info = p.info;
1819
2357
  if (info?.role === "user" && info?.model) {
1820
2358
  const label = formatLabel(st.opts?.modelLabel ?? cfg.modelLabel, info.model);
1821
2359
  enqueue(st, itemFor("model", [{ id: info.model.modelID, providerID: info.model.providerID, name: label }], sid));
1822
2360
  }
1823
- if (info?.role === "assistant" && info.finish === "error") {
2361
+ if (info?.finish === "error") {
1824
2362
  st.badSignal = true;
1825
2363
  }
1826
2364
  } else if (t === "message.part.updated") {
@@ -1830,12 +2368,12 @@ function createEngine(deps) {
1830
2368
  const part = p.part;
1831
2369
  if (part?.state?.status === "error")
1832
2370
  st.badSignal = true;
1833
- enqueue(st, itemFor("part", part, sid));
2371
+ ingestPart(st, sid, part);
1834
2372
  } else if (t === "session.diff") {
1835
2373
  const st = stFor(sid);
1836
2374
  if (!st || !st.activated || st.closed)
1837
2375
  return;
1838
- enqueue(st, itemFor("session_diff", p.diff, sid));
2376
+ enqueue(st, itemFor("session_diff", normalizeV1Diff(p.diff), sid));
1839
2377
  } else if (t === "session.deleted") {
1840
2378
  const isChild = childToParent.has(sid);
1841
2379
  if (isChild) {
@@ -1899,116 +2437,6 @@ function createEngine(deps) {
1899
2437
  // src/v2-server.ts
1900
2438
  import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
1901
2439
  import { join as join3 } from "node:path";
1902
- function toolContentText(content) {
1903
- if (typeof content === "string")
1904
- return content;
1905
- if (Array.isArray(content)) {
1906
- return content.map((item) => {
1907
- if (typeof item === "string")
1908
- return item;
1909
- if (item && typeof item === "object") {
1910
- const record = item;
1911
- if (typeof record.text === "string")
1912
- return record.text;
1913
- }
1914
- return "";
1915
- }).join("");
1916
- }
1917
- return "";
1918
- }
1919
- function convertToolState(state) {
1920
- if (!state || typeof state !== "object")
1921
- return { status: "pending" };
1922
- const record = state;
1923
- const status = String(record.status ?? "pending");
1924
- const mapped = {};
1925
- if (status === "streaming" || status === "running")
1926
- mapped.status = "running";
1927
- else if (status === "completed")
1928
- mapped.status = "completed";
1929
- else if (status === "failed" || status === "error")
1930
- mapped.status = "error";
1931
- else
1932
- mapped.status = "pending";
1933
- if (record.input !== undefined)
1934
- mapped.input = record.input;
1935
- if (record.output !== undefined)
1936
- mapped.output = toolContentText(record.output);
1937
- if (record.error !== undefined)
1938
- mapped.error = record.error;
1939
- if (record.time !== undefined)
1940
- mapped.time = record.time;
1941
- return mapped;
1942
- }
1943
- function convertV2Message(message, sessionID) {
1944
- if (!message || typeof message.id !== "string")
1945
- return;
1946
- const base = {
1947
- id: message.id,
1948
- sessionID
1949
- };
1950
- if (message.type === "user") {
1951
- return {
1952
- info: {
1953
- ...base,
1954
- role: "user",
1955
- time: { created: message.time?.created ?? 0 },
1956
- text: message.text ?? ""
1957
- },
1958
- parts: []
1959
- };
1960
- }
1961
- if (message.type === "assistant") {
1962
- const parts = [];
1963
- const content = Array.isArray(message.content) ? message.content : [];
1964
- content.forEach((block, index) => {
1965
- const record = block;
1966
- const kind = String(record.type ?? "");
1967
- if (kind === "text") {
1968
- parts.push({
1969
- id: `${message.id}_t${index}`,
1970
- messageID: message.id,
1971
- sessionID,
1972
- type: "text",
1973
- text: String(record.text ?? ""),
1974
- synthetic: false,
1975
- time: { start: message.time?.created ?? 0, end: message.time?.completed ?? 0 }
1976
- });
1977
- } else if (kind === "reasoning") {
1978
- parts.push({
1979
- id: `${message.id}_r${index}`,
1980
- messageID: message.id,
1981
- sessionID,
1982
- type: "reasoning",
1983
- text: String(record.text ?? ""),
1984
- synthetic: false,
1985
- time: { start: message.time?.created ?? 0, end: message.time?.completed ?? 0 }
1986
- });
1987
- } else if (kind === "tool") {
1988
- parts.push({
1989
- id: String(record.id ?? `${message.id}_tool${index}`),
1990
- messageID: message.id,
1991
- sessionID,
1992
- type: "tool",
1993
- tool: String(record.name ?? "unknown"),
1994
- state: convertToolState(record.state),
1995
- synthetic: false
1996
- });
1997
- }
1998
- });
1999
- return {
2000
- info: {
2001
- ...base,
2002
- role: "assistant",
2003
- agent: message.agent,
2004
- model: message.model ? { providerID: message.model.providerID, modelID: message.model.modelID ?? message.model.id } : undefined,
2005
- time: { created: message.time?.created ?? 0, completed: message.time?.completed ?? 0 }
2006
- },
2007
- parts
2008
- };
2009
- }
2010
- return;
2011
- }
2012
2440
  function adaptSessionClient(session, message, directory) {
2013
2441
  if (!session)
2014
2442
  return;
@@ -2039,9 +2467,9 @@ function adaptSessionClient(session, message, directory) {
2039
2467
  if (!Array.isArray(data))
2040
2468
  break;
2041
2469
  for (const raw of data) {
2042
- const converted = convertV2Message(raw, sessionID);
2043
- if (converted)
2044
- out.push(converted);
2470
+ if (raw && typeof raw === "object" && typeof raw.id === "string") {
2471
+ out.push({ info: raw, parts: [] });
2472
+ }
2045
2473
  }
2046
2474
  const next = res?.cursor?.next ?? undefined;
2047
2475
  if (!next || data.length === 0)
@@ -2259,7 +2687,7 @@ var server_default = {
2259
2687
  setup: createV2ServerSetup()
2260
2688
  };
2261
2689
  export {
2262
- server_default as default,
2690
+ OpenSharePlugin,
2263
2691
  PLUGIN_ID,
2264
- OpenSharePlugin
2692
+ server_default as default
2265
2693
  };