wontopos 2.2.25 → 2.2.27

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/README.md CHANGED
@@ -52,7 +52,13 @@ registration and never counts against it.
52
52
  ## Recall caching
53
53
 
54
54
  Opt in per search and repeated or extended queries reuse the previous result
55
- at 10% of the normal rate (Tablet and Scroll models):
55
+ at 10% of the normal rate (Tablet and Scroll models).
56
+
57
+ It is not free to turn on: the FIRST call writes the cache and bills the query
58
+ tokens at 2x for a `5m` TTL, 3x for `1h`. Only hits inside the TTL bill at 0.1x.
59
+ So it pays for a query you repeat or extend, and costs more for one you issue
60
+ once — do not switch it on globally. Any write to the store invalidates its cache
61
+ at once, so a hit can never predate a new memory.
56
62
 
57
63
  ```ts
58
64
  const hits = await mem.search("...the conversation so far...", "alice", 10,
@@ -74,6 +80,27 @@ Built in, no configuration needed:
74
80
  meaning "delete everything"; wiping a store is only ever the explicit
75
81
  `deleteAll(userId)` / `deleteStore(userId)`.
76
82
 
83
+ ### Behind a corporate proxy
84
+
85
+ Node's global `fetch` (undici) ignores `HTTP_PROXY` / `HTTPS_PROXY`, so behind an
86
+ egress proxy this SDK could not connect at all — while the Python and Rust SDKs went
87
+ through, because `requests` and `reqwest` both read those variables. Pass a
88
+ proxy-aware `fetch` and the three behave the same:
89
+
90
+ ```ts
91
+ import { ProxyAgent } from "undici";
92
+
93
+ const agent = new ProxyAgent(process.env.HTTPS_PROXY!);
94
+ const mem = new Client({
95
+ apiKey: process.env.WONTOPOS_API_KEY!,
96
+ fetch: (url, init) => fetch(url, { ...init, dispatcher: agent } as any),
97
+ });
98
+ ```
99
+
100
+ The same option is how you add instrumentation or drive the client without a
101
+ network in tests. It replaces the transport, not the client's rules — retries,
102
+ timeouts, redirect refusal and the response cap all still apply.
103
+
77
104
  ## Security
78
105
 
79
106
  Built in, none of it configurable off:
@@ -286,7 +286,9 @@ export interface SearchFilters {
286
286
  export declare function _resetFilterWarnings(): void;
287
287
  /** Known `search` options; extra fields pass through to the API untouched. */
288
288
  export interface SearchOptions {
289
- /** Recall caching: repeated/extended queries within the TTL bill at 0.1×. */
289
+ /** Recall caching. A hit inside the TTL bills at 0.1× — but the FIRST call
290
+ * writes the cache and bills the query tokens at 2× (`5m`) or 3× (`1h`), so
291
+ * this only pays for a query you repeat or extend. Do not enable it globally. */
290
292
  cache_control?: {
291
293
  ttl: "5m" | "1h";
292
294
  };
@@ -336,6 +338,26 @@ export interface ClientOptions {
336
338
  * key on an options object is not an error in TS at runtime). Both names work;
337
339
  * `maxRetries` wins if somehow both are given. */
338
340
  retries?: number;
341
+ /** The `fetch` used for every request (default: the global `fetch`).
342
+ *
343
+ * This is the seam for anything the runtime cannot express through options.
344
+ * The one that matters in practice is a corporate egress proxy: Node's global
345
+ * fetch (undici) IGNORES `HTTP_PROXY` / `HTTPS_PROXY`, so behind such a proxy
346
+ * this SDK could not connect at all — while the Python and Rust SDKs went
347
+ * through, because `requests` and `reqwest` both read those variables. Pass a
348
+ * proxy-aware fetch and the three behave the same again:
349
+ *
350
+ * import { ProxyAgent } from "undici";
351
+ * const agent = new ProxyAgent(process.env.HTTPS_PROXY!);
352
+ * const mem = new Client({
353
+ * apiKey,
354
+ * fetch: (url, init) => fetch(url, { ...init, dispatcher: agent } as any),
355
+ * });
356
+ *
357
+ * It is also how you add instrumentation or drive the client in a test without
358
+ * a network. Retries, timeouts, redirect refusal and the size cap all still
359
+ * apply — this replaces the transport, not the client's rules. */
360
+ fetch?: typeof fetch;
339
361
  }
340
362
  /** Wontopos memory client. Store and recall memories, isolated per `userId`. */
341
363
  export declare class Client {
@@ -345,6 +367,9 @@ export declare class Client {
345
367
  private readonly apiKey;
346
368
  private readonly model;
347
369
  private readonly maxRetries;
370
+ /** Transport. Held as a field (not read off `globalThis` per call) so a caller
371
+ * who passes one gets it for every request, including from cloned clients. */
372
+ private readonly fetchImpl;
348
373
  /** The store every call uses unless one passes `userId`. */
349
374
  private readonly defaultUser;
350
375
  private _rateLimit;
@@ -393,7 +418,14 @@ export declare class Client {
393
418
  addBulk(content: string, userId?: string, category?: string, timestamp?: string, opts?: WriteOptions): Promise<StatusResult>;
394
419
  /** Supersede an old memory with new content. Payload first, userId last — same shape as add/search. */
395
420
  update(oldMemoryId: string, newContent: string, userId?: string, opts?: WriteOptions): Promise<UpdateResult>;
396
- /** Semantic search over a store's memories. Returns memories, most relevant first. */
421
+ /** Semantic search over a store's memories. Returns memories, most relevant first.
422
+ *
423
+ * `limit` bounds the MAIN lane, not the returned array. On a self-lane model
424
+ * (Scroll 1.2+) the service answers in two disjoint lanes — the assistant's own
425
+ * words come back separately — and both are returned here, so the array can hold
426
+ * more than `limit`. They were retrieved and billed either way; dropping them would
427
+ * only hide what you already paid for. Size a prompt window on the array you get
428
+ * back, not on `limit`. `searchSelf()` hands the lanes back apart. */
397
429
  search(query: string, userId?: string, limit?: number, opts?: SearchOptions): Promise<Memory[]>;
398
430
  /** Search a self-memory model (Scroll 1.2+): both lanes from ONE call. Returns
399
431
  * `{ memories, self_memories }` — `memories` is the main lane (what others said /
package/dist/wontopos.js CHANGED
@@ -37,7 +37,7 @@
37
37
  * runtime's (never disabled by this SDK; Node 18+ floors TLS at 1.2). Prefer
38
38
  * `Client.fromEnv()` over keys in source code.
39
39
  */
40
- const VERSION = "2.2.25";
40
+ const VERSION = "2.2.27";
41
41
  /** Runtime info helps support debug a report ("node 18 on Windows...") —
42
42
  * platform only, never anything identifying. Browsers have no `process` (and
43
43
  * silently drop the UA header anyway). */
@@ -91,6 +91,17 @@ const IDEMPOTENCY_KEY_RE = /^[A-Za-z0-9._:\-]{1,128}$/;
91
91
  function normalizeStoreId(id) {
92
92
  return id.toLowerCase().replace(/[^a-z0-9_]/g, "_");
93
93
  }
94
+ /** Ids already warned about, so each is reported once.
95
+ *
96
+ * Bounded on purpose. The warning fires on ids that fold, and the shape it exists
97
+ * to catch is an email — which this SDK's own documented pattern ("one store per
98
+ * end user") turns into one entry PER USER, held for the life of the process and
99
+ * never released, even when the client is. A server passing 50,000 user ids grew
100
+ * this to 50,000 entries, a leak in exactly the case being warned about. A `Set`
101
+ * keeps insertion order, so the oldest entry is the one evicted; dropping the
102
+ * oldest rather than refusing new ones means a collision that first shows up late
103
+ * still gets its one warning. */
104
+ const WARNED_STORE_IDS_MAX = 1024;
94
105
  const warnedStoreIds = new Set();
95
106
  function warnIfStoreIdCollapses(id) {
96
107
  if (!id || warnedStoreIds.has(id))
@@ -99,6 +110,12 @@ function warnIfStoreIdCollapses(id) {
99
110
  if (normalized === id)
100
111
  return;
101
112
  warnedStoreIds.add(id);
113
+ while (warnedStoreIds.size > WARNED_STORE_IDS_MAX) {
114
+ const oldest = warnedStoreIds.values().next().value;
115
+ if (oldest === undefined)
116
+ break;
117
+ warnedStoreIds.delete(oldest);
118
+ }
102
119
  console.warn(`wontopos: store id ${JSON.stringify(id)} is stored as ${JSON.stringify(normalized)} ` +
103
120
  `(lowercased, and anything outside [a-z0-9_] becomes "_"). Ids that differ only by case or ` +
104
121
  `punctuation share ONE store and therefore one set of memories — if these ids come from your ` +
@@ -310,6 +327,9 @@ export class Client {
310
327
  apiKey;
311
328
  model;
312
329
  maxRetries;
330
+ /** Transport. Held as a field (not read off `globalThis` per call) so a caller
331
+ * who passes one gets it for every request, including from cloned clients. */
332
+ fetchImpl;
313
333
  /** The store every call uses unless one passes `userId`. */
314
334
  defaultUser;
315
335
  _rateLimit = null;
@@ -344,6 +364,17 @@ export class Client {
344
364
  // "retries exhausted"; Infinity would retry forever.
345
365
  const mr = opts.maxRetries ?? opts.retries;
346
366
  this.maxRetries = typeof mr === "number" && Number.isFinite(mr) ? Math.max(0, Math.floor(mr)) : 2;
367
+ // Bind the global so `fetch` is not called as a method of `globalThis`, which
368
+ // throws "Illegal invocation" on some runtimes. A caller-supplied fetch is
369
+ // taken as given — it is already whatever they meant to hand us.
370
+ const f = opts.fetch;
371
+ if (f !== undefined && typeof f !== "function") {
372
+ throw new Error("fetch must be a function (a fetch-compatible transport)");
373
+ }
374
+ this.fetchImpl = f ?? (globalThis.fetch ? globalThis.fetch.bind(globalThis) : undefined);
375
+ if (typeof this.fetchImpl !== "function") {
376
+ throw new Error("no fetch available in this runtime — pass one as `fetch` (Node 18+ has it built in; older Node needs undici)");
377
+ }
347
378
  this.headers = {
348
379
  "X-API-Key": this.apiKey,
349
380
  "Content-Type": "application/json",
@@ -395,6 +426,10 @@ export class Client {
395
426
  model: this.model,
396
427
  userId: this.defaultUser,
397
428
  maxRetries: this.maxRetries,
429
+ // Carry the transport across clones. Without this, `withModel(...)` and
430
+ // friends would silently fall back to the global fetch and a proxy-bound
431
+ // client would stop connecting the moment it was cloned.
432
+ fetch: this.fetchImpl,
398
433
  ...overrides,
399
434
  });
400
435
  }
@@ -455,7 +490,14 @@ export class Client {
455
490
  return this.post("/api/v1/memory/supersede", { user_id: this.uid(userId), old_memory_id: oldMemoryId, new_content: newContent }, opts.idempotencyKey);
456
491
  }
457
492
  // ----- read -----
458
- /** Semantic search over a store's memories. Returns memories, most relevant first. */
493
+ /** Semantic search over a store's memories. Returns memories, most relevant first.
494
+ *
495
+ * `limit` bounds the MAIN lane, not the returned array. On a self-lane model
496
+ * (Scroll 1.2+) the service answers in two disjoint lanes — the assistant's own
497
+ * words come back separately — and both are returned here, so the array can hold
498
+ * more than `limit`. They were retrieved and billed either way; dropping them would
499
+ * only hide what you already paid for. Size a prompt window on the array you get
500
+ * back, not on `limit`. `searchSelf()` hands the lanes back apart. */
459
501
  async search(query, userId, limit = 10, opts = {}) {
460
502
  // Reserved fields win over ...opts: an app that forwards untrusted input as
461
503
  // opts must not be able to override the store (user_id), query, or limit.
@@ -710,7 +752,7 @@ export class Client {
710
752
  const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
711
753
  let res;
712
754
  try {
713
- res = await fetch(`${this.base}${path}`, {
755
+ res = await this.fetchImpl(`${this.base}${path}`, {
714
756
  method,
715
757
  headers: extraHeaders ? { ...this.headers, ...extraHeaders } : this.headers,
716
758
  body: body === undefined ? undefined : JSON.stringify(body),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wontopos",
3
- "version": "2.2.25",
3
+ "version": "2.2.27",
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",