@vinaverse/client 0.1.0-alpha.2 → 0.1.0-alpha.4

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.
@@ -1,5 +1,5 @@
1
- import { ConversationEngine, randomId, VinaverseSession, VinaverseClientError, isKnownCard } from '../chunk-AWHPWRGT.js';
2
- export { CONTRACT, VinaverseClientError, isKnownCard } from '../chunk-AWHPWRGT.js';
1
+ import { ConversationEngine, randomId, VinaverseSession, VinaverseClientError, transcriptHasIds, isKnownCard } from '../chunk-MKUNSAMT.js';
2
+ export { CONTRACT, VinaverseClientError, isKnownCard } from '../chunk-MKUNSAMT.js';
3
3
 
4
4
  // src/conversation/answers.ts
5
5
  var CHOICE = /* @__PURE__ */ new Set([
@@ -153,7 +153,8 @@ function buildTimeline(input, previous) {
153
153
  type: "card",
154
154
  key: card.id,
155
155
  card,
156
- busy: input.busy.has(card.id),
156
+ busy: !input.inert?.has(card.id) && input.busy.has(card.id),
157
+ ...input.inert?.has(card.id) ? { restored: true } : {},
157
158
  ...input.decisionErrors?.has(card.id) ? { decisionError: true } : {}
158
159
  },
159
160
  order: input.cardOrder?.get(card.id) ?? card.seq
@@ -185,17 +186,10 @@ function buildTimeline(input, previous) {
185
186
  });
186
187
  }
187
188
  live.sort((a, b) => a.order - b.order);
188
- const liveByKey = new Map(live.map((l) => [l.item.key, l.item]));
189
- const transcript = input.transcriptSeen === true;
190
- const items = [];
191
- const placed = /* @__PURE__ */ new Set();
192
- if (!transcript)
193
- for (const item of input.history) {
194
- const current = liveByKey.get(item.key);
195
- items.push(current ?? item);
196
- if (current) placed.add(item.key);
197
- }
198
- for (const { item } of live) if (!placed.has(item.key)) items.push(item);
189
+ const items = mergeByKey(
190
+ input.transcriptSeen === true ? [] : input.history,
191
+ live.map((l) => l.item)
192
+ );
199
193
  let same = items.length === previous.length;
200
194
  const shared = items.map((item, i) => {
201
195
  const prev = before.get(item.key);
@@ -205,6 +199,24 @@ function buildTimeline(input, previous) {
205
199
  });
206
200
  return same ? previous : shared;
207
201
  }
202
+ function mergeByKey(history, live) {
203
+ if (!history.length) return [...live];
204
+ const items = [...history];
205
+ const at = (key) => items.findIndex((item) => item.key === key);
206
+ let last = -1;
207
+ live.forEach((item, i) => {
208
+ const found = at(item.key);
209
+ if (found >= 0) {
210
+ items[found] = item;
211
+ last = found;
212
+ return;
213
+ }
214
+ const next = live.slice(i + 1).map((l) => at(l.key)).find((p) => p >= 0);
215
+ last = next === void 0 ? items.length : last >= 0 ? last + 1 : next;
216
+ items.splice(last, 0, item);
217
+ });
218
+ return items;
219
+ }
208
220
  function messageItem(m) {
209
221
  const role = m.role === "assistant" ? "agent" : m.role;
210
222
  return {
@@ -236,6 +248,7 @@ function persistable(item) {
236
248
 
237
249
  // src/conversation/store.ts
238
250
  var MAX_TEXT = 2e3;
251
+ var TURN_INPUTS = 50;
239
252
  var PERSIST_DELAY = 500;
240
253
  var PERSIST_VERSION = 1;
241
254
  var NO_ITEMS = [];
@@ -266,6 +279,8 @@ var ConversationStore = class {
266
279
  /** Proposals whose tenant decision threw: shown as recoverable, the person may decide again. */
267
280
  decisionErrors = /* @__PURE__ */ new Set();
268
281
  system = [];
282
+ /** The person's text that started each recent turn, by `turn_id` (`turn.started` echoes its `client_event_id`). */
283
+ turnInputs = /* @__PURE__ */ new Map();
269
284
  /** Numbers system rows that carry no id of their own. */
270
285
  systemSeq = 0;
271
286
  history = NO_ITEMS;
@@ -276,6 +291,8 @@ var ConversationStore = class {
276
291
  readySession = null;
277
292
  cardOrder = /* @__PURE__ */ new Map();
278
293
  kept = [];
294
+ /** Cards an earlier session drew, kept by a merged transcript: drawn as restored, taking no tap. */
295
+ inert = /* @__PURE__ */ new Set();
279
296
  locale;
280
297
  /** The request for a session in flight, to abort when it is no longer wanted. */
281
298
  fetching = null;
@@ -317,6 +334,8 @@ var ConversationStore = class {
317
334
  this.live();
318
335
  if (this.session) {
319
336
  if (this.session.status === "idle") this.session.connect();
337
+ else if (this.session.status === "unauthorized")
338
+ void this.resumeWith("resume");
320
339
  return;
321
340
  }
322
341
  void this.ensureSession("open");
@@ -381,9 +400,19 @@ var ConversationStore = class {
381
400
  session.close();
382
401
  this.update();
383
402
  }
384
- /** Back in the foreground: a new connection to the same session, with a fresh secret when the backend gives one. */
385
- async wake() {
403
+ /**
404
+ * Back in the foreground, or the person acted while away: a new connection to the same session, with a fresh secret
405
+ * when the backend gives one. A session that passed its end meanwhile is never reopened (a fresh secret cannot bring
406
+ * back an expired tool token): the person's action continues the conversation in a new session from `getSession`,
407
+ * and until then nothing connects.
408
+ */
409
+ async wake(byPerson = false) {
386
410
  const info = this.info;
411
+ if (info && this.sessionOver()) {
412
+ if (byPerson) await this.resumeWith("resume");
413
+ else this.update();
414
+ return;
415
+ }
387
416
  this.suspended = false;
388
417
  if (!info) return;
389
418
  let next = info;
@@ -423,7 +452,7 @@ var ConversationStore = class {
423
452
  }
424
453
  if (this.suspended) {
425
454
  this.early.set(id, text);
426
- void this.wake();
455
+ void this.wake(true);
427
456
  this.update();
428
457
  return id;
429
458
  }
@@ -433,6 +462,12 @@ var ConversationStore = class {
433
462
  this.update();
434
463
  return id;
435
464
  }
465
+ if (this.sessionOver()) {
466
+ this.early.set(id, text);
467
+ void this.resumeWith("resume");
468
+ this.update();
469
+ return id;
470
+ }
436
471
  this.nextId = id;
437
472
  this.session.sendText(text);
438
473
  this.nextId = null;
@@ -495,6 +530,24 @@ var ConversationStore = class {
495
530
  this.sent.delete(key);
496
531
  return this.send(sent.text);
497
532
  }
533
+ /**
534
+ * Send again the person's text whose turn failed before any reply (a `turn_failed` row with `data.retry`), as a new
535
+ * message (the person's action). Once per row: its `retry` turns false. Returns the new id, or null when the row
536
+ * allows no retry.
537
+ */
538
+ retryTurn(key) {
539
+ const at = this.system.findIndex(
540
+ (s) => s.item.key === key && s.item.event === "turn_failed"
541
+ );
542
+ const entry = this.system[at];
543
+ if (entry?.item.data.retry !== true) return null;
544
+ const text = this.turnText(String(entry.item.data.turn ?? ""));
545
+ if (text === void 0) return null;
546
+ this.system = this.system.map(
547
+ (s, i) => i === at ? { ...s, item: { ...s.item, data: { ...s.item.data, retry: false } } } : s
548
+ );
549
+ return this.send(text);
550
+ }
498
551
  /** After the conversation continued on another device: continue it here (the person's choice). */
499
552
  continueHere() {
500
553
  return this.resumeWith("continue_here");
@@ -551,11 +604,21 @@ var ConversationStore = class {
551
604
  }
552
605
  open(info) {
553
606
  this.info = info;
607
+ this.suspended = false;
608
+ const refreshSecret = this.options.refreshSecret;
554
609
  const session = new VinaverseSession({
555
610
  ...this.options.transport,
556
611
  session: info,
557
612
  engine: this.engine,
558
- ...this.options.refreshSecret ? { refreshSecret: this.options.refreshSecret } : {},
613
+ ...refreshSecret ? {
614
+ // Past the session's end a fresh secret cannot help: refused here, and the store rolls over instead.
615
+ refreshSecret: (id) => this.sessionOver() ? Promise.reject(
616
+ new VinaverseClientError(
617
+ "needs_new_session",
618
+ "This session has ended."
619
+ )
620
+ ) : refreshSecret(id)
621
+ } : {},
559
622
  createId: () => this.nextId ?? (this.options.createId ?? randomId)()
560
623
  });
561
624
  this.session = session;
@@ -563,7 +626,9 @@ var ConversationStore = class {
563
626
  session.on("state", () => this.update()),
564
627
  session.on("status", (status) => this.statusChanged(status)),
565
628
  session.on("event", (event) => this.event(event)),
566
- session.on("error", (error) => this.options.onError?.(error))
629
+ session.on("error", (error) => {
630
+ if (!this.sessionOver()) this.options.onError?.(error);
631
+ })
567
632
  );
568
633
  session.setForeground(this.foreground);
569
634
  session.connect();
@@ -590,6 +655,10 @@ var ConversationStore = class {
590
655
  this.resuming = this.fetchSession(reason).then((info) => {
591
656
  this.resuming = null;
592
657
  if (!info || this.disposed || !this.session) return;
658
+ if (this.session.status === "closed") {
659
+ this.open(info);
660
+ return;
661
+ }
593
662
  this.info = info;
594
663
  this.session.resume(info);
595
664
  this.flushEarly();
@@ -604,10 +673,18 @@ var ConversationStore = class {
604
673
  }
605
674
  reconnectIfDown() {
606
675
  const session = this.session;
607
- if (session && session.status === "idle" && this.nextFrom(this.engine.state()) === null)
676
+ if (session && session.status === "idle" && !this.sessionOver() && // past its end: the person's next action gets a new session
677
+ this.nextFrom(this.engine.state()) === null)
608
678
  session.connect();
609
679
  }
680
+ /** A refusal already led to a new session, and none has been ready since: another refusal is no cue. */
681
+ rolled = false;
610
682
  statusChanged(status) {
683
+ if (status === "ready") this.rolled = false;
684
+ if (status === "unauthorized" && !this.rolled) {
685
+ this.rolled = true;
686
+ void this.resumeWith("resume");
687
+ }
611
688
  if (status === "idle")
612
689
  this.offlineAfterRetries = this.lastStatus === "reconnecting";
613
690
  else if (status !== "reconnecting") this.offlineAfterRetries = false;
@@ -632,9 +709,18 @@ var ConversationStore = class {
632
709
  const session = frame.session?.id;
633
710
  const moved = typeof session === "string" && session !== this.readySession;
634
711
  if (typeof session === "string") this.readySession = session;
635
- if (frame.resumed === true && Array.isArray(frame.transcript)) {
712
+ if (frame.resumed === true && Array.isArray(frame.transcript) && !transcriptHasIds(frame.transcript)) {
636
713
  this.transcriptSeen = true;
637
714
  this.anchorCards(moved);
715
+ } else if (moved && frame.resumed === true) {
716
+ const pending = new Set(
717
+ (Array.isArray(frame.pending_cards) ? frame.pending_cards : []).map(
718
+ (c) => str(c.id)
719
+ )
720
+ );
721
+ for (const card of this.engine.state().cards)
722
+ if (pending.has(card.id)) this.inert.delete(card.id);
723
+ else this.inert.add(card.id);
638
724
  }
639
725
  return;
640
726
  }
@@ -667,6 +753,17 @@ var ConversationStore = class {
667
753
  code: str(frame.code)
668
754
  });
669
755
  return;
756
+ case "turn.started": {
757
+ const turn = str(frame.turn_id);
758
+ const input = str(frame.client_event_id);
759
+ if (turn === null || input === null) return;
760
+ this.turnInputs.set(turn, input);
761
+ if (this.turnInputs.size > TURN_INPUTS) {
762
+ const oldest = this.turnInputs.keys().next().value;
763
+ if (oldest !== void 0) this.turnInputs.delete(oldest);
764
+ }
765
+ return;
766
+ }
670
767
  case "turn.failed": {
671
768
  const turn = str(frame.turn_id) ?? "";
672
769
  if (this.engine.state().messages.some((m) => m.key === `a:${turn}`))
@@ -675,7 +772,9 @@ var ConversationStore = class {
675
772
  add(`failed:${turn}`, "turn_failed", {
676
773
  code: str(frame.code),
677
774
  message: str(frame.message),
678
- retryable: frame.retryable === true
775
+ retryable: frame.retryable === true,
776
+ retry: frame.retryable === true && this.turnText(turn) !== void 0,
777
+ turn
679
778
  });
680
779
  return;
681
780
  }
@@ -683,6 +782,13 @@ var ConversationStore = class {
683
782
  return;
684
783
  }
685
784
  }
785
+ /** The person's text that started a turn, while its message is on screen. */
786
+ turnText(turn) {
787
+ const input = this.turnInputs.get(turn);
788
+ if (input === void 0) return void 0;
789
+ const message = this.engine.state().messages.find((m) => m.key === input && m.role === "person");
790
+ return message?.text || void 0;
791
+ }
686
792
  /**
687
793
  * A resumed session's transcript replaced the messages, and a chat transcript carries text only: the engine drops
688
794
  * the cards it drew, and draws the pending ones again at the end. Each card on screen is anchored just after the
@@ -773,6 +879,7 @@ var ConversationStore = class {
773
879
  transcriptSeen: this.transcriptSeen,
774
880
  cardOrder: this.cardOrder,
775
881
  kept: this.kept,
882
+ inert: this.inert,
776
883
  system: this.system
777
884
  },
778
885
  prev?.items ?? NO_ITEMS
@@ -784,10 +891,12 @@ var ConversationStore = class {
784
891
  const avatarUrl = this.info?.agent?.avatar_url ?? null;
785
892
  const view = {
786
893
  status,
894
+ conversationId: s.conversationId,
787
895
  banner: banner(status, next),
788
896
  next,
789
897
  agent: sameAgent(prev?.agent, name, avatarUrl) ? prev.agent : { name, avatarUrl },
790
898
  disclosure: s.disclosure ?? this.info?.disclosure ?? null,
899
+ welcome: this.info?.welcome ?? null,
791
900
  takeover: s.takeover,
792
901
  items,
793
902
  activity: prev && sameTools(prev.activity, activity) ? prev.activity : activity,
@@ -894,6 +1003,11 @@ var ConversationStore = class {
894
1003
  }, PERSIST_DELAY);
895
1004
  }
896
1005
  // ------------------------------------------------------------ guards
1006
+ /** Past the session's end (`expires_at`), when the backend passed it on. */
1007
+ sessionOver() {
1008
+ const end = this.info?.expires_at;
1009
+ return end !== void 0 && Date.now() >= Date.parse(end);
1010
+ }
897
1011
  live() {
898
1012
  if (this.disposed)
899
1013
  throw new VinaverseClientError(
@@ -947,6 +1061,42 @@ function shallowEqual(a, b) {
947
1061
  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;
948
1062
  }
949
1063
 
950
- export { ConversationStore, MAX_TEXT, answerProblem, codePoints, createConversation, isAllowedLink, isValidLinkPrefix };
1064
+ // src/conversation/modes.ts
1065
+ function createConversationsByMode(options) {
1066
+ const { getSession, modes = [], ...shared } = options;
1067
+ const base = shared.persistenceKey ?? "vinaverse.conversation";
1068
+ const stores = /* @__PURE__ */ new Map();
1069
+ const get = (mode) => {
1070
+ let store = stores.get(mode);
1071
+ if (!store) {
1072
+ store = createConversation({
1073
+ ...shared,
1074
+ persistenceKey: `${base}.${mode}`,
1075
+ getSession: (request) => getSession({ ...request, mode })
1076
+ });
1077
+ stores.set(mode, store);
1078
+ }
1079
+ return store;
1080
+ };
1081
+ return {
1082
+ get,
1083
+ select(mode) {
1084
+ for (const [other, store] of stores)
1085
+ if (other !== mode) store.setForeground(false);
1086
+ return get(mode);
1087
+ },
1088
+ dispose() {
1089
+ for (const store of stores.values()) store.dispose();
1090
+ stores.clear();
1091
+ },
1092
+ clear() {
1093
+ for (const mode of /* @__PURE__ */ new Set([...stores.keys(), ...modes]))
1094
+ get(mode).clear();
1095
+ stores.clear();
1096
+ }
1097
+ };
1098
+ }
1099
+
1100
+ export { ConversationStore, MAX_TEXT, answerProblem, codePoints, createConversation, createConversationsByMode, isAllowedLink, isValidLinkPrefix };
951
1101
  //# sourceMappingURL=index.js.map
952
1102
  //# sourceMappingURL=index.js.map