wontopos 2.2.13 → 2.2.14

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.
@@ -22,9 +22,10 @@
22
22
  * Pure semantic retrieval — no keyword/BM25 matching, so it works the SAME in every
23
23
  * language. Storing and searching call no LLM; you only pay for embeddings.
24
24
  *
25
- * Reliability: every call retries transient failures (429 / 502 / 503 and
26
- * connection errors) with exponential backoff + jitter, honoring `Retry-After`.
27
- * Tune with `maxRetries` (0 disables) and `timeoutMs`.
25
+ * Reliability: every call retries transient failures with exponential backoff +
26
+ * jitter, honoring `Retry-After` — 429 always; 502/503 and connection errors only
27
+ * when a retry can never double-process a write (idempotent calls, or a failure
28
+ * at connect time). Tune with `maxRetries` (0 disables) and `timeoutMs`.
28
29
  *
29
30
  * Security posture (not configurable off): redirects are refused so the API key
30
31
  * can never follow one to another host; responses over 64MB are refused; the key
@@ -226,8 +227,9 @@ export interface ClientOptions {
226
227
  /** Default store for every call. Override per call by passing `userId`. Defaults
227
228
  * to the account's built-in `default` store. */
228
229
  userId?: string;
229
- /** How many times to retry transient failures (429/502/503 and connection
230
- * errors) before throwing. 0 disables retries (default 2). */
230
+ /** How many times to retry transient failures before throwing — 429 always;
231
+ * 502/503 and connection errors only when a retry can never double-process a
232
+ * write. 0 disables retries (default 2). */
231
233
  maxRetries?: number;
232
234
  }
233
235
  /** Wontopos memory client. Store and recall memories, isolated per `userId`. */
package/dist/wontopos.js CHANGED
@@ -22,9 +22,10 @@
22
22
  * Pure semantic retrieval — no keyword/BM25 matching, so it works the SAME in every
23
23
  * language. Storing and searching call no LLM; you only pay for embeddings.
24
24
  *
25
- * Reliability: every call retries transient failures (429 / 502 / 503 and
26
- * connection errors) with exponential backoff + jitter, honoring `Retry-After`.
27
- * Tune with `maxRetries` (0 disables) and `timeoutMs`.
25
+ * Reliability: every call retries transient failures with exponential backoff +
26
+ * jitter, honoring `Retry-After` — 429 always; 502/503 and connection errors only
27
+ * when a retry can never double-process a write (idempotent calls, or a failure
28
+ * at connect time). Tune with `maxRetries` (0 disables) and `timeoutMs`.
28
29
  *
29
30
  * Security posture (not configurable off): redirects are refused so the API key
30
31
  * can never follow one to another host; responses over 64MB are refused; the key
@@ -32,7 +33,7 @@
32
33
  * runtime's (never disabled by this SDK; Node 18+ floors TLS at 1.2). Prefer
33
34
  * `Client.fromEnv()` over keys in source code.
34
35
  */
35
- const VERSION = "2.2.13";
36
+ const VERSION = "2.2.14";
36
37
  const USER_AGENT = `wontopos-node/${VERSION}`;
37
38
  const DEFAULT_BASE_URL = "https://api.wontopos.com";
38
39
  const DEFAULT_MODEL = "tablet-1";
@@ -49,6 +50,37 @@ const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "0.0.0.0"]);
49
50
  const MAX_RESPONSE_BYTES = 64 * 1024 * 1024;
50
51
  /** Model names travel in a header — only header-safe characters. */
51
52
  const MODEL_RE = /^[A-Za-z0-9._-]+$/;
53
+ /** Error codes that only occur while ESTABLISHING a connection — the request
54
+ * never reached the server, so a retry can't double-process a write. Mid-stream
55
+ * codes (ECONNRESET, EPIPE, UND_ERR_SOCKET, ETIMEDOUT) are ambiguous: the server
56
+ * may already have processed (and billed) the request, so they're excluded. */
57
+ const CONNECT_FAIL_CODES = new Set([
58
+ "ECONNREFUSED",
59
+ "ENOTFOUND",
60
+ "EAI_AGAIN",
61
+ "EHOSTUNREACH",
62
+ "ENETUNREACH",
63
+ "ENETDOWN",
64
+ "EADDRNOTAVAIL",
65
+ "UND_ERR_CONNECT_TIMEOUT",
66
+ ]);
67
+ /** Walk an error's `cause` chain (and AggregateError members — happy-eyeballs
68
+ * connects) looking for a connect-level failure code. */
69
+ function isConnectFailure(e) {
70
+ const stack = [e];
71
+ for (let steps = 0; stack.length && steps < 24; steps++) {
72
+ const cur = stack.pop();
73
+ if (!cur || typeof cur !== "object")
74
+ continue;
75
+ if (typeof cur.code === "string" && CONNECT_FAIL_CODES.has(cur.code))
76
+ return true;
77
+ if (cur.cause)
78
+ stack.push(cur.cause);
79
+ if (Array.isArray(cur.errors))
80
+ stack.push(...cur.errors.slice(0, 8));
81
+ }
82
+ return false;
83
+ }
52
84
  /** `wos-abc...wxyz` — enough to tell keys apart, never enough to use. */
53
85
  function maskKey(key) {
54
86
  return key.length > 12 ? `${key.slice(0, 4)}...${key.slice(-4)}` : "***";
@@ -152,7 +184,11 @@ export class Client {
152
184
  throw new Error("apiKey contains whitespace - check for a stray newline or paste error");
153
185
  this.apiKey = key;
154
186
  this.base = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
155
- this.timeoutMs = opts.timeoutMs ?? 30_000;
187
+ // Coerce a non-finite or non-positive timeout back to the default — NaN
188
+ // (e.g. Number() of an unset env var) would otherwise make the abort timer
189
+ // fire immediately and every request "time out after NaNms".
190
+ const tm = opts.timeoutMs;
191
+ this.timeoutMs = typeof tm === "number" && Number.isFinite(tm) && tm > 0 ? tm : 30_000;
156
192
  this.model = opts.model ?? DEFAULT_MODEL;
157
193
  if (this.model && !MODEL_RE.test(this.model)) {
158
194
  throw new Error(`invalid model name: ${JSON.stringify(this.model)} (letters, digits, '.', '_', '-' only)`);
@@ -458,7 +494,12 @@ export class Client {
458
494
  clearTimeout(timer);
459
495
  // Timeouts are ambiguous (the write may have landed) — don't retry those.
460
496
  const timedOut = e?.name === "AbortError" || ctrl.signal.aborted;
461
- if (!timedOut && attempt + 1 < attempts) {
497
+ // Other network errors retry only when a retry can't double-process a
498
+ // write: idempotent methods always; writes only for connect-level
499
+ // failures (the request never reached the server). A mid-stream drop on
500
+ // a POST may already have stored + billed server-side.
501
+ const safe = IDEMPOTENT_METHODS.has(method.toUpperCase()) || isConnectFailure(e);
502
+ if (!timedOut && safe && attempt + 1 < attempts) {
462
503
  await new Promise((r) => setTimeout(r, this.backoffMs(attempt)));
463
504
  continue;
464
505
  }
@@ -489,7 +530,11 @@ export class Client {
489
530
  if (e?.name === "AbortError" || ctrl.signal.aborted) {
490
531
  throw new APIConnectionError(0, `request timed out after ${this.timeoutMs}ms`);
491
532
  }
492
- throw e;
533
+ if (e instanceof WosError)
534
+ throw e; // the size cap — already the right error
535
+ // A drop while READING the body is a transport failure — surface it as
536
+ // APIConnectionError, not a raw fetch TypeError. Ambiguous, never retried.
537
+ throw new APIConnectionError(0, `network error: ${e?.message ?? e}`);
493
538
  }
494
539
  finally {
495
540
  clearTimeout(timer);
@@ -520,7 +565,16 @@ export class Client {
520
565
  }
521
566
  throw errorFor(res.status, msg, requestId);
522
567
  }
523
- return text ? JSON.parse(text) : null;
568
+ if (!text)
569
+ return null;
570
+ try {
571
+ return JSON.parse(text);
572
+ }
573
+ catch (e) {
574
+ // A 2xx with a corrupt body is a real failure — surface it, don't leak
575
+ // a raw SyntaxError (parity with the Rust SDK).
576
+ throw new WosError(res.status, `invalid JSON in response: ${e?.message ?? e}`);
577
+ }
524
578
  }
525
579
  throw new WosError(0, "retries exhausted"); // unreachable; keeps the compiler happy
526
580
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wontopos",
3
- "version": "2.2.13",
3
+ "version": "2.2.14",
4
4
  "description": "Wontopos — long-term memory for AI agents. Pure semantic retrieval, identical recall in every language, ~100× lower LLM bill.",
5
5
  "keywords": [
6
6
  "memory",