@vinaverse/client 0.1.0-alpha.1 → 0.1.0-alpha.10

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.
Files changed (49) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +111 -1
  3. package/dist/agent/index.cjs +3319 -0
  4. package/dist/agent/index.cjs.map +1 -0
  5. package/dist/agent/index.d.cts +57 -0
  6. package/dist/agent/index.d.ts +57 -0
  7. package/dist/agent/index.js +99 -0
  8. package/dist/agent/index.js.map +1 -0
  9. package/dist/backend/index.cjs +268 -0
  10. package/dist/backend/index.cjs.map +1 -0
  11. package/dist/backend/index.d.cts +72 -0
  12. package/dist/backend/index.d.ts +72 -0
  13. package/dist/backend/index.js +250 -0
  14. package/dist/backend/index.js.map +1 -0
  15. package/dist/chunk-46D3FSLU.js +997 -0
  16. package/dist/chunk-46D3FSLU.js.map +1 -0
  17. package/dist/{chunk-AWHPWRGT.js → chunk-GWLLUOFY.js} +71 -15
  18. package/dist/chunk-GWLLUOFY.js.map +1 -0
  19. package/dist/chunk-O3NLAZGZ.js +859 -0
  20. package/dist/chunk-O3NLAZGZ.js.map +1 -0
  21. package/dist/chunk-YNUHXJD6.js +9 -0
  22. package/dist/chunk-YNUHXJD6.js.map +1 -0
  23. package/dist/contract-DmQs8amJ.d.cts +6 -0
  24. package/dist/contract-DmQs8amJ.d.ts +6 -0
  25. package/dist/conversation/index.cjs +356 -27
  26. package/dist/conversation/index.cjs.map +1 -1
  27. package/dist/conversation/index.d.cts +23 -307
  28. package/dist/conversation/index.d.ts +23 -307
  29. package/dist/conversation/index.js +36 -760
  30. package/dist/conversation/index.js.map +1 -1
  31. package/dist/index.cjs +70 -8
  32. package/dist/index.cjs.map +1 -1
  33. package/dist/index.d.cts +2 -1
  34. package/dist/index.d.ts +2 -1
  35. package/dist/index.js +2 -1
  36. package/dist/runtime-CISPp9nJ.d.cts +371 -0
  37. package/dist/runtime-Z1RI10lU.d.ts +371 -0
  38. package/dist/{session--W6JXfwo.d.cts → session-CPz4pcPu.d.cts} +3249 -1626
  39. package/dist/{session--W6JXfwo.d.ts → session-CPz4pcPu.d.ts} +3249 -1626
  40. package/dist/store-BxiicOKG.d.cts +414 -0
  41. package/dist/store-DZka9T2-.d.ts +414 -0
  42. package/dist/testing/index.cjs +274 -0
  43. package/dist/testing/index.cjs.map +1 -0
  44. package/dist/testing/index.d.cts +60 -0
  45. package/dist/testing/index.d.ts +60 -0
  46. package/dist/testing/index.js +259 -0
  47. package/dist/testing/index.js.map +1 -0
  48. package/package.json +82 -11
  49. package/dist/chunk-AWHPWRGT.js.map +0 -1
@@ -2,8 +2,8 @@
2
2
 
3
3
  // src/contract.ts
4
4
  var CONTRACT = {
5
- api: "v1-draft.17",
6
- commit: "fe4fb7d2945e1ced2bce7d44e85a4b28005e46df"
5
+ api: "v1-draft.22",
6
+ commit: "4695e3dc042e6e0cf1936aae24fcb093eacd27c8"
7
7
  };
8
8
 
9
9
  // src/errors.ts
@@ -36,7 +36,8 @@ var CARD_KINDS = {
36
36
  document: true,
37
37
  question: true,
38
38
  proposal: true,
39
- next_step: true
39
+ next_step: true,
40
+ links: true
40
41
  };
41
42
  function isKnownCard(card) {
42
43
  return Object.hasOwn(CARD_KINDS, card.kind);
@@ -416,7 +417,9 @@ var ConversationEngine = class {
416
417
  this.takeover = null;
417
418
  this.cutOff("interrupted");
418
419
  if (event.resumed !== true) return;
419
- if (event.transcript !== void 0) {
420
+ if (event.transcript !== void 0 && transcriptHasIds(event.transcript)) {
421
+ this.mergeTranscript(asArray(event.transcript));
422
+ } else if (event.transcript !== void 0) {
420
423
  this.messages = [];
421
424
  this.replaced = this.cards;
422
425
  this.cards = /* @__PURE__ */ new Map();
@@ -438,7 +441,61 @@ var ConversationEngine = class {
438
441
  this.hold(card, str(card.turn_id));
439
442
  this.replaced = null;
440
443
  }
441
- hold(raw, turnId) {
444
+ /**
445
+ * A transcript merged by key: an item the app holds keeps its place, with the text the server recorded; one it does
446
+ * not hold goes just after the nearest earlier item it does (just before the first later one when none came before,
447
+ * at the end when it holds none of them). Nothing the app holds is dropped.
448
+ */
449
+ mergeTranscript(items) {
450
+ const seqOf = (key) => key === void 0 ? void 0 : (this.messages.find((m) => m.key === key) ?? this.cards.get(key))?.seq;
451
+ const keys = items.map(
452
+ (item) => item.role === "card" ? str(item.card?.id) : transcriptKey(str(item.role) ?? "", str(item.id), str(item.at))
453
+ );
454
+ let prev;
455
+ items.forEach((item, i) => {
456
+ const key = keys[i];
457
+ if (key === void 0) return;
458
+ const known = seqOf(key);
459
+ let at;
460
+ if (known === void 0) {
461
+ const near = prev ?? keys.slice(i + 1).map(seqOf).find((q) => q !== void 0);
462
+ if (near !== void 0) {
463
+ const seqs = [...this.messages, ...this.cards.values()].map(
464
+ (h) => h.seq
465
+ );
466
+ const bound = prev === void 0 ? Math.max(near - 1, ...seqs.filter((q) => q < near)) : Math.min(...seqs.filter((q) => q > near));
467
+ if (Number.isFinite(bound)) at = (near + bound) / 2;
468
+ }
469
+ }
470
+ if (item.role === "card") this.hold(item.card, void 0, at);
471
+ else {
472
+ const text = str(item.text) ?? "";
473
+ const partial = item.interrupted === true ? "interrupted" : void 0;
474
+ if (known === void 0)
475
+ this.say(
476
+ str(item.role) ?? "",
477
+ text,
478
+ key,
479
+ partial ? { partial } : {},
480
+ at
481
+ );
482
+ else
483
+ this.messages = this.messages.map((m) => {
484
+ if (m.key !== key) return m;
485
+ const { partial: local, ...rest } = m;
486
+ const kept = partial ?? (local === "interrupted" ? void 0 : local);
487
+ return {
488
+ ...rest,
489
+ text: text || m.text,
490
+ ...kept ? { partial: kept } : {}
491
+ };
492
+ });
493
+ }
494
+ prev = seqOf(key);
495
+ });
496
+ this.messages.sort((a, b) => a.seq - b.seq);
497
+ }
498
+ hold(raw, turnId, at) {
442
499
  if (!raw) return;
443
500
  const id = str(raw.id);
444
501
  if (id === void 0) return;
@@ -452,7 +509,7 @@ var ConversationEngine = class {
452
509
  locked: lockedFrom(question),
453
510
  ...turnId !== void 0 ? { turnId } : {},
454
511
  // A card still held keeps its place when it is drawn again (a reconnect's pending cards, a repeated ui.cards).
455
- seq: this.cards.get(id)?.seq ?? this.seq++
512
+ seq: this.cards.get(id)?.seq ?? at ?? this.seq++
456
513
  };
457
514
  for (const held of this.held) {
458
515
  const sent = this.inputs.get(held);
@@ -548,9 +605,9 @@ var ConversationEngine = class {
548
605
  this.unqueue(id);
549
606
  }
550
607
  /** Adds a message once: a key already held (a frame seen twice) changes nothing. */
551
- say(role, text, key, extra = {}, seq = this.seq++) {
608
+ say(role, text, key, extra = {}, seq) {
552
609
  if (this.messages.some((m) => m.key === key)) return;
553
- this.messages.push({ role, text, key, seq, ...extra });
610
+ this.messages.push({ role, text, key, seq: seq ?? this.seq++, ...extra });
554
611
  }
555
612
  /** The turn's streamed text, kept as the agent's message when the turn ends without its `message.completed`. */
556
613
  cutOff(partial) {
@@ -645,6 +702,11 @@ function transcriptKey(role, id, at) {
645
702
  if (role === "staff") return `s:${id}`;
646
703
  return id;
647
704
  }
705
+ function transcriptHasIds(transcript) {
706
+ return asArray(transcript).every(
707
+ (item) => item.role === "card" ? str(item.card?.id) !== void 0 : str(item.id) !== void 0
708
+ );
709
+ }
648
710
  function lockedFrom(question) {
649
711
  if (!question) return [];
650
712
  if (question.state !== "open") {
@@ -1467,13 +1529,17 @@ function buildTimeline(input, previous) {
1467
1529
  type: "card",
1468
1530
  key: card.id,
1469
1531
  card,
1470
- busy: input.busy.has(card.id),
1532
+ busy: !input.inert?.has(card.id) && input.busy.has(card.id),
1533
+ ...input.inert?.has(card.id) ? { restored: true } : {},
1471
1534
  ...input.decisionErrors?.has(card.id) ? { decisionError: true } : {}
1472
1535
  },
1473
- order: card.seq
1536
+ order: input.cardOrder?.get(card.id) ?? card.seq
1474
1537
  });
1475
1538
  }
1476
1539
  for (const { item, order } of input.system) live.push({ item, order });
1540
+ const held = new Set(state.cards.map((c) => c.id));
1541
+ for (const { item, order } of input.kept ?? [])
1542
+ if (!held.has(item.key)) live.push({ item, order });
1477
1543
  for (const [id, sent] of input.sent) {
1478
1544
  const pending = pendingState(id, input);
1479
1545
  if (pending === null) continue;
@@ -1496,17 +1562,10 @@ function buildTimeline(input, previous) {
1496
1562
  });
1497
1563
  }
1498
1564
  live.sort((a, b) => a.order - b.order);
1499
- const liveByKey = new Map(live.map((l) => [l.item.key, l.item]));
1500
- const transcript = input.transcriptSeen === true;
1501
- const items = [];
1502
- const placed = /* @__PURE__ */ new Set();
1503
- if (!transcript)
1504
- for (const item of input.history) {
1505
- const current = liveByKey.get(item.key);
1506
- items.push(current ?? item);
1507
- if (current) placed.add(item.key);
1508
- }
1509
- for (const { item } of live) if (!placed.has(item.key)) items.push(item);
1565
+ const items = mergeByKey(
1566
+ input.transcriptSeen === true ? [] : input.history,
1567
+ live.map((l) => l.item)
1568
+ );
1510
1569
  let same = items.length === previous.length;
1511
1570
  const shared = items.map((item, i) => {
1512
1571
  const prev = before.get(item.key);
@@ -1516,6 +1575,24 @@ function buildTimeline(input, previous) {
1516
1575
  });
1517
1576
  return same ? previous : shared;
1518
1577
  }
1578
+ function mergeByKey(history, live) {
1579
+ if (!history.length) return [...live];
1580
+ const items = [...history];
1581
+ const at = (key) => items.findIndex((item) => item.key === key);
1582
+ let last = -1;
1583
+ live.forEach((item, i) => {
1584
+ const found = at(item.key);
1585
+ if (found >= 0) {
1586
+ items[found] = item;
1587
+ last = found;
1588
+ return;
1589
+ }
1590
+ const next = live.slice(i + 1).map((l) => at(l.key)).find((p) => p >= 0);
1591
+ last = next === void 0 ? items.length : last >= 0 ? last + 1 : next;
1592
+ items.splice(last, 0, item);
1593
+ });
1594
+ return items;
1595
+ }
1519
1596
  function messageItem(m) {
1520
1597
  const role = m.role === "assistant" ? "agent" : m.role;
1521
1598
  return {
@@ -1547,6 +1624,7 @@ function persistable(item) {
1547
1624
 
1548
1625
  // src/conversation/store.ts
1549
1626
  var MAX_TEXT = 2e3;
1627
+ var TURN_INPUTS = 50;
1550
1628
  var PERSIST_DELAY = 500;
1551
1629
  var PERSIST_VERSION = 1;
1552
1630
  var NO_ITEMS = [];
@@ -1577,12 +1655,20 @@ var ConversationStore = class {
1577
1655
  /** Proposals whose tenant decision threw: shown as recoverable, the person may decide again. */
1578
1656
  decisionErrors = /* @__PURE__ */ new Set();
1579
1657
  system = [];
1658
+ /** The person's text that started each recent turn, by `turn_id` (`turn.started` echoes its `client_event_id`). */
1659
+ turnInputs = /* @__PURE__ */ new Map();
1580
1660
  /** Numbers system rows that carry no id of their own. */
1581
1661
  systemSeq = 0;
1582
1662
  history = NO_ITEMS;
1583
1663
  historyState = "none";
1584
1664
  /** A resumed session.ready carried a transcript: the server's history replaced what the device kept. */
1585
1665
  transcriptSeen = false;
1666
+ /** The session of the last session.ready: a card of another one takes no tap (§6). */
1667
+ readySession = null;
1668
+ cardOrder = /* @__PURE__ */ new Map();
1669
+ kept = [];
1670
+ /** Cards an earlier session drew, kept by a merged transcript: drawn as restored, taking no tap. */
1671
+ inert = /* @__PURE__ */ new Set();
1586
1672
  locale;
1587
1673
  /** The request for a session in flight, to abort when it is no longer wanted. */
1588
1674
  fetching = null;
@@ -1624,6 +1710,8 @@ var ConversationStore = class {
1624
1710
  this.live();
1625
1711
  if (this.session) {
1626
1712
  if (this.session.status === "idle") this.session.connect();
1713
+ else if (this.session.status === "unauthorized")
1714
+ void this.resumeWith("resume");
1627
1715
  return;
1628
1716
  }
1629
1717
  void this.ensureSession("open");
@@ -1664,9 +1752,59 @@ var ConversationStore = class {
1664
1752
  /** The app came to the foreground or went to the background (AppState, `visibilitychange`). */
1665
1753
  setForeground(foreground) {
1666
1754
  this.foreground = foreground;
1755
+ if (this.options.closeInBackground && !this.disposed) {
1756
+ if (!foreground) {
1757
+ this.suspend();
1758
+ return;
1759
+ }
1760
+ if (this.suspended) {
1761
+ void this.wake();
1762
+ return;
1763
+ }
1764
+ }
1667
1765
  this.session?.setForeground(foreground);
1668
1766
  if (foreground) this.reconnectIfDown();
1669
1767
  }
1768
+ /** In the background (`closeInBackground`): the connection is closed; the engine keeps what was sent, to resend. */
1769
+ suspended = false;
1770
+ suspend() {
1771
+ const session = this.session;
1772
+ if (!session || this.suspended || session.status === "ended") return;
1773
+ this.suspended = true;
1774
+ for (const off of this.unsubscribe) off();
1775
+ this.unsubscribe = [];
1776
+ session.close();
1777
+ this.update();
1778
+ }
1779
+ /**
1780
+ * Back in the foreground, or the person acted while away: a new connection to the same session, with a fresh secret
1781
+ * when the backend gives one. A session that passed its end meanwhile is never reopened (a fresh secret cannot bring
1782
+ * back an expired tool token): the person's action continues the conversation in a new session from `getSession`,
1783
+ * and until then nothing connects.
1784
+ */
1785
+ async wake(byPerson = false) {
1786
+ const info = this.info;
1787
+ if (info && this.sessionOver()) {
1788
+ if (byPerson) await this.resumeWith("resume");
1789
+ else this.update();
1790
+ return;
1791
+ }
1792
+ this.suspended = false;
1793
+ if (!info) return;
1794
+ let next = info;
1795
+ if (this.options.refreshSecret) {
1796
+ try {
1797
+ next = {
1798
+ ...info,
1799
+ client_secret: await this.options.refreshSecret(info.id)
1800
+ };
1801
+ } catch (error) {
1802
+ this.options.onError?.(error);
1803
+ }
1804
+ }
1805
+ if (this.disposed || this.suspended) return;
1806
+ this.open(next);
1807
+ }
1670
1808
  /** The network as the app sees it; null when unknown. Back online reconnects a connection that gave up. */
1671
1809
  setOnline(online) {
1672
1810
  this.online = online;
@@ -1682,18 +1820,31 @@ var ConversationStore = class {
1682
1820
  throw new RangeError(`A message is at most ${MAX_TEXT} characters.`);
1683
1821
  const id = (this.options.createId ?? randomId)();
1684
1822
  this.sent.set(id, { text, order: lastOrder(this.engine.state()) + 0.5 });
1823
+ this.tellActivity({ type: "message_sent", id });
1685
1824
  if (!this.session) {
1686
1825
  this.early.set(id, text);
1687
1826
  void this.ensureSession("open");
1688
1827
  this.update();
1689
1828
  return id;
1690
1829
  }
1830
+ if (this.suspended) {
1831
+ this.early.set(id, text);
1832
+ void this.wake(true);
1833
+ this.update();
1834
+ return id;
1835
+ }
1691
1836
  if (this.nextFrom(this.engine.state()) === "continue_here") {
1692
1837
  this.early.set(id, text);
1693
1838
  void this.resumeWith("continue_here");
1694
1839
  this.update();
1695
1840
  return id;
1696
1841
  }
1842
+ if (this.sessionOver()) {
1843
+ this.early.set(id, text);
1844
+ void this.resumeWith("resume");
1845
+ this.update();
1846
+ return id;
1847
+ }
1697
1848
  this.nextId = id;
1698
1849
  this.session.sendText(text);
1699
1850
  this.nextId = null;
@@ -1756,6 +1907,24 @@ var ConversationStore = class {
1756
1907
  this.sent.delete(key);
1757
1908
  return this.send(sent.text);
1758
1909
  }
1910
+ /**
1911
+ * Send again the person's text whose turn failed before any reply (a `turn_failed` row with `data.retry`), as a new
1912
+ * message (the person's action). Once per row: its `retry` turns false. Returns the new id, or null when the row
1913
+ * allows no retry.
1914
+ */
1915
+ retryTurn(key) {
1916
+ const at = this.system.findIndex(
1917
+ (s) => s.item.key === key && s.item.event === "turn_failed"
1918
+ );
1919
+ const entry = this.system[at];
1920
+ if (entry?.item.data.retry !== true) return null;
1921
+ const text = this.turnText(String(entry.item.data.turn ?? ""));
1922
+ if (text === void 0) return null;
1923
+ this.system = this.system.map(
1924
+ (s, i) => i === at ? { ...s, item: { ...s.item, data: { ...s.item.data, retry: false } } } : s
1925
+ );
1926
+ return this.send(text);
1927
+ }
1759
1928
  /** After the conversation continued on another device: continue it here (the person's choice). */
1760
1929
  continueHere() {
1761
1930
  return this.resumeWith("continue_here");
@@ -1812,11 +1981,21 @@ var ConversationStore = class {
1812
1981
  }
1813
1982
  open(info) {
1814
1983
  this.info = info;
1984
+ this.suspended = false;
1985
+ const refreshSecret = this.options.refreshSecret;
1815
1986
  const session = new VinaverseSession({
1816
1987
  ...this.options.transport,
1817
1988
  session: info,
1818
1989
  engine: this.engine,
1819
- ...this.options.refreshSecret ? { refreshSecret: this.options.refreshSecret } : {},
1990
+ ...refreshSecret ? {
1991
+ // Past the session's end a fresh secret cannot help: refused here, and the store rolls over instead.
1992
+ refreshSecret: (id) => this.sessionOver() ? Promise.reject(
1993
+ new VinaverseClientError(
1994
+ "needs_new_session",
1995
+ "This session has ended."
1996
+ )
1997
+ ) : refreshSecret(id)
1998
+ } : {},
1820
1999
  createId: () => this.nextId ?? (this.options.createId ?? randomId)()
1821
2000
  });
1822
2001
  this.session = session;
@@ -1824,7 +2003,9 @@ var ConversationStore = class {
1824
2003
  session.on("state", () => this.update()),
1825
2004
  session.on("status", (status) => this.statusChanged(status)),
1826
2005
  session.on("event", (event) => this.event(event)),
1827
- session.on("error", (error) => this.options.onError?.(error))
2006
+ session.on("error", (error) => {
2007
+ if (!this.sessionOver()) this.options.onError?.(error);
2008
+ })
1828
2009
  );
1829
2010
  session.setForeground(this.foreground);
1830
2011
  session.connect();
@@ -1851,6 +2032,10 @@ var ConversationStore = class {
1851
2032
  this.resuming = this.fetchSession(reason).then((info) => {
1852
2033
  this.resuming = null;
1853
2034
  if (!info || this.disposed || !this.session) return;
2035
+ if (this.session.status === "closed") {
2036
+ this.open(info);
2037
+ return;
2038
+ }
1854
2039
  this.info = info;
1855
2040
  this.session.resume(info);
1856
2041
  this.flushEarly();
@@ -1865,19 +2050,48 @@ var ConversationStore = class {
1865
2050
  }
1866
2051
  reconnectIfDown() {
1867
2052
  const session = this.session;
1868
- if (session && session.status === "idle" && this.nextFrom(this.engine.state()) === null)
2053
+ if (session && session.status === "idle" && !this.sessionOver() && // past its end: the person's next action gets a new session
2054
+ this.nextFrom(this.engine.state()) === null)
1869
2055
  session.connect();
1870
2056
  }
2057
+ /** A refusal already led to a new session, and none has been ready since: another refusal is no cue. */
2058
+ rolled = false;
1871
2059
  statusChanged(status) {
2060
+ if (status === "ready") this.rolled = false;
2061
+ if (status === "unauthorized" && !this.rolled) {
2062
+ this.rolled = true;
2063
+ void this.resumeWith("resume");
2064
+ }
1872
2065
  if (status === "idle")
1873
2066
  this.offlineAfterRetries = this.lastStatus === "reconnecting";
1874
2067
  else if (status !== "reconnecting") this.offlineAfterRetries = false;
1875
2068
  this.lastStatus = status;
1876
2069
  this.update();
1877
2070
  }
2071
+ /** The app's analytics hears of a step; what it throws is reported (`onError`), never in the way of a send or a frame. */
2072
+ tellActivity(activity) {
2073
+ try {
2074
+ this.options.onActivity?.(activity);
2075
+ } catch (error) {
2076
+ this.options.onError?.(error);
2077
+ }
2078
+ }
2079
+ /** The live frames analytics hears of: a reply finished, cards drawn. */
2080
+ activity(frame) {
2081
+ if (!this.options.onActivity) return;
2082
+ const on = (a) => this.tellActivity(a);
2083
+ if (frame.type === "message.completed" && typeof frame.turn_id === "string")
2084
+ on({ type: "reply_completed", turnId: frame.turn_id });
2085
+ if (frame.type === "ui.cards" && Array.isArray(frame.cards)) {
2086
+ for (const card of frame.cards)
2087
+ if (typeof card.id === "string" && typeof card.kind === "string")
2088
+ on({ type: "card_shown", cardId: card.id, kind: card.kind });
2089
+ }
2090
+ }
1878
2091
  event(event) {
1879
2092
  const frame = event;
1880
2093
  const str2 = (v) => typeof v === "string" ? v : null;
2094
+ this.activity(frame);
1881
2095
  const add = (key, kind, data) => {
1882
2096
  if (this.system.some((s) => s.item.key === key)) return;
1883
2097
  this.system = [
@@ -1889,10 +2103,25 @@ var ConversationStore = class {
1889
2103
  ];
1890
2104
  };
1891
2105
  switch (frame.type) {
1892
- case "session.ready":
1893
- if (frame.resumed === true && Array.isArray(frame.transcript))
2106
+ case "session.ready": {
2107
+ const session = frame.session?.id;
2108
+ const moved = typeof session === "string" && session !== this.readySession;
2109
+ if (typeof session === "string") this.readySession = session;
2110
+ if (frame.resumed === true && Array.isArray(frame.transcript) && !transcriptHasIds(frame.transcript)) {
1894
2111
  this.transcriptSeen = true;
2112
+ this.anchorCards(moved);
2113
+ } else if (moved && frame.resumed === true) {
2114
+ const pending = new Set(
2115
+ (Array.isArray(frame.pending_cards) ? frame.pending_cards : []).map(
2116
+ (c) => str2(c.id)
2117
+ )
2118
+ );
2119
+ for (const card of this.engine.state().cards)
2120
+ if (pending.has(card.id)) this.inert.delete(card.id);
2121
+ else this.inert.add(card.id);
2122
+ }
1895
2123
  return;
2124
+ }
1896
2125
  // `at` is required (v1-draft.15); the same takeover announced again after a reconnect has the same `at`, so it
1897
2126
  // draws one row. A frame without one still gets a row of its own.
1898
2127
  case "takeover.started":
@@ -1922,6 +2151,17 @@ var ConversationStore = class {
1922
2151
  code: str2(frame.code)
1923
2152
  });
1924
2153
  return;
2154
+ case "turn.started": {
2155
+ const turn = str2(frame.turn_id);
2156
+ const input = str2(frame.client_event_id);
2157
+ if (turn === null || input === null) return;
2158
+ this.turnInputs.set(turn, input);
2159
+ if (this.turnInputs.size > TURN_INPUTS) {
2160
+ const oldest = this.turnInputs.keys().next().value;
2161
+ if (oldest !== void 0) this.turnInputs.delete(oldest);
2162
+ }
2163
+ return;
2164
+ }
1925
2165
  case "turn.failed": {
1926
2166
  const turn = str2(frame.turn_id) ?? "";
1927
2167
  if (this.engine.state().messages.some((m) => m.key === `a:${turn}`))
@@ -1930,7 +2170,9 @@ var ConversationStore = class {
1930
2170
  add(`failed:${turn}`, "turn_failed", {
1931
2171
  code: str2(frame.code),
1932
2172
  message: str2(frame.message),
1933
- retryable: frame.retryable === true
2173
+ retryable: frame.retryable === true,
2174
+ retry: frame.retryable === true && this.turnText(turn) !== void 0,
2175
+ turn
1934
2176
  });
1935
2177
  return;
1936
2178
  }
@@ -1938,6 +2180,45 @@ var ConversationStore = class {
1938
2180
  return;
1939
2181
  }
1940
2182
  }
2183
+ /** The person's text that started a turn, while its message is on screen. */
2184
+ turnText(turn) {
2185
+ const input = this.turnInputs.get(turn);
2186
+ if (input === void 0) return void 0;
2187
+ const message = this.engine.state().messages.find((m) => m.key === input && m.role === "person");
2188
+ return message?.text || void 0;
2189
+ }
2190
+ /**
2191
+ * A resumed session's transcript replaced the messages, and a chat transcript carries text only: the engine drops
2192
+ * the cards it drew, and draws the pending ones again at the end. Each card on screen is anchored just after the
2193
+ * message it followed (by key; the transcript keeps the live keys), so a pending proposal stays where it was, and a
2194
+ * card the transcript dropped stays drawn there; taking no tap when it belongs to an earlier session.
2195
+ */
2196
+ anchorCards(sessionChanged) {
2197
+ const state = this.engine.state();
2198
+ const seqOf = new Map(state.messages.map((m) => [m.key, m.seq]));
2199
+ const first = Math.min(...state.messages.map((m) => m.seq), 0);
2200
+ const held = new Set(state.cards.map((c) => c.id));
2201
+ const order = /* @__PURE__ */ new Map();
2202
+ const kept = [];
2203
+ let anchor = first - 1;
2204
+ let step = 0;
2205
+ for (const item of this.view.items) {
2206
+ if (item.type === "message" && seqOf.has(item.key)) {
2207
+ anchor = seqOf.get(item.key);
2208
+ step = 0;
2209
+ } else if (item.type === "card") {
2210
+ const at = anchor + 0.5 + ++step / 1e3;
2211
+ order.set(item.key, at);
2212
+ if (!held.has(item.key))
2213
+ kept.push({
2214
+ item: sessionChanged ? { ...item, busy: false, restored: true } : item,
2215
+ order: at
2216
+ });
2217
+ }
2218
+ }
2219
+ this.cardOrder = order;
2220
+ this.kept = kept;
2221
+ }
1941
2222
  // ------------------------------------------------------------ the view
1942
2223
  /**
1943
2224
  * Recomputes now (getSnapshot and getStreaming are always current) and notifies from the scheduler, at most once
@@ -1994,6 +2275,9 @@ var ConversationStore = class {
1994
2275
  busy: this.busy,
1995
2276
  decisionErrors: this.decisionErrors,
1996
2277
  transcriptSeen: this.transcriptSeen,
2278
+ cardOrder: this.cardOrder,
2279
+ kept: this.kept,
2280
+ inert: this.inert,
1997
2281
  system: this.system
1998
2282
  },
1999
2283
  prev?.items ?? NO_ITEMS
@@ -2005,10 +2289,12 @@ var ConversationStore = class {
2005
2289
  const avatarUrl = this.info?.agent?.avatar_url ?? null;
2006
2290
  const view = {
2007
2291
  status,
2292
+ conversationId: s.conversationId,
2008
2293
  banner: banner(status, next),
2009
2294
  next,
2010
2295
  agent: sameAgent(prev?.agent, name, avatarUrl) ? prev.agent : { name, avatarUrl },
2011
2296
  disclosure: s.disclosure ?? this.info?.disclosure ?? null,
2297
+ welcome: this.info?.welcome ?? null,
2012
2298
  takeover: s.takeover,
2013
2299
  items,
2014
2300
  activity: prev && sameTools(prev.activity, activity) ? prev.activity : activity,
@@ -2023,6 +2309,7 @@ var ConversationStore = class {
2023
2309
  }
2024
2310
  status() {
2025
2311
  if (this.disposed) return "closed";
2312
+ if (this.suspended) return "idle";
2026
2313
  const session = this.session;
2027
2314
  if (!session) {
2028
2315
  if (this.failed) return "unavailable";
@@ -2114,6 +2401,11 @@ var ConversationStore = class {
2114
2401
  }, PERSIST_DELAY);
2115
2402
  }
2116
2403
  // ------------------------------------------------------------ guards
2404
+ /** Past the session's end (`expires_at`), when the backend passed it on. */
2405
+ sessionOver() {
2406
+ const end = this.info?.expires_at;
2407
+ return end !== void 0 && (this.options.clock?.now() ?? Date.now()) >= Date.parse(end);
2408
+ }
2117
2409
  live() {
2118
2410
  if (this.disposed)
2119
2411
  throw new VinaverseClientError(
@@ -2167,6 +2459,42 @@ function shallowEqual(a, b) {
2167
2459
  return a.status === b.status && a.banner === b.banner && a.next === b.next && a.agent.name === b.agent.name && a.agent.avatarUrl === b.agent.avatarUrl && a.disclosure === b.disclosure && a.takeover?.by === b.takeover?.by && a.takeover?.at === b.takeover?.at && a.takeover === null === (b.takeover === null) && a.items === b.items && a.activity === b.activity && a.responding === b.responding && a.openQuestion === b.openQuestion && a.answersNeedWebSocket === b.answersNeedWebSocket && a.turnsLeft === b.turnsLeft && a.history === b.history;
2168
2460
  }
2169
2461
 
2462
+ // src/conversation/modes.ts
2463
+ function createConversationsByMode(options) {
2464
+ const { getSession, modes = [], ...shared } = options;
2465
+ const base = shared.persistenceKey ?? "vinaverse.conversation";
2466
+ const stores = /* @__PURE__ */ new Map();
2467
+ const get = (mode) => {
2468
+ let store = stores.get(mode);
2469
+ if (!store) {
2470
+ store = createConversation({
2471
+ ...shared,
2472
+ persistenceKey: `${base}.${mode}`,
2473
+ getSession: (request) => getSession({ ...request, mode })
2474
+ });
2475
+ stores.set(mode, store);
2476
+ }
2477
+ return store;
2478
+ };
2479
+ return {
2480
+ get,
2481
+ select(mode) {
2482
+ for (const [other, store] of stores)
2483
+ if (other !== mode) store.setForeground(false);
2484
+ return get(mode);
2485
+ },
2486
+ dispose() {
2487
+ for (const store of stores.values()) store.dispose();
2488
+ stores.clear();
2489
+ },
2490
+ clear() {
2491
+ for (const mode of /* @__PURE__ */ new Set([...stores.keys(), ...modes]))
2492
+ get(mode).clear();
2493
+ stores.clear();
2494
+ }
2495
+ };
2496
+ }
2497
+
2170
2498
  exports.CONTRACT = CONTRACT;
2171
2499
  exports.ConversationStore = ConversationStore;
2172
2500
  exports.MAX_TEXT = MAX_TEXT;
@@ -2174,6 +2502,7 @@ exports.VinaverseClientError = VinaverseClientError;
2174
2502
  exports.answerProblem = answerProblem;
2175
2503
  exports.codePoints = codePoints;
2176
2504
  exports.createConversation = createConversation;
2505
+ exports.createConversationsByMode = createConversationsByMode;
2177
2506
  exports.isAllowedLink = isAllowedLink;
2178
2507
  exports.isKnownCard = isKnownCard;
2179
2508
  exports.isValidLinkPrefix = isValidLinkPrefix;