@harperfast/harper 5.1.20 → 5.1.22

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 (37) hide show
  1. package/dist/resources/DatabaseTransaction.d.ts +28 -0
  2. package/dist/resources/DatabaseTransaction.js +132 -11
  3. package/dist/resources/DatabaseTransaction.js.map +1 -1
  4. package/dist/resources/LMDBTransaction.js +38 -4
  5. package/dist/resources/LMDBTransaction.js.map +1 -1
  6. package/dist/resources/Table.js +21 -2
  7. package/dist/resources/Table.js.map +1 -1
  8. package/npm-shrinkwrap.json +2 -2
  9. package/package.json +1 -1
  10. package/resources/DatabaseTransaction.ts +141 -17
  11. package/resources/LMDBTransaction.ts +38 -8
  12. package/resources/Table.ts +20 -2
  13. package/studio/web/assets/{Chat-SdD1EZca.js → Chat-BK_fyjsV.js} +2 -2
  14. package/studio/web/assets/{Chat-SdD1EZca.js.map → Chat-BK_fyjsV.js.map} +1 -1
  15. package/studio/web/assets/{FloatingChat-GMq6tHK9.js → FloatingChat-BZ2bqVJF.js} +4 -4
  16. package/studio/web/assets/{FloatingChat-GMq6tHK9.js.map → FloatingChat-BZ2bqVJF.js.map} +1 -1
  17. package/studio/web/assets/{applications-B5hhv7uA.js → applications-CPw4VRw-.js} +2 -2
  18. package/studio/web/assets/{applications-B5hhv7uA.js.map → applications-CPw4VRw-.js.map} +1 -1
  19. package/studio/web/assets/{index-VFcpNWgq.js → index-DMgwFden.js} +6 -6
  20. package/studio/web/assets/{index-VFcpNWgq.js.map → index-DMgwFden.js.map} +1 -1
  21. package/studio/web/assets/{index.lazy-DgcJojIA.js → index.lazy-CbfAMESW.js} +4 -4
  22. package/studio/web/assets/{index.lazy-DgcJojIA.js.map → index.lazy-CbfAMESW.js.map} +1 -1
  23. package/studio/web/assets/{profile-BqN-8vzm.js → profile-CwMLqUPI.js} +2 -2
  24. package/studio/web/assets/{profile-BqN-8vzm.js.map → profile-CwMLqUPI.js.map} +1 -1
  25. package/studio/web/assets/{setComponentFile-CHFvFq9u.js → setComponentFile-BNTRdRN4.js} +2 -2
  26. package/studio/web/assets/{setComponentFile-CHFvFq9u.js.map → setComponentFile-BNTRdRN4.js.map} +1 -1
  27. package/studio/web/assets/{setup-CQ3vWJSr.js → setup-BVsWmrtH.js} +2 -2
  28. package/studio/web/assets/{setup-CQ3vWJSr.js.map → setup-BVsWmrtH.js.map} +1 -1
  29. package/studio/web/assets/{status-BsW5fjuh.js → status-gcJRVZMN.js} +2 -2
  30. package/studio/web/assets/{status-BsW5fjuh.js.map → status-gcJRVZMN.js.map} +1 -1
  31. package/studio/web/assets/{swagger-ui-react-DITdeB9-.js → swagger-ui-react-BxoiwE28.js} +2 -2
  32. package/studio/web/assets/{swagger-ui-react-DITdeB9-.js.map → swagger-ui-react-BxoiwE28.js.map} +1 -1
  33. package/studio/web/assets/{tsMode-Dyph-OUs.js → tsMode-CpNrn_Ca.js} +2 -2
  34. package/studio/web/assets/{tsMode-Dyph-OUs.js.map → tsMode-CpNrn_Ca.js.map} +1 -1
  35. package/studio/web/assets/{useEntityRestURL-D0QTUqex.js → useEntityRestURL-CMTYmJCC.js} +2 -2
  36. package/studio/web/assets/{useEntityRestURL-D0QTUqex.js.map → useEntityRestURL-CMTYmJCC.js.map} +1 -1
  37. package/studio/web/index.html +1 -1
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "harper",
3
- "version": "5.1.20",
3
+ "version": "5.1.22",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "harper",
9
- "version": "5.1.20",
9
+ "version": "5.1.22",
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
12
  "@aws-sdk/client-s3": "^3.1012.0",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@harperfast/harper",
3
3
  "description": "Harper is an open-source Node.js performance platform that unifies database, cache, application, and messaging layers into one in-memory process.",
4
- "version": "5.1.20",
4
+ "version": "5.1.22",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://harper.fast",
7
7
  "bugs": {
@@ -34,6 +34,23 @@ let txnExpiration = envMngr.get(CONFIG_PARAMS.STORAGE_MAXTRANSACTIONOPENTIME) ??
34
34
 
35
35
  class StartedTransaction extends Error {}
36
36
 
37
+ /**
38
+ * Built when the long-transaction monitor aborts a write-bearing transaction that stayed open past the
39
+ * limit (STORAGE_MAXTRANSACTIONOPENTIME). Surfacing this instead of silently force-committing a partial
40
+ * write set preserves atomicity and avoids the index corruption described in issue #1407: the
41
+ * application gets an actionable error and owns how it splits long-running work into smaller
42
+ * transactions, while core keeps the consistency guarantee.
43
+ */
44
+ export function transactionOpenTooLongError(): ServerError {
45
+ // 422 rather than 503: the condition is deterministic for a given transaction shape, so a retryable
46
+ // status (503/408) would invite clients and gateways to auto-retry the same doomed long transaction.
47
+ // 422 signals the request itself must change (split the work), which is the actionable response.
48
+ return new ServerError(
49
+ 'Transaction was aborted after exceeding the open-transaction limit; split long-running work into smaller transactions',
50
+ 422
51
+ );
52
+ }
53
+
37
54
  type MaybePromise<T> = T | Promise<T>;
38
55
 
39
56
  export type CommitOptions = {
@@ -72,6 +89,9 @@ export type TransactionWrite = {
72
89
  // the commit handler's most recent decision: true means it took an early-return that left savedBlobs unreferenced.
73
90
  // reset at the top of each commit-handler invocation so retries see a fresh state.
74
91
  skipped?: boolean;
92
+ // sticky: a non-isRetry staging of this write appended its audit entry (set in save(); the retry
93
+ // dedup guards in the commit handler read it to ignore the write's own orphaned entry)
94
+ appendedAuditEntry?: boolean;
75
95
  };
76
96
 
77
97
  type RocksTransactionWithRetry = RocksTransaction & { isRetry?: boolean };
@@ -109,6 +129,10 @@ export class DatabaseTransaction implements Transaction {
109
129
  // An explicit marker rather than overloading `retries`, which is also bumped by transient
110
130
  // conflict retries and never reset, so it cannot reliably signal "this is a replay".
111
131
  declare isReplay?: boolean;
132
+ // Set by the long-transaction monitor when it aborts a write-bearing transaction that exceeded the
133
+ // open-transaction limit. Once poisoned, any further addWrite/commit throws transactionOpenTooLongError
134
+ // so the request rolls back cleanly instead of silently committing a partial write set (issue #1407).
135
+ declare timedOut?: boolean;
112
136
 
113
137
  getReadTxn(): ReadTransaction {
114
138
  this.readTxnRefCount = (this.readTxnRefCount || 0) + 1;
@@ -173,6 +197,7 @@ export class DatabaseTransaction implements Transaction {
173
197
  }
174
198
 
175
199
  addWrite(operation: TransactionWrite) {
200
+ if (this.timedOut) throw transactionOpenTooLongError();
176
201
  this.writes.push(operation);
177
202
  if (!operation.deferSave) {
178
203
  // Setting saved to false means to defer saving
@@ -228,6 +253,14 @@ export class DatabaseTransaction implements Transaction {
228
253
  if (result?.then) this.completions.push(result);
229
254
  }
230
255
  operation.commit(txnTime, operation.entry, this.retries > 0, transaction);
256
+ // Sticky record that THIS write staged with its audit entry appended (log entries are written
257
+ // at staging and are not part of the transaction, so they survive an abort). isRetry stagings
258
+ // skip the log write, so they never set it. The retry dedup guards in the commit handler key
259
+ // off this: a launderable proxy (like last attempt's skipped state) breaks under multi-round
260
+ // retries where a recommit round self-skips before a fresh-transaction replay.
261
+ if (!operation.skipped && !(transaction as RocksTransactionWithRetry).isRetry) {
262
+ operation.appendedAuditEntry = true;
263
+ }
231
264
  if (immediateCommit) {
232
265
  return this.commit({ transaction }); // immediately commit if the harper transaction is closed
233
266
  }
@@ -237,6 +270,7 @@ export class DatabaseTransaction implements Transaction {
237
270
  * Resolves with information on the timestamp and success of the commit
238
271
  */
239
272
  commit(options: CommitOptions = {}): MaybePromise<CommitResolution> {
273
+ if (this.timedOut) throw transactionOpenTooLongError();
240
274
  let transaction = options.transaction ?? this.transaction; // we need to preserve this transaction as we might to resurrect it if we have to retry
241
275
  for (let i = 0; i < this.writes.length; i++) {
242
276
  let operation = this.writes[i];
@@ -374,6 +408,32 @@ export class DatabaseTransaction implements Transaction {
374
408
  // for future transactions
375
409
  this.retries++;
376
410
  harperLogger.debug?.('retrying', transaction.id, this.retries);
411
+ if (error.code === 'ERR_TRY_AGAIN') {
412
+ // ERR_BUSY recovers on recommit: the save loop re-writes each key, re-tracking it at
413
+ // the current sequence, so validation passes once the contention clears. ERR_TRY_AGAIN
414
+ // never does: the memtable history validation needs is gone (flushed during a
415
+ // bulk-ingest burst), and recommitting re-checks the same stranded snapshot, so it
416
+ // fails forever even on an idle database. A source-apply transaction's uncapped retry
417
+ // then spins for good and wedges the replication apply loop at its commit await,
418
+ // freezing every leg of that database on the node. Replay onto a fresh transaction
419
+ // instead: the save loop reloads each entry through it and re-resolves against
420
+ // current state. Carry over the commit hook the transaction-log store attached
421
+ // (aftercommit emit / structure watermarks; it reads its state off the original
422
+ // transaction object, which abort() leaves intact); isRetry in save() keeps the log
423
+ // entries themselves from being re-added.
424
+ const retryTransaction: RocksTransactionWithRetry = new RocksTransaction(
425
+ (this.writes.find((write) => write)?.store.store ?? this.db.store) as RocksStore
426
+ );
427
+ if (this.timestamp) retryTransaction.setTimestamp(this.timestamp);
428
+ (retryTransaction as any).onCommit = (transaction as any).onCommit;
429
+ try {
430
+ transaction.abort();
431
+ } catch (abortError) {
432
+ // usually already released by the failed commit; log for the unexpected case
433
+ harperLogger.debug?.('aborting stranded transaction after failed commit', abortError);
434
+ }
435
+ transaction = retryTransaction;
436
+ }
377
437
  if (this.retries > 2) {
378
438
  // Transactions applying data from a canonical source of truth (replication peer or
379
439
  // external caching source) must never drop a write on a transient conflict: there is no
@@ -384,6 +444,13 @@ export class DatabaseTransaction implements Transaction {
384
444
  const neverDropOnConflict = this.sourceApply;
385
445
  if (this.retries > MAX_RETRIES) {
386
446
  if (!neverDropOnConflict) {
447
+ // giving up: release the current transaction (original or the fresh replay above)
448
+ // so the throw does not leak its native handle
449
+ try {
450
+ transaction.abort();
451
+ } catch (abortError) {
452
+ harperLogger.debug?.('aborting conflicted transaction after exhausting retries', abortError);
453
+ }
387
454
  throw new ServerError(
388
455
  `After ${MAX_RETRIES} retries, unable to commit transaction, transaction is in conflict with ongoing writes`
389
456
  );
@@ -445,6 +512,45 @@ export class DatabaseTransaction implements Transaction {
445
512
  this.writes = [];
446
513
  if (this.#context?.resourceCache) this.#context.resourceCache = null;
447
514
  }
515
+ /**
516
+ * True if this transaction — or any database in its multi-store `next` chain — has writes accumulated
517
+ * that have not yet been committed. Writes to a second database live on `next` (see txnForContext), so a
518
+ * transaction that reads database A (head, tracked via its read snapshot, empty `writes`) and writes
519
+ * database B (`next`) must still count as write-bearing, or the monitor would misclassify it as read-only
520
+ * and force-commit B's writes via the commit cascade (issue #1407, multi-store path).
521
+ */
522
+ hasPendingWrites(): boolean {
523
+ for (let txn: DatabaseTransaction = this; txn; txn = txn.next) {
524
+ if (txn.writes.some((write) => write)) return true;
525
+ }
526
+ return false;
527
+ }
528
+ /**
529
+ * Abort and poison this transaction because it exceeded the open-transaction limit. The next write or
530
+ * commit throws transactionOpenTooLongError so the request fails cleanly and rolls back, rather than the
531
+ * monitor force-committing a partial write set on the application's behalf (issue #1407). The whole
532
+ * multi-store `next` chain is poisoned and aborted: writes to a second database live on `next`, so
533
+ * leaving it un-poisoned would let the head's commit cascade force-commit them (or orphan its resources
534
+ * until it self-times-out).
535
+ */
536
+ abortDueToTimeout(): void {
537
+ // Poison every link first, then abort each, so a throw from one link's abort() can't leave later links
538
+ // in the chain un-poisoned (and thus eligible to be force-committed by a later commit cascade).
539
+ for (let txn: DatabaseTransaction = this; txn; txn = txn.next) {
540
+ txn.timedOut = true;
541
+ // Force the CLOSED path when releasing the read snapshot: doneReadTxn() flushes lingering writes via
542
+ // commit() while open === LINGERING, and commit() now throws transactionOpenTooLongError once poisoned.
543
+ // Closing first makes the release discard (abort) the uncommitted writes instead, which is the intent.
544
+ txn.open = TRANSACTION_STATE.CLOSED;
545
+ }
546
+ for (let txn: DatabaseTransaction = this; txn; txn = txn.next) {
547
+ try {
548
+ txn.abort();
549
+ } catch (error) {
550
+ harperLogger.debug?.(`Error aborting timed-out transaction in chain: ${error.message}`);
551
+ }
552
+ }
553
+ }
448
554
  directCommitSync(): void {
449
555
  trackedTxns.delete(this);
450
556
  this.transaction?.commitSync();
@@ -504,25 +610,43 @@ function startMonitoringTxns() {
504
610
  for (const txn of trackedTxns) {
505
611
  if (txn.timeout <= 0) {
506
612
  const url = (txn.getContext() as any)?.url;
507
- harperLogger.error(
508
- `Transaction was open too long and has been committed, from table: ${
509
- (txn.db as any)?.name + (url ? ' path: ' + url : '')
510
- }`,
511
- ...(txn.startedFrom ? [`was started from ${txn.startedFrom.resourceName}.${txn.startedFrom.method}`] : []),
512
- ...(DEBUG_LONG_TXNS ? ['starting stack trace', txn.stackTraces] : [])
513
- );
514
- // reset the transaction
515
- try {
516
- const result = txn.commit();
517
- if ((result as any)?.then) {
518
- (result as any).catch((error) => {
519
- harperLogger.debug?.(`Error committing timed out transaction: ${error.message}`);
520
- });
613
+ if (txn.hasPendingWrites() && !txn.sourceApply && !txn.isReplay) {
614
+ // Abort and surface an error rather than force-committing a partial write set: silently
615
+ // committing on the application's behalf breaks atomicity and can leave orphaned
616
+ // secondary-index entries that only a full index rebuild repairs (issue #1407). The app
617
+ // owns long-running work (split into smaller transactions); core owns consistency.
618
+ // Canonical-source applies (replication peer / external caching source) and crash-recovery
619
+ // replay are excluded: they have no resubscribe/resume path, so aborting a write would drop
620
+ // it while the resume cursor advances past it — a permanent divergence (harper-pro#348). For
621
+ // those, keep the prior force-commit behavior below.
622
+ harperLogger.error(
623
+ `Transaction was open too long and has been aborted after exceeding the open-transaction limit, from table: ${
624
+ (txn.db as any)?.name + (url ? ' path: ' + url : '')
625
+ }`,
626
+ ...(txn.startedFrom ? [`was started from ${txn.startedFrom.resourceName}.${txn.startedFrom.method}`] : []),
627
+ ...(DEBUG_LONG_TXNS ? ['starting stack trace', txn.stackTraces] : [])
628
+ );
629
+ try {
630
+ txn.abortDueToTimeout();
631
+ } catch (error) {
632
+ harperLogger.debug?.(`Error aborting timed out transaction: ${error.message}`);
633
+ }
634
+ } else {
635
+ // Read-only long transaction (no atomicity/index risk — e.g. a large scan or export), or a
636
+ // canonical-source apply/replay that must never drop a write: preserve the prior behavior of
637
+ // committing to close out the snapshot without poisoning the transaction.
638
+ try {
639
+ const result = txn.commit();
640
+ if ((result as any)?.then) {
641
+ (result as any).catch((error) => {
642
+ harperLogger.debug?.(`Error committing timed out transaction: ${error.message}`);
643
+ });
644
+ }
645
+ } catch (error) {
646
+ harperLogger.debug?.(`Error committing timed out transaction: ${error.message}`);
521
647
  }
522
- } catch (error) {
523
- harperLogger.debug?.(`Error committing timed out transaction: ${error.message}`);
648
+ txn.timeout = txnExpiration;
524
649
  }
525
- txn.timeout = txnExpiration;
526
650
  } else {
527
651
  txn.timeout -= txnExpiration;
528
652
  }
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  DatabaseTransaction,
3
+ transactionOpenTooLongError,
3
4
  type CommitOptions,
4
5
  type TransactionWrite,
5
6
  type CommitResolution,
@@ -83,6 +84,7 @@ export class LMDBTransaction extends DatabaseTransaction {
83
84
  }
84
85
 
85
86
  addWrite(operation: TransactionWrite): any {
87
+ if (this.timedOut) throw transactionOpenTooLongError();
86
88
  if (this.open === TRANSACTION_STATE.CLOSED) {
87
89
  throw new Error('Can not use a transaction that is no longer open');
88
90
  }
@@ -112,6 +114,7 @@ export class LMDBTransaction extends DatabaseTransaction {
112
114
  * Resolves with information on the timestamp and success of the commit
113
115
  */
114
116
  commit(options: CommitOptions = {}): any {
117
+ if (this.timedOut) throw transactionOpenTooLongError();
115
118
  options = options || {};
116
119
  let txnTime = this.timestamp;
117
120
  if (!txnTime) txnTime = this.timestamp = options.timestamp || getNextMonotonicTime();
@@ -347,14 +350,41 @@ function startMonitoringTxns() {
347
350
  for (const txn of trackedTxns) {
348
351
  if (txn.timeout <= 0) {
349
352
  const url = (txn.getContext() as any)?.url;
350
- harperLogger.error(
351
- `Transaction was open too long and has been committed, from table: ${
352
- (txn.db as any)?.name + (url ? ' path: ' + url : '')
353
- }`
354
- );
355
- // reset the transaction
356
- txn.commit();
357
- txn.timeout = txnExpiration;
353
+ if (txn.hasPendingWrites() && !txn.sourceApply && !txn.isReplay) {
354
+ // Abort and surface an error rather than force-committing a partial write set: silently
355
+ // committing on the application's behalf breaks atomicity and can leave orphaned
356
+ // secondary-index entries that only a full index rebuild repairs (issue #1407). The app
357
+ // owns long-running work (split into smaller transactions); core owns consistency.
358
+ // Canonical-source applies (replication peer / external caching source) and crash-recovery
359
+ // replay are excluded: they have no resubscribe/resume path, so aborting a write would drop
360
+ // it while the resume cursor advances past it — a permanent divergence (harper-pro#348). For
361
+ // those, keep the prior force-commit behavior below.
362
+ harperLogger.error(
363
+ `Transaction was open too long and has been aborted after exceeding the open-transaction limit, from table: ${
364
+ (txn.db as any)?.name + (url ? ' path: ' + url : '')
365
+ }`
366
+ );
367
+ try {
368
+ txn.abortDueToTimeout();
369
+ } catch (error) {
370
+ harperLogger.debug?.(`Error aborting timed out transaction: ${error.message}`);
371
+ }
372
+ } else {
373
+ // Read-only long transaction (no atomicity/index risk — e.g. a large scan or export), or a
374
+ // canonical-source apply/replay that must never drop a write: preserve the prior behavior of
375
+ // committing to close out the snapshot without poisoning the transaction.
376
+ try {
377
+ const result = txn.commit();
378
+ if ((result as any)?.then) {
379
+ (result as any).catch((error) => {
380
+ harperLogger.debug?.(`Error committing timed out transaction: ${error.message}`);
381
+ });
382
+ }
383
+ } catch (error) {
384
+ harperLogger.debug?.(`Error committing timed out transaction: ${error.message}`);
385
+ }
386
+ txn.timeout = txnExpiration;
387
+ }
358
388
  } else {
359
389
  txn.timeout -= txnExpiration;
360
390
  }
@@ -1946,6 +1946,16 @@ export function makeTable(options) {
1946
1946
  },
1947
1947
  before: writeToSource(),
1948
1948
  commit: (txnTime: number, existingEntry: Entry, retry: boolean, transaction: any) => {
1949
+ // Whether a prior attempt of THIS write appended its own audit entry (sticky, set in
1950
+ // save(); log entries are not part of the aborted rocks transaction, so they survive).
1951
+ // Only such a write can find its own orphaned entry in the dedup lookups below and must
1952
+ // not treat it as "already applied". Per-write on purpose: the transaction-wide retry
1953
+ // flag would also suppress dedup for a genuine re-delivered duplicate co-batched with
1954
+ // the conflicting write (double-applying it) and for fresh writes staged through a
1955
+ // reused transaction whose retries counter is stale. Sticky on purpose: a proxy read
1956
+ // from the last attempt's skipped state launders when a recommit round self-skips
1957
+ // (walk identity tie against its own staged record) before a fresh-transaction replay.
1958
+ const stagedOwnAuditEntry = retry && write.appendedAuditEntry === true;
1949
1959
  write.skipped = false; // reset on each retry; cleanup happens after commit if still true
1950
1960
  if (retry) {
1951
1961
  if (context && existingEntry?.version > (context.lastModified || 0))
@@ -2066,8 +2076,12 @@ export function makeTable(options) {
2066
2076
  // local audit time, not version, so this version-keyed lookup doesn't apply there (LMDB keeps the
2067
2077
  // exact unbounded walk). A miss (the keyed lookup can lag a back-to-back re-delivery — #1137)
2068
2078
  // simply falls through to the walk, so this never changes correctness; the additionalAuditRefs
2069
- // check above remains the read-your-writes guard.
2070
- if (isRocksDB && dedupVersionCouldBeRetained(txnTime)) {
2079
+ // check above remains the read-your-writes guard. Never when this write staged in a prior
2080
+ // failed attempt: that attempt already appended this write's own audit entry, so the lookup
2081
+ // would find it and skip the write as "already applied" when the record was never committed.
2082
+ // A recommit of the same transaction survived that skip only because the old write batch
2083
+ // still carried the put; a fresh-transaction replay (ERR_TRY_AGAIN) would drop the write.
2084
+ if (isRocksDB && !stagedOwnAuditEntry && dedupVersionCouldBeRetained(txnTime)) {
2071
2085
  const priorAudit = auditStore.get(txnTime, tableId, id, options?.nodeId);
2072
2086
  if (
2073
2087
  priorAudit &&
@@ -2126,7 +2140,11 @@ export function makeTable(options) {
2126
2140
  // A re-delivered write whose exact (version, nodeId) is already in the audit log was already
2127
2141
  // applied; drop it rather than re-applying it (double-applying commutative ops) or writing a
2128
2142
  // duplicate audit-only record. Used by the early-out and the depth-cap block below.
2143
+ // Never a duplicate for a write that staged in a prior failed attempt: that attempt already
2144
+ // appended this write's own audit entry, so the lookup would match it while the record was
2145
+ // never committed (see the up-front keyed dedup above).
2129
2146
  const isReDeliveredDuplicate = () => {
2147
+ if (stagedOwnAuditEntry) return false;
2130
2148
  if (!dedupVersionCouldBeRetained(txnTime)) return false; // pre-retention version — skip the end-of-log scan (best-effort; see above)
2131
2149
  const duplicate = auditStore.get(txnTime, tableId, id, options?.nodeId);
2132
2150
  return (
@@ -1,4 +1,4 @@
1
- import{a as e,t}from"./rolldown-runtime-CNC7AqOf.js";import{g as n,t as r}from"./button-i10JPNcp.js";import{C as i,S as a,_ as o,a as s,b as c,c as l,d as u,g as d,i as f,l as p,m,p as h,r as g,s as _,t as v,u as ee,v as y,w as b,x,y as S}from"./vendor-core-vIXn_E4D.js";import{C as te,I as ne,N as re,j as ie}from"./vendor-tanstack-Dl75hT33.js";import{a as ae}from"./vendor-datadog-DBn-aOxh.js";import{r as oe}from"./vendor-react-B36dp27u.js";import{zt as C}from"./vendor-ui-Bm4W8wqw.js";import{t as w}from"./createLucideIcon-B49W6uF4.js";import{l as se,t as ce}from"./react-SMksfEpE.js";import{a as le,i as ue,n as de,r as fe,t as pe}from"./x-Bru-nCti.js";import{_ as me,d as he,g as ge,h as _e,i as ve,l as ye,m as be,p as xe,r as Se,t as Ce,u as we}from"./setComponentFile-CHFvFq9u.js";import{n as Te}from"./queryClient-DK1L0t7F.js";import{n as Ee,t as De}from"./localStorageKeys-D_mgWLDC.js";import{$n as Oe,At as ke,Dt as Ae,Et as je,G as Me,Kn as Ne,Ot as Pe,Pn as Fe,R as Ie,Rt as Le,Tt as Re,Un as ze,Vn as Be,Wn as Ve,Yn as He,ar as Ue,c as We,d as Ge,ir as Ke,l as qe,lr as Je,mt as Ye,o as Xe,qt as Ze,rr as Qe,rt as $e,st as et,z as tt}from"./index-VFcpNWgq.js";import{t as nt}from"./useEntityRestURL-D0QTUqex.js";import{n as rt}from"./getAnalytics-CmXDoq4g.js";var it=w(`between-horizontal-start`,[[`rect`,{width:`13`,height:`7`,x:`8`,y:`3`,rx:`1`,key:`pkso9a`}],[`path`,{d:`m2 9 3 3-3 3`,key:`1agib5`}],[`rect`,{width:`13`,height:`7`,x:`8`,y:`14`,rx:`1`,key:`1q5fc1`}]]),at=w(`book`,[[`path`,{d:`M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20`,key:`k3hazp`}]]),ot=w(`chart-area`,[[`path`,{d:`M3 3v16a2 2 0 0 0 2 2h16`,key:`c24i48`}],[`path`,{d:`M7 11.207a.5.5 0 0 1 .146-.353l2-2a.5.5 0 0 1 .708 0l3.292 3.292a.5.5 0 0 0 .708 0l4.292-4.292a.5.5 0 0 1 .854.353V16a1 1 0 0 1-1 1H8a1 1 0 0 1-1-1z`,key:`q0gr47`}]]),st=w(`circle-x`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`m15 9-6 6`,key:`1uzhvr`}],[`path`,{d:`m9 9 6 6`,key:`z0biqf`}]]),ct=w(`file-pen`,[[`path`,{d:`M12.659 22H18a2 2 0 0 0 2-2V8a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 14 2H6a2 2 0 0 0-2 2v9.34`,key:`o6klzx`}],[`path`,{d:`M14 2v5a1 1 0 0 0 1 1h5`,key:`wfsgrz`}],[`path`,{d:`M10.378 12.622a1 1 0 0 1 3 3.003L8.36 20.637a2 2 0 0 1-.854.506l-2.867.837a.5.5 0 0 1-.62-.62l.836-2.869a2 2 0 0 1 .506-.853z`,key:`zhnas1`}]]),lt=w(`logs`,[[`path`,{d:`M3 5h1`,key:`1mv5vm`}],[`path`,{d:`M3 12h1`,key:`lp3yf2`}],[`path`,{d:`M3 19h1`,key:`w6f3n9`}],[`path`,{d:`M8 5h1`,key:`1nxr5w`}],[`path`,{d:`M8 12h1`,key:`1con00`}],[`path`,{d:`M8 19h1`,key:`k7p10e`}],[`path`,{d:`M13 5h8`,key:`a7qcls`}],[`path`,{d:`M13 12h8`,key:`h98zly`}],[`path`,{d:`M13 19h8`,key:`c3s6r1`}]]),ut=w(`message-square-heart`,[[`path`,{d:`M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z`,key:`18887p`}],[`path`,{d:`M7.5 9.5c0 .687.265 1.383.697 1.844l3.009 3.264a1.14 1.14 0 0 0 .407.314 1 1 0 0 0 .783-.004 1.14 1.14 0 0 0 .398-.31l3.008-3.264A2.77 2.77 0 0 0 16.5 9.5 2.5 2.5 0 0 0 12 8a2.5 2.5 0 0 0-4.5 1.5`,key:`1faxuh`}]]),dt=w(`send`,[[`path`,{d:`M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z`,key:`1ffxy3`}],[`path`,{d:`m21.854 2.147-10.94 10.939`,key:`12cjpa`}]]),ft=w(`wrench`,[[`path`,{d:`M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z`,key:`1ngwbx`}]]);async function pt(){await n.delete(`/Chat/Messages/`)}var T=e(ae(),1),E=oe();function mt({setMessages:e}){let[t,n]=(0,T.useState)(!1);return(0,E.jsxs)(`button`,{type:`button`,className:`clear-chat-button gap-1`,onClick:(0,T.useCallback)(async()=>{if(!t){n(!0);try{await pt(),e([])}catch(e){console.error(`Failed to clear chat:`,e)}finally{n(!1)}}},[t,e]),disabled:t,title:`Clear chat`,children:[t?(0,E.jsx)(de,{className:`animate-spin`,size:18}):(0,E.jsx)(Ve,{size:18}),`Clear`]})}async function ht(){let{data:e}=await n.get(`/Chat/Messages/`);return e}var gt=`vercel.ai.error`,_t=Symbol.for(gt),vt,yt,D=class e extends (yt=Error,vt=_t,yt){constructor({name:e,message:t,cause:n}){super(t),this[vt]=!0,this.name=e,this.cause=n}static isInstance(t){return e.hasMarker(t,gt)}static hasMarker(e,t){let n=Symbol.for(t);return typeof e==`object`&&!!e&&n in e&&typeof e[n]==`boolean`&&e[n]===!0}};function bt(e){return e==null?`unknown error`:typeof e==`string`?e:e instanceof Error?e.toString():JSON.stringify(e)}var xt=`AI_InvalidArgumentError`,St=`vercel.ai.error.${xt}`,Ct=Symbol.for(St),wt,Tt,Et=class extends (Tt=D,wt=Ct,Tt){constructor({message:e,cause:t,argument:n}){super({name:xt,message:e,cause:t}),this[wt]=!0,this.argument=n}static isInstance(e){return D.hasMarker(e,St)}},Dt=`AI_JSONParseError`,Ot=`vercel.ai.error.${Dt}`,kt=Symbol.for(Ot),At,jt,Mt=class extends (jt=D,At=kt,jt){constructor({text:e,cause:t}){super({name:Dt,message:`JSON parsing failed: Text: ${e}.
1
+ import{a as e,t}from"./rolldown-runtime-CNC7AqOf.js";import{g as n,t as r}from"./button-i10JPNcp.js";import{C as i,S as a,_ as o,a as s,b as c,c as l,d as u,g as d,i as f,l as p,m,p as h,r as g,s as _,t as v,u as ee,v as y,w as b,x,y as S}from"./vendor-core-vIXn_E4D.js";import{C as te,I as ne,N as re,j as ie}from"./vendor-tanstack-Dl75hT33.js";import{a as ae}from"./vendor-datadog-DBn-aOxh.js";import{r as oe}from"./vendor-react-B36dp27u.js";import{zt as C}from"./vendor-ui-Bm4W8wqw.js";import{t as w}from"./createLucideIcon-B49W6uF4.js";import{l as se,t as ce}from"./react-SMksfEpE.js";import{a as le,i as ue,n as de,r as fe,t as pe}from"./x-Bru-nCti.js";import{_ as me,d as he,g as ge,h as _e,i as ve,l as ye,m as be,p as xe,r as Se,t as Ce,u as we}from"./setComponentFile-BNTRdRN4.js";import{n as Te}from"./queryClient-DK1L0t7F.js";import{n as Ee,t as De}from"./localStorageKeys-D_mgWLDC.js";import{$n as Oe,At as ke,Dt as Ae,Et as je,G as Me,Kn as Ne,Ot as Pe,Pn as Fe,R as Ie,Rt as Le,Tt as Re,Un as ze,Vn as Be,Wn as Ve,Yn as He,ar as Ue,c as We,d as Ge,ir as Ke,l as qe,lr as Je,mt as Ye,o as Xe,qt as Ze,rr as Qe,rt as $e,st as et,z as tt}from"./index-DMgwFden.js";import{t as nt}from"./useEntityRestURL-CMTYmJCC.js";import{n as rt}from"./getAnalytics-CmXDoq4g.js";var it=w(`between-horizontal-start`,[[`rect`,{width:`13`,height:`7`,x:`8`,y:`3`,rx:`1`,key:`pkso9a`}],[`path`,{d:`m2 9 3 3-3 3`,key:`1agib5`}],[`rect`,{width:`13`,height:`7`,x:`8`,y:`14`,rx:`1`,key:`1q5fc1`}]]),at=w(`book`,[[`path`,{d:`M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20`,key:`k3hazp`}]]),ot=w(`chart-area`,[[`path`,{d:`M3 3v16a2 2 0 0 0 2 2h16`,key:`c24i48`}],[`path`,{d:`M7 11.207a.5.5 0 0 1 .146-.353l2-2a.5.5 0 0 1 .708 0l3.292 3.292a.5.5 0 0 0 .708 0l4.292-4.292a.5.5 0 0 1 .854.353V16a1 1 0 0 1-1 1H8a1 1 0 0 1-1-1z`,key:`q0gr47`}]]),st=w(`circle-x`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`m15 9-6 6`,key:`1uzhvr`}],[`path`,{d:`m9 9 6 6`,key:`z0biqf`}]]),ct=w(`file-pen`,[[`path`,{d:`M12.659 22H18a2 2 0 0 0 2-2V8a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 14 2H6a2 2 0 0 0-2 2v9.34`,key:`o6klzx`}],[`path`,{d:`M14 2v5a1 1 0 0 0 1 1h5`,key:`wfsgrz`}],[`path`,{d:`M10.378 12.622a1 1 0 0 1 3 3.003L8.36 20.637a2 2 0 0 1-.854.506l-2.867.837a.5.5 0 0 1-.62-.62l.836-2.869a2 2 0 0 1 .506-.853z`,key:`zhnas1`}]]),lt=w(`logs`,[[`path`,{d:`M3 5h1`,key:`1mv5vm`}],[`path`,{d:`M3 12h1`,key:`lp3yf2`}],[`path`,{d:`M3 19h1`,key:`w6f3n9`}],[`path`,{d:`M8 5h1`,key:`1nxr5w`}],[`path`,{d:`M8 12h1`,key:`1con00`}],[`path`,{d:`M8 19h1`,key:`k7p10e`}],[`path`,{d:`M13 5h8`,key:`a7qcls`}],[`path`,{d:`M13 12h8`,key:`h98zly`}],[`path`,{d:`M13 19h8`,key:`c3s6r1`}]]),ut=w(`message-square-heart`,[[`path`,{d:`M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z`,key:`18887p`}],[`path`,{d:`M7.5 9.5c0 .687.265 1.383.697 1.844l3.009 3.264a1.14 1.14 0 0 0 .407.314 1 1 0 0 0 .783-.004 1.14 1.14 0 0 0 .398-.31l3.008-3.264A2.77 2.77 0 0 0 16.5 9.5 2.5 2.5 0 0 0 12 8a2.5 2.5 0 0 0-4.5 1.5`,key:`1faxuh`}]]),dt=w(`send`,[[`path`,{d:`M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z`,key:`1ffxy3`}],[`path`,{d:`m21.854 2.147-10.94 10.939`,key:`12cjpa`}]]),ft=w(`wrench`,[[`path`,{d:`M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z`,key:`1ngwbx`}]]);async function pt(){await n.delete(`/Chat/Messages/`)}var T=e(ae(),1),E=oe();function mt({setMessages:e}){let[t,n]=(0,T.useState)(!1);return(0,E.jsxs)(`button`,{type:`button`,className:`clear-chat-button gap-1`,onClick:(0,T.useCallback)(async()=>{if(!t){n(!0);try{await pt(),e([])}catch(e){console.error(`Failed to clear chat:`,e)}finally{n(!1)}}},[t,e]),disabled:t,title:`Clear chat`,children:[t?(0,E.jsx)(de,{className:`animate-spin`,size:18}):(0,E.jsx)(Ve,{size:18}),`Clear`]})}async function ht(){let{data:e}=await n.get(`/Chat/Messages/`);return e}var gt=`vercel.ai.error`,_t=Symbol.for(gt),vt,yt,D=class e extends (yt=Error,vt=_t,yt){constructor({name:e,message:t,cause:n}){super(t),this[vt]=!0,this.name=e,this.cause=n}static isInstance(t){return e.hasMarker(t,gt)}static hasMarker(e,t){let n=Symbol.for(t);return typeof e==`object`&&!!e&&n in e&&typeof e[n]==`boolean`&&e[n]===!0}};function bt(e){return e==null?`unknown error`:typeof e==`string`?e:e instanceof Error?e.toString():JSON.stringify(e)}var xt=`AI_InvalidArgumentError`,St=`vercel.ai.error.${xt}`,Ct=Symbol.for(St),wt,Tt,Et=class extends (Tt=D,wt=Ct,Tt){constructor({message:e,cause:t,argument:n}){super({name:xt,message:e,cause:t}),this[wt]=!0,this.argument=n}static isInstance(e){return D.hasMarker(e,St)}},Dt=`AI_JSONParseError`,Ot=`vercel.ai.error.${Dt}`,kt=Symbol.for(Ot),At,jt,Mt=class extends (jt=D,At=kt,jt){constructor({text:e,cause:t}){super({name:Dt,message:`JSON parsing failed: Text: ${e}.
2
2
  Error message: ${bt(t)}`,cause:t}),this[At]=!0,this.text=e}static isInstance(e){return D.hasMarker(e,Ot)}},Nt=`AI_TypeValidationError`,Pt=`vercel.ai.error.${Nt}`,Ft=Symbol.for(Pt),It,Lt,O=class e extends (Lt=D,It=Ft,Lt){constructor({value:e,cause:t,context:n}){let r=`Type validation failed`;if(n?.field&&(r+=` for ${n.field}`),n?.entityName||n?.entityId){r+=` (`;let e=[];n.entityName&&e.push(n.entityName),n.entityId&&e.push(`id: "${n.entityId}"`),r+=e.join(`, `),r+=`)`}super({name:Nt,message:`${r}: Value: ${JSON.stringify(e)}.
3
3
  Error message: ${bt(t)}`,cause:t}),this[It]=!0,this.value=e,this.context=n}static isInstance(e){return D.hasMarker(e,Pt)}static wrap({value:t,cause:n,context:r}){return e.isInstance(n)&&n.value===t&&n.context?.field===r?.field&&n.context?.entityName===r?.entityName&&n.context?.entityId===r?.entityId?n:new e({value:t,cause:n,context:r})}},Rt=class extends Error{constructor(e,t){super(e),this.name=`ParseError`,this.type=t.type,this.field=t.field,this.value=t.value,this.line=t.line}},zt=10,Bt=13,k=32;function Vt(e){}function Ht(e){if(typeof e==`function`)throw TypeError("`config` must be an object, got a function instead. Did you mean `createParser({onEvent: fn})`?");let{onEvent:t=Vt,onError:n=Vt,onRetry:r=Vt,onComment:i,maxBufferSize:a}=e,o=[],s=0,c=!0,l,u=``,d=0,f,p=!1;function m(e){if(p)throw Error("Cannot feed parser: it was terminated after exceeding the configured max buffer size. Call `reset()` to resume parsing.");if(c&&(c=!1,e.charCodeAt(0)===239&&e.charCodeAt(1)===187&&e.charCodeAt(2)===191&&(e=e.slice(3))),o.length===0){let t=g(e);t!==``&&(o.push(t),s=t.length),h();return}if(e.indexOf(`
4
4
  `)===-1&&e.indexOf(`\r`)===-1){o.push(e),s+=e.length,h();return}o.push(e);let t=o.join(``);o.length=0,s=0;let n=g(t);n!==``&&(o.push(n),s=n.length),h()}function h(){a!==void 0&&(s+u.length<=a||(p=!0,o.length=0,s=0,l=void 0,u=``,d=0,f=void 0,n(new Rt(`Buffered data exceeded max buffer size of ${a} characters`,{type:`max-buffer-size-exceeded`}))))}function g(e){let n=0;if(e.indexOf(`\r`)===-1){let r=e.indexOf(`
@@ -1946,4 +1946,4 @@ jsResource:
1946
1946
  `,"using-blob-datatype":"---\nname: using-blob-datatype\ndescription: How to use the Blob data type for efficient binary storage in Harper.\nmetadata:\n mode: generate\n sources:\n - reference/v5/database/schema.md#Blob Type\n - reference/v5/database/api.md#Streaming\n - reference/v5/database/api.md#`BlobOptions`\n - reference/v5/database/api.md#Blob Coercion\n sourceCommit: f37a8c4021e20d5c74c1d339a6b6c8c196b5603e\n inputHash: 92e03eb0b830f335\n---\n\n# Using the Blob Data Type\n\nInstructions for the agent to follow when storing and retrieving large binary content using the `Blob` data type in Harper.\n\n## When to Use\n\nApply this rule when a schema field needs to store large binary content such as images, video, audio, or large HTML — typically content larger than 20KB. Use `Blob` instead of `Bytes` when streaming support and out-of-record storage are required. See [handling-binary-data.md](handling-binary-data.md) for broader binary data guidance.\n\n## How It Works\n\n1. **Declare a `Blob` field in your schema**: Add a field typed as `Blob` to your `@table` type.\n\n ```graphql\n type MyTable @table {\n id: Any! @primaryKey\n data: Blob\n }\n ```\n\n2. **Create and store a blob with `createBlob()`**: Pass a buffer or stream to `createBlob()`, then `put` the record.\n\n ```javascript\n let blob = createBlob(largeBuffer);\n await MyTable.put({ id: 'my-record', data: blob });\n ```\n\n3. **Retrieve blob data using standard Web API methods**: The `Blob` type implements the Web API `Blob` interface. Use `.bytes()`, `.text()`, `.arrayBuffer()`, `.stream()`, or `.slice()` as needed.\n\n ```javascript\n let record = await MyTable.get('my-record');\n let buffer = await record.data.bytes(); // ArrayBuffer\n let text = await record.data.text(); // string\n let stream = record.data.stream(); // ReadableStream\n ```\n\n4. **Use `saveBeforeCommit` when full write must precede commit**: By default, `Blob` is not ACID-compliant — a record can reference a blob before it is fully written. Set `saveBeforeCommit: true` to block the transaction until the blob is fully saved.\n\n ```javascript\n let blob = createBlob(stream, { saveBeforeCommit: true });\n await MyTable.put({ id: 'my-record', data: blob });\n // put() resolves only after blob is fully written and record is committed\n ```\n\n5. **Register an error handler when returning a blob via REST**: Interrupted streams must be handled explicitly.\n\n ```javascript\n export class MyEndpoint extends MyTable {\n static async get(target) {\n const record = super.get(target);\n let blob = record.data;\n blob.on('error', () => {\n MyTable.invalidate(target);\n });\n return { status: 200, headers: {}, body: blob };\n }\n }\n ```\n\n6. **Rely on automatic coercion where applicable**: When a field is typed as `Blob` in the schema, any string or buffer assigned via `put`, `patch`, or `publish` is automatically coerced to a `Blob` — no manual `createBlob()` call is needed in those cases.\n\n### `BlobOptions` reference\n\nPass an options object as the second argument to `createBlob()`.\n\n| Option | Type | Default | Description |\n| ------------------ | --------- | ----------- | ------------------------------------------------------------------------------------------------------------------------ |\n| `type` | `string` | `undefined` | MIME type to associate with the blob (e.g., `image/jpeg`). Readable via `blob.type` and used when serving HTTP. |\n| `size` | `number` | `undefined` | Size of the data in bytes, if known ahead of time. Otherwise inferred from a buffer or determined as a stream completes. |\n| `saveBeforeCommit` | `boolean` | `false` | Wait until the blob is fully written before the transaction commits. |\n| `compress` | `boolean` | `false` | Compress the stored data with deflate. |\n| `flush` | `boolean` | `false` | Flush the file to disk after writing, before the `createBlob` promise chain resolves. |\n\n## Examples\n\n**Store an image with a MIME type:**\n\n```javascript\nlet blob = createBlob(imageBuffer, { type: 'image/jpeg' });\nawait Photo.put({ id, data: blob });\n```\n\n**Stream a blob in as it streams out (low-latency passthrough):**\n\n```javascript\nlet blob = createBlob(incomingStream);\n// blob exists, but data is still streaming to storage\nawait MyTable.put({ id: 'my-record', data: blob });\n\nlet record = await MyTable.get('my-record');\n// blob data is accessible as it arrives\nlet outgoingStream = record.data.stream();\n```\n\n**Guarantee full write before commit using `saveBeforeCommit`:**\n\n```javascript\nlet blob = createBlob(stream, { saveBeforeCommit: true });\nawait MyTable.put({ id: 'my-record', data: blob });\n```\n\n## Notes\n\n- `Blob` stores data separately from the record. If you need the binary data to be a true, ACID-committed part of the record, use a `Bytes` field instead.\n- All standard Web API `Blob` methods — `.text()`, `.arrayBuffer()`, `.stream()`, `.slice()`, and `.bytes()` — are available on retrieved blob fields.\n- Without `saveBeforeCommit: true`, blobs are **not** ACID-compliant by default; a record can reference a blob before it is fully written to storage.\n","vector-indexing":'---\nname: vector-indexing\ndescription: How to enable and query vector indexes for similarity search in Harper.\nmetadata:\n mode: generate\n sources:\n - reference/v5/database/schema.md#Vector Indexing\n sourceCommit: 4fe4c9c95e0974eaa77032f6f10e36fbd8ec64ac\n inputHash: d90b1b74597d08a6\n---\n\n# Vector Indexing\n\nInstructions for the agent to enable HNSW vector indexes on table fields and query them for similarity search in Harper.\n\n## When to Use\n\nApply this rule when adding a vector similarity search capability to a Harper table — for example, storing text embeddings and querying for nearest neighbors, filtering by distance threshold, or tuning index construction and search parameters. Use it alongside [adding-tables-with-schemas.md](adding-tables-with-schemas.md) when defining the schema that hosts the vector field.\n\n## How It Works\n\n1. **Declare the vector index on a field**: Add `@indexed(type: "HNSW")` to a `[Float]` field inside a `@table` type. This creates an HNSW (Hierarchical Navigable Small World) index for approximate nearest-neighbor search.\n\n ```graphql\n type Document @table {\n id: Long @primaryKey\n textEmbeddings: [Float] @indexed(type: "HNSW")\n }\n ```\n\n2. **Query by nearest neighbors using `sort`**: Call `.search()` with a `sort` descriptor that specifies the indexed `attribute` and a `target` vector. Use `limit` to cap results.\n\n ```javascript\n let results = Document.search({\n sort: { attribute: \'textEmbeddings\', target: searchVector },\n limit: 5,\n });\n ```\n\n3. **Combine with filter conditions**: Add a `conditions` array alongside `sort` to pre-filter records before ranking by similarity.\n\n ```javascript\n let results = Document.search({\n conditions: [{ attribute: \'price\', comparator: \'lt\', value: 50 }],\n sort: { attribute: \'textEmbeddings\', target: searchVector },\n limit: 5,\n });\n ```\n\n4. **Filter by distance threshold**: To return only records within a similarity cutoff (without ranking), place `target` directly on the condition alongside `comparator` and `value`. This bounds result quality rather than ranking by similarity.\n\n ```javascript\n let results = Document.search({\n conditions: {\n attribute: \'textEmbeddings\',\n comparator: \'lt\',\n value: 0.1,\n target: searchVector,\n },\n });\n ```\n\n5. **Include computed distance in results**: Use the special `$distance` field in `select` to return the distance from the target vector. Available in both `sort`-based and threshold-based queries.\n\n ```javascript\n let results = Document.search({\n select: [\'name\', \'$distance\'],\n sort: { attribute: \'textEmbeddings\', target: searchVector },\n limit: 5,\n });\n ```\n\n6. **Tune per-query search options**: Pass `distance` and `ef` directly on the `sort` descriptor to override index defaults for a single query.\n\n ```javascript\n let results = Document.search({\n sort: { attribute: \'textEmbeddings\', target: searchVector, distance: \'dotProduct\', ef: 200 },\n limit: 5,\n });\n ```\n\n - `distance` — overrides the distance function for this query: `"cosine"`, `"euclidean"`, or `"dotProduct"`.\n - `ef` — overrides the search exploration budget. Higher values improve recall at the cost of latency.\n\n7. **Configure HNSW index parameters**: Pass parameters directly in the `@indexed` directive. Structural parameters (`distance`, `M`, `efConstruction`, `quantization`) trigger an index rebuild when changed; `efConstructionSearch` does not.\n\n ```graphql\n type Document @table {\n id: Long @primaryKey\n textEmbeddings: [Float]\n @indexed(type: "HNSW", distance: "euclidean", optimizeRouting: 0, efConstructionSearch: 100)\n }\n ```\n\n8. **Enable vector quantization**: Use `quantization: "int8"` to store vectors as 8-bit integers, reducing index size and memory usage. Harper re-ranks nearest-neighbor `sort` results against full-precision vectors automatically.\n\n ```graphql\n type Document @table {\n id: Long @primaryKey\n textEmbeddings: [Float] @indexed(type: "HNSW", quantization: "int8")\n }\n ```\n\n## Examples\n\nFull schema with custom HNSW parameters and a nearest-neighbor query with distance output:\n\n```graphql\ntype Document @table {\n id: Long @primaryKey\n textEmbeddings: [Float]\n @indexed(type: "HNSW", distance: "euclidean", optimizeRouting: 0, efConstructionSearch: 100)\n}\n```\n\n```javascript\n// Nearest-neighbor search with distance scores\nlet results = Document.search({\n select: [\'name\', \'$distance\'],\n sort: { attribute: \'textEmbeddings\', target: searchVector },\n limit: 5,\n});\n\n// Distance-threshold query (no ranking)\nlet closeMatches = Document.search({\n conditions: {\n attribute: \'textEmbeddings\',\n comparator: \'lt\',\n value: 0.1,\n target: searchVector,\n },\n});\n```\n\n## Notes\n\n### HNSW Parameters\n\n| Parameter | Default | Description |\n| ---------------------- | ----------------- | ------------------------------------------------------------------------------------------------------ |\n| `distance` | `"cosine"` | Distance function: `"cosine"`, `"euclidean"`, or `"dotProduct"` |\n| `efConstruction` | `100` | Max nodes explored during index construction. Higher = better recall, lower = better performance |\n| `M` | `16` | Preferred connections per graph layer. Higher = more space, better recall for high-dimensional data |\n| `optimizeRouting` | `0.5` | Heuristic aggressiveness for omitting redundant connections (0 = off, 1 = most aggressive) |\n| `mL` | computed from `M` | Normalization factor for level generation |\n| `efConstructionSearch` | auto-scaled | Max nodes explored during search. When unset, auto-scales with index size; setting it fixes the budget |\n| `quantization` | — | `"int8"` stores vectors quantized to int8 |\n\n- The `distance` option on a per-query `sort` descriptor accepts `"cosine"`, `"euclidean"`, or `"dotProduct"`.\n- When no `ef` is passed and `efConstructionSearch` (or `efConstruction`) is not explicitly set on the index, the search budget auto-scales with index size.\n- `efConstruction` seeds the initial value of `efConstructionSearch`; setting either one fixes the search budget.\n- The correct parameter name is `efConstructionSearch` (not `efSearchConstruction`).\n- `$distance` is available in both `sort`-based ranking and `conditions`-based threshold queries.\n- For `quantization: "int8"`, distance-threshold (`lt`/`le`) queries filter on approximate distance; `sort` queries re-rank against full-precision vectors.\n'},oa={name:`readHarperSkill`,description:`Returns documentation for a Harper skill or best practice. Skills provide guidance on developing Harper applications.`,inputSchema:o({skill:g(ia)})};async function sa({input:{skill:e}}){return{success:!!aa[e],message:aa[e]||`No skill found with the name ${e}`}}var ca={...oa,icon:at,execute:sa},la={name:`readLogs`,description:`Returns the matching logs from the server.`,inputSchema:o({log_name:g([`hdb.log`,`system.log`]).default(`hdb.log`),limit:c().or(s()).optional(),level:g([`notify`,`error`,`warn`,`info`,`debug`,`trace`,`undefined`]).or(s()).optional(),from:c().or(s()).optional(),until:c().or(s()).optional()})};async function ua({input:e,instanceClientParams:t}){try{return{success:!0,data:await Xe({...t,logFilters:e,replicated:t.entityType===`cluster`})}}catch(e){return{success:!1,message:`Error: ${e}`}}}var da={...la,icon:lt,execute:ua},fa={name:`readTableRecords`,description:`Retrieves some or all table records from a database on the server.`,inputSchema:o({database:c().trim(),table:c().trim(),pageIndex:d().default(0),pageSize:d().default(10),primaryKey:c(),conditions:l(o({search_attribute:c(),search_type:g([`between`,`eq`,`equals`,`greater_than`,`greater_than_equal`,`less_than`,`less_than_equal`,`ne`,`not_equal`,`starts_with`]),search_value:_()})),sort:o({attribute:c(),descending:p()})})};async function pa({input:{database:e,table:t,conditions:n,primaryKey:r,...i},instanceClientParams:a}){try{if(!n.length){let{data:n}=await je({...a,databaseName:e,tableName:t,onlyIfCached:!0,searchAttribute:r,...i});return{success:!0,data:n}}let{data:o}=await tt({...a,databaseName:e,tableName:t,onlyIfCached:!0,conditions:n,...i});return{success:!0,data:o}}catch(e){return{success:!1,message:`Error: ${e}`}}}var ma={...fa,icon:He,execute:pa},ha={name:`restartHTTPService`,description:`Restarts the HTTP service on the server to allow schema and resource changes to be applied.`,inputSchema:o({})};async function ga({instanceClientParams:e,baseURL:t}){let n=C.loading(`Restarting HTTP service...`,{description:`This may take a bit.`,duration:3e5});try{await Ye({...e,operation:`restart_service`,replicated:e.entityType===`cluster`})}catch(e){return{success:!1,message:`Error: ${e}`}}return C.success(`Done!`,{description:`HTTP Service restarted!`,id:n,duration:5e3}),{success:!0,message:`HTTP Service restarted!`,webURL:t}}var _a={...ha,icon:de,execute:ga,requiresApproval:!0},va={name:`setComponentFile`,description:`Returns the contents of a component file by its full path (which was returned by getComponents)`,inputSchema:o({path:c().trim(),payload:c(),encoding:g([`utf8`,`ASCII`,`binary`,`hex`,`base64`,`utf16le`,`latin1`,`ucs2`])})};async function ya({input:{path:e,encoding:t,payload:n},instanceClientParams:r}){try{let i=e.split(`/`),a=i.shift(),o=i.join(`/`),s=await Ce({...r,file:o,project:a,payload:n,encoding:t});return await Te.invalidateQueries({queryKey:[r.entityId,`get_component_file`,a,o]}),ke(`ReloadApplicationRootEntries`,!0),{success:!0,data:s}}catch(e){return{success:!1,message:`Error: ${e}`}}}var ba={...va,icon:ct,execute:ya,requiresApproval:!0},xa={name:`updateTableRecords`,description:`Updates records in a particular table in a particular database on the server.`,inputSchema:o({database:c().trim(),table:c().trim(),records:l(_())})};async function Sa({input:{database:e,table:t,records:n},instanceClientParams:r,params:i}){try{let a=await We({...r,databaseName:e,tableName:t,records:n}),{databaseName:o,tableName:s}=i;return await Te.invalidateQueries({queryKey:[r.entityId,o,s]}),{success:!0,data:a}}catch(e){return{success:!1,message:`Error: ${e}`}}}var Ca={readHarperSkill:ca,createApp:Ei,readLogs:da,getAnalytics:Fi,listAnalyticsMetrics:ra,restartHTTPService:_a,collectFeedback:Ci,getUserContext:Xi,getComponentFile:Ri,getComponents:Vi,setComponentFile:ba,dropComponentFile:Mi,getDescribeAll:Wi,getDescribeTable:qi,insertTableRecords:$i,readTableRecords:ma,updateTableRecords:{...xa,icon:it,execute:Sa,requiresApproval:!0},deleteTableRecords:ki};function wa(e){return Ca[e]}function Ta(e){return e.state===`input-available`&&!!wa(Zr(e))?.requiresApproval}function Ea(e){let t=[];for(let[n,r]of(e??[]).entries()){if(G(r)){if(Ta(r)){t.push({kind:`part`,part:r,index:n});continue}let e=t.at(-1);e?.kind===`tool-group`?e.parts.push(r):t.push({kind:`tool-group`,parts:[r],index:n});continue}qr(r)&&r.text.length>0&&t.push({kind:`part`,part:r,index:n})}return t}function Da({part:e,onApprove:t,onDeny:n,onAlwaysApprove:i,isApproving:a}){let[o,s]=(0,T.useState)(!1),[c,l]=(0,T.useState)(!1),u=Zr(e),d=wa(u),f=d?.icon||Ke,p=d?.requiresApproval,m=(0,T.useMemo)(()=>!e.input||typeof e.input==`object`&&Object.keys(e.input).length===0,[e.input]),h=(0,T.useMemo)(()=>{let t=JSON.stringify(e.input,null,` `);return{json:t,lines:t?t.split(`
1947
1947
  `).length:0}},[e.input]),g=(0,T.useMemo)(()=>{let t=JSON.stringify(e.output,null,` `);return{json:t,lines:t?t.split(`
1948
1948
  `).length:0}},[e.output]);return(0,E.jsxs)(`div`,{className:`tool-invocation ${e.state}`,children:[(0,E.jsxs)(`div`,{className:`tool-info`,children:[(0,E.jsxs)(`div`,{className:`tool-name`,children:[(0,E.jsx)(f,{size:14}),(0,E.jsx)(`span`,{children:u})]}),(0,E.jsxs)(`div`,{className:`tool-status`,children:[e.state===`input-streaming`&&(0,E.jsx)(`span`,{children:`Thinking...`}),e.state===`input-available`&&(0,E.jsx)(`span`,{children:a?`Executing...`:p?`Awaiting Approval...`:`Executing...`}),e.state===`output-available`&&(e.output?.error?(0,E.jsx)(st,{size:14,className:`text-destructive`}):(0,E.jsx)(le,{size:14}))]})]}),e.state!==`input-streaming`&&(0,E.jsxs)(`div`,{className:`tool-io`,children:[!m&&(0,E.jsxs)(`div`,{className:`tool-args`,children:[(0,E.jsxs)(`div`,{className:`flex items-center justify-between gap-2 mb-1`,children:[(0,E.jsx)(`strong`,{children:`Input:`}),h.lines>3&&(0,E.jsx)(r,{type:`button`,variant:`ghost`,size:`sm`,className:`h-6 px-2 text-[10px] uppercase tracking-wider text-muted-foreground hover:text-foreground`,onClick:()=>s(!o),children:o?(0,E.jsxs)(E.Fragment,{children:[(0,E.jsx)(fe,{size:12}),`Hide`]}):(0,E.jsxs)(E.Fragment,{children:[(0,E.jsx)(ue,{size:12}),`Show`]})})]}),(0,E.jsx)(`div`,{className:o?`whitespace-pre-wrap`:`line-clamp-3 overflow-hidden whitespace-pre-wrap`,children:h.json})]}),e.state===`input-available`&&p&&(0,E.jsxs)(`div`,{className:`flex gap-2 mt-3 pt-3 border-t`,children:[(0,E.jsxs)(r,{size:`sm`,className:`h-8 text-xs bg-green-600 hover:bg-green-700 text-white`,onClick:()=>t?.(e.toolCallId),disabled:a,children:[a?(0,E.jsx)(Qe,{className:`mr-2 h-3 w-3 animate-spin`}):null,`Approve`]}),(0,E.jsx)(r,{type:`button`,size:`sm`,variant:`outline`,className:`h-8 text-xs approval-outline`,onClick:()=>i?.(e.toolCallId),disabled:a,children:`Always Approve`}),(0,E.jsx)(r,{type:`button`,size:`sm`,variant:`outline`,className:`h-8 text-xs approval-outline`,onClick:()=>n?.(e.toolCallId),disabled:a,children:`Deny`})]}),e.state===`output-available`&&(0,E.jsx)(E.Fragment,{children:d?.render?d.render(e):(0,E.jsxs)(`div`,{className:`tool-result`,children:[(0,E.jsxs)(`div`,{className:`flex items-center justify-between gap-2 mb-1`,children:[(0,E.jsx)(`strong`,{children:`Result:`}),g.lines>3&&(0,E.jsx)(r,{type:`button`,variant:`ghost`,size:`sm`,className:`h-6 px-2 text-[10px] uppercase tracking-wider text-muted-foreground hover:text-foreground`,onClick:()=>l(!c),children:c?(0,E.jsxs)(E.Fragment,{children:[(0,E.jsx)(fe,{size:12}),`Hide`]}):(0,E.jsxs)(E.Fragment,{children:[(0,E.jsx)(ue,{size:12}),`Show`]})})]}),(0,E.jsx)(`div`,{className:c?`whitespace-pre-wrap`:`line-clamp-3 overflow-hidden whitespace-pre-wrap`,children:g.json})]})})]})]})}function Oa({parts:e,onApprove:t,onDeny:n,onAlwaysApprove:r,approvingToolCallIds:i}){let[a,o]=(0,T.useState)(!1),s=e.some(e=>e.state!==`output-available`&&e.state!==`output-error`),c=e.some(e=>e.state===`output-error`||e.state===`output-available`&&e.output?.error),l=e.length===1?Zr(e[0]):void 0,u=l&&wa(l)?.icon||ft,d=l??`${e.length} tools`;return(0,E.jsxs)(`div`,{className:`tool-group`,children:[(0,E.jsxs)(`button`,{type:`button`,className:`tool-group-summary`,"aria-expanded":a,onClick:()=>o(!a),children:[a?(0,E.jsx)(ue,{size:14}):(0,E.jsx)(Je,{size:14}),(0,E.jsx)(u,{size:14}),(0,E.jsx)(`span`,{children:s?`Using ${d}...`:`Used ${d}`}),(0,E.jsx)(`span`,{className:`tool-group-status`,children:s?(0,E.jsx)(Qe,{size:14,className:`animate-spin`}):c?(0,E.jsx)(st,{size:14,className:`text-destructive`}):(0,E.jsx)(le,{size:14})})]}),a&&e.map(e=>(0,E.jsx)(Da,{part:e,onApprove:t,onDeny:n,onAlwaysApprove:r,isApproving:i?.has(e.toolCallId)},e.toolCallId))]})}function ka({message:e,onApprove:t,onDeny:n,onAlwaysApprove:r,approvingToolCallIds:i}){return e.parts?.some(e=>qr(e)&&e.text.length>0||G(e))?(0,E.jsxs)(ce.div,{initial:{opacity:0,y:10},animate:{opacity:1,y:0},className:`message-bubble ${e.role===`user`?`user`:`assistant`}`,children:[(0,E.jsx)(`div`,{className:`avatar`,children:e.role===`user`?(0,E.jsx)(Be,{size:18}):(0,E.jsx)(se,{size:18})}),(0,E.jsx)(`div`,{className:`content`,children:Ea(e.parts).map(e=>{if(e.kind===`tool-group`)return(0,E.jsx)(Oa,{parts:e.parts,onApprove:t,onDeny:n,onAlwaysApprove:r,approvingToolCallIds:i},e.parts[0].toolCallId);let{part:a,index:o}=e;return qr(a)?(0,E.jsx)(`div`,{className:`text-block`,children:a.text},o):G(a)?(0,E.jsx)(Da,{part:a,onApprove:t,onDeny:n,onAlwaysApprove:r,isApproving:i?.has(a.toolCallId)},o):null})})]},e.id):null}function Aa(e,t){if(e!==`submitted`&&e!==`streaming`)return!1;if(t?.role!==`assistant`)return!0;let n=t.parts?.at(-1);return n?qr(n)?n.state!==`streaming`||n.text.length===0:!G(n)||n.state===`output-available`||n.state===`output-error`:!0}function ja(){return(0,E.jsxs)(ce.div,{initial:{opacity:0,y:10},animate:{opacity:1,y:0},transition:{delay:.2},className:`message-bubble assistant`,children:[(0,E.jsx)(`div`,{className:`avatar`,children:(0,E.jsx)(se,{size:18})}),(0,E.jsxs)(`div`,{className:`content thinking-indicator`,role:`status`,"aria-label":`Harper Agent is thinking`,children:[(0,E.jsx)(`span`,{className:`thinking-dot`}),(0,E.jsx)(`span`,{className:`thinking-dot`}),(0,E.jsx)(`span`,{className:`thinking-dot`})]})]})}function Ma(e){return ie({queryKey:[`getMyUsage`,e],queryFn:async()=>{let{data:t}=await n.get(`/Chat/Usage/${e}`);return t}})}function Na(){let{organizationId:e}=te({strict:!1});return re(Ma(e))}function Pa(){let{data:e,isLoading:t,error:n}=Na();if(t||n||!e)return null;let{usageUSD:r,monthlyLimitUSD:i,usageBarPercent:a}=e,o=e=>new Intl.NumberFormat(`en-US`,{style:`currency`,currency:`USD`}).format(e);return(0,E.jsxs)(`div`,{className:`usage-container`,children:[(0,E.jsxs)(`div`,{className:`usage-info`,children:[(0,E.jsx)(`span`,{children:`Monthly Org Usage`}),(0,E.jsxs)(`span`,{children:[o(r),` / `,o(i)]}),(0,E.jsxs)(`span`,{children:[Math.round(a),`%`]})]}),(0,E.jsx)(`div`,{className:`usage-bar-bg`,children:(0,E.jsx)(`div`,{className:`usage-bar-fill`,style:{width:`${a}%`}})})]})}function Fa({autoFocus:e,closeChat:t}){let n=te({strict:!1}),{organizationId:r}=n,[i,a]=Ae(`ApplicationChat`,``),[o,s]=(0,T.useState)(!0),[c,l]=(0,T.useState)({}),[u,d]=(0,T.useState)(new Set),[f,p]=Ee(De.ChatAlwaysApprovedTools,[]),m=new Set(f),h=nt(),g=Fe(),_=ne(),{messages:v,sendMessage:ee,status:y,addToolOutput:b,setMessages:x}=vi({transport:si(r),generateId:A(),sendAutomaticallyWhen:oi,onFinish(){_.invalidateQueries({queryKey:[`getMyUsage`]})},async onToolCall({toolCall:e}){if(e.dynamic)return;let t=wa(e.toolName);if(t){if(t.requiresApproval&&!m.has(e.toolName)){let t={type:`tool-call`,toolCallId:e.toolCallId,toolName:e.toolName,input:e.input};l(n=>({...n,[e.toolCallId]:t}));return}let r=await t.execute({input:e.input,instanceClientParams:g,baseURL:h,params:n});b({tool:e.toolName,toolCallId:e.toolCallId,output:r})}}}),S=(0,T.useCallback)(async e=>{let t=c[e];if(t){d(t=>{let n=new Set(t);return n.add(e),n});try{let r=wa(t.toolName);if(r){let i=await r.execute({input:t.input,instanceClientParams:g,baseURL:h,params:n});b({tool:t.toolName,toolCallId:t.toolCallId,output:i}),l(t=>{let n={...t};return delete n[e],n})}}finally{d(t=>{let n=new Set(t);return n.delete(e),n})}}},[c,g,h,b,n]),re=(0,T.useCallback)(e=>{let t=c[e];t&&(b({tool:t.toolName,toolCallId:t.toolCallId,output:{error:`User denied the tool execution.`}}),l(t=>{let n={...t};return delete n[e],n}))},[c,b]),ie=(0,T.useCallback)(async e=>{let t=c[e];t&&(p(e=>Re([...e,t.toolName])),await S(e))},[c,p,S]);(0,T.useEffect)(()=>{(async()=>{try{let e=await ht();Array.isArray(e)&&x(e)}catch(e){console.error(`Failed to fetch initial messages:`,e)}finally{s(!1)}})()},[x]);let ae=y===`streaming`||y===`submitted`,oe=(0,T.useRef)(null);return(0,T.useEffect)(()=>{oe.current?.scrollIntoView({behavior:`smooth`})},[v]),(0,E.jsxs)(`div`,{className:`flex flex-col h-full`,children:[(0,E.jsxs)(`div`,{className:`flex items-start justify-between gap-6 px-4 py-2.5 border-b border-border bg-card`,children:[(0,E.jsxs)(`div`,{className:`flex flex-col gap-1 min-w-0 flex-1`,children:[(0,E.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,E.jsx)(se,{className:`text-primary`,size:20}),(0,E.jsx)(`span`,{className:`font-semibold text-foreground`,children:`Harper Agent`})]}),(0,E.jsx)(Pa,{})]}),(0,E.jsxs)(`div`,{className:`flex items-center gap-2 shrink-0`,children:[(0,E.jsx)(mt,{setMessages:x}),(0,E.jsx)(`button`,{onClick:t,className:`p-1 hover:bg-accent rounded-md transition-colors text-muted-foreground hover:text-foreground`,title:`Close chat`,children:(0,E.jsx)(pe,{size:20})})]})]}),(0,E.jsx)(`div`,{className:`flex-1 overflow-hidden`,children:(0,E.jsxs)(`div`,{className:`chat-interface h-full w-full`,children:[(0,E.jsxs)(`div`,{className:`messages-area`,children:[o&&(0,E.jsx)(bi,{}),!o&&v.length===0&&(0,E.jsxs)(`div`,{className:`empty-state`,children:[(0,E.jsx)(se,{size:48}),(0,E.jsx)(`p`,{children:`Ask me to create a Harper app!`})]}),v.map(e=>(0,E.jsx)(ka,{message:e,onApprove:S,onDeny:re,onAlwaysApprove:ie,approvingToolCallIds:u},e.id)),Aa(y,v.at(-1))&&(0,E.jsx)(ja,{}),(0,E.jsx)(`div`,{ref:oe})]}),(0,E.jsx)(yi,{input:i,setInput:a,onSubmit:e=>{e.preventDefault(),i.trim()&&!ae&&!o&&(ee({text:i}),a(``))},disabled:o,autoFocus:e})]})})]})}export{Fa as Chat};
1949
- //# sourceMappingURL=Chat-SdD1EZca.js.map
1949
+ //# sourceMappingURL=Chat-BK_fyjsV.js.map