wontopos 2.2.38 → 2.2.39

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
@@ -80,12 +80,12 @@ const hits = await mem.search("...the conversation so far...", "alice", 10,
80
80
 
81
81
  Built in, no configuration needed:
82
82
 
83
- - **Automatic retries** — 429 always, and 502 / 503 or a connection error only when a
84
- retry cannot double-process a write. The writes and the searches are POSTs, and a
85
- 502 on one of those may have been returned *after* the service already stored and
86
- billed it, so those get 429 and connect-level failures only. The reads and the
83
+ - **Automatic retries** — 429 always, and 408 / 502 / 503 / 504 or a connection error
84
+ only when a retry cannot double-process a write. The writes and the searches are
85
+ POSTs, and any of those four may have been returned *after* the write already
86
+ landed, so those get 429 and connect-level failures only. The reads and the
87
87
  deletes that address a whole store — `listStores`, `listSpeakers`, `deleteStore`,
88
- `removeSpeaker`, `forgetImage` — are GET or DELETE and do retry a 502. Twice, with
88
+ `removeSpeaker`, `forgetImage` — are GET or DELETE and do retry all four. Twice, with
89
89
  exponential backoff + jitter, honoring the server's `Retry-After`. Tune with
90
90
  `new Client({ maxRetries })`; `0` disables.
91
91
  - **Redirects refused** — the API key never follows a 3xx to another host.
@@ -106,9 +106,7 @@ Built in, no configuration needed:
106
106
  ### Behind a corporate proxy
107
107
 
108
108
  Node's global `fetch` (undici) ignores `HTTP_PROXY` / `HTTPS_PROXY`, so behind an
109
- egress proxy this SDK could not connect at all — while the Python and Rust SDKs went
110
- through, because `requests` and `reqwest` both read those variables. Pass a
111
- proxy-aware `fetch` and the three behave the same:
109
+ egress proxy no request leaves at all. Pass a proxy-aware `fetch` and they go through:
112
110
 
113
111
  This one needs `npm i undici` — Node's global `fetch` is built on undici, but the
114
112
  package itself is not installed for you.
@@ -211,7 +209,7 @@ data to fix it.
211
209
  ## Changelog
212
210
 
213
211
  The three clients release in lockstep — same version, same surface, same day. Patch
214
- releases are additive. Four inside 2.2 were not, deliberately and each with its
212
+ releases are additive. Five inside 2.2 were not, deliberately and each with its
215
213
  reason; the changelog lists them.
216
214
 
217
215
  See [CHANGELOG.md](https://github.com/wontopos/wontopos-node/blob/main/CHANGELOG.md).
@@ -26,8 +26,8 @@
26
26
  * and searching call no LLM.
27
27
  *
28
28
  * Reliability: every call retries transient failures with exponential backoff +
29
- * jitter, honoring `Retry-After` — 429 always; 502/503 and connection errors only
30
- * when a retry can never double-process a write (idempotent calls, or a failure
29
+ * jitter, honoring `Retry-After` — 429 always; 408/502/503/504 and connection errors
30
+ * only when a retry can never double-process a write (idempotent calls, or a failure
31
31
  * at connect time). Tune with `maxRetries` (0 disables) and `timeoutMs`, or per
32
32
  * call site via the `withTimeout()` / `withRetries()` clones.
33
33
  *
@@ -86,8 +86,8 @@ export declare class RateLimitError extends WosError {
86
86
  /**
87
87
  * 5xx — the service failed.
88
88
  *
89
- * `502` / `503` are transient: the client already retries them for calls where a
90
- * retry cannot double-process a write, and retrying yourself is reasonable.
89
+ * `502` / `503` / `504` are transient: the client already retries them for calls
90
+ * where a retry cannot double-process a write, and retrying yourself is reasonable.
91
91
  *
92
92
  * `501` is NOT transient. It means the engine behind the model you selected does
93
93
  * not implement that endpoint at all (the error carries `model` and `endpoint`).
@@ -428,6 +428,9 @@ export interface EngramCatalog {
428
428
  engrams: EngramInfo[];
429
429
  forms: EngramInfo[];
430
430
  note?: string;
431
+ /** Anything else the reply carried. Responses widen; the runtime keeps those fields
432
+ * and this is what lets a typed caller read them. */
433
+ [key: string]: unknown;
431
434
  }
432
435
  /** `createStore` / `deleteStore` → `{ user_id, status }`. */
433
436
  export interface StoreOpResult {
@@ -548,9 +551,9 @@ export interface SearchOptions {
548
551
  * arrives with a different body. Use it when a retry is your own (a job that died and
549
552
  * was re-run, a queue that redelivers). The SDK retries a write on exactly one
550
553
  * status: 429, which the service answers before it processes anything, so nothing
551
- * was stored. It never retries a write on 502 / 503 or a dropped body, where the
552
- * request may already have been stored and billed — without a key it cannot know
553
- * whether that first attempt landed.
554
+ * was stored. It never retries a write on 408 / 502 / 503 / 504 or a dropped body,
555
+ * where the first attempt may already have landed — without a key the client cannot
556
+ * know whether it did.
554
557
  *
555
558
  * The key must be UNIQUE PER LOGICAL WRITE — derive it from the thing being stored
556
559
  * (`` `import:${row.id}` ``), never a constant, or the second write replays the first
@@ -582,8 +585,8 @@ export interface ClientOptions {
582
585
  * to the account's built-in `default` store. */
583
586
  userId?: string;
584
587
  /** How many times to retry transient failures before throwing — 429 always;
585
- * 502/503 and connection errors only when a retry can never double-process a
586
- * write. 0 disables retries (default 2). */
588
+ * 408/502/503/504 and connection errors only when a retry can never
589
+ * double-process a write. 0 disables retries (default 2). */
587
590
  maxRetries?: number;
588
591
  /** Alias of `maxRetries`. The Python SDK calls this option `retries`, and these
589
592
  * SDKs are published in lockstep as "the same surface" — so code ported between
@@ -611,10 +614,8 @@ export interface ClientOptions {
611
614
  *
612
615
  * This is the seam for anything the runtime cannot express through options.
613
616
  * The one that matters in practice is a corporate egress proxy: Node's global
614
- * fetch (undici) IGNORES `HTTP_PROXY` / `HTTPS_PROXY`, so behind such a proxy
615
- * this SDK could not connect at all — while the Python and Rust SDKs went
616
- * through, because `requests` and `reqwest` both read those variables. Pass a
617
- * proxy-aware fetch and the three behave the same again:
617
+ * fetch (undici) IGNORES `HTTP_PROXY` / `HTTPS_PROXY`, so behind such a proxy no
618
+ * request leaves at all. Pass a proxy-aware fetch and they go through:
618
619
  *
619
620
  * import { ProxyAgent } from "undici";
620
621
  * const agent = new ProxyAgent(process.env.HTTPS_PROXY!);
@@ -696,7 +697,7 @@ export declare class Client {
696
697
  addTurn(userMsg: string, assistantMsg: string, userId?: string, opts?: WriteOptions): Promise<StatusResult>;
697
698
  /** Bulk-ingest a large blob of text in one call. For backfilling.
698
699
  * The call most worth an `opts.idempotencyKey`: a backfill that dies halfway and is
699
- * re-run would otherwise re-ingest and re-bill the whole blob. */
700
+ * re-run would otherwise ingest the whole blob a second time. */
700
701
  addBulk(content: string, userId?: string, category?: string, timestamp?: string, opts?: WriteOptions): Promise<StatusResult>;
701
702
  /** Supersede an old memory with new content. Payload first, userId last — same shape as add/search. */
702
703
  update(oldMemoryId: string, newContent: string, userId?: string, opts?: WriteOptions): Promise<UpdateResult>;
@@ -812,6 +813,11 @@ export declare class Client {
812
813
  }): AsyncGenerator<Memory>;
813
814
  /** Collect ALL of a store's memories into an array (the text you stored, and its metadata). */
814
815
  exportMemories(userId?: string): Promise<Memory[]>;
816
+ /** Collect ALL of a store's image memories into an array — the image-side pair of
817
+ * `exportMemories`. */
818
+ exportImages(userId?: string, opts?: {
819
+ pageSize?: number;
820
+ }): Promise<Memory[]>;
815
821
  /**
816
822
  * Fetch the bytes of an image memory — the picture the SERVICE holds.
817
823
  *
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.WME = exports.Client = exports.ServerError = exports.RateLimitError = exports.ConflictError = exports.NotFoundError = exports.PermissionDeniedError = exports.PaymentRequiredError = exports.AuthenticationError = exports.BadRequestError = exports.APIConnectionError = exports.WosError = exports.CONTEXT_LIMIT_MAX = exports.CONTEXT_LIMIT_MIN = exports.SEARCH_LIMIT_MAX = exports.SEARCH_LIMIT_MIN = void 0;
4
4
  exports._resetStoreIdWarnings = _resetStoreIdWarnings;
5
5
  exports._resetFilterWarnings = _resetFilterWarnings;
6
- const VERSION = "2.2.38";
6
+ const VERSION = "2.2.39";
7
7
  const RUNTIME = (() => {
8
8
  const p = globalThis?.process;
9
9
  return p?.version ? ` (node/${p.version}; ${p.platform}-${p.arch})` : "";
@@ -18,7 +18,7 @@ function logDebug(msg) {
18
18
  const DEFAULT_BASE_URL = "https://api.wontopos.com";
19
19
  const DEFAULT_MODEL = "tablet-2";
20
20
  const RETRY_ALWAYS = new Set([429]);
21
- const RETRY_IF_IDEMPOTENT = new Set([502, 503]);
21
+ const RETRY_IF_IDEMPOTENT = new Set([408, 502, 503, 504]);
22
22
  const IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
23
23
  const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "0.0.0.0"]);
24
24
  const MAX_RESPONSE_BYTES = 64 * 1024 * 1024;
@@ -223,6 +223,7 @@ const KNOWN_SEARCH_KEYS = new Set([
223
223
  ]);
224
224
  const RESERVED_SEARCH_KEYS = new Set(["user_id", "query", "max_results"]);
225
225
  const KNOWN_RECALL_KEYS = new Set(["form", "tz", "limit", "context_limit"]);
226
+ const KNOWN_ENGRAM_KEYS = new Set(["form", "tz"]);
226
227
  function checkSearchOpts(opts) {
227
228
  checkOpts(opts, KNOWN_SEARCH_KEYS, "search");
228
229
  }
@@ -237,8 +238,7 @@ function checkOpts(opts, known, what) {
237
238
  const near = [...known].find((n) => n !== "extra" && editWithin(k, n, 2));
238
239
  throw new Error(`unknown ${what} option ${JSON.stringify(k)}` +
239
240
  (near ? ` — did you mean ${JSON.stringify(near)}?` : "") +
240
- `. The service drops keys it does not know and answers anyway, so this would ` +
241
- `have looked like it worked.` +
241
+ `. It would have had no effect, and the call would have looked like it worked.` +
242
242
  (known.has("extra")
243
243
  ? ` Pass it under \`extra\` if the service accepts it and this client does not know it yet.`
244
244
  : ""));
@@ -407,25 +407,25 @@ class Client {
407
407
  withRetries(maxRetries) {
408
408
  return this.clone({ maxRetries });
409
409
  }
410
- add(content, userId, metadata = {}, opts = {}) {
410
+ async add(content, userId, metadata = {}, opts = {}) {
411
411
  const body = { user_id: this.uid(userId), content, metadata };
412
412
  if (opts.image !== undefined)
413
413
  body.image = normalizeImage(opts.image);
414
414
  return this.post("/api/v1/memory/store", body, opts.idempotencyKey);
415
415
  }
416
- store(content, userId, metadata = {}, opts = {}) {
416
+ async store(content, userId, metadata = {}, opts = {}) {
417
417
  return this.add(content, userId, metadata, opts);
418
418
  }
419
- addTurn(userMsg, assistantMsg, userId, opts = {}) {
419
+ async addTurn(userMsg, assistantMsg, userId, opts = {}) {
420
420
  return this.post("/api/v1/memory/store-turn", { user_id: this.uid(userId), user_msg: userMsg, assistant_msg: assistantMsg }, opts.idempotencyKey);
421
421
  }
422
- addBulk(content, userId, category = "general", timestamp, opts = {}) {
422
+ async addBulk(content, userId, category = "general", timestamp, opts = {}) {
423
423
  const body = { user_id: this.uid(userId), content, category };
424
424
  if (timestamp)
425
425
  body.timestamp = timestamp;
426
426
  return this.post("/api/v1/memory/bulk-store", body, opts.idempotencyKey);
427
427
  }
428
- update(oldMemoryId, newContent, userId, opts = {}) {
428
+ async update(oldMemoryId, newContent, userId, opts = {}) {
429
429
  return this.post("/api/v1/memory/supersede", { user_id: this.uid(userId), old_memory_id: oldMemoryId, new_content: newContent }, opts.idempotencyKey);
430
430
  }
431
431
  async search(query, userId, limit = 10, opts = {}) {
@@ -470,14 +470,15 @@ class Client {
470
470
  }
471
471
  return this.post("/api/v1/memory/recall", withForm(body, opts.form, opts.tz));
472
472
  }
473
- engram(name, query, userId, opts = {}) {
473
+ async engram(name, query, userId, opts = {}) {
474
+ checkOpts(opts, KNOWN_ENGRAM_KEYS, "engram");
474
475
  return this.post("/api/v1/engram/run", withForm({ name, user_id: this.uid(userId), query }, opts.form, opts.tz));
475
476
  }
476
477
  async history(userId) {
477
478
  const r = await this.post("/api/v1/memory/history", { user_id: this.uid(userId) });
478
479
  return asRecords(r.turns);
479
480
  }
480
- stats(userId) {
481
+ async stats(userId) {
481
482
  return this.post("/api/v1/memory/stats", { user_id: this.uid(userId) });
482
483
  }
483
484
  async usage(days = 7) {
@@ -486,13 +487,13 @@ class Client {
486
487
  }
487
488
  return this.request("GET", `/api/v1/won/usage?days=${days}`);
488
489
  }
489
- get(userId, memoryId) {
490
+ async get(userId, memoryId) {
490
491
  if (typeof memoryId !== "string" || !memoryId.trim()) {
491
492
  throw new Error("memoryId is required — the id that add/store or listMemories returned.");
492
493
  }
493
494
  return this.post("/api/v1/memory/get", { user_id: this.uid(userId), memory_id: memoryId.trim() }).then((r) => asObj(r.memory));
494
495
  }
495
- listMemories(userId, opts = {}) {
496
+ async listMemories(userId, opts = {}) {
496
497
  const body = { user_id: this.uid(userId), limit: opts.limit ?? 100 };
497
498
  if (opts.cursor)
498
499
  body.cursor = opts.cursor;
@@ -520,13 +521,19 @@ class Client {
520
521
  out.push(m);
521
522
  return out;
522
523
  }
523
- getImage(userId, memoryId) {
524
+ async exportImages(userId, opts = {}) {
525
+ const out = [];
526
+ for await (const m of this.iterImages(userId, opts))
527
+ out.push(m);
528
+ return out;
529
+ }
530
+ async getImage(userId, memoryId) {
524
531
  if (typeof memoryId !== "string" || !memoryId.trim()) {
525
532
  throw new Error("memoryId is required — the id that add/store or listImages returned.");
526
533
  }
527
534
  return this.requestBytes("/api/v1/memory/image", { user_id: this.uid(userId), memory_id: memoryId.trim() });
528
535
  }
529
- forgetImage(userId, memoryId, opts = {}) {
536
+ async forgetImage(userId, memoryId, opts = {}) {
530
537
  if (typeof memoryId !== "string" || !memoryId.trim()) {
531
538
  throw new Error("memoryId is required — the id of the memory whose image you want removed.");
532
539
  }
@@ -535,7 +542,7 @@ class Client {
535
542
  body.preview = true;
536
543
  return this.request("DELETE", "/api/v1/memory/image", body);
537
544
  }
538
- listImages(userId, opts = {}) {
545
+ async listImages(userId, opts = {}) {
539
546
  const body = { user_id: this.uid(userId) };
540
547
  if (opts.limit !== undefined)
541
548
  body.limit = opts.limit;
@@ -565,7 +572,7 @@ class Client {
565
572
  throw new Error(`stopped after ${MAX_PAGES} pages — the store did not end. This is a truncated ` +
566
573
  `answer, not the whole store.`);
567
574
  }
568
- revisions(userId, opts = {}) {
575
+ async revisions(userId, opts = {}) {
569
576
  const body = {};
570
577
  if (opts.include !== undefined)
571
578
  body.include = opts.include;
@@ -578,13 +585,13 @@ class Client {
578
585
  body.user_id = this.uid(userId);
579
586
  return this.post("/api/v1/won/revisions", body);
580
587
  }
581
- lineage(userId, memoryId) {
588
+ async lineage(userId, memoryId) {
582
589
  if (typeof memoryId !== "string" || !memoryId.trim()) {
583
590
  throw new Error("memoryId is required — the memory whose history you want.");
584
591
  }
585
592
  return this.post("/api/v1/memory/lineage", { user_id: this.uid(userId), memory_id: memoryId.trim() });
586
593
  }
587
- bySpeaker(speaker, userId, opts = {}) {
594
+ async bySpeaker(speaker, userId, opts = {}) {
588
595
  if (!speaker || typeof speaker !== "string" || speaker.trim() === "") {
589
596
  throw new Error('speaker is required — "me" for the assistant, or a person\'s name.');
590
597
  }
@@ -608,41 +615,42 @@ class Client {
608
615
  async listEngrams() {
609
616
  const data = await this.request("GET", "/api/v1/engram");
610
617
  return {
618
+ ...data,
611
619
  engrams: asRecords(data.engrams),
612
620
  forms: asRecords(data.forms),
613
621
  note: typeof data.note === "string" ? data.note : undefined,
614
622
  };
615
623
  }
616
- createStore(userId) {
624
+ async createStore(userId) {
617
625
  return this.post("/api/v1/memory/collection", { user_id: this.uid(userId) });
618
626
  }
619
627
  async listStores() {
620
628
  const r = await this.request("GET", "/api/v1/memory/collections");
621
629
  return asRecords(r.collections);
622
630
  }
623
- deleteStore(userId) {
631
+ async deleteStore(userId) {
624
632
  if (typeof userId !== "string" || !userId.trim())
625
633
  throw new Error("userId is required (a non-blank string) — deleteStore never falls back to the default store.");
626
634
  warnIfStoreIdCollapses(userId);
627
635
  return this.request("DELETE", "/api/v1/memory/collection", { user_id: userId });
628
636
  }
629
- addSpeaker(speaker, userId) {
637
+ async addSpeaker(speaker, userId) {
630
638
  return this.post("/api/v1/memory/speakers", { user_id: this.uid(userId), speaker });
631
639
  }
632
640
  async listSpeakers(userId) {
633
641
  const r = await this.request("GET", `/api/v1/memory/speakers?user_id=${encodeURIComponent(this.uid(userId))}`);
634
642
  return { ...r, speakers: asRecords(r?.speakers) };
635
643
  }
636
- removeSpeaker(speaker, userId) {
644
+ async removeSpeaker(speaker, userId) {
637
645
  return this.request("DELETE", "/api/v1/memory/speakers", { user_id: this.uid(userId), speaker });
638
646
  }
639
- delete(userId, memoryId) {
647
+ async delete(userId, memoryId) {
640
648
  if (typeof memoryId !== "string" || !memoryId.trim()) {
641
649
  throw new Error("memoryId is required (non-blank). To delete every memory in a store, call deleteAll(userId) explicitly.");
642
650
  }
643
651
  return this.post("/api/v1/memory/forget", { user_id: this.uid(userId), memory_id: memoryId.trim() });
644
652
  }
645
- deleteAll(userId) {
653
+ async deleteAll(userId) {
646
654
  if (typeof userId !== "string" || !userId.trim())
647
655
  throw new Error("userId is required (a non-blank string) for deleteAll — anything else would wipe the default store.");
648
656
  warnIfStoreIdCollapses(userId);
@@ -777,7 +785,9 @@ class Client {
777
785
  if (e?.name === "AbortError" || att.signal.aborted) {
778
786
  throw new APIConnectionError(0, this.abortMessage(att.why()));
779
787
  }
780
- throw e;
788
+ if (e instanceof WosError)
789
+ throw e;
790
+ throw new APIConnectionError(0, `network error: ${e?.message ?? e}`);
781
791
  }
782
792
  if (buf.byteLength === 0) {
783
793
  throw new WosError(res.status, "empty image body — the service returned no bytes");
@@ -910,7 +920,14 @@ class Client {
910
920
  }
911
921
  if (e instanceof WosError)
912
922
  throw e;
913
- throw new APIConnectionError(0, `network error: ${e?.message ?? e}`);
923
+ if (!(IDEMPOTENT_METHODS.has(method.toUpperCase()) && attempt + 1 < attempts)) {
924
+ throw new APIConnectionError(0, `network error: ${e?.message ?? e}`);
925
+ }
926
+ att.clear();
927
+ const delay = this.backoffMs(attempt);
928
+ logDebug(`${method} ${logPath} — body dropped mid-stream, retrying in ${delay}ms (attempt ${attempt + 1}/${attempts})`);
929
+ await this.backoffSleep(delay, deadlineAt);
930
+ continue;
914
931
  }
915
932
  finally {
916
933
  att.clear();
@@ -26,8 +26,8 @@
26
26
  * and searching call no LLM.
27
27
  *
28
28
  * Reliability: every call retries transient failures with exponential backoff +
29
- * jitter, honoring `Retry-After` — 429 always; 502/503 and connection errors only
30
- * when a retry can never double-process a write (idempotent calls, or a failure
29
+ * jitter, honoring `Retry-After` — 429 always; 408/502/503/504 and connection errors
30
+ * only when a retry can never double-process a write (idempotent calls, or a failure
31
31
  * at connect time). Tune with `maxRetries` (0 disables) and `timeoutMs`, or per
32
32
  * call site via the `withTimeout()` / `withRetries()` clones.
33
33
  *
@@ -86,8 +86,8 @@ export declare class RateLimitError extends WosError {
86
86
  /**
87
87
  * 5xx — the service failed.
88
88
  *
89
- * `502` / `503` are transient: the client already retries them for calls where a
90
- * retry cannot double-process a write, and retrying yourself is reasonable.
89
+ * `502` / `503` / `504` are transient: the client already retries them for calls
90
+ * where a retry cannot double-process a write, and retrying yourself is reasonable.
91
91
  *
92
92
  * `501` is NOT transient. It means the engine behind the model you selected does
93
93
  * not implement that endpoint at all (the error carries `model` and `endpoint`).
@@ -428,6 +428,9 @@ export interface EngramCatalog {
428
428
  engrams: EngramInfo[];
429
429
  forms: EngramInfo[];
430
430
  note?: string;
431
+ /** Anything else the reply carried. Responses widen; the runtime keeps those fields
432
+ * and this is what lets a typed caller read them. */
433
+ [key: string]: unknown;
431
434
  }
432
435
  /** `createStore` / `deleteStore` → `{ user_id, status }`. */
433
436
  export interface StoreOpResult {
@@ -548,9 +551,9 @@ export interface SearchOptions {
548
551
  * arrives with a different body. Use it when a retry is your own (a job that died and
549
552
  * was re-run, a queue that redelivers). The SDK retries a write on exactly one
550
553
  * status: 429, which the service answers before it processes anything, so nothing
551
- * was stored. It never retries a write on 502 / 503 or a dropped body, where the
552
- * request may already have been stored and billed — without a key it cannot know
553
- * whether that first attempt landed.
554
+ * was stored. It never retries a write on 408 / 502 / 503 / 504 or a dropped body,
555
+ * where the first attempt may already have landed — without a key the client cannot
556
+ * know whether it did.
554
557
  *
555
558
  * The key must be UNIQUE PER LOGICAL WRITE — derive it from the thing being stored
556
559
  * (`` `import:${row.id}` ``), never a constant, or the second write replays the first
@@ -582,8 +585,8 @@ export interface ClientOptions {
582
585
  * to the account's built-in `default` store. */
583
586
  userId?: string;
584
587
  /** How many times to retry transient failures before throwing — 429 always;
585
- * 502/503 and connection errors only when a retry can never double-process a
586
- * write. 0 disables retries (default 2). */
588
+ * 408/502/503/504 and connection errors only when a retry can never
589
+ * double-process a write. 0 disables retries (default 2). */
587
590
  maxRetries?: number;
588
591
  /** Alias of `maxRetries`. The Python SDK calls this option `retries`, and these
589
592
  * SDKs are published in lockstep as "the same surface" — so code ported between
@@ -611,10 +614,8 @@ export interface ClientOptions {
611
614
  *
612
615
  * This is the seam for anything the runtime cannot express through options.
613
616
  * The one that matters in practice is a corporate egress proxy: Node's global
614
- * fetch (undici) IGNORES `HTTP_PROXY` / `HTTPS_PROXY`, so behind such a proxy
615
- * this SDK could not connect at all — while the Python and Rust SDKs went
616
- * through, because `requests` and `reqwest` both read those variables. Pass a
617
- * proxy-aware fetch and the three behave the same again:
617
+ * fetch (undici) IGNORES `HTTP_PROXY` / `HTTPS_PROXY`, so behind such a proxy no
618
+ * request leaves at all. Pass a proxy-aware fetch and they go through:
618
619
  *
619
620
  * import { ProxyAgent } from "undici";
620
621
  * const agent = new ProxyAgent(process.env.HTTPS_PROXY!);
@@ -696,7 +697,7 @@ export declare class Client {
696
697
  addTurn(userMsg: string, assistantMsg: string, userId?: string, opts?: WriteOptions): Promise<StatusResult>;
697
698
  /** Bulk-ingest a large blob of text in one call. For backfilling.
698
699
  * The call most worth an `opts.idempotencyKey`: a backfill that dies halfway and is
699
- * re-run would otherwise re-ingest and re-bill the whole blob. */
700
+ * re-run would otherwise ingest the whole blob a second time. */
700
701
  addBulk(content: string, userId?: string, category?: string, timestamp?: string, opts?: WriteOptions): Promise<StatusResult>;
701
702
  /** Supersede an old memory with new content. Payload first, userId last — same shape as add/search. */
702
703
  update(oldMemoryId: string, newContent: string, userId?: string, opts?: WriteOptions): Promise<UpdateResult>;
@@ -812,6 +813,11 @@ export declare class Client {
812
813
  }): AsyncGenerator<Memory>;
813
814
  /** Collect ALL of a store's memories into an array (the text you stored, and its metadata). */
814
815
  exportMemories(userId?: string): Promise<Memory[]>;
816
+ /** Collect ALL of a store's image memories into an array — the image-side pair of
817
+ * `exportMemories`. */
818
+ exportImages(userId?: string, opts?: {
819
+ pageSize?: number;
820
+ }): Promise<Memory[]>;
815
821
  /**
816
822
  * Fetch the bytes of an image memory — the picture the SERVICE holds.
817
823
  *
package/dist/wontopos.js CHANGED
@@ -1,4 +1,4 @@
1
- const VERSION = "2.2.38";
1
+ const VERSION = "2.2.39";
2
2
  const RUNTIME = (() => {
3
3
  const p = globalThis?.process;
4
4
  return p?.version ? ` (node/${p.version}; ${p.platform}-${p.arch})` : "";
@@ -13,7 +13,7 @@ function logDebug(msg) {
13
13
  const DEFAULT_BASE_URL = "https://api.wontopos.com";
14
14
  const DEFAULT_MODEL = "tablet-2";
15
15
  const RETRY_ALWAYS = new Set([429]);
16
- const RETRY_IF_IDEMPOTENT = new Set([502, 503]);
16
+ const RETRY_IF_IDEMPOTENT = new Set([408, 502, 503, 504]);
17
17
  const IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
18
18
  const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "0.0.0.0"]);
19
19
  const MAX_RESPONSE_BYTES = 64 * 1024 * 1024;
@@ -208,6 +208,7 @@ const KNOWN_SEARCH_KEYS = new Set([
208
208
  ]);
209
209
  const RESERVED_SEARCH_KEYS = new Set(["user_id", "query", "max_results"]);
210
210
  const KNOWN_RECALL_KEYS = new Set(["form", "tz", "limit", "context_limit"]);
211
+ const KNOWN_ENGRAM_KEYS = new Set(["form", "tz"]);
211
212
  function checkSearchOpts(opts) {
212
213
  checkOpts(opts, KNOWN_SEARCH_KEYS, "search");
213
214
  }
@@ -222,8 +223,7 @@ function checkOpts(opts, known, what) {
222
223
  const near = [...known].find((n) => n !== "extra" && editWithin(k, n, 2));
223
224
  throw new Error(`unknown ${what} option ${JSON.stringify(k)}` +
224
225
  (near ? ` — did you mean ${JSON.stringify(near)}?` : "") +
225
- `. The service drops keys it does not know and answers anyway, so this would ` +
226
- `have looked like it worked.` +
226
+ `. It would have had no effect, and the call would have looked like it worked.` +
227
227
  (known.has("extra")
228
228
  ? ` Pass it under \`extra\` if the service accepts it and this client does not know it yet.`
229
229
  : ""));
@@ -392,25 +392,25 @@ export class Client {
392
392
  withRetries(maxRetries) {
393
393
  return this.clone({ maxRetries });
394
394
  }
395
- add(content, userId, metadata = {}, opts = {}) {
395
+ async add(content, userId, metadata = {}, opts = {}) {
396
396
  const body = { user_id: this.uid(userId), content, metadata };
397
397
  if (opts.image !== undefined)
398
398
  body.image = normalizeImage(opts.image);
399
399
  return this.post("/api/v1/memory/store", body, opts.idempotencyKey);
400
400
  }
401
- store(content, userId, metadata = {}, opts = {}) {
401
+ async store(content, userId, metadata = {}, opts = {}) {
402
402
  return this.add(content, userId, metadata, opts);
403
403
  }
404
- addTurn(userMsg, assistantMsg, userId, opts = {}) {
404
+ async addTurn(userMsg, assistantMsg, userId, opts = {}) {
405
405
  return this.post("/api/v1/memory/store-turn", { user_id: this.uid(userId), user_msg: userMsg, assistant_msg: assistantMsg }, opts.idempotencyKey);
406
406
  }
407
- addBulk(content, userId, category = "general", timestamp, opts = {}) {
407
+ async addBulk(content, userId, category = "general", timestamp, opts = {}) {
408
408
  const body = { user_id: this.uid(userId), content, category };
409
409
  if (timestamp)
410
410
  body.timestamp = timestamp;
411
411
  return this.post("/api/v1/memory/bulk-store", body, opts.idempotencyKey);
412
412
  }
413
- update(oldMemoryId, newContent, userId, opts = {}) {
413
+ async update(oldMemoryId, newContent, userId, opts = {}) {
414
414
  return this.post("/api/v1/memory/supersede", { user_id: this.uid(userId), old_memory_id: oldMemoryId, new_content: newContent }, opts.idempotencyKey);
415
415
  }
416
416
  async search(query, userId, limit = 10, opts = {}) {
@@ -455,14 +455,15 @@ export class Client {
455
455
  }
456
456
  return this.post("/api/v1/memory/recall", withForm(body, opts.form, opts.tz));
457
457
  }
458
- engram(name, query, userId, opts = {}) {
458
+ async engram(name, query, userId, opts = {}) {
459
+ checkOpts(opts, KNOWN_ENGRAM_KEYS, "engram");
459
460
  return this.post("/api/v1/engram/run", withForm({ name, user_id: this.uid(userId), query }, opts.form, opts.tz));
460
461
  }
461
462
  async history(userId) {
462
463
  const r = await this.post("/api/v1/memory/history", { user_id: this.uid(userId) });
463
464
  return asRecords(r.turns);
464
465
  }
465
- stats(userId) {
466
+ async stats(userId) {
466
467
  return this.post("/api/v1/memory/stats", { user_id: this.uid(userId) });
467
468
  }
468
469
  async usage(days = 7) {
@@ -471,13 +472,13 @@ export class Client {
471
472
  }
472
473
  return this.request("GET", `/api/v1/won/usage?days=${days}`);
473
474
  }
474
- get(userId, memoryId) {
475
+ async get(userId, memoryId) {
475
476
  if (typeof memoryId !== "string" || !memoryId.trim()) {
476
477
  throw new Error("memoryId is required — the id that add/store or listMemories returned.");
477
478
  }
478
479
  return this.post("/api/v1/memory/get", { user_id: this.uid(userId), memory_id: memoryId.trim() }).then((r) => asObj(r.memory));
479
480
  }
480
- listMemories(userId, opts = {}) {
481
+ async listMemories(userId, opts = {}) {
481
482
  const body = { user_id: this.uid(userId), limit: opts.limit ?? 100 };
482
483
  if (opts.cursor)
483
484
  body.cursor = opts.cursor;
@@ -505,13 +506,19 @@ export class Client {
505
506
  out.push(m);
506
507
  return out;
507
508
  }
508
- getImage(userId, memoryId) {
509
+ async exportImages(userId, opts = {}) {
510
+ const out = [];
511
+ for await (const m of this.iterImages(userId, opts))
512
+ out.push(m);
513
+ return out;
514
+ }
515
+ async getImage(userId, memoryId) {
509
516
  if (typeof memoryId !== "string" || !memoryId.trim()) {
510
517
  throw new Error("memoryId is required — the id that add/store or listImages returned.");
511
518
  }
512
519
  return this.requestBytes("/api/v1/memory/image", { user_id: this.uid(userId), memory_id: memoryId.trim() });
513
520
  }
514
- forgetImage(userId, memoryId, opts = {}) {
521
+ async forgetImage(userId, memoryId, opts = {}) {
515
522
  if (typeof memoryId !== "string" || !memoryId.trim()) {
516
523
  throw new Error("memoryId is required — the id of the memory whose image you want removed.");
517
524
  }
@@ -520,7 +527,7 @@ export class Client {
520
527
  body.preview = true;
521
528
  return this.request("DELETE", "/api/v1/memory/image", body);
522
529
  }
523
- listImages(userId, opts = {}) {
530
+ async listImages(userId, opts = {}) {
524
531
  const body = { user_id: this.uid(userId) };
525
532
  if (opts.limit !== undefined)
526
533
  body.limit = opts.limit;
@@ -550,7 +557,7 @@ export class Client {
550
557
  throw new Error(`stopped after ${MAX_PAGES} pages — the store did not end. This is a truncated ` +
551
558
  `answer, not the whole store.`);
552
559
  }
553
- revisions(userId, opts = {}) {
560
+ async revisions(userId, opts = {}) {
554
561
  const body = {};
555
562
  if (opts.include !== undefined)
556
563
  body.include = opts.include;
@@ -563,13 +570,13 @@ export class Client {
563
570
  body.user_id = this.uid(userId);
564
571
  return this.post("/api/v1/won/revisions", body);
565
572
  }
566
- lineage(userId, memoryId) {
573
+ async lineage(userId, memoryId) {
567
574
  if (typeof memoryId !== "string" || !memoryId.trim()) {
568
575
  throw new Error("memoryId is required — the memory whose history you want.");
569
576
  }
570
577
  return this.post("/api/v1/memory/lineage", { user_id: this.uid(userId), memory_id: memoryId.trim() });
571
578
  }
572
- bySpeaker(speaker, userId, opts = {}) {
579
+ async bySpeaker(speaker, userId, opts = {}) {
573
580
  if (!speaker || typeof speaker !== "string" || speaker.trim() === "") {
574
581
  throw new Error('speaker is required — "me" for the assistant, or a person\'s name.');
575
582
  }
@@ -593,41 +600,42 @@ export class Client {
593
600
  async listEngrams() {
594
601
  const data = await this.request("GET", "/api/v1/engram");
595
602
  return {
603
+ ...data,
596
604
  engrams: asRecords(data.engrams),
597
605
  forms: asRecords(data.forms),
598
606
  note: typeof data.note === "string" ? data.note : undefined,
599
607
  };
600
608
  }
601
- createStore(userId) {
609
+ async createStore(userId) {
602
610
  return this.post("/api/v1/memory/collection", { user_id: this.uid(userId) });
603
611
  }
604
612
  async listStores() {
605
613
  const r = await this.request("GET", "/api/v1/memory/collections");
606
614
  return asRecords(r.collections);
607
615
  }
608
- deleteStore(userId) {
616
+ async deleteStore(userId) {
609
617
  if (typeof userId !== "string" || !userId.trim())
610
618
  throw new Error("userId is required (a non-blank string) — deleteStore never falls back to the default store.");
611
619
  warnIfStoreIdCollapses(userId);
612
620
  return this.request("DELETE", "/api/v1/memory/collection", { user_id: userId });
613
621
  }
614
- addSpeaker(speaker, userId) {
622
+ async addSpeaker(speaker, userId) {
615
623
  return this.post("/api/v1/memory/speakers", { user_id: this.uid(userId), speaker });
616
624
  }
617
625
  async listSpeakers(userId) {
618
626
  const r = await this.request("GET", `/api/v1/memory/speakers?user_id=${encodeURIComponent(this.uid(userId))}`);
619
627
  return { ...r, speakers: asRecords(r?.speakers) };
620
628
  }
621
- removeSpeaker(speaker, userId) {
629
+ async removeSpeaker(speaker, userId) {
622
630
  return this.request("DELETE", "/api/v1/memory/speakers", { user_id: this.uid(userId), speaker });
623
631
  }
624
- delete(userId, memoryId) {
632
+ async delete(userId, memoryId) {
625
633
  if (typeof memoryId !== "string" || !memoryId.trim()) {
626
634
  throw new Error("memoryId is required (non-blank). To delete every memory in a store, call deleteAll(userId) explicitly.");
627
635
  }
628
636
  return this.post("/api/v1/memory/forget", { user_id: this.uid(userId), memory_id: memoryId.trim() });
629
637
  }
630
- deleteAll(userId) {
638
+ async deleteAll(userId) {
631
639
  if (typeof userId !== "string" || !userId.trim())
632
640
  throw new Error("userId is required (a non-blank string) for deleteAll — anything else would wipe the default store.");
633
641
  warnIfStoreIdCollapses(userId);
@@ -762,7 +770,9 @@ export class Client {
762
770
  if (e?.name === "AbortError" || att.signal.aborted) {
763
771
  throw new APIConnectionError(0, this.abortMessage(att.why()));
764
772
  }
765
- throw e;
773
+ if (e instanceof WosError)
774
+ throw e;
775
+ throw new APIConnectionError(0, `network error: ${e?.message ?? e}`);
766
776
  }
767
777
  if (buf.byteLength === 0) {
768
778
  throw new WosError(res.status, "empty image body — the service returned no bytes");
@@ -895,7 +905,14 @@ export class Client {
895
905
  }
896
906
  if (e instanceof WosError)
897
907
  throw e;
898
- throw new APIConnectionError(0, `network error: ${e?.message ?? e}`);
908
+ if (!(IDEMPOTENT_METHODS.has(method.toUpperCase()) && attempt + 1 < attempts)) {
909
+ throw new APIConnectionError(0, `network error: ${e?.message ?? e}`);
910
+ }
911
+ att.clear();
912
+ const delay = this.backoffMs(attempt);
913
+ logDebug(`${method} ${logPath} — body dropped mid-stream, retrying in ${delay}ms (attempt ${attempt + 1}/${attempts})`);
914
+ await this.backoffSleep(delay, deadlineAt);
915
+ continue;
899
916
  }
900
917
  finally {
901
918
  att.clear();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wontopos",
3
- "version": "2.2.38",
3
+ "version": "2.2.39",
4
4
  "description": "Wontopos \u2014 long-term memory for AI agents. Identical recall in every language, ~100\u00d7 lower LLM bill.",
5
5
  "keywords": [
6
6
  "memory",