@alexkroman1/aai-ui 2.0.0 → 3.0.0

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 (45) hide show
  1. package/dist/_module-url-C_4gRVL0.js +15 -0
  2. package/dist/audio.d.ts +42 -6
  3. package/dist/audio.js +79 -69
  4. package/dist/{chat-view-DKFhxMAT.js → chat-view-C1oJxsWz.js} +4 -4
  5. package/dist/client-config.d.ts +5 -6
  6. package/dist/components/chat-view.js +1 -1
  7. package/dist/components/console-shell.d.ts +2 -2
  8. package/dist/components/controls.js +1 -1
  9. package/dist/components/url-chips.d.ts +0 -3
  10. package/dist/context.d.ts +1 -1
  11. package/dist/context.js +1 -4
  12. package/dist/{controls-4OJoekj6.js → controls-DV368uhb.js} +1 -4
  13. package/dist/default-client/assets/_module-url-BX0RuRU2.js +1 -0
  14. package/dist/default-client/assets/audio-BGHiiDY_.js +1 -0
  15. package/dist/default-client/assets/{capture-processor-DLHxAIfT.js → capture-processor-BLPsqnGl.js} +4 -4
  16. package/dist/default-client/assets/index-DXf7-M0Q.js +244 -0
  17. package/dist/default-client/assets/index-Dv-Q5VRL.css +2 -0
  18. package/dist/default-client/assets/{playback-processor-bMTdFp-8.js → playback-processor-B_T_1KQP.js} +28 -21
  19. package/dist/default-client/index.html +2 -5
  20. package/dist/define-client-Cyf1jqAl.js +150 -0
  21. package/dist/define-client.js +1 -1
  22. package/dist/index.d.ts +1 -3
  23. package/dist/index.js +7 -6
  24. package/dist/{session-core-BLhiQ18c.js → session-core-BM3WHeoY.js} +30 -128
  25. package/dist/session-core-messages.d.ts +0 -4
  26. package/dist/session-core-types.d.ts +1 -27
  27. package/dist/session-core.js +1 -1
  28. package/dist/types.d.ts +1 -1
  29. package/dist/types.js +2 -2
  30. package/dist/worklets/_module-url.d.ts +10 -0
  31. package/dist/worklets/capture-processor.d.ts +2 -2
  32. package/dist/worklets/capture-processor.js +5 -5
  33. package/dist/worklets/playback-processor.d.ts +3 -3
  34. package/dist/worklets/playback-processor.js +31 -24
  35. package/package.json +2 -2
  36. package/dist/components/workflow-view.d.ts +0 -13
  37. package/dist/default-client/assets/audio-Cgviqo9t.js +0 -1
  38. package/dist/default-client/assets/index-BgbIWnfG.css +0 -2
  39. package/dist/default-client/assets/index-DbKKR3UE.js +0 -62
  40. package/dist/default-client/assets/rolldown-runtime-BpQH8Ho1.js +0 -1
  41. package/dist/default-client/assets/types-Bpg3ZIZK.js +0 -64
  42. package/dist/define-client-yoGybEYR.js +0 -710
  43. package/dist/session-core-upload.d.ts +0 -16
  44. package/dist/sync-mic.d.ts +0 -61
  45. package/dist/sync-session.d.ts +0 -49
@@ -0,0 +1,2 @@
1
+ /*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
2
+ @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial;--tw-ease:initial}}}@layer theme{:root,:host{--font-sans:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--font-weight-normal:400;--font-weight-medium:500;--tracking-wide:.025em;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--ease-in-out:cubic-bezier(.4, 0, .2, 1);--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--font-aai:"Monument Grotesk", "ABC Monument Grotesk", ui-sans-serif, system-ui, -apple-system, sans-serif;--font-aai-serif:"Source Serif 4", "Source Serif Pro", Charter, "Iowan Old Style", Georgia, serif;--font-aai-mono:"JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;--radius-aai:4px}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}html,body{margin:0;padding:0}}@layer components;@layer utilities{.collapse{visibility:collapse}.invisible{visibility:hidden}.absolute{position:absolute}.relative{position:relative}.static{position:static}.container{width:100%}@media (width>=40rem){.container{max-width:40rem}}@media (width>=48rem){.container{max-width:48rem}}@media (width>=64rem){.container{max-width:64rem}}@media (width>=80rem){.container{max-width:80rem}}@media (width>=96rem){.container{max-width:96rem}}.m-0{margin:0}.mx-auto{margin-inline:auto}.mt-2{margin-top:calc(var(--spacing) * 2)}.ml-auto{margin-left:auto}.box-border{box-sizing:border-box}.block{display:block}.flex{display:flex}.grid{display:grid}.inline{display:inline}.inline-flex{display:inline-flex}.h-1\.5{height:calc(var(--spacing) * 1.5)}.h-4{height:calc(var(--spacing) * 4)}.h-9{height:calc(var(--spacing) * 9)}.h-11{height:calc(var(--spacing) * 11)}.h-\[7px\]{height:7px}.h-screen{height:100vh}.max-h-64{max-height:calc(var(--spacing) * 64)}.min-h-0{min-height:0}.min-h-5{min-height:calc(var(--spacing) * 5)}.w-1\.5{width:calc(var(--spacing) * 1.5)}.w-4{width:calc(var(--spacing) * 4)}.w-\[7px\]{width:7px}.w-fit{width:fit-content}.w-full{width:100%}.max-w-75{max-width:calc(var(--spacing) * 75)}.max-w-105{max-width:calc(var(--spacing) * 105)}.max-w-190{max-width:calc(var(--spacing) * 190)}.max-w-\[60\%\]{max-width:60%}.max-w-\[82\%\]{max-width:82%}.max-w-\[min\(78\%\,64ch\)\]{max-width:min(78%,64ch)}.min-w-0{min-width:0}.flex-1{flex:1}.shrink-0{flex-shrink:0}.rotate-90{rotate:90deg}.cursor-pointer{cursor:pointer}.\[scrollbar-width\:none\]{scrollbar-width:none}.appearance-none{appearance:none}.flex-col{flex-direction:column}.items-center{align-items:center}.items-end{align-items:flex-end}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-1{gap:var(--spacing)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-2\.5{gap:calc(var(--spacing) * 2.5)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-5{gap:calc(var(--spacing) * 5)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.rounded-aai{border-radius:var(--radius-aai)}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-none{--tw-border-style:none;border-style:none}.bg-transparent{background-color:#0000}.p-7{padding:calc(var(--spacing) * 7)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3\.5{padding-inline:calc(var(--spacing) * 3.5)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.px-7{padding-inline:calc(var(--spacing) * 7)}.px-10{padding-inline:calc(var(--spacing) * 10)}.py-1{padding-block:var(--spacing)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-12{padding-block:calc(var(--spacing) * 12)}.text-center{text-align:center}.text-left{text-align:left}.font-aai{font-family:var(--font-aai)}.font-aai-mono{font-family:var(--font-aai-mono)}.font-aai-serif{font-family:var(--font-aai-serif)}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[13px\]{font-size:13px}.text-\[15px\]{font-size:15px}.text-\[22px\]{font-size:22px}.text-\[32px\]{font-size:32px}.leading-4{--tw-leading:calc(var(--spacing) * 4);line-height:calc(var(--spacing) * 4)}.leading-\[1\.2\]{--tw-leading:1.2;line-height:1.2}.leading-\[1\.15\]{--tw-leading:1.15;line-height:1.15}.leading-\[22px\]{--tw-leading:22px;line-height:22px}.leading-\[23px\]{--tw-leading:23px;line-height:23px}.leading-\[130\%\]{--tw-leading:130%;line-height:130%}.leading-none{--tw-leading:1;line-height:1}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.tracking-\[-0\.2px\]{--tw-tracking:-.2px;letter-spacing:-.2px}.tracking-\[1\.2px\]{--tw-tracking:1.2px;letter-spacing:1.2px}.tracking-\[1\.4px\]{--tw-tracking:1.4px;letter-spacing:1.4px}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.text-balance{text-wrap:balance}.wrap-break-word{overflow-wrap:break-word}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.uppercase{text-transform:uppercase}.ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-150{--tw-duration:.15s;transition-duration:.15s}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}@media (width>=40rem){.sm\:px-16{padding-inline:calc(var(--spacing) * 16)}.sm\:py-14{padding-block:calc(var(--spacing) * 14)}}}@keyframes aai-pulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.45;transform:scale(.82)}}@keyframes aai-bounce{0%,80%,to{opacity:.3;transform:scale(.8)}40%{opacity:1;transform:scale(1)}}@keyframes aai-shimmer{0%{background-position:-200% 0}to{background-position:200% 0}}.tool-shimmer{-webkit-text-fill-color:transparent;background:linear-gradient(90deg,currentColor 25%,#0000 50%,currentColor 75%) 0 0/200% 100%;-webkit-background-clip:text;background-clip:text;animation:2s infinite aai-shimmer}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}
@@ -1,9 +1,12 @@
1
- import{m as e}from"./types-Bpg3ZIZK.js";var t=`
1
+ import{i as e}from"./index-DXf7-M0Q.js";import{t}from"./_module-url-BX0RuRU2.js";var n=t(`
2
2
  class PlaybackProcessor extends AudioWorkletProcessor {
3
3
  constructor(options) {
4
4
  super();
5
5
  const opts = options.processorOptions || {};
6
- const rate = opts.sampleRate ?? 24000;
6
+ // The node's context IS the playback context (audio.ts asserts its rate),
7
+ // so the worklet-global sampleRate is authoritative; the option exists
8
+ // for the node-less test harness.
9
+ const rate = opts.sampleRate ?? sampleRate;
7
10
  // Fill target for the start of a turn. If 'done' arrives first (short
8
11
  // utterance), start immediately instead of waiting for audio that is
9
12
  // never coming.
@@ -16,11 +19,11 @@ class PlaybackProcessor extends AudioWorkletProcessor {
16
19
  this.concealCapacity = Math.max(1, Math.floor((rate * 40) / 1000));
17
20
  this.concealBuf = new Float32Array(this.concealCapacity);
18
21
  this.concealDecay = Math.exp(Math.log(${e}) / this.concealCapacity);
19
- // Float32 ring buffer — 60s at the context sample rate. Allocated once for
20
- // the node's lifetime; per-turn state resets via resetTurn(). writePos and
21
- // readPos are absolute (monotonic) sample counts; the buffer is indexed
22
- // modulo capacity so a reply longer than 60s keeps playing instead of
23
- // writing past the end and going silent.
22
+ // Float32 ring buffer — PLAYBACK_BUFFER_SECONDS at the context sample
23
+ // rate. Allocated once for the node's lifetime; per-turn state resets via
24
+ // resetTurn(). writePos and readPos are absolute (monotonic) sample
25
+ // counts; the buffer is indexed modulo capacity so a longer reply keeps
26
+ // playing instead of writing past the end and going silent.
24
27
  this.capacity = rate * 60;
25
28
  this.samples = new Float32Array(this.capacity);
26
29
  // Platform endianness probe: the wire format is PCM16 little-endian, so
@@ -34,7 +37,12 @@ class PlaybackProcessor extends AudioWorkletProcessor {
34
37
  if (d.event === 'write') {
35
38
  this.ingestBytes(d.buffer);
36
39
  } else if (d.event === 'interrupt') {
37
- this.interrupted = true;
40
+ // Applied eagerly, not deferred to the next process(): onmessage and
41
+ // process() never interleave (one audio thread), and a deferred
42
+ // interrupt would let 'write'/'done' frames for the NEXT turn
43
+ // coalesce in ahead of it and be wiped by the reset along with the
44
+ // cancelled turn's audio.
45
+ this.stopTurn('interrupt');
38
46
  } else if (d.event === 'done') {
39
47
  this.isDone = true;
40
48
  }
@@ -44,7 +52,6 @@ class PlaybackProcessor extends AudioWorkletProcessor {
44
52
  // Reset per-turn state so the node is reusable across replies without
45
53
  // reallocating the sample buffer or re-instantiating the worklet.
46
54
  resetTurn() {
47
- this.interrupted = false;
48
55
  this.isDone = false;
49
56
  this.playing = false;
50
57
  // Whether any real audio has been rendered this turn. Separates a turn's
@@ -105,7 +112,10 @@ class PlaybackProcessor extends AudioWorkletProcessor {
105
112
  const total = out.length - start;
106
113
  const len = this.concealLen;
107
114
  let silent = 0;
108
- if (len === 0) {
115
+ // Second condition: once the fade has decayed to the floor, every sample
116
+ // the loop would emit is 0 anyway — bulk-fill instead of running 128
117
+ // branchy iterations per quantum for the whole tail of a long stall.
118
+ if (len === 0 || this.concealGain < ${e}) {
109
119
  out.fill(0, start);
110
120
  silent = total;
111
121
  } else {
@@ -139,10 +149,13 @@ class PlaybackProcessor extends AudioWorkletProcessor {
139
149
  rememberTail(out, n) {
140
150
  const cap = this.concealCapacity;
141
151
  const take = Math.min(n, cap);
142
- for (let i = n - take; i < n; i++) {
143
- this.concealBuf[this.concealWrite] = out[i];
144
- this.concealWrite = this.concealWrite + 1 === cap ? 0 : this.concealWrite + 1;
145
- }
152
+ // Bulk copies (this runs on every cleanly rendered quantum): the tail is
153
+ // one contiguous source run, landing in at most two ring runs.
154
+ const tail = out.subarray(n - take, n);
155
+ const first = Math.min(take, cap - this.concealWrite);
156
+ this.concealBuf.set(tail.subarray(0, first), this.concealWrite);
157
+ if (take > first) this.concealBuf.set(tail.subarray(first), 0);
158
+ this.concealWrite = (this.concealWrite + take) % cap;
146
159
  this.concealLen = Math.min(cap, this.concealLen + take);
147
160
  // Read the loop oldest-first; once the ring is full the write cursor is
148
161
  // the oldest retained sample.
@@ -210,12 +223,6 @@ class PlaybackProcessor extends AudioWorkletProcessor {
210
223
  // session), so guard like the capture processor does.
211
224
  if (!outputs[0] || !outputs[0][0]) return true;
212
225
  const out = outputs[0][0];
213
- if (this.interrupted) {
214
- out.fill(0);
215
- this.stopTurn('interrupt');
216
- return true;
217
- }
218
-
219
226
  const avail = this.writePos - this.readPos;
220
227
 
221
228
  // Filling: wait for the target. 'done' short-circuits it — what is
@@ -268,4 +275,4 @@ class PlaybackProcessor extends AudioWorkletProcessor {
268
275
  }
269
276
 
270
277
  registerProcessor('playback-processor', PlaybackProcessor);
271
- `,n=new Blob([t],{type:`application/javascript`}),r=URL.createObjectURL(n);export{r as default};
278
+ `);export{n as default};
@@ -6,11 +6,8 @@
6
6
  <title>aai</title>
7
7
  <link rel="icon" href="data:," />
8
8
  <style>html, body { background: #FBF8F2; margin: 0; }</style>
9
- <script type="module" crossorigin src="./assets/index-DbKKR3UE.js"></script>
10
- <link rel="modulepreload" crossorigin href="./assets/rolldown-runtime-BpQH8Ho1.js">
11
- <link rel="modulepreload" crossorigin href="./assets/types-Bpg3ZIZK.js">
12
- <link rel="modulepreload" crossorigin href="./assets/audio-Cgviqo9t.js">
13
- <link rel="stylesheet" crossorigin href="./assets/index-BgbIWnfG.css">
9
+ <script type="module" crossorigin src="./assets/index-DXf7-M0Q.js"></script>
10
+ <link rel="stylesheet" crossorigin href="./assets/index-Dv-Q5VRL.css">
14
11
  </head>
15
12
  <body>
16
13
  <main id="app"></main>
@@ -0,0 +1,150 @@
1
+ import { SessionProvider, ThemeProvider } from "./context.js";
2
+ import { t as ChatView } from "./chat-view-C1oJxsWz.js";
3
+ import { n as ToolConfigContext } from "./tool-call-block-DIxpG8GM.js";
4
+ import { SidebarLayout } from "./components/sidebar-layout.js";
5
+ import { StartScreen } from "./components/start-screen.js";
6
+ import { t as createSessionCore } from "./session-core-BM3WHeoY.js";
7
+ import { CLIENT_CONFIG_PATH, ClientConfigResponseSchema } from "@alexkroman1/aai/protocol";
8
+ import { createElement, useEffect, useState } from "react";
9
+ import { jsx } from "react/jsx-runtime";
10
+ import { flushSync } from "react-dom";
11
+ import { createRoot } from "react-dom/client";
12
+ //#region client-config.ts
13
+ /**
14
+ * Pre-connection client-config lookup.
15
+ *
16
+ * `GET client-config` (relative to the agent's base URL — see
17
+ * `sdk/client-config.ts` in `@alexkroman1/aai`) gives the default client the
18
+ * agent's display name and greeting before any connection exists. Every
19
+ * failure path — network error, 404 from an older server, malformed body —
20
+ * degrades to the empty default, so this lookup can never break an existing
21
+ * agent.
22
+ */
23
+ /** Resolve a relative endpoint path against the agent's base URL. */
24
+ function buildAgentUrl(platformUrl, endpointPath) {
25
+ return new URL(endpointPath, platformUrl.endsWith("/") ? platformUrl : `${platformUrl}/`);
26
+ }
27
+ const AGENT_DEFAULT = {};
28
+ /** Fetch the agent's client config; any failure yields the agent default. */
29
+ async function fetchClientConfig(platformUrl, fetchFn) {
30
+ const doFetch = fetchFn ?? ((input, init) => globalThis.fetch(input, init));
31
+ try {
32
+ const resp = await doFetch(buildAgentUrl(platformUrl, CLIENT_CONFIG_PATH).href);
33
+ if (!resp.ok) return AGENT_DEFAULT;
34
+ const parsed = ClientConfigResponseSchema.safeParse(await resp.json());
35
+ return parsed.success ? parsed.data : AGENT_DEFAULT;
36
+ } catch {
37
+ return AGENT_DEFAULT;
38
+ }
39
+ }
40
+ //#endregion
41
+ //#region define-client.tsx
42
+ /** @jsxImportSource react */
43
+ function resolveContainer(target = "#app") {
44
+ if (typeof target !== "string") return target;
45
+ const el = document.querySelector(target);
46
+ if (!el) throw new Error(`Element not found: ${target}`);
47
+ return el;
48
+ }
49
+ /**
50
+ * Default shell rendered in config tier.
51
+ * Wraps StartScreen → (SidebarLayout →) ChatView.
52
+ */
53
+ function DefaultShell({ name, Sidebar, sidebarWidth }) {
54
+ const chat = /* @__PURE__ */ jsx(ChatView, { title: name });
55
+ return /* @__PURE__ */ jsx(StartScreen, {
56
+ title: name,
57
+ children: Sidebar ? /* @__PURE__ */ jsx(SidebarLayout, {
58
+ sidebar: /* @__PURE__ */ jsx(Sidebar, {}),
59
+ sidebarWidth,
60
+ children: chat
61
+ }) : chat
62
+ });
63
+ }
64
+ /**
65
+ * Config-tier root: fetches the server-declared display name via
66
+ * `GET client-config` and renders the chat shell. The shell renders
67
+ * immediately — optimistically — while the lookup is in flight; servers
68
+ * without the endpoint (every lookup failure resolves to the empty default)
69
+ * work exactly as before.
70
+ */
71
+ function DefaultRoot({ platformUrl, name, Sidebar, sidebarWidth }) {
72
+ const [resolved, setResolved] = useState(null);
73
+ useEffect(() => {
74
+ let cancelled = false;
75
+ fetchClientConfig(platformUrl).then((cfg) => {
76
+ if (!cancelled) setResolved(cfg);
77
+ });
78
+ return () => {
79
+ cancelled = true;
80
+ };
81
+ }, [platformUrl]);
82
+ return /* @__PURE__ */ jsx(DefaultShell, {
83
+ name: name ?? resolved?.name,
84
+ Sidebar,
85
+ sidebarWidth
86
+ });
87
+ }
88
+ /**
89
+ * Define and mount a client UI for a voice agent.
90
+ *
91
+ * **Tier 1 (config-only):** Pass options without `component` to get the
92
+ * default shell (StartScreen + ChatView, optional sidebar).
93
+ *
94
+ * **Tier 2 (custom component):** Pass `component` to render a fully custom
95
+ * root component inside the providers.
96
+ *
97
+ * @example Tier 1
98
+ * ```tsx
99
+ * client({
100
+ * name: "Pizza Ordering",
101
+ * theme: { bg: "#1a1a1a", primary: "#e55" },
102
+ * sidebar: OrderPanel,
103
+ * tools: { add_pizza: { icon: "🍕", label: "Adding pizza" } },
104
+ * });
105
+ * ```
106
+ *
107
+ * @example Tier 2
108
+ * ```tsx
109
+ * client({ component: MyCustomApp });
110
+ * ```
111
+ *
112
+ * @returns A {@link ClientHandle} for cleanup.
113
+ * @throws If the target element is not found in the DOM.
114
+ *
115
+ * @public
116
+ */
117
+ function client(config) {
118
+ const container = resolveContainer(config.target);
119
+ const platformUrl = config.platformUrl ?? globalThis.location.origin + globalThis.location.pathname;
120
+ const session = createSessionCore({
121
+ platformUrl,
122
+ onSessionId: config.onSessionId,
123
+ resumeSessionId: config.resumeSessionId,
124
+ WebSocket: config.WebSocket
125
+ });
126
+ const rootNode = config.component ? createElement(config.component) : createElement(DefaultRoot, {
127
+ platformUrl,
128
+ name: config.name,
129
+ Sidebar: config.sidebar,
130
+ sidebarWidth: config.sidebarWidth
131
+ });
132
+ const toolConfig = config.tools ?? {};
133
+ const root = createRoot(container);
134
+ flushSync(() => {
135
+ root.render(createElement(ToolConfigContext.Provider, { value: toolConfig }, createElement(ThemeProvider, { value: config.theme }, createElement(SessionProvider, { value: session }, rootNode))));
136
+ });
137
+ const handle = {
138
+ session,
139
+ dispose() {
140
+ root.unmount();
141
+ session[Symbol.dispose]();
142
+ },
143
+ [Symbol.dispose]() {
144
+ handle.dispose();
145
+ }
146
+ };
147
+ return handle;
148
+ }
149
+ //#endregion
150
+ export { buildAgentUrl as n, fetchClientConfig as r, client as t };
@@ -1,4 +1,4 @@
1
- import { t as client } from "./define-client-yoGybEYR.js";
1
+ import { t as client } from "./define-client-Cyf1jqAl.js";
2
2
  import "./context.js";
3
3
  import "./components/sidebar-layout.js";
4
4
  import "./components/start-screen.js";
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export { floatToPcm16 } from "./audio.ts";
1
2
  export { buildAgentUrl, type ClientConfigResponse, fetchClientConfig, } from "./client-config.ts";
2
3
  export { Button } from "./components/button.tsx";
3
4
  export { ChatView } from "./components/chat-view.tsx";
@@ -8,7 +9,6 @@ export { StartScreen } from "./components/start-screen.tsx";
8
9
  export type { ToolDisplayConfig } from "./components/tool-config-context.ts";
9
10
  export { ToolConfigContext } from "./components/tool-config-context.ts";
10
11
  export { ApiUrlChip, SessionUrlChips, UiUrlChip } from "./components/url-chips.tsx";
11
- export { WorkflowView } from "./components/workflow-view.tsx";
12
12
  export type { Session } from "./context.ts";
13
13
  export { SessionProvider, ThemeProvider, useSession, useSessionSelector, useTheme, } from "./context.ts";
14
14
  export type { ClientConfig, ClientHandle, } from "./define-client.tsx";
@@ -16,7 +16,5 @@ export { client } from "./define-client.tsx";
16
16
  export { useEvent, useToolCallStart, useToolResult } from "./hooks.ts";
17
17
  export type { CustomEvent, SessionCore, SessionCoreOptions, SessionSnapshot, } from "./session-core.ts";
18
18
  export { createSessionCore } from "./session-core.ts";
19
- export { CAPTURE_WORKLET_MODULE_URL, createPttRecorder, DEFAULT_SYNC_MIC_SAMPLE_RATE, floatToPcm16, type PttRecorder, } from "./sync-mic.ts";
20
- export { base64ToPcm16, createSyncSession, pcm16ToBase64, type SyncSession, type SyncSessionOptions, type SyncTurnResult, } from "./sync-session.ts";
21
19
  export type { AgentState, ChatMessage, ClientTheme, SessionError, SessionErrorCode, ToolCallInfo, VoiceSessionOptions, WebSocketConstructor, } from "./types.ts";
22
20
  export { VOICE_CAPTURE_CONSTRAINTS } from "./types.ts";
package/dist/index.js CHANGED
@@ -1,13 +1,14 @@
1
- import { a as pcm16ToBase64, c as createPttRecorder, d as fetchClientConfig, i as createSyncSession, l as floatToPcm16, n as WorkflowView, o as CAPTURE_WORKLET_MODULE_URL, r as base64ToPcm16, s as DEFAULT_SYNC_MIC_SAMPLE_RATE, t as client, u as buildAgentUrl } from "./define-client-yoGybEYR.js";
1
+ import { VOICE_CAPTURE_CONSTRAINTS } from "./types.js";
2
+ import { floatToPcm16 } from "./audio.js";
3
+ import { n as buildAgentUrl, r as fetchClientConfig, t as client } from "./define-client-Cyf1jqAl.js";
2
4
  import { SessionProvider, ThemeProvider, useSession, useSessionSelector, useTheme } from "./context.js";
3
5
  import { Button } from "./components/button.js";
4
- import { t as ChatView } from "./chat-view-DKFhxMAT.js";
5
- import { i as UiUrlChip, n as ApiUrlChip, r as SessionUrlChips, t as Controls } from "./controls-4OJoekj6.js";
6
+ import { t as ChatView } from "./chat-view-C1oJxsWz.js";
7
+ import { i as UiUrlChip, n as ApiUrlChip, r as SessionUrlChips, t as Controls } from "./controls-DV368uhb.js";
6
8
  import { n as ToolConfigContext } from "./tool-call-block-DIxpG8GM.js";
7
9
  import { MessageList } from "./components/message-list.js";
8
10
  import { SidebarLayout } from "./components/sidebar-layout.js";
9
11
  import { StartScreen } from "./components/start-screen.js";
10
- import { VOICE_CAPTURE_CONSTRAINTS } from "./types.js";
11
- import { t as createSessionCore } from "./session-core-BLhiQ18c.js";
12
+ import { t as createSessionCore } from "./session-core-BM3WHeoY.js";
12
13
  import { useEvent, useToolCallStart, useToolResult } from "./hooks.js";
13
- export { ApiUrlChip, Button, CAPTURE_WORKLET_MODULE_URL, ChatView, Controls, DEFAULT_SYNC_MIC_SAMPLE_RATE, MessageList, SessionProvider, SessionUrlChips, SidebarLayout, StartScreen, ThemeProvider, ToolConfigContext, UiUrlChip, VOICE_CAPTURE_CONSTRAINTS, WorkflowView, base64ToPcm16, buildAgentUrl, client, createPttRecorder, createSessionCore, createSyncSession, fetchClientConfig, floatToPcm16, pcm16ToBase64, useEvent, useSession, useSessionSelector, useTheme, useToolCallStart, useToolResult };
14
+ export { ApiUrlChip, Button, ChatView, Controls, MessageList, SessionProvider, SessionUrlChips, SidebarLayout, StartScreen, ThemeProvider, ToolConfigContext, UiUrlChip, VOICE_CAPTURE_CONSTRAINTS, buildAgentUrl, client, createSessionCore, fetchClientConfig, floatToPcm16, useEvent, useSession, useSessionSelector, useTheme, useToolCallStart, useToolResult };
@@ -1,8 +1,7 @@
1
- import { FILE_SEND_BACKOFF_MS, MIC_SEND_MAX_BUFFERED_BYTES } from "./types.js";
1
+ import { MIC_SEND_MAX_BUFFERED_BYTES } from "./types.js";
2
+ import { DEFAULT_MAX_HISTORY, WS_OPEN, errorMessage, safeJsonParse } from "@alexkroman1/aai";
2
3
  import { ServerMessageSchema, lenientParse } from "@alexkroman1/aai/protocol";
3
- import { DEFAULT_MAX_HISTORY, FILE_UPLOAD_CHUNK_BYTES, WS_OPEN, errorMessage, safeJsonParse } from "@alexkroman1/aai";
4
4
  import ReconnectingWebSocket from "partysocket/ws";
5
- import { MAX_SYNC_AUDIO_SECONDS } from "@alexkroman1/aai/stt";
6
5
  //#region session-core-audio-setup.ts
7
6
  /**
8
7
  * Audio-path initialization for the voice session core.
@@ -36,6 +35,7 @@ async function initAudioCapture(conn, msg, deps, fatal) {
36
35
  const gen = conn.generation;
37
36
  const stale = () => conn.generation !== gen || !conn.ws || conn.ws.readyState !== WS_OPEN;
38
37
  const reportAudioFailure = (message) => {
38
+ deps.cleanupAudio();
39
39
  if (fatal) deps.updateState({
40
40
  state: "error",
41
41
  error: {
@@ -45,16 +45,13 @@ async function initAudioCapture(conn, msg, deps, fatal) {
45
45
  running: false,
46
46
  recording: false
47
47
  });
48
- else {
49
- deps.cleanupAudio();
50
- deps.updateState({
51
- error: {
52
- code: "audio",
53
- message
54
- },
55
- recording: false
56
- });
57
- }
48
+ else deps.updateState({
49
+ error: {
50
+ code: "audio",
51
+ message
52
+ },
53
+ recording: false
54
+ });
58
55
  };
59
56
  try {
60
57
  const [{ createVoiceIO }, captureWorklet, playbackWorklet] = await Promise.all([
@@ -155,7 +152,7 @@ function appendCapped(list, item, cap) {
155
152
  * dedup) that previously lived as closure locals in `createSessionCore`.
156
153
  */
157
154
  function createMessageHandlers(deps) {
158
- const { getSnapshot, updateState, conn, discardUpload, cleanupAudio } = deps;
155
+ const { getSnapshot, updateState, conn, cleanupAudio } = deps;
159
156
  /** Incremented on each turn boundary -- stale async callbacks compare against this. */
160
157
  let handlerGeneration = 0;
161
158
  /** Monotonically increasing counter for custom events -- used by useEvent to deduplicate. */
@@ -213,6 +210,7 @@ function createMessageHandlers(deps) {
213
210
  } });
214
211
  else {
215
212
  cleanupAudio();
213
+ conn.generation++;
216
214
  updateState({
217
215
  state: "error",
218
216
  error: {
@@ -280,7 +278,6 @@ function createMessageHandlers(deps) {
280
278
  break;
281
279
  case "reset":
282
280
  handlerGeneration++;
283
- discardUpload();
284
281
  conn.voiceIO?.flush();
285
282
  updateState({
286
283
  ...CLEARED_SESSION_STATE,
@@ -353,7 +350,6 @@ function createMessageHandlers(deps) {
353
350
  if (msg.type === "config") return {
354
351
  sampleRate: msg.sampleRate,
355
352
  ttsSampleRate: msg.ttsSampleRate,
356
- audioOut: msg.audioOut,
357
353
  sid: msg.sessionId
358
354
  };
359
355
  if (msg.type === "audio_done") {
@@ -404,84 +400,6 @@ function reconnectPending(socket) {
404
400
  return socket instanceof ReconnectingWebSocket && socket.shouldReconnect && socket.retryCount < RECONNECT_OPTIONS.maxRetries;
405
401
  }
406
402
  //#endregion
407
- //#region session-core-upload.ts
408
- /**
409
- * File-upload transcription for the session core (`sendAudioFile`).
410
- *
411
- * Split out of `session-core.ts`: this module owns the decode → frame →
412
- * reliable-send pipeline for uploaded audio, plus the two guards that keep
413
- * it exclusive — an in-flight lock (no second upload, no mic start
414
- * mid-upload) and an epoch that in-flight sends compare against so a
415
- * reset/close/reconnect abandons them instead of streaming a stale clip
416
- * into the fresh session.
417
- */
418
- function createUploadSender(deps) {
419
- const { conn, getSnapshot, sendJson } = deps;
420
- /** True while a `sendAudioFile` upload is decoding or streaming. Blocks the
421
- * mic (and a second upload) so file bytes never interleave with live audio. */
422
- let uploadInFlight = false;
423
- /** Bumped whenever conversation/connection state is discarded (reset, close,
424
- * reconnect). An in-flight upload compares against it between chunks and
425
- * aborts instead of streaming a stale clip into the fresh session. */
426
- let uploadEpoch = 0;
427
- /** Stream `bytes` to the socket in chunks, waiting out backpressure.
428
- * Unlike live mic frames (dropped under backpressure — stale speech is
429
- * worthless), file audio must arrive completely. Aborts if the connection
430
- * closes or the session is reset (`epoch` moves on) between chunks. */
431
- async function sendBytesReliably(bytes, epoch) {
432
- for (let i = 0; i < bytes.byteLength; i += FILE_UPLOAD_CHUNK_BYTES) {
433
- while (epoch === uploadEpoch && conn.ws && conn.ws.readyState === WS_OPEN && conn.ws.bufferedAmount > MIC_SEND_MAX_BUFFERED_BYTES) await new Promise((r) => setTimeout(r, FILE_SEND_BACKOFF_MS));
434
- if (epoch !== uploadEpoch) throw new Error("sendAudioFile: session was reset mid-send");
435
- if (!conn.ws || conn.ws.readyState !== WS_OPEN) throw new Error("sendAudioFile: connection closed mid-send");
436
- conn.ws.send(bytes.subarray(i, i + FILE_UPLOAD_CHUNK_BYTES));
437
- }
438
- }
439
- /** Validate that an upload may start; returns the session's ready config. */
440
- function assertUploadReady() {
441
- const cfg = conn.readyConfig;
442
- if (!(cfg && conn.ws) || conn.ws.readyState !== WS_OPEN) throw new Error("sendAudioFile: session is not connected");
443
- const snap = getSnapshot();
444
- if (snap.audioOut) throw new Error("sendAudioFile is only available in text-only sessions (tts: none()) — voice sessions stream the microphone instead");
445
- if (snap.recording) throw new Error("sendAudioFile: stop recording before uploading a file");
446
- if (uploadInFlight) throw new Error("sendAudioFile: another upload is already in progress");
447
- return cfg;
448
- }
449
- async function sendAudioFile(file) {
450
- const cfg = assertUploadReady();
451
- uploadInFlight = true;
452
- const epoch = uploadEpoch;
453
- try {
454
- const { decodeAudioToPcm16 } = await import("./audio.js");
455
- const clip = await decodeAudioToPcm16(await file.arrayBuffer(), cfg.sampleRate);
456
- if (getSnapshot().recording || conn.audioSetupInFlight) throw new Error("sendAudioFile: stop recording before uploading a file");
457
- if (epoch !== uploadEpoch) throw new Error("sendAudioFile: session was reset mid-send");
458
- if (clip.length / cfg.sampleRate <= MAX_SYNC_AUDIO_SECONDS) {
459
- const bytes = new Uint8Array(clip.buffer, clip.byteOffset, clip.byteLength);
460
- sendJson({
461
- type: "transcribe_file_start",
462
- sampleRate: cfg.sampleRate,
463
- byteLength: bytes.byteLength
464
- });
465
- await sendBytesReliably(bytes, epoch);
466
- sendJson({ type: "transcribe_file_end" });
467
- return;
468
- }
469
- const padded = new Int16Array(clip.length + cfg.sampleRate);
470
- padded.set(clip);
471
- await sendBytesReliably(new Uint8Array(padded.buffer), epoch);
472
- } finally {
473
- uploadInFlight = false;
474
- }
475
- }
476
- return {
477
- sendAudioFile,
478
- inFlight: () => uploadInFlight,
479
- discard: () => {
480
- uploadEpoch++;
481
- }
482
- };
483
- }
484
- //#endregion
485
403
  //#region session-core-url.ts
486
404
  /** Build the session WebSocket URL from the platform URL and resume state. */
487
405
  function buildWsUrl(platformUrl, resume, sessionId) {
@@ -524,7 +442,6 @@ function createSessionCore(options) {
524
442
  contentVersion: 0,
525
443
  started: false,
526
444
  running: false,
527
- audioOut: true,
528
445
  recording: false,
529
446
  apiUrl: buildWsUrl(options.platformUrl, false).toString()
530
447
  };
@@ -570,8 +487,16 @@ function createSessionCore(options) {
570
487
  };
571
488
  let connectionController = null;
572
489
  let hasConnected = false;
490
+ /**
491
+ * The session ID to resume: seeded from `options.resumeSessionId`, then
492
+ * kept current from every `config` frame. Reconnect URLs carry it as
493
+ * `?sessionId=<id>` so the server re-registers the SAME session id —
494
+ * that key is what per-session tool state (`ctx.state`) lives under, so
495
+ * a reconnect that omits it gets a fresh session with none of the
496
+ * agent's context, greeting suppression aside.
497
+ */
498
+ let sessionId = options.resumeSessionId;
573
499
  function cleanupAudio() {
574
- upload.discard();
575
500
  conn.audioSetupInFlight = false;
576
501
  conn.voiceIO?.close().catch(() => {});
577
502
  conn.voiceIO = null;
@@ -589,16 +514,10 @@ function createSessionCore(options) {
589
514
  if (conn.ws.bufferedAmount > MIC_SEND_MAX_BUFFERED_BYTES) return;
590
515
  conn.ws.send(bytes);
591
516
  }
592
- const upload = createUploadSender({
593
- conn,
594
- getSnapshot,
595
- sendJson
596
- });
597
517
  const { handleMessage, settleWhenAudioDrained } = createMessageHandlers({
598
518
  getSnapshot,
599
519
  updateState,
600
520
  conn,
601
- discardUpload: upload.discard,
602
521
  cleanupAudio
603
522
  });
604
523
  const audioDeps = {
@@ -619,20 +538,17 @@ function createSessionCore(options) {
619
538
  /** React to the server's `config` message: record it, set up the audio
620
539
  * path for the session's mode, and replay history on reconnect. */
621
540
  function onServerConfig(config) {
622
- if (config.sid) options.onSessionId?.(config.sid);
541
+ if (config.sid) {
542
+ sessionId = config.sid;
543
+ options.onSessionId?.(config.sid);
544
+ }
623
545
  const isReconnect = hasConnected;
624
546
  hasConnected = true;
625
547
  conn.readyConfig = {
626
548
  sampleRate: config.sampleRate,
627
549
  ttsSampleRate: config.ttsSampleRate
628
550
  };
629
- const audioOut = config.audioOut !== false;
630
- updateState({ audioOut });
631
- if (audioOut) initAudioCapture(conn, config, audioDeps, true);
632
- else {
633
- sendJson({ type: "audio_ready" });
634
- updateState({ state: "listening" });
635
- }
551
+ initAudioCapture(conn, config, audioDeps, true);
636
552
  if (isReconnect && currentSnapshot.messages.length > 0) sendJson({
637
553
  type: "history",
638
554
  messages: currentSnapshot.messages.map((m) => ({
@@ -645,11 +561,12 @@ function createSessionCore(options) {
645
561
  * The WebSocket URL for the *next* connection attempt. Evaluated per
646
562
  * attempt (partysocket takes it as a URL provider), so once the first
647
563
  * `config` arrives, every reconnect — automatic or explicit — carries
648
- * `resume=1` and the session resumes instead of starting over.
564
+ * `?sessionId=<id>` and the server resumes the SAME session (id, tool
565
+ * state) instead of minting a new one. `resume=1` remains only as the
566
+ * greeting-suppression fallback for a server whose config carried no id.
649
567
  */
650
568
  function currentWsUrl() {
651
- const resumeId = !hasConnected ? options.resumeSessionId : void 0;
652
- return buildWsUrl(options.platformUrl, hasConnected, resumeId).toString();
569
+ return buildWsUrl(options.platformUrl, hasConnected, sessionId).toString();
653
570
  }
654
571
  /** Open a socket: an injected constructor as-is (tests), or partysocket's
655
572
  * reconnecting WebSocket — same interface, plus reconnect-on-close. */
@@ -729,7 +646,6 @@ function createSessionCore(options) {
729
646
  sendJson({ type: "cancel" });
730
647
  }
731
648
  function reset() {
732
- upload.discard();
733
649
  conn.voiceIO?.flush();
734
650
  if (conn.ws && conn.ws.readyState === WS_OPEN) {
735
651
  sendJson({ type: "reset" });
@@ -748,17 +664,6 @@ function createSessionCore(options) {
748
664
  recording: false
749
665
  });
750
666
  }
751
- function startRecording() {
752
- if (currentSnapshot.audioOut || currentSnapshot.recording || conn.audioSetupInFlight || upload.inFlight()) return;
753
- const cfg = conn.readyConfig;
754
- if (!(cfg && conn.ws) || conn.ws.readyState !== WS_OPEN) return;
755
- initAudioCapture(conn, cfg, audioDeps, false);
756
- }
757
- function stopRecording() {
758
- if (currentSnapshot.audioOut || !currentSnapshot.recording) return;
759
- cleanupAudio();
760
- updateState({ recording: false });
761
- }
762
667
  function start() {
763
668
  updateState({
764
669
  started: true,
@@ -783,9 +688,6 @@ function createSessionCore(options) {
783
688
  disconnect,
784
689
  start,
785
690
  toggle,
786
- startRecording,
787
- stopRecording,
788
- sendAudioFile: upload.sendAudioFile,
789
691
  [Symbol.dispose]() {
790
692
  disconnect();
791
693
  }
@@ -17,8 +17,6 @@ export declare const CLEARED_SESSION_STATE: {
17
17
  export type SessionConfigMessage = {
18
18
  sampleRate: number;
19
19
  ttsSampleRate: number;
20
- /** False for text-only agents (`tts: none()`) — see ReadyConfigSchema. */
21
- audioOut?: boolean | undefined;
22
20
  sid?: string | undefined;
23
21
  };
24
22
  /** Dependencies the message handlers need from the owning session core. */
@@ -26,8 +24,6 @@ type MessageHandlerDeps = {
26
24
  getSnapshot: () => SessionSnapshot;
27
25
  updateState: (partial: Partial<SessionSnapshot>) => void;
28
26
  conn: ConnState;
29
- /** Invalidate any in-flight file upload (the upload sender's `discard`). */
30
- discardUpload: () => void;
31
27
  /** Release the microphone/VoiceIO (the session core's `cleanupAudio`). */
32
28
  cleanupAudio: () => void;
33
29
  };