wontopos 2.2.33 → 2.2.34

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
@@ -4,10 +4,13 @@
4
4
  npm install wontopos
5
5
  ```
6
6
 
7
+ Get an API key in the [console](https://wontopos.com). Keys look like `wos-live-...`;
8
+ the client also reads `WONTOPOS_API_KEY` from the environment.
9
+
7
10
  ```ts
8
11
  import { Client } from "wontopos";
9
12
 
10
- const mem = new Client({ apiKey: "wos-..." });
13
+ const mem = new Client({ apiKey: "wos-live-..." });
11
14
 
12
15
  // Each end-user / agent / topic gets its own store — create it once.
13
16
  // (A `default` store already exists, so you can skip this and omit the id.)
@@ -20,15 +23,15 @@ const ctx = await mem.recall("what does alice drink?", "alice");
20
23
 
21
24
  ## Why
22
25
 
23
- - **Pure semantic retrieval** — no keyword/BM25. Identical recall in every language (한국어 · 日本語 · 中文 · English).
26
+ - **Pure semantic retrieval** — no keyword/BM25. Identical recall in every language (Korean · Japanese · Chinese · English).
24
27
  - **No LLM in the loop** — `store` / `search` / `recall` never call a language model. You pay embeddings, not generation.
25
28
  - **Bounded retrieval** — `recall()` returns a small, fixed-size slice (~1,200 tokens) regardless of how much you've stored.
26
29
 
27
30
  ## Methods
28
31
 
29
- `add` · `addTurn` · `addBulk` · `update` · `search` · `recall` · `history` · `stats` · `get` · `listMemories` · `delete` · `deleteAll`
32
+ `add` · `addTurn` · `addBulk` · `update` · `search` · `searchFull` · `recall` · `history` · `stats` · `get` · `listMemories` · `delete` · `deleteAll`
30
33
 
31
- All methods take `{ userId }` — it names the **store**: one isolated memory space per end-user, agent, or topic, then per account. WHO said each memory inside a store is the `speaker` tag below — storing the assistant's own words never needs a separate id.
34
+ Every method takes the store id as its second argument — `mem.add("...", "alice")`. Omit it to use the client's default store. A store is one isolated memory space per end-user, agent, or topic. WHO said each memory inside a store is the `speaker` tag below — storing the assistant's own words never needs a separate id.
32
35
 
33
36
  ## Who said it (speakers)
34
37
 
@@ -87,6 +90,9 @@ egress proxy this SDK could not connect at all — while the Python and Rust SDK
87
90
  through, because `requests` and `reqwest` both read those variables. Pass a
88
91
  proxy-aware `fetch` and the three behave the same:
89
92
 
93
+ This one needs `npm i undici` — Node's global `fetch` is built on undici, but the
94
+ package itself is not installed for you.
95
+
90
96
  ```ts
91
97
  import { ProxyAgent } from "undici";
92
98
 
@@ -126,10 +132,17 @@ update. `search` options are typed too (`SearchOptions`: `cache_control`,
126
132
  `speaker`, plus pass-through).
127
133
 
128
134
  ```ts
135
+ import { type Memory } from "wontopos";
136
+
129
137
  const hits: Memory[] = await mem.search("what did Bob say?", "alice", 10, { speaker: "Bob" });
130
138
  console.log(hits[0].speaker, hits[0].time_bucket); // typed: string | undefined
131
139
  ```
132
140
 
141
+ `hits` arrives best-first — take it in the order given. `similarity` is a raw
142
+ closeness score, not the ranking key: what produces the order is internal and is
143
+ not returned, so sorting by `similarity` overrides the ranking and makes results
144
+ worse. There is no `score` field.
145
+
133
146
  ## Errors
134
147
 
135
148
  Any non-2xx response throws `WosError` with `status`, `message`, and — when the
@@ -162,8 +175,7 @@ const mem = new Client({ apiKey: "...", baseUrl: "https://wos.your-host.com" });
162
175
 
163
176
  ## Reporting a bug
164
177
 
165
- Found something wrong, or something that looks unsafe? Tell us — one person reads
166
- every report.
178
+ Found something wrong, or something that looks unsafe? Tell us — every report gets read.
167
179
 
168
180
  - Bugs: <https://wontopos.com/contact?topic=bug>
169
181
  - Security: <https://wontopos.com/contact?topic=security> (also published at
@@ -84,8 +84,8 @@ export declare class RateLimitError extends WosError {
84
84
  * `501` is NOT transient. It means the engine behind the model you selected does
85
85
  * not implement that endpoint at all (the error carries `model` and `endpoint`).
86
86
  * Retrying can never succeed — pick a model that supports it (`listModels`) or
87
- * drop the call. This class used to say "safe to retry" for the whole 5xx range,
88
- * which sent callers into a loop that could not end.
87
+ * drop the call. Treating the whole 5xx range as retryable sends a caller into a
88
+ * loop that cannot end.
89
89
  */
90
90
  export declare class ServerError extends WosError {
91
91
  }
@@ -99,7 +99,10 @@ export interface Memory {
99
99
  content?: string;
100
100
  /** Cognitive category, e.g. "general". */
101
101
  category?: string;
102
- /** Semantic relevance to the query — higher is closer. This IS the relevance score; there is no `score` field. */
102
+ /** Raw closeness to the query — higher is closer. NOT the ranking key: results already
103
+ * arrive best-first, and what produces that order is internal and not returned, so
104
+ * re-sorting by `similarity` overrides the ranking and makes results worse. Take the
105
+ * list in the order given. There is no `score` field. */
103
106
  similarity?: number;
104
107
  /** Learned importance weight of the memory. */
105
108
  importance?: number;
@@ -231,7 +234,7 @@ export interface RevisionsResult {
231
234
  revised?: number;
232
235
  /** Memories nothing has touched since they were written. Always `total - revised`. */
233
236
  unrevised?: number;
234
- /** Every memory in the store. Atoms — the fragments search uses internally — are not counted. */
237
+ /** Every memory in the store. Internal records nobody stored directly are not counted. */
235
238
  total?: number;
236
239
  /** What `revised` counts, spelled out by the service. */
237
240
  counts?: string;
@@ -280,7 +283,7 @@ export interface LineageResult {
280
283
  export interface SpeakerPage {
281
284
  speaker?: string;
282
285
  memories: Memory[];
283
- /** Sentence-level fragments behind these memories — what a delete would actually remove. */
286
+ /** How many underlying records a delete would actually remove, beyond what was returned. */
284
287
  chunks?: number;
285
288
  points_to_delete?: number;
286
289
  returned?: number;
@@ -296,6 +299,25 @@ export interface UpdateResult {
296
299
  status?: string;
297
300
  [key: string]: unknown;
298
301
  }
302
+ /** `searchFull` → everything one search answered with, not just the merged memories.
303
+ *
304
+ * `search` returns the memories and nothing else, which is the right answer for
305
+ * almost every call. Two options make it the wrong one: `max_images` asks for a lane
306
+ * that is delivered SEPARATELY (Tablet 2 splits photo results from text), and
307
+ * `verify` is reported on by `verify_used`. Merging away both means paying for a
308
+ * search you shaped and never seeing what came of it. */
309
+ export interface SearchResult {
310
+ /** The main lane — what others said / general memories. */
311
+ memories: Memory[];
312
+ /** The assistant's own words (speaker "me"); `[]` on a model without a self lane. */
313
+ self_memories: Memory[];
314
+ /** Image memories, when `max_images` asked for any; `[]` otherwise. */
315
+ images: Memory[];
316
+ /** Re-ask passes actually performed. Present only when `verify` was sent; lower
317
+ * than you asked for means the store had nothing further to add. */
318
+ verify_used?: number;
319
+ [key: string]: unknown;
320
+ }
299
321
  export interface RecallResult {
300
322
  short_term: {
301
323
  turns: unknown[];
@@ -452,7 +474,7 @@ export interface SearchOptions {
452
474
  * that need several distinct memories from far apart in the history; it does little
453
475
  * on a single-fact lookup.
454
476
  *
455
- * ⚠️Requires a re-ask-capable model. Older ones REFUSE the call (403) rather than
477
+ * Requires a re-ask-capable model. Older ones REFUSE the call (403) rather than
456
478
  * quietly charging you for passes that never happened.
457
479
  */
458
480
  verify?: number;
@@ -461,7 +483,7 @@ export interface SearchOptions {
461
483
  * Out of range is refused, not clamped — silently cutting 5 to 1 would leave you
462
484
  * believing you got five.
463
485
  *
464
- * ⚠️Requires an image-capable model, and is refused (403) on one without it rather
486
+ * Requires an image-capable model, and is refused (403) on one without it rather
465
487
  * than answering with no images.
466
488
  */
467
489
  max_images?: number;
@@ -578,8 +600,7 @@ export declare class Client {
578
600
  withRetries(maxRetries: number): Client;
579
601
  /** Store one memory. `metadata` optional — e.g. `{ event_date: "2026-03-01" }` for when the
580
602
  * content actually happened. A plain date works (read as UTC midnight); a full RFC3339
581
- * timestamp also works. The API used to demand the full form and answered a bare date with
582
- * an opaque 400, which is why older examples here show the long one.
603
+ * timestamp also works; older examples here show the long form.
583
604
  * `metadata.speaker`: "me" = the assistant's own words, or a person's name (up to 50 per store).
584
605
  *
585
606
  * `opts.idempotencyKey`: see {@link WriteOptions} — pass one to make YOUR retry
@@ -610,6 +631,21 @@ export declare class Client {
610
631
  * speaker "me"), kept separate so the reader never confuses who said what. On a model
611
632
  * without a self lane, `self_memories` is `[]`. `userId` optional (default store). */
612
633
  searchSelf(query: string, userId?: string, limit?: number, opts?: SearchOptions): Promise<SelfSearchResult>;
634
+ /**
635
+ * Search, and keep every lane the answer came in.
636
+ *
637
+ * Same request as {@link Client.search} — reach for this one when the options make
638
+ * the merged array an incomplete answer:
639
+ *
640
+ * const r = await mem.searchFull("the day we moved", "alice", 10,
641
+ * { max_images: 3, verify: 2 });
642
+ * r.images.length; // the photo lane, which `search` drops
643
+ * r.verify_used; // how many re-ask passes actually ran (you are billed per pass)
644
+ *
645
+ * Both options need Tablet 2 or newer and are refused (403) on an older engine
646
+ * rather than accepted and ignored.
647
+ */
648
+ searchFull(query: string, userId?: string, limit?: number, opts?: SearchOptions): Promise<SearchResult>;
613
649
  /** One-call LLM context: short-term turns + long-term matches + surrounding context.
614
650
  * `form` ("memoir"/"archive", Scroll 1.2+) renders each long-term memory's time in
615
651
  * that form; `tz` is your UTC-offset hours for that rendering. */
@@ -620,7 +656,7 @@ export declare class Client {
620
656
  * Long-term memories to recall, 5–20. Default 10. Out of range is refused, not
621
657
  * clamped — asking for 20 and silently getting 10 reads as "that is all there is".
622
658
  *
623
- * ⚠️Needs a limit-aware model. An older one recalls a fixed ten no matter what you
659
+ * Needs a limit-aware model. An older one recalls a fixed ten no matter what you
624
660
  * send, so the API refuses the call (403) rather than answering with a number you
625
661
  * did not ask for.
626
662
  */
@@ -735,10 +771,10 @@ export declare class Client {
735
771
  *
736
772
  * Counts memories a transform touched (supersede, update, retract, image removed).
737
773
  * Deletions are NOT counted: a deleted memory leaves nothing to count. Neither are
738
- * atoms, the sentence fragments search uses internally — nobody stored one, so they
739
- * do not belong in a ratio that answers "how much of MY memory changed".
774
+ * the internal records derived from what you stored — nobody stored those directly,
775
+ * so they do not belong in a ratio that answers "how much of MY memory changed".
740
776
  *
741
- * ★By default this returns COUNTS ONLY, and the answer is the same size for a store
777
+ * By default this returns COUNTS ONLY, and the answer is the same size for a store
742
778
  * of a hundred memories and a store of a hundred million. That is deliberate: a model
743
779
  * asks this mid-conversation, and an answer that grew with the store would be
744
780
  * unusable for exactly the customers who most need to ask.
@@ -759,10 +795,10 @@ export declare class Client {
759
795
  * }
760
796
  * ```
761
797
  *
762
- * ⚠️Pages are ordered by when each memory was STORED, not by when it was edited.
798
+ * Pages are ordered by when each memory was STORED, not by when it was edited.
763
799
  * The response says so in `ordered_by`.
764
800
  *
765
- * ★Served from `/api/v1/won/*`, not `/api/v1/memory/*`. Won is the surface for calls
801
+ * Served from `/api/v1/won/*`, not `/api/v1/memory/*`. Won is the surface for calls
766
802
  * a model makes ABOUT its memory rather than calls an application makes WITH it, and
767
803
  * the address says so. The old path still answers, for clients published before
768
804
  * 2026-08-18, and both share one rate-limit budget.
@@ -813,10 +849,9 @@ export declare class Client {
813
849
  /**
814
850
  * The engrams (and delivery forms) the selected model can actually run.
815
851
  *
816
- * `engram()` could always RUN one, but there was no way to ASK what exists — so a
817
- * caller had to hard-code names from the docs, and every engram we shipped after
818
- * their code was written stayed invisible to them. The MCP server hit exactly this
819
- * and stopped hard-coding for the same reason. The service is the authority: what
852
+ * Ask rather than hard-code: a name copied from the docs freezes a caller to the
853
+ * catalogue as it was that day, and anything added later stays invisible. The
854
+ * service is the authority. What
820
855
  * comes back depends on the model (delivery forms need Scroll 1.2+), so pass
821
856
  * `withModel()` if you want another model's catalogue.
822
857
  *
@@ -870,10 +905,9 @@ export declare class Client {
870
905
  private readCapped;
871
906
  /** The same cap, for a body that is not text.
872
907
  *
873
- * `getImage` used to call `arrayBuffer()` here, which buffers whatever arrives. The
874
- * cap exists for a hostile or broken `baseUrl`, and a photo endpoint is exactly where
875
- * that shows up — the JSON paths were guarded while the one path that returns
876
- * megabytes was not. One implementation, two callers, so they cannot drift. */
908
+ * `arrayBuffer()` would buffer whatever arrives. The cap exists for a hostile or
909
+ * broken `baseUrl`, and an endpoint that returns megabytes is where that costs the
910
+ * most. One implementation, two callers, so they cannot drift. */
877
911
  private readCappedBytes;
878
912
  private request;
879
913
  }
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.33";
40
+ const VERSION = "2.2.34";
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). */
@@ -55,7 +55,14 @@ function logDebug(msg) {
55
55
  }
56
56
  }
57
57
  const DEFAULT_BASE_URL = "https://api.wontopos.com";
58
- const DEFAULT_MODEL = "tablet-1";
58
+ /** The engine every call uses unless the caller names another.
59
+ *
60
+ * Tablet 2 costs the same per token as Tablet 1 and is the only released line that
61
+ * can serve images, re-ask passes (`verify`), and the self lane, so a caller who
62
+ * names nothing gets the engine that can answer the most. All models read the same
63
+ * memory, so switching is a header, not a migration. Pin an older one explicitly
64
+ * with `new Client({ apiKey, model: "tablet-1" })` or `withModel("tablet-1")`. */
65
+ const DEFAULT_MODEL = "tablet-2";
59
66
  /** 429 = rate-limited BEFORE processing → always safe to retry (no write, no bill). */
60
67
  const RETRY_ALWAYS = new Set([429]);
61
68
  /** 502/503 are ambiguous for a write (gateway may return them AFTER the backend
@@ -96,8 +103,9 @@ function normalizeStoreId(id) {
96
103
  * Bounded on purpose. The warning fires on ids that fold, and the shape it exists
97
104
  * to catch is an email — which this SDK's own documented pattern ("one store per
98
105
  * 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`
106
+ * never released, even when the client is. A server that forwards 50,000 distinct user
107
+ * ids would grow this to 50,000 entries, leaking in exactly the case the warning
108
+ * exists to catch. A `Set`
101
109
  * keeps insertion order, so the oldest entry is the one evicted; dropping the
102
110
  * oldest rather than refusing new ones means a collision that first shows up late
103
111
  * still gets its one warning. */
@@ -151,16 +159,15 @@ function normalizeImage(image) {
151
159
  if (typeof image.data !== "string" || image.data.trim() === "") {
152
160
  throw new Error("image.data is required — base64 of the image (a data: URL is fine).");
153
161
  }
154
- // 🔴**다듬고 나서 접두어를 본다.** 예전엔 원본에 바로 `startsWith("data:")` 를
155
- // 걸어서, 앞에 공백이 하나만 있어도(` data:image/png;base64,…`) 접두어를 못 떼고
156
- // **`data:image/png;base64,` 문자열까지 통째로 base64 로** 보냈다. 엔진은
157
- // "이미지를 읽을 수 없다"며 400 을 내고, 부르는 쪽은 왜인지 모른다.
162
+ // Trim before looking for the prefix. Checked against the raw string, one leading
163
+ // space (` data:image/png;base64,…`) hides the prefix, and the literal
164
+ // `data:image/png;base64,` travels as part of the base64. The engine answers 400
165
+ // "image could not be read" and the caller has no way to tell why.
158
166
  const raw = image.data.trim();
159
167
  const comma = raw.startsWith("data:") ? raw.indexOf(",") : -1;
160
- // 🔴그리고 **가운데 공백까지** 없앤다. `base64` 명령과 `openssl base64` 는 76자마다
161
- // 줄을 바꾼다 — 파일을 그렇게 만들어 붙여넣는 건 흔한 길인데, 양끝만 다듬으면
162
- // 그 줄바꿈이 그대로 payload 에 남는다. base64 알파벳에 공백은 없으므로
163
- // 전부 빼는 게 안전하다.
168
+ // Strip whitespace in the middle as well. `base64` and `openssl base64` wrap at 76
169
+ // columns, and trimming only the ends leaves those line breaks in the payload. The
170
+ // base64 alphabet contains no whitespace, so removing all of it is safe.
164
171
  const data = (comma === -1 ? raw : raw.slice(comma + 1)).replace(/\s+/g, "");
165
172
  if (data === "")
166
173
  throw new Error("image.data is empty after stripping its data: prefix.");
@@ -309,8 +316,8 @@ export class RateLimitError extends WosError {
309
316
  * `501` is NOT transient. It means the engine behind the model you selected does
310
317
  * not implement that endpoint at all (the error carries `model` and `endpoint`).
311
318
  * Retrying can never succeed — pick a model that supports it (`listModels`) or
312
- * drop the call. This class used to say "safe to retry" for the whole 5xx range,
313
- * which sent callers into a loop that could not end.
319
+ * drop the call. Treating the whole 5xx range as retryable sends a caller into a
320
+ * loop that cannot end.
314
321
  */
315
322
  export class ServerError extends WosError {
316
323
  }
@@ -545,8 +552,7 @@ export class Client {
545
552
  // store (set via `new Client({ apiKey, userId })` or `withUser`).
546
553
  /** Store one memory. `metadata` optional — e.g. `{ event_date: "2026-03-01" }` for when the
547
554
  * content actually happened. A plain date works (read as UTC midnight); a full RFC3339
548
- * timestamp also works. The API used to demand the full form and answered a bare date with
549
- * an opaque 400, which is why older examples here show the long one.
555
+ * timestamp also works; older examples here show the long form.
550
556
  * `metadata.speaker`: "me" = the assistant's own words, or a person's name (up to 50 per store).
551
557
  *
552
558
  * `opts.idempotencyKey`: see {@link WriteOptions} — pass one to make YOUR retry
@@ -604,6 +610,32 @@ export class Client {
604
610
  const r = await this.post("/api/v1/memory/search", { ...opts, user_id: this.uid(userId), query, max_results: limit });
605
611
  return { memories: asRecords(r.memories), self_memories: asRecords(r.self_memories) };
606
612
  }
613
+ /**
614
+ * Search, and keep every lane the answer came in.
615
+ *
616
+ * Same request as {@link Client.search} — reach for this one when the options make
617
+ * the merged array an incomplete answer:
618
+ *
619
+ * const r = await mem.searchFull("the day we moved", "alice", 10,
620
+ * { max_images: 3, verify: 2 });
621
+ * r.images.length; // the photo lane, which `search` drops
622
+ * r.verify_used; // how many re-ask passes actually ran (you are billed per pass)
623
+ *
624
+ * Both options need Tablet 2 or newer and are refused (403) on an older engine
625
+ * rather than accepted and ignored.
626
+ */
627
+ async searchFull(query, userId, limit = 10, opts = {}) {
628
+ warnOnUnknownFilters(opts.filters);
629
+ const r = await this.post("/api/v1/memory/search", { ...opts, user_id: this.uid(userId), query, max_results: limit });
630
+ // Spread first so a lane added later still arrives; the known ones are then
631
+ // normalized, because a broken proxy can null any of them.
632
+ return {
633
+ ...r,
634
+ memories: asRecords(r.memories),
635
+ self_memories: asRecords(r.self_memories),
636
+ images: asRecords(r.images),
637
+ };
638
+ }
607
639
  /** One-call LLM context: short-term turns + long-term matches + surrounding context.
608
640
  * `form` ("memoir"/"archive", Scroll 1.2+) renders each long-term memory's time in
609
641
  * that form; `tz` is your UTC-offset hours for that rendering. */
@@ -759,12 +791,23 @@ export class Client {
759
791
  async *iterImages(userId, opts = {}) {
760
792
  let before;
761
793
  let skipIds;
794
+ // Same repeat-cursor guard as `iterMemories`. A server that hands back the
795
+ // cursor it was just given would otherwise re-yield one page MAX_PAGES times,
796
+ // and the caller reads those repeats as more images. The page cap alone bounds
797
+ // the walk; it does not stop the duplicates.
798
+ const seen = new Set();
762
799
  for (let page = 0; page < MAX_PAGES; page++) {
763
800
  const p = await this.listImages(userId, { limit: opts.pageSize, before, skipIds });
764
801
  for (const m of asRecords(p.images))
765
802
  yield m;
766
803
  if (!p.has_more || !p.next_before)
767
804
  break;
805
+ // The cursor is the PAIR: several images can share a timestamp, so `before`
806
+ // alone repeats across pages legitimately.
807
+ const key = `${p.next_before}|${(Array.isArray(p.next_skip_ids) ? p.next_skip_ids : []).join(",")}`;
808
+ if (seen.has(key))
809
+ break;
810
+ seen.add(key);
768
811
  before = p.next_before;
769
812
  skipIds = Array.isArray(p.next_skip_ids) ? p.next_skip_ids : undefined;
770
813
  }
@@ -780,10 +823,10 @@ export class Client {
780
823
  *
781
824
  * Counts memories a transform touched (supersede, update, retract, image removed).
782
825
  * Deletions are NOT counted: a deleted memory leaves nothing to count. Neither are
783
- * atoms, the sentence fragments search uses internally — nobody stored one, so they
784
- * do not belong in a ratio that answers "how much of MY memory changed".
826
+ * the internal records derived from what you stored — nobody stored those directly,
827
+ * so they do not belong in a ratio that answers "how much of MY memory changed".
785
828
  *
786
- * ★By default this returns COUNTS ONLY, and the answer is the same size for a store
829
+ * By default this returns COUNTS ONLY, and the answer is the same size for a store
787
830
  * of a hundred memories and a store of a hundred million. That is deliberate: a model
788
831
  * asks this mid-conversation, and an answer that grew with the store would be
789
832
  * unusable for exactly the customers who most need to ask.
@@ -804,10 +847,10 @@ export class Client {
804
847
  * }
805
848
  * ```
806
849
  *
807
- * ⚠️Pages are ordered by when each memory was STORED, not by when it was edited.
850
+ * Pages are ordered by when each memory was STORED, not by when it was edited.
808
851
  * The response says so in `ordered_by`.
809
852
  *
810
- * ★Served from `/api/v1/won/*`, not `/api/v1/memory/*`. Won is the surface for calls
853
+ * Served from `/api/v1/won/*`, not `/api/v1/memory/*`. Won is the surface for calls
811
854
  * a model makes ABOUT its memory rather than calls an application makes WITH it, and
812
855
  * the address says so. The old path still answers, for clients published before
813
856
  * 2026-08-18, and both share one rate-limit budget.
@@ -881,10 +924,9 @@ export class Client {
881
924
  /**
882
925
  * The engrams (and delivery forms) the selected model can actually run.
883
926
  *
884
- * `engram()` could always RUN one, but there was no way to ASK what exists — so a
885
- * caller had to hard-code names from the docs, and every engram we shipped after
886
- * their code was written stayed invisible to them. The MCP server hit exactly this
887
- * and stopped hard-coding for the same reason. The service is the authority: what
927
+ * Ask rather than hard-code: a name copied from the docs freezes a caller to the
928
+ * catalogue as it was that day, and anything added later stays invisible. The
929
+ * service is the authority. What
888
930
  * comes back depends on the model (delivery forms need Scroll 1.2+), so pass
889
931
  * `withModel()` if you want another model's catalogue.
890
932
  *
@@ -915,9 +957,9 @@ export class Client {
915
957
  }
916
958
  /** Delete a store and ALL its memories. Returns `{ user_id, status }`. */
917
959
  deleteStore(userId) {
918
- // Validate BEFORE warning. The warning helper lowercases the id, so a non-string
919
- // used to die there as "id.toLowerCase is not a function" — the caller got a
920
- // TypeError from our internals instead of being told what was wrong with the id.
960
+ // Validate before warning. The warning helper lowercases the id, so a non-string
961
+ // reaches it and dies as "id.toLowerCase is not a function" — a TypeError from
962
+ // inside the SDK instead of a sentence about the id.
921
963
  if (typeof userId !== "string" || !userId.trim())
922
964
  throw new Error("userId is required (a non-blank string) — deleteStore never falls back to the default store.");
923
965
  warnIfStoreIdCollapses(userId); // destructive: the collision warning belongs here too
@@ -945,11 +987,10 @@ export class Client {
945
987
  // Guard: without a memory_id the API's forget endpoint means "delete the
946
988
  // whole store". An undefined/"" slipping in here must never become a wipe.
947
989
  //
948
- // `.trim()` matters as much as the emptiness check: " " is truthy, so it used to
949
- // pass this guard and travel as memory_id. A server that trims it back to nothing
950
- // reads the request as the whole-store form — the single most destructive way for
951
- // this SDK to be wrong. `deleteAll` below already trimmed; the more dangerous path
952
- // was the one that did not.
990
+ // `.trim()` matters as much as the emptiness check: " " is truthy, so without it
991
+ // a blank string passes the guard and travels as memory_id. A server that trims it
992
+ // back to nothing reads the request as the whole-store form, which is the most
993
+ // destructive way for this SDK to be wrong.
953
994
  if (typeof memoryId !== "string" || !memoryId.trim()) {
954
995
  throw new Error("memoryId is required (non-blank). To delete every memory in a store, call deleteAll(userId) explicitly.");
955
996
  }
@@ -1069,10 +1110,9 @@ export class Client {
1069
1110
  }
1070
1111
  /** The same cap, for a body that is not text.
1071
1112
  *
1072
- * `getImage` used to call `arrayBuffer()` here, which buffers whatever arrives. The
1073
- * cap exists for a hostile or broken `baseUrl`, and a photo endpoint is exactly where
1074
- * that shows up — the JSON paths were guarded while the one path that returns
1075
- * megabytes was not. One implementation, two callers, so they cannot drift. */
1113
+ * `arrayBuffer()` would buffer whatever arrives. The cap exists for a hostile or
1114
+ * broken `baseUrl`, and an endpoint that returns megabytes is where that costs the
1115
+ * most. One implementation, two callers, so they cannot drift. */
1076
1116
  async readCappedBytes(res) {
1077
1117
  const cl = res.headers.get("content-length");
1078
1118
  if (cl && Number(cl) > MAX_RESPONSE_BYTES) {
@@ -1209,10 +1249,9 @@ export class Client {
1209
1249
  msg = msg.slice(0, MAX_ERR_MSG) + "…(truncated)";
1210
1250
  throw errorFor(res.status, msg, requestId);
1211
1251
  }
1212
- // An empty body is only legal when the STATUS says there is no body. This
1213
- // used to accept any empty 2xx, so an `add()` answered by a truncating proxy
1214
- // returned `{}` and read as a successful write with no id — while the Python
1215
- // and Rust SDKs raised on the same response. Same rule in all three now.
1252
+ // An empty body is only legal when the STATUS says there is no body. Accepting
1253
+ // any empty 2xx would make an `add()` answered by a truncating proxy return `{}`,
1254
+ // which reads as a successful write with no id. All three SDKs apply this rule.
1216
1255
  if (!text) {
1217
1256
  if (NO_BODY_STATUS.has(res.status))
1218
1257
  return {};
@@ -1234,10 +1273,10 @@ export class Client {
1234
1273
  const got = data === null ? "null" : Array.isArray(data) ? "array" : typeof data;
1235
1274
  throw new WosError(res.status, `expected a JSON object in the response, got ${got}`);
1236
1275
  }
1237
- // 멱등키를 쓴 쓰기가 **저장된 것인지 재생된 것인지**를 호출자가 알아야 한다.
1238
- // 서버는 `Idempotent-Replayed: true` 로 말해주는데 지금까지 SDK 가 삼켰다 —
1239
- // 멱등키를 쓰는 사람이 가장 알고 싶은 사실이 그것이다(재시도가 실제로
1240
- // 저장을 일으켰나, 아니면 첫 응답이 되풀이된 것인가).
1276
+ // Surface whether the write was stored or replayed. The server says so with
1277
+ // `Idempotent-Replayed: true`, and it answers the question someone using an
1278
+ // idempotency key is asking: did my retry write, or is this the first response
1279
+ // coming back again?
1241
1280
  if (res.headers.get("Idempotent-Replayed") === "true") {
1242
1281
  data.replayed = true;
1243
1282
  }
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "wontopos",
3
- "version": "2.2.33",
4
- "description": "Wontopos — long-term memory for AI agents. Pure semantic retrieval, identical recall in every language, ~100× lower LLM bill.",
3
+ "version": "2.2.34",
4
+ "description": "Wontopos \u2014 long-term memory for AI agents. Pure semantic retrieval, identical recall in every language, ~100\u00d7 lower LLM bill.",
5
+ "repository": "github:wontopos/wontopos-typescript",
5
6
  "keywords": [
6
7
  "memory",
7
8
  "ai",
@@ -37,7 +38,7 @@
37
38
  },
38
39
  "scripts": {
39
40
  "build": "tsc",
40
- "test": "npm run build && node --test test/client.test.mjs",
41
+ "test": "npm run build && node --test tests/client.test.mjs",
41
42
  "prepublishOnly": "npm run build"
42
43
  },
43
44
  "devDependencies": {