@copilotkit/learning 0.0.1 → 1.81.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.
package/dist/index.mjs CHANGED
@@ -115,13 +115,17 @@ function createRedactor() {
115
115
  const toggled = (records) => {
116
116
  for (const record of records) if (record.oldValue?.toLowerCase() === "password") seen.add(record.target);
117
117
  };
118
- /** Replaces remembered values, raw and in their JSON and URL encodings. */
119
- const scrub = (text) => {
118
+ /**
119
+ * Replaces remembered values, raw and in their JSON and URL encodings, plus
120
+ * `normalize(value)` when given and at least 4 characters long.
121
+ */
122
+ const scrub = (text, normalize) => {
120
123
  for (const value of [...remembered.values()].sort((a, b) => b.length - a.length)) for (const form of [
121
124
  value,
122
125
  JSON.stringify(value).slice(1, -1),
123
126
  encodeURIComponent(value),
124
- new URLSearchParams({ v: value }).toString().slice(2)
127
+ new URLSearchParams({ v: value }).toString().slice(2),
128
+ ...[normalize?.(value) ?? ""].filter((normal) => normal.length >= 4)
125
129
  ]) text = /^\d+$/.test(form) ? text.replace(RegExp(`(?<!\\d)${form}(?!\\d)`, "g"), REDACTED) : text.split(form).join(REDACTED);
126
130
  return text;
127
131
  };
@@ -139,6 +143,7 @@ function createRedactor() {
139
143
  };
140
144
  return {
141
145
  isPassword,
146
+ scrub,
142
147
  watch() {
143
148
  try {
144
149
  for (const input of document.querySelectorAll("input[type=password]")) seen.add(input);
@@ -222,6 +227,172 @@ function describeTarget(el, redact) {
222
227
  }
223
228
  };
224
229
  }
230
+ const MAX_LABEL_LENGTH = 80;
231
+ const MAX_WALK_NODES = 1e3;
232
+ const MAX_WALK_CHARS = 4 * MAX_LABEL_LENGTH;
233
+ const CONTROL_ROLE = /^(button|link|tab|menuitem(checkbox|radio)?|option|checkbox|switch|radio|treeitem)$/;
234
+ const CONTENTEDITABLE = "[contenteditable]:not([contenteditable=\"false\"])";
235
+ const TEXT_ENTRY = `${CONTENTEDITABLE}, [role~=textbox], [role~=searchbox], [role~=combobox]`;
236
+ const EDITABLE = `input, textarea, select, ${TEXT_ENTRY}`;
237
+ const HEADING = "h1, h2, h3, h4, h5, h6, [role~=heading]";
238
+ const STRUCTURE = `${HEADING}, p, ul, ol, li, dl, table, article, section, blockquote`;
239
+ const SKIPPED = "script, style, template, [hidden], [aria-hidden=\"true\"]";
240
+ const PHRASING = /^(b|i|u|em|strong|mark|small|sub|sup|abbr|code|kbd|s|q|cite|dfn|var|time|data|bdi|bdo|wbr|span)$/;
241
+ const CAPTION_INPUT = /^(submit|button|reset|image)$/;
242
+ const NON_TEXT_INPUT = /^(submit|button|reset|image|checkbox|radio|range|color|file|hidden)$/;
243
+ const IGNORABLE = /\p{Default_Ignorable_Code_Point}/gu;
244
+ const URL_LIKE = /(?:\b[a-z][\w+.-]{0,31}:\/\/|\bwww\.)[^\s<>"']*[^\s<>"'.,;:!?)]/gi;
245
+ const EMAIL = /[\p{L}\p{N}_.%+-]{1,64}@[\p{L}\p{N}-]{1,63}(?:\.[\p{L}\p{N}-]{1,63})+/gu;
246
+ const JWT = /(^|[^\w-])(?:eyJ[\w-]*\.[\w-]*\.[\w-]*|[\w-]{16,}\.[\w-]{16,}\.[\w-]{16,}|(?=[\w-]*\d)[\w-]{8,}\.(?=[\w-]*\d)[\w-]{8,}\.(?=[\w-]*\d)[\w-]{8,})/g;
247
+ const TOKEN_RUN = /[\w+/=-]{24,}/g;
248
+ const roleOf = (element) => (element.getAttribute("role") ?? "").trim().split(/\s+/)[0].toLowerCase();
249
+ const idRefs = (element, name) => (element.getAttribute(name) ?? "").split(/\s+/).filter(Boolean);
250
+ const rootOf = (element) => {
251
+ const root = element.getRootNode();
252
+ return root instanceof Document || root instanceof ShadowRoot ? root : null;
253
+ };
254
+ /** Buttons, links with `href`, `summary`, and elements with a control role. */
255
+ const isControl = (element) => {
256
+ const role = roleOf(element);
257
+ return role ? CONTROL_ROLE.test(role) : element.matches("button, a[href], summary");
258
+ };
259
+ /** Text-like inputs, text areas, selects, and anything inside editable content or a text-entry role. */
260
+ const holdsValue = (element) => element instanceof HTMLInputElement ? !NON_TEXT_INPUT.test(element.type) : element.matches("textarea, select") || element.closest(TEXT_ENTRY) !== null;
261
+ /**
262
+ * The name an image contributes: `alt` of an `img`, or `aria-label` (else a
263
+ * plain `<title>`) of an `svg` or `role="img"` element. `undefined` when the
264
+ * element is not an image.
265
+ */
266
+ function imageName(element) {
267
+ if (element.localName === "img") return element.getAttribute("alt");
268
+ if (element.localName !== "svg" && roleOf(element) !== "img") return void 0;
269
+ const title = Array.from(element.children).find((child) => child.localName === "title" && child.childElementCount === 0);
270
+ return element.getAttribute("aria-label") ?? title?.textContent ?? null;
271
+ }
272
+ /**
273
+ * Rendered text. Inline formatting joins words; other elements separate them.
274
+ * Skips hidden subtrees, uses image names, and does
275
+ * not walk into images. Yields nothing for editable content or editable
276
+ * descendants. In `strict` mode (a control's own text) it also yields nothing
277
+ * past the walk bounds, for card-like structure, and for text that looks like
278
+ * separate fields: two `div`s that directly hold text, or three sibling
279
+ * elements that each carry text.
280
+ */
281
+ function safeText(root, strict) {
282
+ if (holdsValue(root)) return "";
283
+ let text = "";
284
+ let nodes = 0;
285
+ let paragraphs = 0;
286
+ let div;
287
+ const bearers = /* @__PURE__ */ new Map();
288
+ const add = (part) => text = (text + part).replace(/\s+/g, " ");
289
+ const bear = (start) => {
290
+ if (!strict) return true;
291
+ for (let child = start, parent = start.parentNode; child !== root && parent !== null; child = parent, parent = parent.parentNode) {
292
+ const siblings = bearers.get(parent) ?? /* @__PURE__ */ new Set();
293
+ bearers.set(parent, siblings);
294
+ if (siblings.has(child)) break;
295
+ if (siblings.add(child).size > 2) return false;
296
+ }
297
+ return true;
298
+ };
299
+ let node = root.firstChild;
300
+ while (node !== null) {
301
+ if (++nodes > MAX_WALK_NODES || text.length > MAX_WALK_CHARS) {
302
+ if (strict) return "";
303
+ break;
304
+ }
305
+ let descend = false;
306
+ if (node instanceof Text) {
307
+ const parent = node.parentNode;
308
+ let holder = parent;
309
+ while (holder !== root && PHRASING.test(holder.localName)) holder = holder.parentElement;
310
+ if (/\S/.test(node.data) && (!bear(parent) || strict && holder.localName === "div" && (div ??= holder) !== holder)) return "";
311
+ add(node.data);
312
+ } else if (node instanceof Element) {
313
+ if (node.matches(EDITABLE)) return "";
314
+ if (strict && node.matches(STRUCTURE) && !(root.localName === "summary" && node.matches(HEADING)) && (node.localName !== "p" || ++paragraphs > 1)) return "";
315
+ const name = node.matches(SKIPPED) ? null : imageName(node);
316
+ if (name) {
317
+ if (!bear(node)) return "";
318
+ add(` ${name} `);
319
+ } else if (name === void 0) {
320
+ if (!PHRASING.test(node.localName)) add(" ");
321
+ descend = true;
322
+ }
323
+ }
324
+ if (descend && node.firstChild !== null) node = node.firstChild;
325
+ else {
326
+ while (node !== root && node.nextSibling === null) {
327
+ node = node.parentNode;
328
+ if (!PHRASING.test(node.localName)) add(" ");
329
+ }
330
+ node = node === root ? null : node.nextSibling;
331
+ }
332
+ }
333
+ return text;
334
+ }
335
+ /** An option or tree item of an autocomplete or search popup, whose text usually repeats what the user typed. */
336
+ const isSuggestion = (element) => (element.localName === "option" || /^(option|treeitem)$/.test(roleOf(element))) && inSuggestionList(element);
337
+ /** Whether `element` is, or is inside, a combobox, a datalist, or the popup a combobox or autocomplete field controls or owns. */
338
+ function inSuggestionList(element) {
339
+ if (element.closest("[role~=combobox], datalist") !== null) return true;
340
+ const root = rootOf(element);
341
+ return root !== null && Array.from(root.querySelectorAll("[role~=combobox], [aria-autocomplete]")).some((owner) => [...idRefs(owner, "aria-controls"), ...idRefs(owner, "aria-owns")].some((id) => root.getElementById(id)?.contains(element)));
342
+ }
343
+ function labelledBy(element) {
344
+ const root = rootOf(element);
345
+ return root === null ? null : idRefs(element, "aria-labelledby").map((id) => {
346
+ const referenced = root.getElementById(id);
347
+ return referenced === null || inSuggestionList(referenced) ? "" : safeText(referenced, false);
348
+ }).join(" ");
349
+ }
350
+ /** A control's own text, unless it is longer than a label: then it is content, not a name. */
351
+ const normalize = (text) => text.replace(IGNORABLE, "").replace(/\s+/g, " ").trim();
352
+ function contentText(element) {
353
+ const text = safeText(element, true);
354
+ return Array.from(normalize(text)).length > MAX_LABEL_LENGTH ? "" : text;
355
+ }
356
+ /** Removes invisible characters, credentials, emails, URLs, and tokens, then caps the length. */
357
+ function cleanLabel(raw, redact) {
358
+ const chars = Array.from(redact.scrub(normalize(redact.body(raw.replace(IGNORABLE, "")) ?? ""), normalize).replace(URL_LIKE, "[url]").replace(EMAIL, "[email]").replace(JWT, `$1${REDACTED}`).replace(TOKEN_RUN, (run) => /\d/.test(run) ? REDACTED : run));
359
+ return chars.length > MAX_LABEL_LENGTH ? `${chars.slice(0, MAX_LABEL_LENGTH - 1).join("").trimEnd()}…` : chars.join("");
360
+ }
361
+ function firstLabel(sources, redact) {
362
+ for (const source of sources) {
363
+ const label = cleanLabel(source() ?? "", redact);
364
+ if (label) return label;
365
+ }
366
+ }
367
+ /**
368
+ * A short name for the clicked control, from the first source that yields one:
369
+ * `aria-labelledby`, `aria-label`, an input caption or `alt`, `title` (controls
370
+ * only), then the text of a control named by its content. Containers, cards,
371
+ * and editable content never contribute their text. An autocomplete suggestion,
372
+ * or anything inside one, gets no label from any source; `data-copilotkit-action`
373
+ * names it instead. An icon without its own name takes the name of the control
374
+ * around it.
375
+ */
376
+ function clickLabel(element, redact) {
377
+ const option = element.closest("option, [role~=option], [role~=treeitem]");
378
+ if (option !== null && isSuggestion(option)) return void 0;
379
+ const named = [() => labelledBy(element), () => element.getAttribute("aria-label")];
380
+ if (/^(img|svg)$/.test(element.localName) || /^(img|presentation|none)$/.test(roleOf(element))) {
381
+ const own = firstLabel([...named, () => imageName(element) ?? null], redact);
382
+ if (own !== void 0) return own;
383
+ let control = element.parentElement;
384
+ while (control !== null && !isControl(control)) control = control.parentElement;
385
+ return control === null ? void 0 : clickLabel(control, redact);
386
+ }
387
+ const control = isControl(element) && !holdsValue(element);
388
+ const captionInput = element instanceof HTMLInputElement && CAPTION_INPUT.test(element.type);
389
+ return firstLabel([
390
+ ...named,
391
+ () => captionInput ? element.type === "image" ? element.getAttribute("alt") : element.value : null,
392
+ () => control || captionInput ? element.getAttribute("title") : null,
393
+ () => control ? contentText(element) : null
394
+ ], redact);
395
+ }
225
396
  /** Snapshot live control properties; HTML attributes do not reflect edited values. */
226
397
  function readControlValue(element, redact) {
227
398
  if (element instanceof HTMLInputElement) return {
@@ -239,7 +410,7 @@ function readControlValue(element, redact) {
239
410
  selectedValues: Array.from(element.selectedOptions, (option) => option.value)
240
411
  };
241
412
  if (element instanceof HTMLTextAreaElement) return { value: element.value };
242
- if (element.matches("[contenteditable]:not([contenteditable=\"false\"])")) return { value: element.textContent ?? "" };
413
+ if (element.matches(CONTENTEDITABLE)) return { value: element.textContent ?? "" };
243
414
  return {};
244
415
  }
245
416
  function eventElement(event) {
@@ -260,12 +431,21 @@ function isNear(origin, event) {
260
431
  function installClickCapture(params) {
261
432
  const { emit, getRoute, enrich, isTrusted = (event) => event.isTrusted, redact = createRedactor() } = params;
262
433
  let origin = null;
434
+ const describeClick = (el) => {
435
+ const described = describeTarget(el, redact);
436
+ if (described === null) return null;
437
+ try {
438
+ const label = clickLabel(described.element, redact);
439
+ if (label !== void 0) described.target.label = label;
440
+ } catch {}
441
+ return described;
442
+ };
263
443
  const onPointerDown = (event) => {
264
444
  try {
265
445
  if (event.isPrimary === false) return;
266
446
  const el = eventElement(event);
267
447
  origin = {
268
- described: el === null ? null : describeTarget(el, redact),
448
+ described: el === null ? null : describeClick(el),
269
449
  x: event.clientX,
270
450
  y: event.clientY
271
451
  };
@@ -280,7 +460,7 @@ function installClickCapture(params) {
280
460
  if (!isTrusted(event)) return;
281
461
  const el = eventElement(event);
282
462
  if (el === null) return;
283
- const described = pointerOrigin !== null && isRootOrDetached(el) && isNear(pointerOrigin, event) ? pointerOrigin.described : describeTarget(el, redact);
463
+ const described = pointerOrigin !== null && isRootOrDetached(el) && isNear(pointerOrigin, event) ? pointerOrigin.described : describeClick(el);
284
464
  if (described === null) return;
285
465
  const input = event.detail === 0 ? "keyboard" : "pointer";
286
466
  emit("click", {
@@ -310,41 +490,138 @@ function installClickCapture(params) {
310
490
 
311
491
  //#endregion
312
492
  //#region src/inputs.ts
313
- /** Records user edits without changing the control or interfering with its listeners. */
493
+ const TEXT_DEBOUNCE_MS = 300;
494
+ const TEXT_INPUT_TYPES = new Set([
495
+ "text",
496
+ "search",
497
+ "tel",
498
+ "url",
499
+ "email",
500
+ "password",
501
+ "number"
502
+ ]);
503
+ /** Records settled user edits without interfering with the host app's controls. */
314
504
  function installInputCapture(params) {
315
505
  const { emit, isTrusted = (event) => event.isTrusted, redact = createRedactor() } = params;
316
- const onInput = (event) => {
506
+ let active = true;
507
+ const pending = /* @__PURE__ */ new Map();
508
+ const changeEcho = /* @__PURE__ */ new WeakMap();
509
+ const composing = /* @__PURE__ */ new WeakMap();
510
+ const cancel = (element) => {
511
+ clearTimeout(pending.get(element)?.timer);
512
+ pending.delete(element);
513
+ };
514
+ const deliver = (element, edit) => {
317
515
  try {
318
- if (!isTrusted(event)) return;
319
- const [element] = event.composedPath();
320
- if (!(element instanceof Element)) return;
321
- const described = describeTarget(element, redact);
322
- if (described === null) return;
323
- const value = readControlValue(element, redact);
324
- if (!("value" in value)) return;
325
- emit("input", {
326
- eventType: event.type,
516
+ if (!active || describeTarget(element, redact) === null) return;
517
+ if (redact.isPassword(element) && edit.state.value !== REDACTED) return;
518
+ if (edit.payload.eventType === "input") changeEcho.set(element, JSON.stringify(edit.state));
519
+ edit.payload.url = redact.url(edit.payload.url);
520
+ edit.payload.target.text = redact.url(edit.payload.target.text);
521
+ edit.payload.target.attributes = Object.fromEntries(Object.entries(edit.payload.target.attributes).map(([key, value]) => [key, redact.url(value)]));
522
+ emit("input", edit.payload);
523
+ } catch {}
524
+ };
525
+ const flush = () => {
526
+ const edits = [...pending];
527
+ for (const [element] of edits) cancel(element);
528
+ for (const [element, edit] of edits) deliver(element, edit);
529
+ };
530
+ const observe = (element, eventType, settle) => {
531
+ if (pending.size > 0 && !pending.has(element)) flush();
532
+ if (!active) return;
533
+ const described = describeTarget(element, redact);
534
+ if (described === null) {
535
+ cancel(element);
536
+ return;
537
+ }
538
+ const state = readControlValue(element, redact);
539
+ if (!("value" in state)) return;
540
+ const text = element instanceof HTMLTextAreaElement || element instanceof HTMLInputElement && TEXT_INPUT_TYPES.has(element.type) || element.matches("[contenteditable]:not([contenteditable=\"false\"])");
541
+ if (!text) flush();
542
+ const signature = JSON.stringify(state);
543
+ const echo = eventType === "change" && (signature === JSON.stringify(pending.get(element)?.state) || signature === changeEcho.get(element));
544
+ changeEcho.delete(element);
545
+ if (echo) {
546
+ flush();
547
+ changeEcho.delete(element);
548
+ return;
549
+ }
550
+ cancel(element);
551
+ const edit = {
552
+ state,
553
+ payload: {
554
+ eventType,
327
555
  target: {
328
556
  ...described.target,
329
- ...value
557
+ ...state
330
558
  },
331
559
  url: redact.url(location.href),
332
560
  route: location.pathname
333
- });
561
+ }
562
+ };
563
+ if (!text) {
564
+ deliver(element, edit);
565
+ return;
566
+ }
567
+ if (settle) edit.timer = setTimeout(() => {
568
+ cancel(element);
569
+ deliver(element, edit);
570
+ }, TEXT_DEBOUNCE_MS);
571
+ pending.set(element, edit);
572
+ };
573
+ const onEdit = (event) => {
574
+ try {
575
+ if (!active || !isTrusted(event)) return;
576
+ const [element] = event.composedPath();
577
+ if (!(element instanceof Element)) return;
578
+ if (event.type === "compositionstart") {
579
+ composing.set(element, true);
580
+ clearTimeout(pending.get(element)?.timer);
581
+ return;
582
+ }
583
+ if (event.type === "compositionend") composing.set(element, false);
584
+ const inComposition = composing.get(element) === true || composing.get(element) !== false && "isComposing" in event && event.isComposing === true;
585
+ observe(element, event.type === "compositionend" ? "input" : event.type, !inComposition);
586
+ if (event.type === "change") flush();
334
587
  } catch {}
335
588
  };
336
- window.addEventListener("input", onInput, {
589
+ const onAction = (event) => {
590
+ if (event.type !== "pagehide" && !isTrusted(event)) return;
591
+ if (event.type === "keydown") {
592
+ const [element] = event.composedPath();
593
+ if (!(event instanceof KeyboardEvent) || event.key !== "Enter" || event.isComposing || event.keyCode === 229 || element instanceof Element && composing.get(element) === true) return;
594
+ }
595
+ flush();
596
+ };
597
+ const editTypes = [
598
+ "input",
599
+ "change",
600
+ "compositionstart",
601
+ "compositionend"
602
+ ];
603
+ const actionTypes = [
604
+ "click",
605
+ "submit",
606
+ "focusout",
607
+ "pagehide",
608
+ "keydown"
609
+ ];
610
+ for (const type of editTypes) window.addEventListener(type, onEdit, {
337
611
  capture: true,
338
612
  passive: true
339
613
  });
340
- window.addEventListener("change", onInput, {
614
+ for (const type of actionTypes) window.addEventListener(type, onAction, {
341
615
  capture: true,
342
616
  passive: true
343
617
  });
344
- return () => {
345
- window.removeEventListener("input", onInput, { capture: true });
346
- window.removeEventListener("change", onInput, { capture: true });
618
+ const uninstall = () => {
619
+ active = false;
620
+ for (const element of pending.keys()) cancel(element);
621
+ for (const type of editTypes) window.removeEventListener(type, onEdit, true);
622
+ for (const type of actionTypes) window.removeEventListener(type, onAction, true);
347
623
  };
624
+ return Object.assign(uninstall, { flush });
348
625
  }
349
626
 
350
627
  //#endregion
@@ -864,6 +1141,8 @@ function createCollector(options) {
864
1141
  const record = (name, value) => {
865
1142
  const current = session;
866
1143
  if (current === null) return;
1144
+ if (name !== "input" && name !== "network") current.flushInputs?.();
1145
+ if (session !== current) return;
867
1146
  try {
868
1147
  seq += 1;
869
1148
  const event = {
@@ -876,7 +1155,7 @@ function createCollector(options) {
876
1155
  timestamp: Date.now()
877
1156
  };
878
1157
  const kept = beforeSend === void 0 ? event : beforeSend(event);
879
- if (kept === null) return;
1158
+ if (kept === null || session !== current) return;
880
1159
  current.queue.push(kept);
881
1160
  if (current.queue.length >= FLUSH_SIZE) flush();
882
1161
  } catch {}
@@ -919,10 +1198,14 @@ function createCollector(options) {
919
1198
  ignoreUrls,
920
1199
  redact
921
1200
  }));
922
- if (capture.inputs) current.uninstalls.push(installInputCapture({
923
- emit: recordCurrent,
924
- redact
925
- }));
1201
+ if (capture.inputs) {
1202
+ const inputs = installInputCapture({
1203
+ emit: recordCurrent,
1204
+ redact
1205
+ });
1206
+ current.flushInputs = inputs.flush;
1207
+ current.uninstalls.push(inputs);
1208
+ }
926
1209
  const onPageHide = () => flush({ beacon: true });
927
1210
  window.addEventListener("pagehide", onPageHide);
928
1211
  current.uninstalls.push(() => window.removeEventListener("pagehide", onPageHide));
@@ -937,6 +1220,8 @@ function createCollector(options) {
937
1220
  const stop = () => {
938
1221
  const current = session;
939
1222
  if (current === null) return;
1223
+ current.flushInputs?.();
1224
+ if (session !== current) return;
940
1225
  clearInterval(current.timer);
941
1226
  for (const uninstall of current.uninstalls) uninstall();
942
1227
  flush();
@@ -1016,7 +1301,9 @@ function isJson(value, ancestors = /* @__PURE__ */ new Set()) {
1016
1301
  */
1017
1302
  function createTrajectoryCollector(options) {
1018
1303
  let active = false;
1304
+ let generation = 0;
1019
1305
  let uninstalls = [];
1306
+ let flushInputs;
1020
1307
  const report = (code, message) => {
1021
1308
  try {
1022
1309
  options.onError?.({
@@ -1028,6 +1315,9 @@ function createTrajectoryCollector(options) {
1028
1315
  const getRoute = () => toRoute(location.pathname);
1029
1316
  const record = (name, value) => {
1030
1317
  if (!active) return;
1318
+ const current = generation;
1319
+ if (name !== "input" && name !== "network") flushInputs?.();
1320
+ if (!active || generation !== current) return;
1031
1321
  let payload;
1032
1322
  try {
1033
1323
  const event = {
@@ -1037,7 +1327,7 @@ function createTrajectoryCollector(options) {
1037
1327
  value
1038
1328
  };
1039
1329
  const kept = options.beforeSend === void 0 ? event : options.beforeSend(event);
1040
- if (kept === null || !active) return;
1330
+ if (kept === null || !active || generation !== current) return;
1041
1331
  if (kept.type !== "CUSTOM" || typeof kept.name !== "string" || !kept.name.trim() || kept.name.length > 200 || kept.name.includes("\0") || !Number.isFinite(kept.timestamp) || kept.timestamp <= 0 || kept.timestamp >= 0xe677d21fdc00 || !isJson(kept.value)) {
1042
1332
  report("INVALID_EVENT", "The event must contain a name, timestamp, and JSON value.");
1043
1333
  return;
@@ -1067,6 +1357,12 @@ function createTrajectoryCollector(options) {
1067
1357
  record(name, value);
1068
1358
  };
1069
1359
  const stop = () => {
1360
+ const current = generation;
1361
+ const flush = flushInputs;
1362
+ flushInputs = void 0;
1363
+ flush?.();
1364
+ if (generation !== current) return;
1365
+ generation++;
1070
1366
  active = false;
1071
1367
  const installed = uninstalls;
1072
1368
  uninstalls = [];
@@ -1076,6 +1372,7 @@ function createTrajectoryCollector(options) {
1076
1372
  start() {
1077
1373
  if (active || typeof window === "undefined") return;
1078
1374
  active = true;
1375
+ generation++;
1079
1376
  if (typeof window.addEventListener !== "function") return;
1080
1377
  const redact = createRedactor();
1081
1378
  try {
@@ -1089,10 +1386,14 @@ function createTrajectoryCollector(options) {
1089
1386
  emit: capture,
1090
1387
  redact
1091
1388
  }));
1092
- if (options.capture?.inputs !== false) uninstalls.push(installInputCapture({
1093
- emit: capture,
1094
- redact
1095
- }));
1389
+ if (options.capture?.inputs !== false) {
1390
+ const inputs = installInputCapture({
1391
+ emit: capture,
1392
+ redact
1393
+ });
1394
+ flushInputs = inputs.flush;
1395
+ uninstalls.push(inputs);
1396
+ }
1096
1397
  if (options.capture?.network !== false) uninstalls.push(installNetworkCapture({
1097
1398
  emit: capture,
1098
1399
  ignoreUrls: options.ignoreUrls ?? [],