@vinaverse/client 0.1.0-alpha.6 → 0.1.0-alpha.7

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 (35) hide show
  1. package/README.md +66 -6
  2. package/dist/agent/index.cjs +523 -75
  3. package/dist/agent/index.cjs.map +1 -1
  4. package/dist/agent/index.d.cts +2 -2
  5. package/dist/agent/index.d.ts +2 -2
  6. package/dist/agent/index.js +2 -2
  7. package/dist/backend/index.cjs +268 -0
  8. package/dist/backend/index.cjs.map +1 -0
  9. package/dist/backend/index.d.cts +72 -0
  10. package/dist/backend/index.d.ts +72 -0
  11. package/dist/backend/index.js +250 -0
  12. package/dist/backend/index.js.map +1 -0
  13. package/dist/chunk-3BLRAWNC.js +840 -0
  14. package/dist/chunk-3BLRAWNC.js.map +1 -0
  15. package/dist/{chunk-ZWG2TMUL.js → chunk-46D3FSLU.js} +24 -2
  16. package/dist/chunk-46D3FSLU.js.map +1 -0
  17. package/dist/conversation/index.cjs +22 -0
  18. package/dist/conversation/index.cjs.map +1 -1
  19. package/dist/conversation/index.d.cts +2 -2
  20. package/dist/conversation/index.d.ts +2 -2
  21. package/dist/conversation/index.js +2 -2
  22. package/dist/{runtime-DLJbJkSr.d.ts → runtime-C3ZknHdG.d.cts} +137 -20
  23. package/dist/{runtime-CErMdYxJ.d.cts → runtime-CZ1hLvAl.d.ts} +137 -20
  24. package/dist/{store-C7FtS5AY.d.cts → store-7WU1kD9W.d.cts} +31 -1
  25. package/dist/{store-DFk0ayh8.d.ts → store-DrXSPHu6.d.ts} +31 -1
  26. package/dist/testing/index.cjs +38 -1
  27. package/dist/testing/index.cjs.map +1 -1
  28. package/dist/testing/index.d.cts +14 -3
  29. package/dist/testing/index.d.ts +14 -3
  30. package/dist/testing/index.js +40 -3
  31. package/dist/testing/index.js.map +1 -1
  32. package/package.json +21 -2
  33. package/dist/chunk-XPES5PZ3.js +0 -416
  34. package/dist/chunk-XPES5PZ3.js.map +0 -1
  35. package/dist/chunk-ZWG2TMUL.js.map +0 -1
@@ -1816,6 +1816,7 @@ var ConversationStore = class {
1816
1816
  throw new RangeError(`A message is at most ${MAX_TEXT} characters.`);
1817
1817
  const id = (this.options.createId ?? randomId)();
1818
1818
  this.sent.set(id, { text: text2, order: lastOrder(this.engine.state()) + 0.5 });
1819
+ this.tellActivity({ type: "message_sent", id });
1819
1820
  if (!this.session) {
1820
1821
  this.early.set(id, text2);
1821
1822
  void this.ensureSession("open");
@@ -2063,9 +2064,30 @@ var ConversationStore = class {
2063
2064
  this.lastStatus = status;
2064
2065
  this.update();
2065
2066
  }
2067
+ /** The app's analytics hears of a step; what it throws is reported (`onError`), never in the way of a send or a frame. */
2068
+ tellActivity(activity) {
2069
+ try {
2070
+ this.options.onActivity?.(activity);
2071
+ } catch (error) {
2072
+ this.options.onError?.(error);
2073
+ }
2074
+ }
2075
+ /** The live frames analytics hears of: a reply finished, cards drawn. */
2076
+ activity(frame) {
2077
+ if (!this.options.onActivity) return;
2078
+ const on = (a) => this.tellActivity(a);
2079
+ if (frame.type === "message.completed" && typeof frame.turn_id === "string")
2080
+ on({ type: "reply_completed", turnId: frame.turn_id });
2081
+ if (frame.type === "ui.cards" && Array.isArray(frame.cards)) {
2082
+ for (const card of frame.cards)
2083
+ if (typeof card.id === "string" && typeof card.kind === "string")
2084
+ on({ type: "card_shown", cardId: card.id, kind: card.kind });
2085
+ }
2086
+ }
2066
2087
  event(event) {
2067
2088
  const frame = event;
2068
2089
  const str2 = (v) => typeof v === "string" ? v : null;
2090
+ this.activity(frame);
2069
2091
  const add = (key, kind, data) => {
2070
2092
  if (this.system.some((s) => s.item.key === key)) return;
2071
2093
  this.system = [
@@ -2448,7 +2470,106 @@ var AgentRefusal = class extends Error {
2448
2470
  reason;
2449
2471
  code;
2450
2472
  };
2473
+ var GREETED = "vinaverse.greeted";
2474
+ var GREETED_BEFORE = "vinaverse.agent.greeted";
2475
+ function greetedKey(subject, mode) {
2476
+ return `${GREETED}.${JSON.stringify([subject, mode])}`;
2477
+ }
2478
+ function tupleOf(key, prefix) {
2479
+ if (!key.startsWith(`${prefix}.[`)) return null;
2480
+ try {
2481
+ const t = JSON.parse(key.slice(prefix.length + 1));
2482
+ return Array.isArray(t) ? t : null;
2483
+ } catch {
2484
+ return null;
2485
+ }
2486
+ }
2487
+ function legacyTranscripts(keys, prefix) {
2488
+ const out = [];
2489
+ for (const key of keys) {
2490
+ const t = tupleOf(key, prefix);
2491
+ if (t)
2492
+ out.push(
2493
+ `${prefix}.${t[1] ?? "guest"}|${t[2]}|${t[3] ?? "-"}|${t[4] ?? "chat"}`
2494
+ );
2495
+ }
2496
+ return out;
2497
+ }
2498
+ function legacyGreetings(pairs) {
2499
+ const out = [];
2500
+ for (const [subject, mode] of pairs)
2501
+ out.push(`${GREETED_BEFORE}.${subject ?? "guest"}:${mode}`);
2502
+ return out;
2503
+ }
2451
2504
  var NOT_KEPT = /* @__PURE__ */ new Set(["starting", "unavailable", "unauthorized", "closed"]);
2505
+ var CLEARED = { screen: "other" };
2506
+ function hashKey(key, salt) {
2507
+ return `k${sha256(`${salt}\0${key}`).slice(0, 32)}`;
2508
+ }
2509
+ var K;
2510
+ var H;
2511
+ function sha256(text2) {
2512
+ if (!K || !H) {
2513
+ K = [];
2514
+ H = [];
2515
+ for (let n = 2; K.length < 64; n++) {
2516
+ let prime = true;
2517
+ for (let d = 2; d * d <= n; d++) if (n % d === 0) prime = false;
2518
+ if (!prime) continue;
2519
+ if (H.length < 8) H.push(Math.sqrt(n) % 1 * 2 ** 32 | 0);
2520
+ K.push(Math.cbrt(n) % 1 * 2 ** 32 | 0);
2521
+ }
2522
+ }
2523
+ const bytes = [];
2524
+ const s = encodeURIComponent(text2);
2525
+ for (let i = 0; i < s.length; i++)
2526
+ if (s[i] === "%") {
2527
+ bytes.push(Number.parseInt(s.slice(i + 1, i + 3), 16));
2528
+ i += 2;
2529
+ } else bytes.push(s.charCodeAt(i));
2530
+ const bits = bytes.length * 8;
2531
+ bytes.push(128);
2532
+ while (bytes.length % 64 !== 56) bytes.push(0);
2533
+ for (const word of [Math.floor(bits / 2 ** 32), bits >>> 0])
2534
+ bytes.push(
2535
+ word >>> 24,
2536
+ word >>> 16 & 255,
2537
+ word >>> 8 & 255,
2538
+ word & 255
2539
+ );
2540
+ const h = [...H];
2541
+ const w = new Array(64);
2542
+ const ror = (x, n) => x >>> n | x << 32 - n;
2543
+ for (let at = 0; at < bytes.length; at += 64) {
2544
+ for (let i = 0; i < 64; i++) {
2545
+ if (i < 16) {
2546
+ const b2 = at + i * 4;
2547
+ w[i] = bytes[b2] << 24 | bytes[b2 + 1] << 16 | bytes[b2 + 2] << 8 | bytes[b2 + 3];
2548
+ } else {
2549
+ const x = w[i - 15];
2550
+ const y = w[i - 2];
2551
+ w[i] = w[i - 16] + (ror(x, 7) ^ ror(x, 18) ^ x >>> 3) + w[i - 7] + (ror(y, 17) ^ ror(y, 19) ^ y >>> 10) | 0;
2552
+ }
2553
+ }
2554
+ let [a, b, c, d, e, f, g, k] = h;
2555
+ for (let i = 0; i < 64; i++) {
2556
+ const t1 = k + (ror(e, 6) ^ ror(e, 11) ^ ror(e, 25)) + (e & f ^ ~e & g) + K[i] + w[i] | 0;
2557
+ const t2 = (ror(a, 2) ^ ror(a, 13) ^ ror(a, 22)) + (a & b ^ a & c ^ b & c) | 0;
2558
+ k = g;
2559
+ g = f;
2560
+ f = e;
2561
+ e = d + t1 | 0;
2562
+ d = c;
2563
+ c = b;
2564
+ b = a;
2565
+ a = t1 + t2 | 0;
2566
+ }
2567
+ [a, b, c, d, e, f, g, k].forEach((v, i) => {
2568
+ h[i] = h[i] + v | 0;
2569
+ });
2570
+ }
2571
+ return h.map((v) => (v >>> 0).toString(16).padStart(8, "0")).join("");
2572
+ }
2452
2573
  var NO_FEATURES = {
2453
2574
  history: false,
2454
2575
  memory: false,
@@ -2456,8 +2577,14 @@ var NO_FEATURES = {
2456
2577
  businessSetup: false
2457
2578
  };
2458
2579
  var SYSTEM_CLOCK = { now: () => Date.now() };
2459
- function agentKey(key) {
2460
- return `${key.subject ?? "guest"}|${key.mode}|${key.scope ?? "-"}|${key.thread ?? "chat"}`;
2580
+ function agentKey(key, agent = null) {
2581
+ return JSON.stringify([
2582
+ agent,
2583
+ key.subject,
2584
+ key.mode,
2585
+ key.scope ?? null,
2586
+ key.thread ?? "chat"
2587
+ ]);
2461
2588
  }
2462
2589
  var samePlace = (a, b) => a?.screen === b?.screen && (a?.entity?.type ?? null) === (b?.entity?.type ?? null) && (a?.entity?.id ?? null) === (b?.entity?.id ?? null) && JSON.stringify(a?.filters ?? null) === JSON.stringify(b?.filters ?? null);
2463
2590
  function usable(session, insecure) {
@@ -2467,9 +2594,11 @@ function warnInsecure(insecure, warn) {
2467
2594
  if (!(insecure.realtime || insecure.turns)) return;
2468
2595
  if (globalThis.__DEV__ === true) return;
2469
2596
  warn(
2470
- "@vinaverse/client/agent: insecureAddresses lets sessions use plaintext ws:// or http:// addresses. It is for mocks and local development only; remove it from release builds."
2597
+ "@vinaverse/client/agent: insecureAddresses lets sessions use plaintext ws:// or http:// addresses. For mocks and local development only."
2471
2598
  );
2472
2599
  }
2600
+ var OPEN_KEPT_MS = 24 * 36e5;
2601
+ var MAX_WAITING = 100;
2473
2602
  function createAgentRuntime(options) {
2474
2603
  const { backend } = options;
2475
2604
  const policy = {
@@ -2487,7 +2616,74 @@ function createAgentRuntime(options) {
2487
2616
  let status = null;
2488
2617
  let reading = null;
2489
2618
  let sessionName = null;
2490
- const emit = (event) => options.onEvent?.(event);
2619
+ let generation = 0;
2620
+ const keyOf = (key) => agentKey(key, options.agent ?? null);
2621
+ const prefix = options.conversation?.persistenceKey ?? "vinaverse.agent";
2622
+ const storage = guardedStorage(
2623
+ options.conversation?.persistence,
2624
+ prefix,
2625
+ () => generation
2626
+ );
2627
+ const greetings = guardedStorage(
2628
+ options.greetingStorage,
2629
+ GREETED,
2630
+ () => generation
2631
+ );
2632
+ const warn = options.onWarning ?? ((m) => console.warn(m));
2633
+ const isolated = (what, call) => {
2634
+ try {
2635
+ call();
2636
+ } catch (error) {
2637
+ warn(
2638
+ `@vinaverse/client/agent: the app's ${what} threw (${error instanceof Error ? error.message : String(error)}); the conversation went on.`
2639
+ );
2640
+ }
2641
+ };
2642
+ const saltStore = options.conversation?.persistence ?? options.greetingStorage;
2643
+ const saltKey = `${prefix}:salt`;
2644
+ let salt = null;
2645
+ const waiting = [];
2646
+ const loadSalt = () => {
2647
+ if (!options.onEvent) return;
2648
+ const settle = (kept2) => {
2649
+ if (salt !== null) return;
2650
+ if (kept2) salt = kept2;
2651
+ else {
2652
+ salt = randomId().slice(3);
2653
+ void Promise.resolve(saltStore?.setItem(saltKey, salt)).catch(
2654
+ () => void 0
2655
+ );
2656
+ }
2657
+ for (const event of waiting.splice(0)) deliver(event);
2658
+ };
2659
+ try {
2660
+ const got = saltStore?.getItem(saltKey);
2661
+ if (typeof got === "string" || got == null) settle(got);
2662
+ else got.then(settle, () => settle(null));
2663
+ } catch {
2664
+ settle(null);
2665
+ }
2666
+ };
2667
+ const deliver = (event) => isolated(
2668
+ "onEvent",
2669
+ () => options.onEvent?.(
2670
+ event.key ? { ...event, key: hashKey(event.key, salt ?? "") } : event
2671
+ )
2672
+ );
2673
+ let dropping = false;
2674
+ const emit = (event) => {
2675
+ if (!options.onEvent) return;
2676
+ if (salt !== null) return deliver(event);
2677
+ if (waiting.push(event) > MAX_WAITING) {
2678
+ waiting.shift();
2679
+ if (!dropping)
2680
+ warn(
2681
+ "@vinaverse/client/agent: the analytics salt is still being read from storage; the oldest events are dropped."
2682
+ );
2683
+ dropping = true;
2684
+ }
2685
+ };
2686
+ loadSalt();
2491
2687
  const changed = () => {
2492
2688
  for (const l of [...listeners]) l();
2493
2689
  };
@@ -2499,6 +2695,7 @@ function createAgentRuntime(options) {
2499
2695
  const entry = live.get(k);
2500
2696
  if (!entry) return;
2501
2697
  live.delete(k);
2698
+ clearTimeout(entry.timer);
2502
2699
  entry.store.dispose();
2503
2700
  emit({ type: "conversation_ended", key: k });
2504
2701
  changed();
@@ -2511,32 +2708,45 @@ function createAgentRuntime(options) {
2511
2708
  );
2512
2709
  async function open(entry, key, request) {
2513
2710
  if (live.get(entry.key) !== entry) throw new AgentRefusal("unavailable");
2514
- const told = entry.place;
2515
- const conversationId = request.conversationId ?? current.get(entry.key) ?? null;
2711
+ const held = entry.failedOpen && clock.now() - entry.failedOpen.at < OPEN_KEPT_MS ? entry.failedOpen : void 0;
2712
+ entry.failedOpen = void 0;
2713
+ const asked = held ? {
2714
+ ...held.request,
2715
+ ...request.signal ? { signal: request.signal } : {}
2716
+ } : {
2717
+ ...request,
2718
+ conversationId: request.conversationId ?? current.get(entry.key) ?? null,
2719
+ key,
2720
+ openId: `op_${randomId().slice(3)}`,
2721
+ agent: options.agent ?? null,
2722
+ place: entry.place,
2723
+ placeRevision: ++entry.revision,
2724
+ entryPoint: entry.entryPoint
2725
+ };
2726
+ const { conversationId, openId, place: told } = asked;
2516
2727
  let session;
2517
2728
  try {
2518
2729
  session = await patiently(
2519
- () => backend.openSession({
2520
- ...request,
2521
- conversationId,
2522
- key,
2523
- place: told,
2524
- entryPoint: entry.entryPoint
2525
- }),
2730
+ () => backend.openSession(asked),
2526
2731
  request.signal,
2527
2732
  entry.key
2528
2733
  );
2529
2734
  if (!usable(session, insecure))
2530
2735
  throw new AgentRefusal("unavailable", {
2531
2736
  reason: "invalid_session",
2532
- message: "The session is not usable (an id, a secret, a wss:// realtime_url and an https:// turns_url)."
2737
+ message: "Unusable session: an id, a secret, wss:// and https:// addresses."
2533
2738
  });
2534
2739
  } catch (error) {
2535
2740
  if (live.get(entry.key) === entry) {
2536
- const failure = error instanceof AgentRefusal ? error.kind === "ended" && conversationId !== null ? "ended" : "unavailable" : policy.isRetryable(error) ? "unreachable" : "unavailable";
2741
+ const failure = isRefusal(error) ? error.kind === "ended" && conversationId !== null ? "ended" : "unavailable" : policy.isRetryable(error) ? "unreachable" : "unavailable";
2537
2742
  const reason = reasonOf(error, failure);
2538
- const code = error instanceof AgentRefusal ? error.code : codeOf(error);
2539
- update(entry, { failure, reason, code });
2743
+ const code = isRefusal(error) ? error.code : codeOf(error);
2744
+ if (failure === "unreachable") {
2745
+ const { signal, ...again } = asked;
2746
+ entry.failedOpen = { request: again, at: held?.at ?? clock.now() };
2747
+ }
2748
+ const unsettled = reason === "unknown_open" ? String(field(error, "openId") ?? openId) : null;
2749
+ update(entry, { failure, reason, code, openId: unsettled });
2540
2750
  emit({ type: "session_failed", key: entry.key, failure, reason });
2541
2751
  }
2542
2752
  throw error;
@@ -2555,7 +2765,9 @@ function createAgentRuntime(options) {
2555
2765
  failure: null,
2556
2766
  reason: null,
2557
2767
  code: null,
2768
+ openId: null,
2558
2769
  sessionId: session.id,
2770
+ conversationId: session.conversation_id ?? conversationId,
2559
2771
  moved: !samePlace(told, entry.place)
2560
2772
  });
2561
2773
  emit({
@@ -2566,25 +2778,106 @@ function createAgentRuntime(options) {
2566
2778
  });
2567
2779
  return session;
2568
2780
  }
2569
- const kept = (k) => `${options.conversation?.persistenceKey ?? "vinaverse.agent"}.${k}`;
2781
+ const kept = (k) => `${prefix}.${k}`;
2782
+ const signOut = async (erasing) => {
2783
+ generation++;
2784
+ if (erasing) salt = null;
2785
+ for (const entry of live.values()) {
2786
+ clearTimeout(entry.timer);
2787
+ entry.store.clear();
2788
+ }
2789
+ const known = /* @__PURE__ */ new Set([...live.keys(), ...current.keys()]);
2790
+ live.clear();
2791
+ current.clear();
2792
+ sessionName = null;
2793
+ status = null;
2794
+ reading = null;
2795
+ changed();
2796
+ const pairs = [];
2797
+ await Promise.all([
2798
+ backend.endSessions?.().catch(() => void 0),
2799
+ storage?.purge(
2800
+ [`${prefix}.`],
2801
+ (keys) => {
2802
+ const all = /* @__PURE__ */ new Set([...keys, ...[...known].map(kept)]);
2803
+ for (const key of all) {
2804
+ const t = tupleOf(key, prefix);
2805
+ if (t) pairs.push([t[1], t[2]]);
2806
+ }
2807
+ return [
2808
+ ...all,
2809
+ ...legacyTranscripts(all, prefix),
2810
+ ...options.legacyKeys ?? []
2811
+ ];
2812
+ },
2813
+ `${GREETED}.`
2814
+ )
2815
+ ]);
2816
+ if (!erasing) return;
2817
+ await Promise.all([
2818
+ Promise.resolve(saltStore?.removeItem(saltKey)).catch(() => void 0),
2819
+ greetings?.purge([`${GREETED}.`, `${GREETED_BEFORE}.`], (keys) => {
2820
+ for (const key of keys) {
2821
+ const t = tupleOf(key, GREETED);
2822
+ if (t) pairs.push([t[0], t[1]]);
2823
+ }
2824
+ return [...legacyGreetings(pairs), ...options.legacyKeys ?? []];
2825
+ })
2826
+ ]);
2827
+ loadSalt();
2828
+ };
2829
+ const forget = (k) => {
2830
+ current.delete(k);
2831
+ const entry = live.get(k);
2832
+ if (entry) {
2833
+ entry.store.clear();
2834
+ end(k);
2835
+ } else void storage?.removeItem(kept(k));
2836
+ };
2837
+ const activity = (entry, a) => {
2838
+ const sessionId = entry.state.sessionId;
2839
+ if (a.type === "reply_completed" && sessionId) {
2840
+ entry.turns.set(a.turnId, sessionId);
2841
+ update(entry, {});
2842
+ }
2843
+ emit({
2844
+ type: a.type,
2845
+ key: entry.key,
2846
+ ...sessionId ? { sessionId } : {},
2847
+ ...a.type === "reply_completed" ? { turnId: a.turnId } : {},
2848
+ ...a.type === "card_shown" ? { cardId: a.cardId, kind: a.kind } : {}
2849
+ });
2850
+ };
2570
2851
  function make(key) {
2571
- const k = agentKey(key);
2852
+ const k = keyOf(key);
2572
2853
  const own = options.conversation ?? {};
2573
2854
  const refresh = backend.refreshSecret?.bind(backend);
2574
2855
  const entry = {
2575
2856
  key: k,
2576
2857
  store: createConversation({
2577
2858
  ...own,
2578
- ...own.persistence ? {
2859
+ ...storage ? {
2860
+ persistence: storage,
2579
2861
  persistenceKey: kept(k)
2580
2862
  } : {},
2863
+ onActivity: (a) => {
2864
+ activity(entry, a);
2865
+ isolated("onActivity", () => own.onActivity?.(a));
2866
+ },
2581
2867
  getSession: (request) => open(entry, key, request),
2582
2868
  ...refresh ? {
2583
- refreshSecret: (id) => patiently(() => refresh(id), void 0, k)
2869
+ // A refusal (`session_closed`) is the store's cue to go on in a new session, never a fault.
2870
+ refreshSecret: (id) => patiently(() => refresh(id), void 0, k).catch((error) => {
2871
+ throw isRefusal(error) ? new VinaverseClientError(
2872
+ "needs_new_session",
2873
+ error.message
2874
+ ) : error;
2875
+ })
2584
2876
  } : {},
2585
2877
  clock,
2586
2878
  onError: (error) => {
2587
- if (!(error instanceof AgentRefusal)) own.onError?.(error);
2879
+ if (!isRefusal(error) && codeOf(error) !== "needs_new_session")
2880
+ own.onError?.(error);
2588
2881
  }
2589
2882
  }),
2590
2883
  listeners: /* @__PURE__ */ new Set(),
@@ -2594,7 +2887,9 @@ function createAgentRuntime(options) {
2594
2887
  code: null,
2595
2888
  moved: false,
2596
2889
  ratings: /* @__PURE__ */ new Map(),
2597
- sessionId: null
2890
+ sessionId: null,
2891
+ conversationId: current.get(k) ?? null,
2892
+ openId: null
2598
2893
  },
2599
2894
  place: null,
2600
2895
  told: null,
@@ -2604,32 +2899,59 @@ function createAgentRuntime(options) {
2604
2899
  shown: 0,
2605
2900
  started: false,
2606
2901
  suspended: false,
2607
- sentOnce: /* @__PURE__ */ new Set()
2902
+ sentOnce: /* @__PURE__ */ new Set(),
2903
+ timer: void 0,
2904
+ turns: /* @__PURE__ */ new Map(),
2905
+ failedOpen: void 0
2608
2906
  };
2609
2907
  return entry;
2610
2908
  }
2611
2909
  const over = (entry) => entry.expiresAt !== null && entry.expiresAt <= clock.now();
2910
+ const expireLater = (entry) => {
2911
+ clearTimeout(entry.timer);
2912
+ if (entry.expiresAt === null) return;
2913
+ entry.timer = setTimeout(
2914
+ () => {
2915
+ entry.timer = void 0;
2916
+ if (live.get(entry.key) !== entry || entry.shown > 0) return;
2917
+ if (over(entry)) end(entry.key);
2918
+ else expireLater(entry);
2919
+ },
2920
+ Math.max(0, entry.expiresAt - clock.now())
2921
+ );
2922
+ };
2612
2923
  const tell = (entry, at) => {
2613
- entry.place = at.place ?? null;
2614
- entry.entryPoint = at.entryPoint ?? null;
2924
+ if (at.place !== void 0) entry.place = at.place;
2925
+ if (at.entryPoint !== void 0) entry.entryPoint = at.entryPoint;
2615
2926
  const sessionId = entry.state.sessionId;
2616
2927
  const moved = sessionId !== null && !samePlace(entry.told, entry.place);
2617
2928
  update(entry, { moved });
2618
- const place = entry.place;
2619
- if (!moved || !place || !backend.updateContext || sessionId === null)
2620
- return;
2621
- const revision = ++entry.revision;
2622
- backend.updateContext(sessionId, place, revision).then(
2623
- () => {
2624
- if (live.get(entry.key) !== entry || entry.revision !== revision)
2625
- return;
2626
- entry.told = place;
2627
- update(entry, { moved: !samePlace(entry.told, entry.place) });
2628
- emit({ type: "context_updated", key: entry.key, sessionId });
2629
- },
2630
- () => void 0
2631
- // not served, or refused: the question about this place stays the way to say it
2632
- );
2929
+ const updateContext = backend.updateContext?.bind(backend);
2930
+ if (!moved || !updateContext || sessionId === null) return;
2931
+ const asked = generation;
2932
+ const current2 = () => asked === generation && live.get(entry.key) === entry && entry.state.sessionId === sessionId;
2933
+ const send = (again) => {
2934
+ const place = entry.place;
2935
+ const revision = ++entry.revision;
2936
+ updateContext(sessionId, place ?? CLEARED, revision).then(
2937
+ () => {
2938
+ if (!current2() || entry.revision !== revision) return;
2939
+ entry.told = place;
2940
+ update(entry, { moved: !samePlace(entry.told, entry.place) });
2941
+ emit({ type: "context_updated", key: entry.key, sessionId });
2942
+ },
2943
+ (error) => {
2944
+ const theirs = field(error, "currentRevision");
2945
+ const conflict = field(error, "outcome") === "context_conflict" || field(error, "reason") === "conflict";
2946
+ if (!conflict || again || !current2() || entry.revision !== revision)
2947
+ return;
2948
+ if (typeof theirs === "number" && theirs >= entry.revision)
2949
+ entry.revision = theirs;
2950
+ send(true);
2951
+ }
2952
+ );
2953
+ };
2954
+ send(false);
2633
2955
  };
2634
2956
  function handle(entry) {
2635
2957
  return {
@@ -2642,6 +2964,8 @@ function createAgentRuntime(options) {
2642
2964
  return () => void 0;
2643
2965
  }
2644
2966
  entry.shown++;
2967
+ clearTimeout(entry.timer);
2968
+ entry.timer = void 0;
2645
2969
  if (entry.suspended) {
2646
2970
  entry.suspended = false;
2647
2971
  entry.store.setForeground(true);
@@ -2663,6 +2987,7 @@ function createAgentRuntime(options) {
2663
2987
  }
2664
2988
  entry.suspended = true;
2665
2989
  entry.store.setForeground(false);
2990
+ expireLater(entry);
2666
2991
  };
2667
2992
  },
2668
2993
  tell: (at) => tell(entry, at),
@@ -2672,13 +2997,18 @@ function createAgentRuntime(options) {
2672
2997
  return () => entry.listeners.delete(listener);
2673
2998
  },
2674
2999
  retry() {
2675
- update(entry, { failure: null, reason: null, code: null });
3000
+ update(entry, {
3001
+ failure: null,
3002
+ reason: null,
3003
+ code: null,
3004
+ openId: null
3005
+ });
2676
3006
  entry.store.start();
2677
3007
  },
3008
+ canRate: (turnId) => entry.turns.has(turnId),
2678
3009
  rate(turnId, rating) {
2679
- const sessionId = entry.state.sessionId;
2680
- if (sessionId === null || entry.state.ratings.get(turnId) === rating)
2681
- return;
3010
+ const sessionId = entry.turns.get(turnId);
3011
+ if (!sessionId || entry.state.ratings.get(turnId) === rating) return;
2682
3012
  update(entry, {
2683
3013
  ratings: new Map(entry.state.ratings).set(turnId, rating)
2684
3014
  });
@@ -2694,7 +3024,7 @@ function createAgentRuntime(options) {
2694
3024
  }
2695
3025
  const handles = /* @__PURE__ */ new WeakMap();
2696
3026
  const lookup = (key) => {
2697
- const k = agentKey(key);
3027
+ const k = keyOf(key);
2698
3028
  let entry = live.get(k);
2699
3029
  if (!entry) {
2700
3030
  entry = make(key);
@@ -2709,63 +3039,72 @@ function createAgentRuntime(options) {
2709
3039
  };
2710
3040
  return {
2711
3041
  conversation(key) {
2712
- const k = agentKey(key);
3042
+ const k = keyOf(key);
2713
3043
  const entry = live.get(k);
2714
3044
  if (entry && entry.shown === 0 && over(entry)) end(k);
2715
3045
  return lookup(key);
2716
3046
  },
2717
3047
  peek: lookup,
2718
3048
  startNew(key) {
2719
- const k = agentKey(key);
3049
+ const k = keyOf(key);
2720
3050
  current.delete(k);
2721
3051
  end(k);
2722
3052
  },
2723
3053
  dispose(key) {
2724
- end(agentKey(key));
3054
+ end(keyOf(key));
3055
+ },
3056
+ forget(key) {
3057
+ forget(keyOf(key));
3058
+ },
3059
+ sweep() {
3060
+ for (const entry of [...live.values()])
3061
+ if (entry.shown === 0 && over(entry)) end(entry.key);
2725
3062
  },
2726
3063
  continue(key, conversationId) {
2727
- const k = agentKey(key);
3064
+ const k = keyOf(key);
2728
3065
  if (current.get(k) !== conversationId) {
3066
+ forget(k);
2729
3067
  current.set(k, conversationId);
2730
- const entry = live.get(k);
2731
- if (entry) {
2732
- entry.store.clear();
2733
- end(k);
2734
- } else {
2735
- try {
2736
- void Promise.resolve(
2737
- options.conversation?.persistence?.removeItem(kept(k))
2738
- ).catch(() => void 0);
2739
- } catch {
2740
- }
2741
- }
2742
3068
  }
2743
3069
  return lookup(key);
2744
3070
  },
2745
3071
  async signOut() {
2746
- for (const entry of live.values()) entry.store.clear();
2747
- live.clear();
2748
- current.clear();
2749
- sessionName = null;
2750
- changed();
2751
- await backend.endSessions?.().catch(() => void 0);
3072
+ await signOut(false);
3073
+ },
3074
+ async erase() {
3075
+ await signOut(true);
2752
3076
  },
3077
+ async deleteConversation(key, conversationId) {
3078
+ if (!backend.deleteConversation)
3079
+ throw new Error(
3080
+ "@vinaverse/client/agent: deleteConversation needs AgentBackend.deleteConversation."
3081
+ );
3082
+ await backend.deleteConversation(conversationId);
3083
+ const k = keyOf(key);
3084
+ const continued = current.get(k) ?? live.get(k)?.store.getSnapshot().conversationId ?? null;
3085
+ if (continued === conversationId) forget(k);
3086
+ },
3087
+ greetings: greetings ?? null,
2753
3088
  status() {
2754
3089
  if (status && clock.now() - status.at < staleMs)
2755
3090
  return Promise.resolve(status.value);
2756
- reading ??= readStatus(backend).then(
3091
+ if (reading) return reading;
3092
+ const asked = generation;
3093
+ const read = readStatus(backend).then(
2757
3094
  (value) => {
3095
+ if (asked !== generation) return value;
2758
3096
  reading = null;
2759
3097
  status = { value, at: clock.now() };
2760
3098
  changed();
2761
3099
  return value;
2762
3100
  },
2763
3101
  (error) => {
2764
- reading = null;
3102
+ if (asked === generation) reading = null;
2765
3103
  throw error;
2766
3104
  }
2767
3105
  );
2768
- return reading;
3106
+ reading = read;
3107
+ return read;
2769
3108
  },
2770
3109
  getStatus: () => status?.value ?? null,
2771
3110
  getName: () => sessionName ?? status?.value.displayName ?? null,
@@ -2776,6 +3115,100 @@ function createAgentRuntime(options) {
2776
3115
  emit
2777
3116
  };
2778
3117
  }
3118
+ function guardedStorage(raw, prefix, generation) {
3119
+ if (!raw) return void 0;
3120
+ const indexKey = `${prefix}:index`;
3121
+ const chains = /* @__PURE__ */ new Map();
3122
+ const pending = /* @__PURE__ */ new Map();
3123
+ const queued = (key, op) => {
3124
+ pending.set(key, (pending.get(key) ?? 0) + 1);
3125
+ const run = (chains.get(key) ?? Promise.resolve()).then(op);
3126
+ const settled = run.then(
3127
+ () => void 0,
3128
+ () => void 0
3129
+ );
3130
+ chains.set(key, settled);
3131
+ void settled.then(() => {
3132
+ const left = (pending.get(key) ?? 1) - 1;
3133
+ if (left > 0) pending.set(key, left);
3134
+ else {
3135
+ pending.delete(key);
3136
+ if (chains.get(key) === settled) chains.delete(key);
3137
+ }
3138
+ });
3139
+ return run;
3140
+ };
3141
+ const safe = (p) => p.catch(() => void 0);
3142
+ const readIndex = async () => {
3143
+ try {
3144
+ const value = await raw.getItem(indexKey);
3145
+ const list3 = value ? JSON.parse(value) : [];
3146
+ return new Set(
3147
+ Array.isArray(list3) ? list3.filter((k) => typeof k === "string") : []
3148
+ );
3149
+ } catch {
3150
+ return /* @__PURE__ */ new Set();
3151
+ }
3152
+ };
3153
+ const listed = /* @__PURE__ */ new Set();
3154
+ const list2 = (key) => {
3155
+ if (listed.has(key)) return;
3156
+ listed.add(key);
3157
+ void safe(
3158
+ queued(indexKey, async () => {
3159
+ const all = await readIndex();
3160
+ if (all.has(key)) return;
3161
+ all.add(key);
3162
+ await raw.setItem(indexKey, JSON.stringify([...all]));
3163
+ })
3164
+ );
3165
+ };
3166
+ return {
3167
+ getItem(key) {
3168
+ const asked = generation();
3169
+ const read = pending.has(key) ? queued(key, () => raw.getItem(key)) : raw.getItem(key);
3170
+ if (typeof read === "string" || read === null) return read;
3171
+ return Promise.resolve(read).then(
3172
+ (v) => asked === generation() ? v ?? null : null,
3173
+ () => null
3174
+ );
3175
+ },
3176
+ setItem(key, value) {
3177
+ const asked = generation();
3178
+ return safe(
3179
+ queued(key, () => {
3180
+ if (asked !== generation()) return;
3181
+ list2(key);
3182
+ return raw.setItem(key, value);
3183
+ })
3184
+ );
3185
+ },
3186
+ removeItem: (key) => safe(queued(key, () => raw.removeItem(key))),
3187
+ /**
3188
+ * Everything kept: the index, what the app's storage finds under `under` (`removePrefix`, else `keys`), and `extra`
3189
+ * (older key shapes). Nothing under `keep` goes, so `removePrefix` is used only for a prefix that cannot reach it.
3190
+ */
3191
+ async purge(under, extra = () => [], keep) {
3192
+ const all = /* @__PURE__ */ new Set([...listed, ...await readIndex()]);
3193
+ listed.clear();
3194
+ for (const p of under)
3195
+ try {
3196
+ if (raw.removePrefix && !keep?.startsWith(p))
3197
+ await raw.removePrefix(p);
3198
+ else if (raw.keys) {
3199
+ for (const key of await raw.keys(p))
3200
+ if (!keep || !key.startsWith(keep)) all.add(key);
3201
+ }
3202
+ } catch {
3203
+ }
3204
+ for (const key of extra(all)) all.add(key);
3205
+ await Promise.all(
3206
+ [...all].map((key) => safe(queued(key, () => raw.removeItem(key))))
3207
+ );
3208
+ await safe(queued(indexKey, () => raw.removeItem(indexKey)));
3209
+ }
3210
+ };
3211
+ }
2779
3212
  async function readStatus(backend) {
2780
3213
  const read = backend.status ? await backend.status() : { available: true };
2781
3214
  const available = read.available === true;
@@ -2793,10 +3226,20 @@ async function readStatus(backend) {
2793
3226
  }
2794
3227
  };
2795
3228
  }
3229
+ function isRefusal(error) {
3230
+ return error instanceof AgentRefusal || field(error, "name") === "AgentRefusal" && (field(error, "kind") === "ended" || field(error, "kind") === "unavailable");
3231
+ }
2796
3232
  var field = (error, name) => typeof error === "object" && error !== null ? error[name] : void 0;
2797
3233
  var retryable = (error) => field(error, "retryable") === true;
2798
3234
  var REASONS = /* @__PURE__ */ new Set([
2799
3235
  "ended",
3236
+ "not_found",
3237
+ "expired",
3238
+ "mismatch",
3239
+ "completed",
3240
+ "forbidden",
3241
+ "conflict",
3242
+ "unknown_open",
2800
3243
  "auth",
2801
3244
  "off",
2802
3245
  "rate_limited",
@@ -2806,7 +3249,7 @@ var REASONS = /* @__PURE__ */ new Set([
2806
3249
  "unknown"
2807
3250
  ]);
2808
3251
  function reasonOf(error, failure) {
2809
- if (error instanceof AgentRefusal) return error.reason;
3252
+ if (isRefusal(error)) return error.reason;
2810
3253
  const given = field(error, "reason");
2811
3254
  if (typeof given === "string" && REASONS.has(given))
2812
3255
  return given;
@@ -2817,7 +3260,7 @@ function reasonOf(error, failure) {
2817
3260
  return failure === "unreachable" ? "network" : "unknown";
2818
3261
  }
2819
3262
  function codeOf(error) {
2820
- const code = field(error, "code");
3263
+ const code = field(error, "code") ?? field(error, "outcome");
2821
3264
  return typeof code === "string" ? code : null;
2822
3265
  }
2823
3266
  async function retrying(run, policy, signal, onRetry) {
@@ -2825,9 +3268,11 @@ async function retrying(run, policy, signal, onRetry) {
2825
3268
  try {
2826
3269
  return await run();
2827
3270
  } catch (error) {
2828
- const wait = policy.delays[attempt];
2829
- if (wait === void 0 || !policy.isRetryable(error) || signal?.aborted)
3271
+ const delay = policy.delays[attempt];
3272
+ if (delay === void 0 || !policy.isRetryable(error) || signal?.aborted)
2830
3273
  throw error;
3274
+ const asked = field(error, "retryAfterMs");
3275
+ const wait = typeof asked === "number" && asked > delay ? Math.min(asked, 6e4) : delay;
2831
3276
  await new Promise((resolve, reject) => {
2832
3277
  const timer = setTimeout(() => {
2833
3278
  signal?.removeEventListener("abort", stop);
@@ -2847,6 +3292,9 @@ async function retrying(run, policy, signal, onRetry) {
2847
3292
  exports.AgentRefusal = AgentRefusal;
2848
3293
  exports.agentKey = agentKey;
2849
3294
  exports.createAgentRuntime = createAgentRuntime;
3295
+ exports.greetedKey = greetedKey;
3296
+ exports.hashKey = hashKey;
3297
+ exports.isRefusal = isRefusal;
2850
3298
  exports.readCard = readCard;
2851
3299
  //# sourceMappingURL=index.cjs.map
2852
3300
  //# sourceMappingURL=index.cjs.map