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

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/README.md +69 -7
  2. package/dist/agent/index.cjs +526 -75
  3. package/dist/agent/index.cjs.map +1 -1
  4. package/dist/agent/index.d.cts +3 -3
  5. package/dist/agent/index.d.ts +3 -3
  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-ZWG2TMUL.js → chunk-46D3FSLU.js} +24 -2
  14. package/dist/chunk-46D3FSLU.js.map +1 -0
  15. package/dist/chunk-DI4DPUNA.js +843 -0
  16. package/dist/chunk-DI4DPUNA.js.map +1 -0
  17. package/dist/chunk-YNUHXJD6.js +9 -0
  18. package/dist/{chunk-VSJDVLCP.js.map → chunk-YNUHXJD6.js.map} +1 -1
  19. package/dist/contract-DmQs8amJ.d.cts +6 -0
  20. package/dist/contract-DmQs8amJ.d.ts +6 -0
  21. package/dist/conversation/index.cjs +24 -2
  22. package/dist/conversation/index.cjs.map +1 -1
  23. package/dist/conversation/index.d.cts +5 -5
  24. package/dist/conversation/index.d.ts +5 -5
  25. package/dist/conversation/index.js +3 -3
  26. package/dist/index.cjs +2 -2
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts +2 -2
  29. package/dist/index.d.ts +2 -2
  30. package/dist/index.js +1 -1
  31. package/dist/{runtime-DLJbJkSr.d.ts → runtime-A4ovUdrc.d.cts} +147 -21
  32. package/dist/{runtime-CErMdYxJ.d.cts → runtime-B6_l8--U.d.ts} +147 -21
  33. package/dist/{session-8W3fRNeb.d.cts → session-CPz4pcPu.d.cts} +246 -7
  34. package/dist/{session-8W3fRNeb.d.ts → session-CPz4pcPu.d.ts} +246 -7
  35. package/dist/{store-C7FtS5AY.d.cts → store-BxiicOKG.d.cts} +32 -2
  36. package/dist/{store-DFk0ayh8.d.ts → store-DZka9T2-.d.ts} +32 -2
  37. package/dist/testing/index.cjs +38 -1
  38. package/dist/testing/index.cjs.map +1 -1
  39. package/dist/testing/index.d.cts +15 -4
  40. package/dist/testing/index.d.ts +15 -4
  41. package/dist/testing/index.js +40 -3
  42. package/dist/testing/index.js.map +1 -1
  43. package/package.json +41 -2
  44. package/dist/chunk-VSJDVLCP.js +0 -9
  45. package/dist/chunk-XPES5PZ3.js +0 -416
  46. package/dist/chunk-XPES5PZ3.js.map +0 -1
  47. package/dist/chunk-ZWG2TMUL.js.map +0 -1
  48. package/dist/contract-_NMdPjgN.d.cts +0 -6
  49. package/dist/contract-_NMdPjgN.d.ts +0 -6
@@ -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,12 @@ 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.",
2598
+ { code: "insecure_addresses" }
2471
2599
  );
2472
2600
  }
2601
+ var OPEN_KEPT_MS = 24 * 36e5;
2602
+ var MAX_WAITING = 100;
2473
2603
  function createAgentRuntime(options) {
2474
2604
  const { backend } = options;
2475
2605
  const policy = {
@@ -2487,7 +2617,76 @@ function createAgentRuntime(options) {
2487
2617
  let status = null;
2488
2618
  let reading = null;
2489
2619
  let sessionName = null;
2490
- const emit = (event) => options.onEvent?.(event);
2620
+ let generation = 0;
2621
+ const keyOf = (key) => agentKey(key, options.agent ?? null);
2622
+ const prefix = options.conversation?.persistenceKey ?? "vinaverse.agent";
2623
+ const storage = guardedStorage(
2624
+ options.conversation?.persistence,
2625
+ prefix,
2626
+ () => generation
2627
+ );
2628
+ const greetings = guardedStorage(
2629
+ options.greetingStorage,
2630
+ GREETED,
2631
+ () => generation
2632
+ );
2633
+ const warn = options.onWarning ?? ((m) => console.warn(m));
2634
+ const isolated = (what, call) => {
2635
+ try {
2636
+ call();
2637
+ } catch (error) {
2638
+ warn(
2639
+ `@vinaverse/client/agent: the app's ${what} threw (${error instanceof Error ? error.message : String(error)}); the conversation went on.`,
2640
+ { code: "callback_threw", callback: what }
2641
+ );
2642
+ }
2643
+ };
2644
+ const saltStore = options.conversation?.persistence ?? options.greetingStorage;
2645
+ const saltKey = `${prefix}:salt`;
2646
+ let salt = null;
2647
+ const waiting = [];
2648
+ const loadSalt = () => {
2649
+ if (!options.onEvent) return;
2650
+ const settle = (kept2) => {
2651
+ if (salt !== null) return;
2652
+ if (kept2) salt = kept2;
2653
+ else {
2654
+ salt = randomId().slice(3);
2655
+ void Promise.resolve(saltStore?.setItem(saltKey, salt)).catch(
2656
+ () => void 0
2657
+ );
2658
+ }
2659
+ for (const event of waiting.splice(0)) deliver(event);
2660
+ };
2661
+ try {
2662
+ const got = saltStore?.getItem(saltKey);
2663
+ if (typeof got === "string" || got == null) settle(got);
2664
+ else got.then(settle, () => settle(null));
2665
+ } catch {
2666
+ settle(null);
2667
+ }
2668
+ };
2669
+ const deliver = (event) => isolated(
2670
+ "onEvent",
2671
+ () => options.onEvent?.(
2672
+ event.key ? { ...event, key: hashKey(event.key, salt ?? "") } : event
2673
+ )
2674
+ );
2675
+ let dropping = false;
2676
+ const emit = (event) => {
2677
+ if (!options.onEvent) return;
2678
+ if (salt !== null) return deliver(event);
2679
+ if (waiting.push(event) > MAX_WAITING) {
2680
+ waiting.shift();
2681
+ if (!dropping)
2682
+ warn(
2683
+ "@vinaverse/client/agent: the analytics salt is still being read from storage; the oldest events are dropped.",
2684
+ { code: "events_dropped" }
2685
+ );
2686
+ dropping = true;
2687
+ }
2688
+ };
2689
+ loadSalt();
2491
2690
  const changed = () => {
2492
2691
  for (const l of [...listeners]) l();
2493
2692
  };
@@ -2499,6 +2698,7 @@ function createAgentRuntime(options) {
2499
2698
  const entry = live.get(k);
2500
2699
  if (!entry) return;
2501
2700
  live.delete(k);
2701
+ clearTimeout(entry.timer);
2502
2702
  entry.store.dispose();
2503
2703
  emit({ type: "conversation_ended", key: k });
2504
2704
  changed();
@@ -2511,32 +2711,45 @@ function createAgentRuntime(options) {
2511
2711
  );
2512
2712
  async function open(entry, key, request) {
2513
2713
  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;
2714
+ const held = entry.failedOpen && clock.now() - entry.failedOpen.at < OPEN_KEPT_MS ? entry.failedOpen : void 0;
2715
+ entry.failedOpen = void 0;
2716
+ const asked = held ? {
2717
+ ...held.request,
2718
+ ...request.signal ? { signal: request.signal } : {}
2719
+ } : {
2720
+ ...request,
2721
+ conversationId: request.conversationId ?? current.get(entry.key) ?? null,
2722
+ key,
2723
+ openId: `op_${randomId().slice(3)}`,
2724
+ agent: options.agent ?? null,
2725
+ place: entry.place,
2726
+ placeRevision: ++entry.revision,
2727
+ entryPoint: entry.entryPoint
2728
+ };
2729
+ const { conversationId, openId, place: told } = asked;
2516
2730
  let session;
2517
2731
  try {
2518
2732
  session = await patiently(
2519
- () => backend.openSession({
2520
- ...request,
2521
- conversationId,
2522
- key,
2523
- place: told,
2524
- entryPoint: entry.entryPoint
2525
- }),
2733
+ () => backend.openSession(asked),
2526
2734
  request.signal,
2527
2735
  entry.key
2528
2736
  );
2529
2737
  if (!usable(session, insecure))
2530
2738
  throw new AgentRefusal("unavailable", {
2531
2739
  reason: "invalid_session",
2532
- message: "The session is not usable (an id, a secret, a wss:// realtime_url and an https:// turns_url)."
2740
+ message: "Unusable session: an id, a secret, wss:// and https:// addresses."
2533
2741
  });
2534
2742
  } catch (error) {
2535
2743
  if (live.get(entry.key) === entry) {
2536
- const failure = error instanceof AgentRefusal ? error.kind === "ended" && conversationId !== null ? "ended" : "unavailable" : policy.isRetryable(error) ? "unreachable" : "unavailable";
2744
+ const failure = isRefusal(error) ? error.kind === "ended" && conversationId !== null ? "ended" : "unavailable" : policy.isRetryable(error) ? "unreachable" : "unavailable";
2537
2745
  const reason = reasonOf(error, failure);
2538
- const code = error instanceof AgentRefusal ? error.code : codeOf(error);
2539
- update(entry, { failure, reason, code });
2746
+ const code = isRefusal(error) ? error.code : codeOf(error);
2747
+ if (failure === "unreachable") {
2748
+ const { signal, ...again } = asked;
2749
+ entry.failedOpen = { request: again, at: held?.at ?? clock.now() };
2750
+ }
2751
+ const unsettled = reason === "unknown_open" ? String(field(error, "openId") ?? openId) : null;
2752
+ update(entry, { failure, reason, code, openId: unsettled });
2540
2753
  emit({ type: "session_failed", key: entry.key, failure, reason });
2541
2754
  }
2542
2755
  throw error;
@@ -2555,7 +2768,9 @@ function createAgentRuntime(options) {
2555
2768
  failure: null,
2556
2769
  reason: null,
2557
2770
  code: null,
2771
+ openId: null,
2558
2772
  sessionId: session.id,
2773
+ conversationId: session.conversation_id ?? conversationId,
2559
2774
  moved: !samePlace(told, entry.place)
2560
2775
  });
2561
2776
  emit({
@@ -2566,25 +2781,106 @@ function createAgentRuntime(options) {
2566
2781
  });
2567
2782
  return session;
2568
2783
  }
2569
- const kept = (k) => `${options.conversation?.persistenceKey ?? "vinaverse.agent"}.${k}`;
2784
+ const kept = (k) => `${prefix}.${k}`;
2785
+ const signOut = async (erasing) => {
2786
+ generation++;
2787
+ if (erasing) salt = null;
2788
+ for (const entry of live.values()) {
2789
+ clearTimeout(entry.timer);
2790
+ entry.store.clear();
2791
+ }
2792
+ const known = /* @__PURE__ */ new Set([...live.keys(), ...current.keys()]);
2793
+ live.clear();
2794
+ current.clear();
2795
+ sessionName = null;
2796
+ status = null;
2797
+ reading = null;
2798
+ changed();
2799
+ const pairs = [];
2800
+ await Promise.all([
2801
+ backend.endSessions?.().catch(() => void 0),
2802
+ storage?.purge(
2803
+ [`${prefix}.`],
2804
+ (keys) => {
2805
+ const all = /* @__PURE__ */ new Set([...keys, ...[...known].map(kept)]);
2806
+ for (const key of all) {
2807
+ const t = tupleOf(key, prefix);
2808
+ if (t) pairs.push([t[1], t[2]]);
2809
+ }
2810
+ return [
2811
+ ...all,
2812
+ ...legacyTranscripts(all, prefix),
2813
+ ...options.legacyKeys ?? []
2814
+ ];
2815
+ },
2816
+ `${GREETED}.`
2817
+ )
2818
+ ]);
2819
+ if (!erasing) return;
2820
+ await Promise.all([
2821
+ Promise.resolve(saltStore?.removeItem(saltKey)).catch(() => void 0),
2822
+ greetings?.purge([`${GREETED}.`, `${GREETED_BEFORE}.`], (keys) => {
2823
+ for (const key of keys) {
2824
+ const t = tupleOf(key, GREETED);
2825
+ if (t) pairs.push([t[0], t[1]]);
2826
+ }
2827
+ return [...legacyGreetings(pairs), ...options.legacyKeys ?? []];
2828
+ })
2829
+ ]);
2830
+ loadSalt();
2831
+ };
2832
+ const forget = (k) => {
2833
+ current.delete(k);
2834
+ const entry = live.get(k);
2835
+ if (entry) {
2836
+ entry.store.clear();
2837
+ end(k);
2838
+ } else void storage?.removeItem(kept(k));
2839
+ };
2840
+ const activity = (entry, a) => {
2841
+ const sessionId = entry.state.sessionId;
2842
+ if (a.type === "reply_completed" && sessionId) {
2843
+ entry.turns.set(a.turnId, sessionId);
2844
+ update(entry, {});
2845
+ }
2846
+ emit({
2847
+ type: a.type,
2848
+ key: entry.key,
2849
+ ...sessionId ? { sessionId } : {},
2850
+ ...a.type === "reply_completed" ? { turnId: a.turnId } : {},
2851
+ ...a.type === "card_shown" ? { cardId: a.cardId, kind: a.kind } : {}
2852
+ });
2853
+ };
2570
2854
  function make(key) {
2571
- const k = agentKey(key);
2855
+ const k = keyOf(key);
2572
2856
  const own = options.conversation ?? {};
2573
2857
  const refresh = backend.refreshSecret?.bind(backend);
2574
2858
  const entry = {
2575
2859
  key: k,
2576
2860
  store: createConversation({
2577
2861
  ...own,
2578
- ...own.persistence ? {
2862
+ ...storage ? {
2863
+ persistence: storage,
2579
2864
  persistenceKey: kept(k)
2580
2865
  } : {},
2866
+ onActivity: (a) => {
2867
+ activity(entry, a);
2868
+ isolated("onActivity", () => own.onActivity?.(a));
2869
+ },
2581
2870
  getSession: (request) => open(entry, key, request),
2582
2871
  ...refresh ? {
2583
- refreshSecret: (id) => patiently(() => refresh(id), void 0, k)
2872
+ // A refusal (`session_closed`) is the store's cue to go on in a new session, never a fault.
2873
+ refreshSecret: (id) => patiently(() => refresh(id), void 0, k).catch((error) => {
2874
+ throw isRefusal(error) ? new VinaverseClientError(
2875
+ "needs_new_session",
2876
+ error.message
2877
+ ) : error;
2878
+ })
2584
2879
  } : {},
2585
2880
  clock,
2586
2881
  onError: (error) => {
2587
- if (!(error instanceof AgentRefusal)) own.onError?.(error);
2882
+ if (!isRefusal(error) && codeOf(error) !== "needs_new_session")
2883
+ own.onError?.(error);
2588
2884
  }
2589
2885
  }),
2590
2886
  listeners: /* @__PURE__ */ new Set(),
@@ -2594,7 +2890,9 @@ function createAgentRuntime(options) {
2594
2890
  code: null,
2595
2891
  moved: false,
2596
2892
  ratings: /* @__PURE__ */ new Map(),
2597
- sessionId: null
2893
+ sessionId: null,
2894
+ conversationId: current.get(k) ?? null,
2895
+ openId: null
2598
2896
  },
2599
2897
  place: null,
2600
2898
  told: null,
@@ -2604,32 +2902,59 @@ function createAgentRuntime(options) {
2604
2902
  shown: 0,
2605
2903
  started: false,
2606
2904
  suspended: false,
2607
- sentOnce: /* @__PURE__ */ new Set()
2905
+ sentOnce: /* @__PURE__ */ new Set(),
2906
+ timer: void 0,
2907
+ turns: /* @__PURE__ */ new Map(),
2908
+ failedOpen: void 0
2608
2909
  };
2609
2910
  return entry;
2610
2911
  }
2611
2912
  const over = (entry) => entry.expiresAt !== null && entry.expiresAt <= clock.now();
2913
+ const expireLater = (entry) => {
2914
+ clearTimeout(entry.timer);
2915
+ if (entry.expiresAt === null) return;
2916
+ entry.timer = setTimeout(
2917
+ () => {
2918
+ entry.timer = void 0;
2919
+ if (live.get(entry.key) !== entry || entry.shown > 0) return;
2920
+ if (over(entry)) end(entry.key);
2921
+ else expireLater(entry);
2922
+ },
2923
+ Math.max(0, entry.expiresAt - clock.now())
2924
+ );
2925
+ };
2612
2926
  const tell = (entry, at) => {
2613
- entry.place = at.place ?? null;
2614
- entry.entryPoint = at.entryPoint ?? null;
2927
+ if (at.place !== void 0) entry.place = at.place;
2928
+ if (at.entryPoint !== void 0) entry.entryPoint = at.entryPoint;
2615
2929
  const sessionId = entry.state.sessionId;
2616
2930
  const moved = sessionId !== null && !samePlace(entry.told, entry.place);
2617
2931
  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
- );
2932
+ const updateContext = backend.updateContext?.bind(backend);
2933
+ if (!moved || !updateContext || sessionId === null) return;
2934
+ const asked = generation;
2935
+ const current2 = () => asked === generation && live.get(entry.key) === entry && entry.state.sessionId === sessionId;
2936
+ const send = (again) => {
2937
+ const place = entry.place;
2938
+ const revision = ++entry.revision;
2939
+ updateContext(sessionId, place ?? CLEARED, revision).then(
2940
+ () => {
2941
+ if (!current2() || entry.revision !== revision) return;
2942
+ entry.told = place;
2943
+ update(entry, { moved: !samePlace(entry.told, entry.place) });
2944
+ emit({ type: "context_updated", key: entry.key, sessionId });
2945
+ },
2946
+ (error) => {
2947
+ const theirs = field(error, "currentRevision");
2948
+ const conflict = field(error, "outcome") === "context_conflict" || field(error, "reason") === "conflict";
2949
+ if (!conflict || again || !current2() || entry.revision !== revision)
2950
+ return;
2951
+ if (typeof theirs === "number" && theirs >= entry.revision)
2952
+ entry.revision = theirs;
2953
+ send(true);
2954
+ }
2955
+ );
2956
+ };
2957
+ send(false);
2633
2958
  };
2634
2959
  function handle(entry) {
2635
2960
  return {
@@ -2642,6 +2967,8 @@ function createAgentRuntime(options) {
2642
2967
  return () => void 0;
2643
2968
  }
2644
2969
  entry.shown++;
2970
+ clearTimeout(entry.timer);
2971
+ entry.timer = void 0;
2645
2972
  if (entry.suspended) {
2646
2973
  entry.suspended = false;
2647
2974
  entry.store.setForeground(true);
@@ -2663,6 +2990,7 @@ function createAgentRuntime(options) {
2663
2990
  }
2664
2991
  entry.suspended = true;
2665
2992
  entry.store.setForeground(false);
2993
+ expireLater(entry);
2666
2994
  };
2667
2995
  },
2668
2996
  tell: (at) => tell(entry, at),
@@ -2672,13 +3000,18 @@ function createAgentRuntime(options) {
2672
3000
  return () => entry.listeners.delete(listener);
2673
3001
  },
2674
3002
  retry() {
2675
- update(entry, { failure: null, reason: null, code: null });
3003
+ update(entry, {
3004
+ failure: null,
3005
+ reason: null,
3006
+ code: null,
3007
+ openId: null
3008
+ });
2676
3009
  entry.store.start();
2677
3010
  },
3011
+ canRate: (turnId) => entry.turns.has(turnId),
2678
3012
  rate(turnId, rating) {
2679
- const sessionId = entry.state.sessionId;
2680
- if (sessionId === null || entry.state.ratings.get(turnId) === rating)
2681
- return;
3013
+ const sessionId = entry.turns.get(turnId);
3014
+ if (!sessionId || entry.state.ratings.get(turnId) === rating) return;
2682
3015
  update(entry, {
2683
3016
  ratings: new Map(entry.state.ratings).set(turnId, rating)
2684
3017
  });
@@ -2694,7 +3027,7 @@ function createAgentRuntime(options) {
2694
3027
  }
2695
3028
  const handles = /* @__PURE__ */ new WeakMap();
2696
3029
  const lookup = (key) => {
2697
- const k = agentKey(key);
3030
+ const k = keyOf(key);
2698
3031
  let entry = live.get(k);
2699
3032
  if (!entry) {
2700
3033
  entry = make(key);
@@ -2709,63 +3042,72 @@ function createAgentRuntime(options) {
2709
3042
  };
2710
3043
  return {
2711
3044
  conversation(key) {
2712
- const k = agentKey(key);
3045
+ const k = keyOf(key);
2713
3046
  const entry = live.get(k);
2714
3047
  if (entry && entry.shown === 0 && over(entry)) end(k);
2715
3048
  return lookup(key);
2716
3049
  },
2717
3050
  peek: lookup,
2718
3051
  startNew(key) {
2719
- const k = agentKey(key);
3052
+ const k = keyOf(key);
2720
3053
  current.delete(k);
2721
3054
  end(k);
2722
3055
  },
2723
3056
  dispose(key) {
2724
- end(agentKey(key));
3057
+ end(keyOf(key));
3058
+ },
3059
+ forget(key) {
3060
+ forget(keyOf(key));
3061
+ },
3062
+ sweep() {
3063
+ for (const entry of [...live.values()])
3064
+ if (entry.shown === 0 && over(entry)) end(entry.key);
2725
3065
  },
2726
3066
  continue(key, conversationId) {
2727
- const k = agentKey(key);
3067
+ const k = keyOf(key);
2728
3068
  if (current.get(k) !== conversationId) {
3069
+ forget(k);
2729
3070
  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
3071
  }
2743
3072
  return lookup(key);
2744
3073
  },
2745
3074
  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);
3075
+ await signOut(false);
3076
+ },
3077
+ async erase() {
3078
+ await signOut(true);
2752
3079
  },
3080
+ async deleteConversation(key, conversationId) {
3081
+ if (!backend.deleteConversation)
3082
+ throw new Error(
3083
+ "@vinaverse/client/agent: deleteConversation needs AgentBackend.deleteConversation."
3084
+ );
3085
+ await backend.deleteConversation(conversationId);
3086
+ const k = keyOf(key);
3087
+ const continued = current.get(k) ?? live.get(k)?.store.getSnapshot().conversationId ?? null;
3088
+ if (continued === conversationId) forget(k);
3089
+ },
3090
+ greetings: greetings ?? null,
2753
3091
  status() {
2754
3092
  if (status && clock.now() - status.at < staleMs)
2755
3093
  return Promise.resolve(status.value);
2756
- reading ??= readStatus(backend).then(
3094
+ if (reading) return reading;
3095
+ const asked = generation;
3096
+ const read = readStatus(backend).then(
2757
3097
  (value) => {
3098
+ if (asked !== generation) return value;
2758
3099
  reading = null;
2759
3100
  status = { value, at: clock.now() };
2760
3101
  changed();
2761
3102
  return value;
2762
3103
  },
2763
3104
  (error) => {
2764
- reading = null;
3105
+ if (asked === generation) reading = null;
2765
3106
  throw error;
2766
3107
  }
2767
3108
  );
2768
- return reading;
3109
+ reading = read;
3110
+ return read;
2769
3111
  },
2770
3112
  getStatus: () => status?.value ?? null,
2771
3113
  getName: () => sessionName ?? status?.value.displayName ?? null,
@@ -2776,6 +3118,100 @@ function createAgentRuntime(options) {
2776
3118
  emit
2777
3119
  };
2778
3120
  }
3121
+ function guardedStorage(raw, prefix, generation) {
3122
+ if (!raw) return void 0;
3123
+ const indexKey = `${prefix}:index`;
3124
+ const chains = /* @__PURE__ */ new Map();
3125
+ const pending = /* @__PURE__ */ new Map();
3126
+ const queued = (key, op) => {
3127
+ pending.set(key, (pending.get(key) ?? 0) + 1);
3128
+ const run = (chains.get(key) ?? Promise.resolve()).then(op);
3129
+ const settled = run.then(
3130
+ () => void 0,
3131
+ () => void 0
3132
+ );
3133
+ chains.set(key, settled);
3134
+ void settled.then(() => {
3135
+ const left = (pending.get(key) ?? 1) - 1;
3136
+ if (left > 0) pending.set(key, left);
3137
+ else {
3138
+ pending.delete(key);
3139
+ if (chains.get(key) === settled) chains.delete(key);
3140
+ }
3141
+ });
3142
+ return run;
3143
+ };
3144
+ const safe = (p) => p.catch(() => void 0);
3145
+ const readIndex = async () => {
3146
+ try {
3147
+ const value = await raw.getItem(indexKey);
3148
+ const list3 = value ? JSON.parse(value) : [];
3149
+ return new Set(
3150
+ Array.isArray(list3) ? list3.filter((k) => typeof k === "string") : []
3151
+ );
3152
+ } catch {
3153
+ return /* @__PURE__ */ new Set();
3154
+ }
3155
+ };
3156
+ const listed = /* @__PURE__ */ new Set();
3157
+ const list2 = (key) => {
3158
+ if (listed.has(key)) return;
3159
+ listed.add(key);
3160
+ void safe(
3161
+ queued(indexKey, async () => {
3162
+ const all = await readIndex();
3163
+ if (all.has(key)) return;
3164
+ all.add(key);
3165
+ await raw.setItem(indexKey, JSON.stringify([...all]));
3166
+ })
3167
+ );
3168
+ };
3169
+ return {
3170
+ getItem(key) {
3171
+ const asked = generation();
3172
+ const read = pending.has(key) ? queued(key, () => raw.getItem(key)) : raw.getItem(key);
3173
+ if (typeof read === "string" || read === null) return read;
3174
+ return Promise.resolve(read).then(
3175
+ (v) => asked === generation() ? v ?? null : null,
3176
+ () => null
3177
+ );
3178
+ },
3179
+ setItem(key, value) {
3180
+ const asked = generation();
3181
+ return safe(
3182
+ queued(key, () => {
3183
+ if (asked !== generation()) return;
3184
+ list2(key);
3185
+ return raw.setItem(key, value);
3186
+ })
3187
+ );
3188
+ },
3189
+ removeItem: (key) => safe(queued(key, () => raw.removeItem(key))),
3190
+ /**
3191
+ * Everything kept: the index, what the app's storage finds under `under` (`removePrefix`, else `keys`), and `extra`
3192
+ * (older key shapes). Nothing under `keep` goes, so `removePrefix` is used only for a prefix that cannot reach it.
3193
+ */
3194
+ async purge(under, extra = () => [], keep) {
3195
+ const all = /* @__PURE__ */ new Set([...listed, ...await readIndex()]);
3196
+ listed.clear();
3197
+ for (const p of under)
3198
+ try {
3199
+ if (raw.removePrefix && !keep?.startsWith(p))
3200
+ await raw.removePrefix(p);
3201
+ else if (raw.keys) {
3202
+ for (const key of await raw.keys(p))
3203
+ if (!keep || !key.startsWith(keep)) all.add(key);
3204
+ }
3205
+ } catch {
3206
+ }
3207
+ for (const key of extra(all)) all.add(key);
3208
+ await Promise.all(
3209
+ [...all].map((key) => safe(queued(key, () => raw.removeItem(key))))
3210
+ );
3211
+ await safe(queued(indexKey, () => raw.removeItem(indexKey)));
3212
+ }
3213
+ };
3214
+ }
2779
3215
  async function readStatus(backend) {
2780
3216
  const read = backend.status ? await backend.status() : { available: true };
2781
3217
  const available = read.available === true;
@@ -2793,10 +3229,20 @@ async function readStatus(backend) {
2793
3229
  }
2794
3230
  };
2795
3231
  }
3232
+ function isRefusal(error) {
3233
+ return error instanceof AgentRefusal || field(error, "name") === "AgentRefusal" && (field(error, "kind") === "ended" || field(error, "kind") === "unavailable");
3234
+ }
2796
3235
  var field = (error, name) => typeof error === "object" && error !== null ? error[name] : void 0;
2797
3236
  var retryable = (error) => field(error, "retryable") === true;
2798
3237
  var REASONS = /* @__PURE__ */ new Set([
2799
3238
  "ended",
3239
+ "not_found",
3240
+ "expired",
3241
+ "mismatch",
3242
+ "completed",
3243
+ "forbidden",
3244
+ "conflict",
3245
+ "unknown_open",
2800
3246
  "auth",
2801
3247
  "off",
2802
3248
  "rate_limited",
@@ -2806,7 +3252,7 @@ var REASONS = /* @__PURE__ */ new Set([
2806
3252
  "unknown"
2807
3253
  ]);
2808
3254
  function reasonOf(error, failure) {
2809
- if (error instanceof AgentRefusal) return error.reason;
3255
+ if (isRefusal(error)) return error.reason;
2810
3256
  const given = field(error, "reason");
2811
3257
  if (typeof given === "string" && REASONS.has(given))
2812
3258
  return given;
@@ -2817,7 +3263,7 @@ function reasonOf(error, failure) {
2817
3263
  return failure === "unreachable" ? "network" : "unknown";
2818
3264
  }
2819
3265
  function codeOf(error) {
2820
- const code = field(error, "code");
3266
+ const code = field(error, "code") ?? field(error, "outcome");
2821
3267
  return typeof code === "string" ? code : null;
2822
3268
  }
2823
3269
  async function retrying(run, policy, signal, onRetry) {
@@ -2825,9 +3271,11 @@ async function retrying(run, policy, signal, onRetry) {
2825
3271
  try {
2826
3272
  return await run();
2827
3273
  } catch (error) {
2828
- const wait = policy.delays[attempt];
2829
- if (wait === void 0 || !policy.isRetryable(error) || signal?.aborted)
3274
+ const delay = policy.delays[attempt];
3275
+ if (delay === void 0 || !policy.isRetryable(error) || signal?.aborted)
2830
3276
  throw error;
3277
+ const asked = field(error, "retryAfterMs");
3278
+ const wait = typeof asked === "number" && asked > delay ? Math.min(asked, 6e4) : delay;
2831
3279
  await new Promise((resolve, reject) => {
2832
3280
  const timer = setTimeout(() => {
2833
3281
  signal?.removeEventListener("abort", stop);
@@ -2847,6 +3295,9 @@ async function retrying(run, policy, signal, onRetry) {
2847
3295
  exports.AgentRefusal = AgentRefusal;
2848
3296
  exports.agentKey = agentKey;
2849
3297
  exports.createAgentRuntime = createAgentRuntime;
3298
+ exports.greetedKey = greetedKey;
3299
+ exports.hashKey = hashKey;
3300
+ exports.isRefusal = isRefusal;
2850
3301
  exports.readCard = readCard;
2851
3302
  //# sourceMappingURL=index.cjs.map
2852
3303
  //# sourceMappingURL=index.cjs.map