@vinaverse/client 0.1.0-alpha.3 → 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 { Q as Question, A as AnswerInput, H as HeldCard, T as ToolLine, S as SessionInfo, C as ClientSecret, a as SessionOptions, P as ProposalOutcome } from '../session--W6JXfwo.cjs';
2
- export { b as CONTRACT, c as Card, d as CardKind, e as ClientErrorCode, f as HeldQuestion, g as Proposal, V as VinaverseClientError, i as isKnownCard } from '../session--W6JXfwo.cjs';
1
+ import { Q as Question, A as AnswerInput, H as HeldCard, W as Welcome, T as ToolLine, S as SessionInfo, C as ClientSecret, a as SessionOptions, P as ProposalOutcome } from '../session-C3qZwGPG.cjs';
2
+ export { b as CONTRACT, c as Card, d as CardGroup, e as CardImage, f as CardKind, g as CardLink, h as ClientErrorCode, i as HeldQuestion, j as Proposal, V as VinaverseClientError, k as isKnownCard } from '../session-C3qZwGPG.cjs';
3
3
 
4
4
  type AnswerProblem =
5
5
  /** Nothing chosen or typed, and the question is required (or skipped while required). */
@@ -47,7 +47,10 @@ type ConversationStatus =
47
47
  | 'sse'
48
48
  /** The session ended; `next` says how the conversation goes on. */
49
49
  | 'ended'
50
- /** The client secret was refused and no fresh one could be had. */
50
+ /**
51
+ * The client secret was refused, and the new session the store then asked for was refused as well: `start()` (the
52
+ * person's "Try again") asks for another.
53
+ */
51
54
  | 'unauthorized'
52
55
  /** `dispose()` or `clear()` closed it. */
53
56
  | 'closed';
@@ -123,6 +126,11 @@ interface ConversationView {
123
126
  readonly name: string | null;
124
127
  readonly avatarUrl: string | null;
125
128
  };
129
+ /**
130
+ * The tenant's greeting and chips for the session's mode (v1-draft.20, `Session.welcome`), or null. A UI shows it on
131
+ * an empty conversation; a chip sends its `message` as the person's own text (`send`).
132
+ */
133
+ readonly welcome: Welcome | null;
126
134
  /** The server's AI disclosure; null until the first one, when the UI shows its own line. Never hidden. */
127
135
  readonly disclosure: string | null;
128
136
  /** While a person from the business replies: their role (`by`, never a name). */
@@ -183,6 +191,13 @@ type ConversationSession = SessionInfo & {
183
191
  readonly avatar_url?: string;
184
192
  };
185
193
  readonly disclosure?: string;
194
+ /**
195
+ * When the session ends (`Session.expires_at`: the earliest of its own limit and the tenant's tool token). Past it,
196
+ * the conversation goes on in a new session from `getSession`, never with a fresh secret for this one.
197
+ */
198
+ readonly expires_at?: string;
199
+ /** v1-draft.20: the tenant's greeting and chips for the session's mode (`Session.welcome`); no model call. */
200
+ readonly welcome?: Welcome;
186
201
  };
187
202
  /**
188
203
  * Runs a callback before the next frame; `requestAnimationFrame` where there is one. Every listener, of the view and
@@ -265,6 +280,8 @@ declare class ConversationStore {
265
280
  private readySession;
266
281
  private cardOrder;
267
282
  private kept;
283
+ /** Cards an earlier session drew, kept by a merged transcript: drawn as restored, taking no tap. */
284
+ private readonly inert;
268
285
  private locale;
269
286
  /** The request for a session in flight, to abort when it is no longer wanted. */
270
287
  private fetching;
@@ -303,7 +320,12 @@ declare class ConversationStore {
303
320
  /** In the background (`closeInBackground`): the connection is closed; the engine keeps what was sent, to resend. */
304
321
  private suspended;
305
322
  private suspend;
306
- /** Back in the foreground: a new connection to the same session, with a fresh secret when the backend gives one. */
323
+ /**
324
+ * Back in the foreground, or the person acted while away: a new connection to the same session, with a fresh secret
325
+ * when the backend gives one. A session that passed its end meanwhile is never reopened (a fresh secret cannot bring
326
+ * back an expired tool token): the person's action continues the conversation in a new session from `getSession`,
327
+ * and until then nothing connects.
328
+ */
307
329
  private wake;
308
330
  /** The network as the app sees it; null when unknown. Back online reconnects a connection that gave up. */
309
331
  setOnline(online: boolean | null): void;
@@ -348,6 +370,8 @@ declare class ConversationStore {
348
370
  /** The person acted after the session ended idle, expired or at its limit: the next session takes what they sent. */
349
371
  private continueIfEnded;
350
372
  private reconnectIfDown;
373
+ /** A refusal already led to a new session, and none has been ready since: another refusal is no cue. */
374
+ private rolled;
351
375
  private statusChanged;
352
376
  private event;
353
377
  /** The person's text that started a turn, while its message is on screen. */
@@ -376,10 +400,37 @@ declare class ConversationStore {
376
400
  private key;
377
401
  private restore;
378
402
  private schedulePersist;
403
+ /** Past the session's end (`expires_at`), when the backend passed it on. */
404
+ private sessionOver;
379
405
  private live;
380
406
  private required;
381
407
  }
382
408
  /** One conversation with an agent, for any UI: see `ConversationStore`. */
383
409
  declare function createConversation(options: ConversationOptions): ConversationStore;
384
410
 
385
- export { AnswerInput, type AnswerProblem, type CardItem, type ConversationBanner, type ConversationOptions, type ConversationPersistence, type ConversationSession, type ConversationStatus, ConversationStore, type ConversationView, type FrameScheduler, HeldCard, MAX_TEXT, type MessageItem, type MessageState, ProposalOutcome, Question, type SessionReason, type SessionRequest, type StreamingText, type SystemEvent, type SystemItem, type TimelineItem, ToolLine, answerProblem, codePoints, createConversation, isAllowedLink, isValidLinkPrefix };
411
+ /** A session request, with the mode it is for. */
412
+ interface ModeSessionRequest extends SessionRequest {
413
+ readonly mode: string;
414
+ }
415
+ interface ConversationsByModeOptions extends Omit<ConversationOptions, 'getSession'> {
416
+ /** Your backend creates the session for the mode's agent (its `subject.type`, its agent), with its service key. */
417
+ readonly getSession: (request: ModeSessionRequest) => Promise<ConversationSession>;
418
+ /** Every mode the app has, so `clear()` forgets each one's kept history, even one not opened since launch. */
419
+ readonly modes?: readonly string[];
420
+ }
421
+ interface ConversationsByMode {
422
+ /** The mode's store, made the first time it is asked for; the same store after that. */
423
+ get(mode: string): ConversationStore;
424
+ /**
425
+ * The mode the person switched to: its store, with every other mode's sent to the background (no pings; closed
426
+ * with `closeInBackground`). The kit then drives the selected one with the app's foreground and background.
427
+ */
428
+ select(mode: string): ConversationStore;
429
+ /** Close every mode's store. */
430
+ dispose(): void;
431
+ /** Close every mode's store and forget what each kept on the device (sign-out). */
432
+ clear(): void;
433
+ }
434
+ declare function createConversationsByMode(options: ConversationsByModeOptions): ConversationsByMode;
435
+
436
+ export { AnswerInput, type AnswerProblem, type CardItem, type ConversationBanner, type ConversationOptions, type ConversationPersistence, type ConversationSession, type ConversationStatus, ConversationStore, type ConversationView, type ConversationsByMode, type ConversationsByModeOptions, type FrameScheduler, HeldCard, MAX_TEXT, type MessageItem, type MessageState, type ModeSessionRequest, ProposalOutcome, Question, type SessionReason, type SessionRequest, type StreamingText, type SystemEvent, type SystemItem, type TimelineItem, ToolLine, Welcome, answerProblem, codePoints, createConversation, createConversationsByMode, isAllowedLink, isValidLinkPrefix };
@@ -1,5 +1,5 @@
1
- import { Q as Question, A as AnswerInput, H as HeldCard, T as ToolLine, S as SessionInfo, C as ClientSecret, a as SessionOptions, P as ProposalOutcome } from '../session--W6JXfwo.js';
2
- export { b as CONTRACT, c as Card, d as CardKind, e as ClientErrorCode, f as HeldQuestion, g as Proposal, V as VinaverseClientError, i as isKnownCard } from '../session--W6JXfwo.js';
1
+ import { Q as Question, A as AnswerInput, H as HeldCard, W as Welcome, T as ToolLine, S as SessionInfo, C as ClientSecret, a as SessionOptions, P as ProposalOutcome } from '../session-C3qZwGPG.js';
2
+ export { b as CONTRACT, c as Card, d as CardGroup, e as CardImage, f as CardKind, g as CardLink, h as ClientErrorCode, i as HeldQuestion, j as Proposal, V as VinaverseClientError, k as isKnownCard } from '../session-C3qZwGPG.js';
3
3
 
4
4
  type AnswerProblem =
5
5
  /** Nothing chosen or typed, and the question is required (or skipped while required). */
@@ -47,7 +47,10 @@ type ConversationStatus =
47
47
  | 'sse'
48
48
  /** The session ended; `next` says how the conversation goes on. */
49
49
  | 'ended'
50
- /** The client secret was refused and no fresh one could be had. */
50
+ /**
51
+ * The client secret was refused, and the new session the store then asked for was refused as well: `start()` (the
52
+ * person's "Try again") asks for another.
53
+ */
51
54
  | 'unauthorized'
52
55
  /** `dispose()` or `clear()` closed it. */
53
56
  | 'closed';
@@ -123,6 +126,11 @@ interface ConversationView {
123
126
  readonly name: string | null;
124
127
  readonly avatarUrl: string | null;
125
128
  };
129
+ /**
130
+ * The tenant's greeting and chips for the session's mode (v1-draft.20, `Session.welcome`), or null. A UI shows it on
131
+ * an empty conversation; a chip sends its `message` as the person's own text (`send`).
132
+ */
133
+ readonly welcome: Welcome | null;
126
134
  /** The server's AI disclosure; null until the first one, when the UI shows its own line. Never hidden. */
127
135
  readonly disclosure: string | null;
128
136
  /** While a person from the business replies: their role (`by`, never a name). */
@@ -183,6 +191,13 @@ type ConversationSession = SessionInfo & {
183
191
  readonly avatar_url?: string;
184
192
  };
185
193
  readonly disclosure?: string;
194
+ /**
195
+ * When the session ends (`Session.expires_at`: the earliest of its own limit and the tenant's tool token). Past it,
196
+ * the conversation goes on in a new session from `getSession`, never with a fresh secret for this one.
197
+ */
198
+ readonly expires_at?: string;
199
+ /** v1-draft.20: the tenant's greeting and chips for the session's mode (`Session.welcome`); no model call. */
200
+ readonly welcome?: Welcome;
186
201
  };
187
202
  /**
188
203
  * Runs a callback before the next frame; `requestAnimationFrame` where there is one. Every listener, of the view and
@@ -265,6 +280,8 @@ declare class ConversationStore {
265
280
  private readySession;
266
281
  private cardOrder;
267
282
  private kept;
283
+ /** Cards an earlier session drew, kept by a merged transcript: drawn as restored, taking no tap. */
284
+ private readonly inert;
268
285
  private locale;
269
286
  /** The request for a session in flight, to abort when it is no longer wanted. */
270
287
  private fetching;
@@ -303,7 +320,12 @@ declare class ConversationStore {
303
320
  /** In the background (`closeInBackground`): the connection is closed; the engine keeps what was sent, to resend. */
304
321
  private suspended;
305
322
  private suspend;
306
- /** Back in the foreground: a new connection to the same session, with a fresh secret when the backend gives one. */
323
+ /**
324
+ * Back in the foreground, or the person acted while away: a new connection to the same session, with a fresh secret
325
+ * when the backend gives one. A session that passed its end meanwhile is never reopened (a fresh secret cannot bring
326
+ * back an expired tool token): the person's action continues the conversation in a new session from `getSession`,
327
+ * and until then nothing connects.
328
+ */
307
329
  private wake;
308
330
  /** The network as the app sees it; null when unknown. Back online reconnects a connection that gave up. */
309
331
  setOnline(online: boolean | null): void;
@@ -348,6 +370,8 @@ declare class ConversationStore {
348
370
  /** The person acted after the session ended idle, expired or at its limit: the next session takes what they sent. */
349
371
  private continueIfEnded;
350
372
  private reconnectIfDown;
373
+ /** A refusal already led to a new session, and none has been ready since: another refusal is no cue. */
374
+ private rolled;
351
375
  private statusChanged;
352
376
  private event;
353
377
  /** The person's text that started a turn, while its message is on screen. */
@@ -376,10 +400,37 @@ declare class ConversationStore {
376
400
  private key;
377
401
  private restore;
378
402
  private schedulePersist;
403
+ /** Past the session's end (`expires_at`), when the backend passed it on. */
404
+ private sessionOver;
379
405
  private live;
380
406
  private required;
381
407
  }
382
408
  /** One conversation with an agent, for any UI: see `ConversationStore`. */
383
409
  declare function createConversation(options: ConversationOptions): ConversationStore;
384
410
 
385
- export { AnswerInput, type AnswerProblem, type CardItem, type ConversationBanner, type ConversationOptions, type ConversationPersistence, type ConversationSession, type ConversationStatus, ConversationStore, type ConversationView, type FrameScheduler, HeldCard, MAX_TEXT, type MessageItem, type MessageState, ProposalOutcome, Question, type SessionReason, type SessionRequest, type StreamingText, type SystemEvent, type SystemItem, type TimelineItem, ToolLine, answerProblem, codePoints, createConversation, isAllowedLink, isValidLinkPrefix };
411
+ /** A session request, with the mode it is for. */
412
+ interface ModeSessionRequest extends SessionRequest {
413
+ readonly mode: string;
414
+ }
415
+ interface ConversationsByModeOptions extends Omit<ConversationOptions, 'getSession'> {
416
+ /** Your backend creates the session for the mode's agent (its `subject.type`, its agent), with its service key. */
417
+ readonly getSession: (request: ModeSessionRequest) => Promise<ConversationSession>;
418
+ /** Every mode the app has, so `clear()` forgets each one's kept history, even one not opened since launch. */
419
+ readonly modes?: readonly string[];
420
+ }
421
+ interface ConversationsByMode {
422
+ /** The mode's store, made the first time it is asked for; the same store after that. */
423
+ get(mode: string): ConversationStore;
424
+ /**
425
+ * The mode the person switched to: its store, with every other mode's sent to the background (no pings; closed
426
+ * with `closeInBackground`). The kit then drives the selected one with the app's foreground and background.
427
+ */
428
+ select(mode: string): ConversationStore;
429
+ /** Close every mode's store. */
430
+ dispose(): void;
431
+ /** Close every mode's store and forget what each kept on the device (sign-out). */
432
+ clear(): void;
433
+ }
434
+ declare function createConversationsByMode(options: ConversationsByModeOptions): ConversationsByMode;
435
+
436
+ export { AnswerInput, type AnswerProblem, type CardItem, type ConversationBanner, type ConversationOptions, type ConversationPersistence, type ConversationSession, type ConversationStatus, ConversationStore, type ConversationView, type ConversationsByMode, type ConversationsByModeOptions, type FrameScheduler, HeldCard, MAX_TEXT, type MessageItem, type MessageState, type ModeSessionRequest, ProposalOutcome, Question, type SessionReason, type SessionRequest, type StreamingText, type SystemEvent, type SystemItem, type TimelineItem, ToolLine, Welcome, answerProblem, codePoints, createConversation, createConversationsByMode, isAllowedLink, isValidLinkPrefix };
@@ -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 {
@@ -279,6 +291,8 @@ var ConversationStore = class {
279
291
  readySession = null;
280
292
  cardOrder = /* @__PURE__ */ new Map();
281
293
  kept = [];
294
+ /** Cards an earlier session drew, kept by a merged transcript: drawn as restored, taking no tap. */
295
+ inert = /* @__PURE__ */ new Set();
282
296
  locale;
283
297
  /** The request for a session in flight, to abort when it is no longer wanted. */
284
298
  fetching = null;
@@ -320,6 +334,8 @@ var ConversationStore = class {
320
334
  this.live();
321
335
  if (this.session) {
322
336
  if (this.session.status === "idle") this.session.connect();
337
+ else if (this.session.status === "unauthorized")
338
+ void this.resumeWith("resume");
323
339
  return;
324
340
  }
325
341
  void this.ensureSession("open");
@@ -384,9 +400,19 @@ var ConversationStore = class {
384
400
  session.close();
385
401
  this.update();
386
402
  }
387
- /** Back in the foreground: a new connection to the same session, with a fresh secret when the backend gives one. */
388
- 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) {
389
410
  const info = this.info;
411
+ if (info && this.sessionOver()) {
412
+ if (byPerson) await this.resumeWith("resume");
413
+ else this.update();
414
+ return;
415
+ }
390
416
  this.suspended = false;
391
417
  if (!info) return;
392
418
  let next = info;
@@ -426,7 +452,7 @@ var ConversationStore = class {
426
452
  }
427
453
  if (this.suspended) {
428
454
  this.early.set(id, text);
429
- void this.wake();
455
+ void this.wake(true);
430
456
  this.update();
431
457
  return id;
432
458
  }
@@ -436,6 +462,12 @@ var ConversationStore = class {
436
462
  this.update();
437
463
  return id;
438
464
  }
465
+ if (this.sessionOver()) {
466
+ this.early.set(id, text);
467
+ void this.resumeWith("resume");
468
+ this.update();
469
+ return id;
470
+ }
439
471
  this.nextId = id;
440
472
  this.session.sendText(text);
441
473
  this.nextId = null;
@@ -572,11 +604,21 @@ var ConversationStore = class {
572
604
  }
573
605
  open(info) {
574
606
  this.info = info;
607
+ this.suspended = false;
608
+ const refreshSecret = this.options.refreshSecret;
575
609
  const session = new VinaverseSession({
576
610
  ...this.options.transport,
577
611
  session: info,
578
612
  engine: this.engine,
579
- ...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
+ } : {},
580
622
  createId: () => this.nextId ?? (this.options.createId ?? randomId)()
581
623
  });
582
624
  this.session = session;
@@ -584,7 +626,9 @@ var ConversationStore = class {
584
626
  session.on("state", () => this.update()),
585
627
  session.on("status", (status) => this.statusChanged(status)),
586
628
  session.on("event", (event) => this.event(event)),
587
- session.on("error", (error) => this.options.onError?.(error))
629
+ session.on("error", (error) => {
630
+ if (!this.sessionOver()) this.options.onError?.(error);
631
+ })
588
632
  );
589
633
  session.setForeground(this.foreground);
590
634
  session.connect();
@@ -611,6 +655,10 @@ var ConversationStore = class {
611
655
  this.resuming = this.fetchSession(reason).then((info) => {
612
656
  this.resuming = null;
613
657
  if (!info || this.disposed || !this.session) return;
658
+ if (this.session.status === "closed") {
659
+ this.open(info);
660
+ return;
661
+ }
614
662
  this.info = info;
615
663
  this.session.resume(info);
616
664
  this.flushEarly();
@@ -625,10 +673,18 @@ var ConversationStore = class {
625
673
  }
626
674
  reconnectIfDown() {
627
675
  const session = this.session;
628
- 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)
629
678
  session.connect();
630
679
  }
680
+ /** A refusal already led to a new session, and none has been ready since: another refusal is no cue. */
681
+ rolled = false;
631
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
+ }
632
688
  if (status === "idle")
633
689
  this.offlineAfterRetries = this.lastStatus === "reconnecting";
634
690
  else if (status !== "reconnecting") this.offlineAfterRetries = false;
@@ -653,9 +709,18 @@ var ConversationStore = class {
653
709
  const session = frame.session?.id;
654
710
  const moved = typeof session === "string" && session !== this.readySession;
655
711
  if (typeof session === "string") this.readySession = session;
656
- if (frame.resumed === true && Array.isArray(frame.transcript)) {
712
+ if (frame.resumed === true && Array.isArray(frame.transcript) && !transcriptHasIds(frame.transcript)) {
657
713
  this.transcriptSeen = true;
658
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);
659
724
  }
660
725
  return;
661
726
  }
@@ -814,6 +879,7 @@ var ConversationStore = class {
814
879
  transcriptSeen: this.transcriptSeen,
815
880
  cardOrder: this.cardOrder,
816
881
  kept: this.kept,
882
+ inert: this.inert,
817
883
  system: this.system
818
884
  },
819
885
  prev?.items ?? NO_ITEMS
@@ -830,6 +896,7 @@ var ConversationStore = class {
830
896
  next,
831
897
  agent: sameAgent(prev?.agent, name, avatarUrl) ? prev.agent : { name, avatarUrl },
832
898
  disclosure: s.disclosure ?? this.info?.disclosure ?? null,
899
+ welcome: this.info?.welcome ?? null,
833
900
  takeover: s.takeover,
834
901
  items,
835
902
  activity: prev && sameTools(prev.activity, activity) ? prev.activity : activity,
@@ -936,6 +1003,11 @@ var ConversationStore = class {
936
1003
  }, PERSIST_DELAY);
937
1004
  }
938
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
+ }
939
1011
  live() {
940
1012
  if (this.disposed)
941
1013
  throw new VinaverseClientError(
@@ -989,6 +1061,42 @@ function shallowEqual(a, b) {
989
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;
990
1062
  }
991
1063
 
992
- 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 };
993
1101
  //# sourceMappingURL=index.js.map
994
1102
  //# sourceMappingURL=index.js.map