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 +18 -6
- package/dist/wontopos.d.ts +57 -23
- package/dist/wontopos.js +84 -45
- package/package.json +4 -3
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 (
|
|
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
|
-
|
|
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 —
|
|
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
|
package/dist/wontopos.d.ts
CHANGED
|
@@ -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.
|
|
88
|
-
*
|
|
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
|
-
/**
|
|
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.
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
817
|
-
*
|
|
818
|
-
*
|
|
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
|
-
* `
|
|
874
|
-
*
|
|
875
|
-
*
|
|
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.
|
|
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
|
-
|
|
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
|
|
100
|
-
* this to 50,000 entries,
|
|
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
|
-
//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
//
|
|
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
|
-
//
|
|
161
|
-
//
|
|
162
|
-
//
|
|
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.
|
|
313
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
885
|
-
*
|
|
886
|
-
*
|
|
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
|
|
919
|
-
//
|
|
920
|
-
//
|
|
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
|
|
949
|
-
//
|
|
950
|
-
// reads the request as the whole-store form
|
|
951
|
-
// this SDK to be wrong.
|
|
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
|
-
* `
|
|
1073
|
-
*
|
|
1074
|
-
*
|
|
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.
|
|
1213
|
-
//
|
|
1214
|
-
//
|
|
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
|
-
//
|
|
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.
|
|
4
|
-
"description": "Wontopos
|
|
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
|
|
41
|
+
"test": "npm run build && node --test tests/client.test.mjs",
|
|
41
42
|
"prepublishOnly": "npm run build"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|