wontopos 2.2.46 → 2.2.47
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 +118 -10
- package/dist/cjs/wontopos.d.cts +56 -27
- package/dist/cjs/wontopos.js +96 -20
- package/dist/wontopos.d.ts +56 -27
- package/dist/wontopos.js +95 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,7 +29,42 @@ const ctx = await mem.recall("what does alice drink?", "alice");
|
|
|
29
29
|
|
|
30
30
|
## Methods
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
| Method | What it does |
|
|
33
|
+
|---|---|
|
|
34
|
+
| **Write** | |
|
|
35
|
+
| `add(content, userId?, metadata?, opts?)` | Store one memory. Alias `store`. `opts.image` attaches a photo. |
|
|
36
|
+
| `addTurn(userMsg, assistantMsg, userId?)` | Store one exchange of a conversation. |
|
|
37
|
+
| `addBulk(content, userId?, category?, timestamp?)` | Backfill a long history in one call. |
|
|
38
|
+
| `update(oldMemoryId, newContent, userId?)` | Replace a fact that changed. The old version stays in its history. |
|
|
39
|
+
| **Read** | |
|
|
40
|
+
| `recall(query, userId?, opts?)` | The recent turns, the memories that match and the conversation around the best one, in one call. |
|
|
41
|
+
| `search(query, userId?, limit?, opts?)` | The memories that match, best first. |
|
|
42
|
+
| `searchFull(...)` | The same search with each part kept apart, `images` and `verify_used` included. |
|
|
43
|
+
| `searchSelf(...)` | The assistant's own words, kept apart from what others said. |
|
|
44
|
+
| `history(userId?)` | The recent conversation. |
|
|
45
|
+
| `get(userId, memoryId)` | One memory by id. |
|
|
46
|
+
| `listMemories` · `iterMemories` · `exportMemories` | Every memory in a store: a page at a time, as a stream, or all at once. |
|
|
47
|
+
| `lineage(userId, memoryId)` | The edits that followed one memory. |
|
|
48
|
+
| `revisions(userId?, opts?)` | How many of a store's memories were edited, and which. |
|
|
49
|
+
| `stats(userId?)` | Counts for a store. |
|
|
50
|
+
| `engram(name, query, userId?)` · `listEngrams()` | Built-in retrieval routines, and the ones a model can run. |
|
|
51
|
+
| **Photos** | |
|
|
52
|
+
| `listImages` · `iterImages` · `exportImages` | A store's image memories, newest first. |
|
|
53
|
+
| `getImage(userId, memoryId)` | The bytes of one image. |
|
|
54
|
+
| `forgetImage(userId, memoryId, { preview })` | Remove the photo from a memory. `preview: true` first says whether the memory itself stays. |
|
|
55
|
+
| **People** | |
|
|
56
|
+
| `addSpeaker` · `listSpeakers` · `removeSpeaker` | Register, list and unregister the people a store hears from. |
|
|
57
|
+
| `bySpeaker(speaker, userId?)` | What one person said, newest first. |
|
|
58
|
+
| **Stores** | |
|
|
59
|
+
| `createStore` · `listStores` · `deleteStore` | One isolated memory space per end-user, agent or topic. |
|
|
60
|
+
| `delete(userId, memoryId)` · `deleteAll(userId)` | Delete one memory, or every memory in a store. |
|
|
61
|
+
| **Account** | |
|
|
62
|
+
| `listModels()` | The models you can use, and what each can do. |
|
|
63
|
+
| `usage(days?)` | What this key, its workspace and its stores have spent. Free. |
|
|
64
|
+
| `ping()` | Check the key and the connection. |
|
|
65
|
+
| **Client** | |
|
|
66
|
+
| `withModel` · `withUser` · `withDeadline` · `withTimeout` · `withRetries` · `withSignal` | A copy of the client with another model, store, budget, timeout, retry count or cancel signal. |
|
|
67
|
+
| `Client.fromEnv()` · `rateLimit` | A client with the key from the environment; the last call's rate-limit numbers. |
|
|
33
68
|
|
|
34
69
|
Where the store id goes depends on whether the method takes a payload. A method that
|
|
35
70
|
writes or searches puts the payload first and the store second — `mem.add("...", "alice")`,
|
|
@@ -41,6 +76,29 @@ an explicit `undefined` there, because a second required argument follows it. Py
|
|
|
41
76
|
omits it in all five, and Rust takes it positionally (`None` for the default store, where there is one).
|
|
42
77
|
Let your editor confirm the order: every signature is typed. 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.
|
|
43
78
|
|
|
79
|
+
## Putting recall into a prompt
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
const ctx = await mem.recall(userMessage, "alice");
|
|
83
|
+
|
|
84
|
+
const system = [
|
|
85
|
+
"What you remember about this user, most relevant first:",
|
|
86
|
+
...ctx.long_term.memories.map((m) => `- ${m.content}`),
|
|
87
|
+
"",
|
|
88
|
+
"Around the most relevant memory:",
|
|
89
|
+
...ctx.context.around_top_memory,
|
|
90
|
+
"",
|
|
91
|
+
"The conversation so far:",
|
|
92
|
+
...ctx.short_term.turns.map((t) => `${t.role}: ${t.content}`),
|
|
93
|
+
].join("\n");
|
|
94
|
+
|
|
95
|
+
// Call your language model with `system` and the user's message, then store the exchange:
|
|
96
|
+
await mem.addTurn(userMessage, reply, "alice");
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Each row of `around_top_memory` starts with `[before] `, `[match] ` or `[after] `: where it
|
|
100
|
+
sits next to the best memory. Keep every list in the order it arrives.
|
|
101
|
+
|
|
44
102
|
## Who said it (speakers)
|
|
45
103
|
|
|
46
104
|
Every memory can carry a speaker: `"me"` for the assistant's own words, or a
|
|
@@ -60,6 +118,40 @@ await mem.search("what did Bob say about deadlines?", "alice", 10, { speaker: "B
|
|
|
60
118
|
up to 50 people to start (a limit we plan to raise); `"me"` never needs
|
|
61
119
|
registration and never counts against it.
|
|
62
120
|
|
|
121
|
+
## Photos
|
|
122
|
+
|
|
123
|
+
On a model whose `listModels()` capabilities include `images`, a memory can carry a photo.
|
|
124
|
+
The caption is still required.
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
128
|
+
|
|
129
|
+
const data = readFileSync("beach.jpg").toString("base64");
|
|
130
|
+
const stored = await mem.add("At the beach with Bob", "alice", {}, { image: { data, taken_at: "2026-08-14" } });
|
|
131
|
+
|
|
132
|
+
const { bytes, contentType } = await mem.getImage("alice", stored.id!);
|
|
133
|
+
writeFileSync(`beach.${contentType.split("/")[1]}`, bytes);
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Both edges must be 700px or more, the long edge at most 2500px and the file at most 7MB;
|
|
137
|
+
anything else is refused (400) with the reason. A long edge over 1568px is stored at
|
|
138
|
+
1568px, so keep your own copy of the original. `search` returns image memories after
|
|
139
|
+
the others, and `listImages` pages through them newest first. `forgetImage` removes the
|
|
140
|
+
photo, and it can remove the whole memory with it: call it with `preview: true` first,
|
|
141
|
+
and `memory_kept` says whether the memory stays.
|
|
142
|
+
|
|
143
|
+
## Export, edit history and usage
|
|
144
|
+
|
|
145
|
+
- **Export:** `exportMemories(userId)` returns every memory in a store, the text you
|
|
146
|
+
stored and its metadata; `exportImages(userId)` every image memory. For a large store,
|
|
147
|
+
`iterMemories` and `iterImages` stream them a page at a time.
|
|
148
|
+
- **Edit history:** `update` replaces a fact that changed. `lineage(userId, memoryId)` lists
|
|
149
|
+
the versions from the id you pass forward, oldest first, and marks the one in force with
|
|
150
|
+
`is_current`; pass the first version's id to see the whole history. `revisions()` counts
|
|
151
|
+
how many of a store's memories were edited.
|
|
152
|
+
- **Usage:** `usage(days)` reports this key's spend, the workspace's spend over the window,
|
|
153
|
+
the stores that spent the most, and the balance. It is free, and works at a zero balance.
|
|
154
|
+
|
|
63
155
|
## Recall caching
|
|
64
156
|
|
|
65
157
|
Opt in per search and a repeated query reuses the previous result at 10% of the
|
|
@@ -85,12 +177,22 @@ Built in, no configuration needed:
|
|
|
85
177
|
|
|
86
178
|
- **Automatic retries** — 429 and a 409 for a write already in flight always, and 408 /
|
|
87
179
|
502 / 503 / 504 or a connection error only when a retry cannot double-process a write.
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
`
|
|
92
|
-
`
|
|
93
|
-
|
|
180
|
+
The calls that are safe to send again retry all four: the reads sent as POST (`search`,
|
|
181
|
+
`recall`, `get`, `listMemories`, `stats`, ...), the GET and DELETE calls (`ping`,
|
|
182
|
+
`listStores`, `listSpeakers`, `listModels`, `listEngrams`, `usage`, `deleteStore`,
|
|
183
|
+
`removeSpeaker`, `forgetImage`), and the memory writes (`add`, `addTurn`, `addBulk`,
|
|
184
|
+
`update`). Those carry an idempotency key, so the service answers a repeat with the
|
|
185
|
+
first attempt's result instead of storing again. The other writes sent as POST
|
|
186
|
+
(`createStore`, `addSpeaker`, `delete`, `deleteAll`) get 429, that 409 and
|
|
187
|
+
connect-level failures only. Twice, with exponential backoff + jitter, honoring the
|
|
188
|
+
server's `Retry-After` up to 30s. Tune with `new Client({ maxRetries })`; `0` disables.
|
|
189
|
+
- **Your own retries** — pass `idempotencyKey` when the retry is yours (a job that is
|
|
190
|
+
re-run, a queue that redelivers). The service keeps a key for 24 hours and answers a
|
|
191
|
+
repeat with the first response, marked `replayed: true`. When the first attempt did
|
|
192
|
+
not come back with an answer, so whether it was applied is not known, a repeat
|
|
193
|
+
is refused with a `ConflictError` (409) rather than stored twice: check before sending
|
|
194
|
+
it again under a new key. Derive the key from what you store
|
|
195
|
+
(`` `import:${row.id}` ``), never a constant.
|
|
94
196
|
- **Redirects refused** — the API key never follows a 3xx to another host.
|
|
95
197
|
- **Timeouts** — 30s per attempt by default (`timeoutMs`), and a total budget for
|
|
96
198
|
the whole call across every retry with `deadlineMs` / `withDeadline(ms)`. At the
|
|
@@ -131,8 +233,13 @@ timeouts, redirect refusal and the response cap all still apply.
|
|
|
131
233
|
|
|
132
234
|
## Security
|
|
133
235
|
|
|
134
|
-
Built in
|
|
236
|
+
Built in:
|
|
135
237
|
|
|
238
|
+
- **No key in a browser page** — anyone who opens a page can read a key in it, and with
|
|
239
|
+
it every memory in its workspace. A client in a browser page that would send the key to
|
|
240
|
+
the service is refused: call Wontopos from your server, or point `baseUrl` at a proxy of
|
|
241
|
+
your own that holds the key. `dangerouslyAllowBrowser: true` turns the refusal off, for
|
|
242
|
+
a local demo with a key you will revoke.
|
|
136
243
|
- **Redirects refused** — a 3xx is an error, so the key never follows one to
|
|
137
244
|
another host.
|
|
138
245
|
- **Response size cap** — anything over 64MB is refused instead of buffered.
|
|
@@ -188,7 +295,8 @@ try {
|
|
|
188
295
|
|
|
189
296
|
A retired model answers `410`, thrown as `GoneError`: retrying cannot succeed, so name a
|
|
190
297
|
live model (`listModels()` lists them, with `retires_at` on one that is scheduled to
|
|
191
|
-
retire).
|
|
298
|
+
retire). Before the day, the client warns once with the date the model you use retires.
|
|
299
|
+
`deleteStore` still works under a retired model.
|
|
192
300
|
|
|
193
301
|
## A different API host
|
|
194
302
|
|
|
@@ -219,7 +327,7 @@ data to fix it.
|
|
|
219
327
|
## Changelog
|
|
220
328
|
|
|
221
329
|
The three clients release in lockstep — same version, same surface, same day. Patch
|
|
222
|
-
releases are additive.
|
|
330
|
+
releases are additive. Ten inside 2.2 were not, deliberately and each with its
|
|
223
331
|
reason; the changelog lists them.
|
|
224
332
|
|
|
225
333
|
See [CHANGELOG.md](https://github.com/wontopos/wontopos-node/blob/main/CHANGELOG.md).
|
package/dist/cjs/wontopos.d.cts
CHANGED
|
@@ -28,9 +28,10 @@
|
|
|
28
28
|
* Reliability: every call retries transient failures with exponential backoff +
|
|
29
29
|
* jitter, honoring `Retry-After` up to 30s — 429 and a 409 that says another write
|
|
30
30
|
* to the store was in flight, always; 408/502/503/504 and connection errors only when
|
|
31
|
-
* a retry can never double-process a write (reads, the DELETE calls,
|
|
32
|
-
* connect time). Tune with `maxRetries`
|
|
33
|
-
*
|
|
31
|
+
* a retry can never double-process a write (reads, the DELETE calls, the memory writes,
|
|
32
|
+
* which carry an idempotency key, or a failure at connect time). Tune with `maxRetries`
|
|
33
|
+
* (0 disables) and `timeoutMs`, or per call site via the `withTimeout()` /
|
|
34
|
+
* `withRetries()` clones.
|
|
34
35
|
*
|
|
35
36
|
* Debugging: set `WONTOPOS_LOG=debug` to log method/path/status/timing/retries
|
|
36
37
|
* to stderr — never memory content, request bodies, or the API key.
|
|
@@ -48,6 +49,8 @@ export declare const SEARCH_LIMIT_MAX = 20;
|
|
|
48
49
|
/** `recall`'s surrounding-context count. 0 to 20 inclusive; 0 attaches none. */
|
|
49
50
|
export declare const CONTEXT_LIMIT_MIN = 0;
|
|
50
51
|
export declare const CONTEXT_LIMIT_MAX = 20;
|
|
52
|
+
/** @deprecated Test hook. The warn-once set is process-global by design. */
|
|
53
|
+
export declare function _resetRetirementWarnings(): void;
|
|
51
54
|
/** Quota from a response's `X-RateLimit-*` headers, or `null` if absent. */
|
|
52
55
|
export interface RateLimit {
|
|
53
56
|
limit: number | null;
|
|
@@ -100,8 +103,11 @@ export declare class PermissionDeniedError extends WosError {
|
|
|
100
103
|
/** 404 — the store or resource doesn't exist. */
|
|
101
104
|
export declare class NotFoundError extends WosError {
|
|
102
105
|
}
|
|
103
|
-
/** 409: another write to this store was in flight
|
|
104
|
-
*
|
|
106
|
+
/** 409: another write to this store was in flight, or the first request under this
|
|
107
|
+
* write's idempotency key is still running (both retried automatically; nothing was
|
|
108
|
+
* stored); the store id collides with an existing one (not retried); or the first
|
|
109
|
+
* request under the key did not come back with an answer, so whether it was stored is
|
|
110
|
+
* not known (not retried: check before sending it again under a new key). */
|
|
105
111
|
export declare class ConflictError extends WosError {
|
|
106
112
|
/** The existing store id this one collides with. Set only on a collision, which
|
|
107
113
|
* retrying cannot fix. */
|
|
@@ -184,7 +190,9 @@ export interface StoreResult {
|
|
|
184
190
|
note?: string;
|
|
185
191
|
/** `true` when this response was REPLAYED from a previous request with the same
|
|
186
192
|
* `idempotencyKey` — nothing new was stored. Absent on a fresh write. Read it to
|
|
187
|
-
* tell "my retry landed" from "my retry was a no-op".
|
|
193
|
+
* tell "my retry landed" from "my retry was a no-op". This client sets it only for
|
|
194
|
+
* a key you passed, and then the earlier request can also be an attempt of this same
|
|
195
|
+
* call that the client sent again after an answer that left the outcome open. */
|
|
188
196
|
replayed?: boolean;
|
|
189
197
|
[key: string]: unknown;
|
|
190
198
|
}
|
|
@@ -371,17 +379,25 @@ export interface SearchResult {
|
|
|
371
379
|
[key: string]: unknown;
|
|
372
380
|
}
|
|
373
381
|
export interface RecallResult {
|
|
382
|
+
/** The recent conversation, oldest first. */
|
|
374
383
|
short_term: {
|
|
375
|
-
turns:
|
|
384
|
+
turns: HistoryTurn[];
|
|
376
385
|
count: number;
|
|
386
|
+
[key: string]: unknown;
|
|
377
387
|
};
|
|
388
|
+
/** The stored memories that match the query, best first. */
|
|
378
389
|
long_term: {
|
|
379
390
|
memories: Memory[];
|
|
380
391
|
count: number;
|
|
392
|
+
[key: string]: unknown;
|
|
381
393
|
};
|
|
394
|
+
/** The conversation around the best memory, in the order it was said. Each row is
|
|
395
|
+
* text that starts with where it sits next to that memory: `[before] `, `[match] `
|
|
396
|
+
* or `[after] `. Empty when there is no best memory or `context_limit` is 0. */
|
|
382
397
|
context: {
|
|
383
|
-
around_top_memory:
|
|
398
|
+
around_top_memory: string[];
|
|
384
399
|
count: number;
|
|
400
|
+
[key: string]: unknown;
|
|
385
401
|
};
|
|
386
402
|
[key: string]: unknown;
|
|
387
403
|
}
|
|
@@ -396,7 +412,7 @@ export interface EngramResult {
|
|
|
396
412
|
usage?: Usage;
|
|
397
413
|
[key: string]: unknown;
|
|
398
414
|
}
|
|
399
|
-
/** One turn of the short-term window, as `history()`
|
|
415
|
+
/** One turn of the short-term window, as `history()` and `recall()` return it. */
|
|
400
416
|
export interface HistoryTurn {
|
|
401
417
|
/** `"user"` or `"assistant"`. */
|
|
402
418
|
role?: string;
|
|
@@ -636,25 +652,24 @@ export interface SearchOptions {
|
|
|
636
652
|
/**
|
|
637
653
|
* Per-call options for a write.
|
|
638
654
|
*
|
|
639
|
-
*
|
|
640
|
-
*
|
|
641
|
-
*
|
|
642
|
-
*
|
|
643
|
-
*
|
|
644
|
-
*
|
|
645
|
-
* on 408 / 502 / 503 / 504 or a dropped body, where the write may already have been
|
|
646
|
-
* applied — without a key the client cannot know whether it was.
|
|
655
|
+
* Every memory write (`add`, `addTurn`, `addBulk`, `update`) carries an idempotency key:
|
|
656
|
+
* `idempotencyKey` when you pass one, otherwise one this client makes for the call. With
|
|
657
|
+
* it the client retries the write on 408 / 502 / 503 / 504 and on a dropped connection,
|
|
658
|
+
* as it does a read: the service answers a repeat with the first attempt's result instead
|
|
659
|
+
* of storing again, and one that arrives while the first is still running with a 409 to
|
|
660
|
+
* wait, which is retried as well.
|
|
647
661
|
*
|
|
648
|
-
*
|
|
649
|
-
*
|
|
650
|
-
*
|
|
662
|
+
* Pass your own key when the retry is yours: a job that died and was re-run, a queue
|
|
663
|
+
* that redelivers. The service keeps a key for 24 hours and answers a repeat with the
|
|
664
|
+
* first successful response, marked `replayed: true`; the same key with a different body
|
|
665
|
+
* is refused (422). A first attempt that failed is not kept, so a repeat of it runs,
|
|
666
|
+
* unless it did not come back with an answer, so whether it was applied is not known:
|
|
667
|
+
* then the repeat is refused with a `ConflictError` (409) instead of storing it twice.
|
|
668
|
+
* Check before sending it again under a new key.
|
|
651
669
|
*
|
|
652
670
|
* The key must be UNIQUE PER LOGICAL WRITE — derive it from the thing being stored
|
|
653
671
|
* (`` `import:${row.id}` ``), never a constant, or the second write replays the first
|
|
654
672
|
* and is silently lost. Format: 1-128 chars of `[A-Za-z0-9._:-]`, checked locally.
|
|
655
|
-
*
|
|
656
|
-
* The window is best-effort and can be shorter than 10 minutes; it is not a durable
|
|
657
|
-
* de-duplication record.
|
|
658
673
|
*/
|
|
659
674
|
export interface WriteOptions {
|
|
660
675
|
idempotencyKey?: string;
|
|
@@ -782,6 +797,12 @@ export interface ClientOptions {
|
|
|
782
797
|
* (`res.redirected`) is refused. A fetch that sends the request to another URL
|
|
783
798
|
* on purpose, such as a proxy that rewrites it, is fine. */
|
|
784
799
|
fetch?: FetchLike;
|
|
800
|
+
/** Run in a browser page anyway. Off by default: an API key in a page is readable by
|
|
801
|
+
* everyone who opens it, and with it every memory in its workspace, so a client in a
|
|
802
|
+
* page that would send the key to the service itself is refused. A `baseUrl` of your
|
|
803
|
+
* own, a proxy that holds the real key, is not refused. Turn this on only for a local
|
|
804
|
+
* demo with a key you will revoke. */
|
|
805
|
+
dangerouslyAllowBrowser?: boolean;
|
|
785
806
|
}
|
|
786
807
|
/** Wontopos memory client. Store and recall memories, isolated per `userId`. */
|
|
787
808
|
export declare class Client {
|
|
@@ -803,6 +824,8 @@ export declare class Client {
|
|
|
803
824
|
private readonly pageRelative;
|
|
804
825
|
/** Whether the caller supplied `fetch`. */
|
|
805
826
|
private readonly customFetch;
|
|
827
|
+
/** Whether the caller asked to run in a browser page anyway. */
|
|
828
|
+
private readonly allowBrowser;
|
|
806
829
|
/** The store every call uses unless one passes `userId`. */
|
|
807
830
|
private readonly defaultUser;
|
|
808
831
|
private _rateLimit;
|
|
@@ -1022,10 +1045,10 @@ export declare class Client {
|
|
|
1022
1045
|
*/
|
|
1023
1046
|
getImage(userId: string | undefined, memoryId: string): Promise<ImageBytes>;
|
|
1024
1047
|
/**
|
|
1025
|
-
* Remove the PHOTO from a memory
|
|
1048
|
+
* Remove the PHOTO from a memory. It can take the whole memory with it.
|
|
1026
1049
|
*
|
|
1027
|
-
* `preview: true` reports what would happen, including `memory_kept
|
|
1028
|
-
* nothing. Retried like `deleteStore`; without `preview`, a 404 after an ambiguous
|
|
1050
|
+
* `preview: true` reports what would happen, including `memory_kept` (`false` when
|
|
1051
|
+
* the memory itself would go), and changes nothing. Retried like `deleteStore`; without `preview`, a 404 after an ambiguous
|
|
1029
1052
|
* failure carries the same note.
|
|
1030
1053
|
*
|
|
1031
1054
|
* const p = await mem.forgetImage(undefined, id, { preview: true });
|
|
@@ -1182,6 +1205,11 @@ export declare class Client {
|
|
|
1182
1205
|
* this is destructive, so it never falls back to the default store. */
|
|
1183
1206
|
deleteAll(userId: string): Promise<StatusResult>;
|
|
1184
1207
|
private post;
|
|
1208
|
+
/** A memory write. It always carries an `Idempotency-Key`, the caller's or one made
|
|
1209
|
+
* for this call, so an answer that leaves the outcome open (408/502/503/504, a
|
|
1210
|
+
* dropped connection) can be retried: the service answers the retry with the first
|
|
1211
|
+
* attempt's result instead of storing again. */
|
|
1212
|
+
private write;
|
|
1185
1213
|
/** A DELETE that removes something: a 404 after an ambiguous failure says an earlier
|
|
1186
1214
|
* attempt may already have removed it. */
|
|
1187
1215
|
private remove;
|
|
@@ -1223,7 +1251,8 @@ export declare class Client {
|
|
|
1223
1251
|
* Retried: 429, and a 409 whose error carries `retry_after_ms` (another write to the
|
|
1224
1252
|
* store was in flight; nothing was stored), on every method; 408/502/503/504 and
|
|
1225
1253
|
* dropped connections on a call that only reads (an idempotent method, or a POST
|
|
1226
|
-
* that only reads)
|
|
1254
|
+
* that only reads) or a write that carries an idempotency key; connect failures on
|
|
1255
|
+
* every method. A retry whose wait is over the
|
|
1227
1256
|
* cap, or does not fit the deadline, is not made: the call fails with the error of
|
|
1228
1257
|
* the response it has. So does a retry the deadline leaves no time to send, and the
|
|
1229
1258
|
* retry of a call that only reads that the deadline cuts short. Any other retry cut
|
package/dist/cjs/wontopos.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.WME = exports.Client = exports.ServerError = exports.RateLimitError = exports.GoneError = 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
|
+
exports._resetRetirementWarnings = _resetRetirementWarnings;
|
|
5
6
|
exports._resetFilterWarnings = _resetFilterWarnings;
|
|
6
|
-
const VERSION = "2.2.
|
|
7
|
+
const VERSION = "2.2.47";
|
|
7
8
|
const RUNTIME = (() => {
|
|
8
9
|
const p = globalThis?.process;
|
|
9
10
|
return p?.version ? ` (node/${p.version}; ${p.platform}-${p.arch})` : "";
|
|
@@ -268,12 +269,7 @@ const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "
|
|
|
268
269
|
const IMF_FIXDATE = /^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), (\d{2}) ([A-Z][a-z]{2}) (\d{4}) (\d{2}):(\d{2}):(\d{2}) GMT$/;
|
|
269
270
|
const RFC850_DATE = /^(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (\d{2})-([A-Z][a-z]{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2}) GMT$/;
|
|
270
271
|
const ASCTIME_DATE = /^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) ([A-Z][a-z]{2}) ([ \d]\d) (\d{2}):(\d{2}):(\d{2}) (\d{4})$/;
|
|
271
|
-
function
|
|
272
|
-
if (v == null)
|
|
273
|
-
return undefined;
|
|
274
|
-
const s = v.trim();
|
|
275
|
-
if (/^\d+$/.test(s))
|
|
276
|
-
return Math.min(Number(s), Number.MAX_SAFE_INTEGER / 1000) * 1000;
|
|
272
|
+
function httpDateMs(s, now = Date.now()) {
|
|
277
273
|
let parts;
|
|
278
274
|
let m;
|
|
279
275
|
if ((m = IMF_FIXDATE.exec(s)))
|
|
@@ -293,7 +289,37 @@ function retryAfterMs(v, now = Date.now()) {
|
|
|
293
289
|
const d = Number(day), h = Number(hh), mi = Number(mm), se = Number(ss);
|
|
294
290
|
if (month < 0 || d < 1 || d > 31 || h > 23 || mi > 59 || se > 60)
|
|
295
291
|
return undefined;
|
|
296
|
-
return
|
|
292
|
+
return Date.UTC(year, month, d, h, mi, se);
|
|
293
|
+
}
|
|
294
|
+
function retryAfterMs(v, now = Date.now()) {
|
|
295
|
+
if (v == null)
|
|
296
|
+
return undefined;
|
|
297
|
+
const s = v.trim();
|
|
298
|
+
if (/^\d+$/.test(s))
|
|
299
|
+
return Math.min(Number(s), Number.MAX_SAFE_INTEGER / 1000) * 1000;
|
|
300
|
+
const at = httpDateMs(s, now);
|
|
301
|
+
return at === undefined ? undefined : Math.max(0, at - now);
|
|
302
|
+
}
|
|
303
|
+
const warnedRetirements = new Set();
|
|
304
|
+
const WARNED_RETIREMENTS_MAX = 64;
|
|
305
|
+
function warnIfRetiring(model, h) {
|
|
306
|
+
const v = h.get("Sunset");
|
|
307
|
+
if (!v)
|
|
308
|
+
return;
|
|
309
|
+
const at = httpDateMs(v.trim());
|
|
310
|
+
if (at === undefined)
|
|
311
|
+
return;
|
|
312
|
+
const day = new Date(at).toISOString().slice(0, 10);
|
|
313
|
+
const k = `${model}|${day}`;
|
|
314
|
+
if (warnedRetirements.has(k) || warnedRetirements.size >= WARNED_RETIREMENTS_MAX)
|
|
315
|
+
return;
|
|
316
|
+
warnedRetirements.add(k);
|
|
317
|
+
const which = model ? `model ${JSON.stringify(model)}` : "the model this client uses";
|
|
318
|
+
console.warn(`wontopos: ${which} retires on ${day} (UTC). From then on its calls answer 410, except ` +
|
|
319
|
+
"deleteStore. listModels() lists the models you can move to.");
|
|
320
|
+
}
|
|
321
|
+
function _resetRetirementWarnings() {
|
|
322
|
+
warnedRetirements.clear();
|
|
297
323
|
}
|
|
298
324
|
const BRIEF_BODY_MS = 1_000;
|
|
299
325
|
class SlowBody extends Error {
|
|
@@ -318,6 +344,23 @@ function tryUrl(s, base) {
|
|
|
318
344
|
return undefined;
|
|
319
345
|
}
|
|
320
346
|
}
|
|
347
|
+
function inBrowserPage() {
|
|
348
|
+
try {
|
|
349
|
+
const g = globalThis;
|
|
350
|
+
return typeof g.window !== "undefined" && typeof g.window.document !== "undefined" && typeof g.navigator !== "undefined";
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
function isServiceHost(host) {
|
|
357
|
+
const h = host.toLowerCase();
|
|
358
|
+
return h === "wontopos.com" || h.endsWith(".wontopos.com");
|
|
359
|
+
}
|
|
360
|
+
const BROWSER_REFUSAL = "wontopos: refusing to run in a browser page. The API key would be readable by anyone who " +
|
|
361
|
+
"opens the page, and with it every memory in its workspace. Call Wontopos from your server, " +
|
|
362
|
+
"or put a proxy of your own in front of it and point baseUrl there. For a local demo with a " +
|
|
363
|
+
"key you will revoke, pass dangerouslyAllowBrowser: true.";
|
|
321
364
|
function pageHref() {
|
|
322
365
|
try {
|
|
323
366
|
const href = globalThis.location?.href;
|
|
@@ -394,6 +437,18 @@ const memoryFromGet = (r) => {
|
|
|
394
437
|
return o.memory;
|
|
395
438
|
return typeof o.id === "string" ? o : {};
|
|
396
439
|
};
|
|
440
|
+
function recallShape(r) {
|
|
441
|
+
const o = asObj(r);
|
|
442
|
+
const st = asObj(o.short_term);
|
|
443
|
+
const lt = asObj(o.long_term);
|
|
444
|
+
const cx = asObj(o.context);
|
|
445
|
+
return {
|
|
446
|
+
...o,
|
|
447
|
+
short_term: { ...st, turns: asRecords(st.turns) },
|
|
448
|
+
long_term: { ...lt, memories: asRecords(lt.memories) },
|
|
449
|
+
context: { ...cx, around_top_memory: asList(cx.around_top_memory).filter((x) => typeof x === "string") },
|
|
450
|
+
};
|
|
451
|
+
}
|
|
397
452
|
function mergeResults(r) {
|
|
398
453
|
const out = asRecords(r?.memories);
|
|
399
454
|
const seen = new Set(out.map((m) => m.id).filter((id) => typeof id === "string"));
|
|
@@ -569,6 +624,15 @@ function writeKey(opts, takesImage = false) {
|
|
|
569
624
|
(takesImage ? "add takes idempotencyKey and image." : "This call takes idempotencyKey only."));
|
|
570
625
|
return opts.idempotencyKey;
|
|
571
626
|
}
|
|
627
|
+
function newIdempotencyKey() {
|
|
628
|
+
const c = globalThis.crypto;
|
|
629
|
+
if (typeof c?.randomUUID === "function")
|
|
630
|
+
return `sdk-${String(c.randomUUID()).replace(/-/g, "")}`;
|
|
631
|
+
let hex = "";
|
|
632
|
+
for (let i = 0; i < 4; i++)
|
|
633
|
+
hex += Math.floor(Math.random() * 0x100000000).toString(16).padStart(8, "0");
|
|
634
|
+
return `sdk-${hex}`;
|
|
635
|
+
}
|
|
572
636
|
function warnOnUnknownFilters(filters) {
|
|
573
637
|
warnOnceUnknownKeys(filters, KNOWN_FILTER_KEYS, warnedFilterKeys, (k) => `wontopos: unknown search filter ${JSON.stringify(k)} — the API drops keys it does not know, ` +
|
|
574
638
|
`so this filter has NO effect and the search is wider than you think. ` +
|
|
@@ -597,6 +661,7 @@ class Client {
|
|
|
597
661
|
userinfo;
|
|
598
662
|
pageRelative;
|
|
599
663
|
customFetch;
|
|
664
|
+
allowBrowser;
|
|
600
665
|
defaultUser;
|
|
601
666
|
_rateLimit = null;
|
|
602
667
|
get rateLimit() {
|
|
@@ -630,6 +695,10 @@ class Client {
|
|
|
630
695
|
throw new Error(notAUrl(trimmed));
|
|
631
696
|
this.pageRelative = !url;
|
|
632
697
|
this.userinfo = Boolean(parsed.username || parsed.password);
|
|
698
|
+
this.allowBrowser = opts.dangerouslyAllowBrowser === true;
|
|
699
|
+
if (!this.allowBrowser && inBrowserPage() && isServiceHost(parsed.hostname)) {
|
|
700
|
+
throw new Error(BROWSER_REFUSAL);
|
|
701
|
+
}
|
|
633
702
|
this.timeoutMs = checkMs(opts.timeoutMs) ?? 30_000;
|
|
634
703
|
this.model = opts.model ?? DEFAULT_MODEL;
|
|
635
704
|
if (this.model && !MODEL_RE.test(this.model)) {
|
|
@@ -698,6 +767,7 @@ class Client {
|
|
|
698
767
|
fetch: this.fetchImpl,
|
|
699
768
|
deadlineMs: this.deadlineMs,
|
|
700
769
|
signal: this.signal,
|
|
770
|
+
dangerouslyAllowBrowser: this.allowBrowser,
|
|
701
771
|
...overrides,
|
|
702
772
|
});
|
|
703
773
|
}
|
|
@@ -729,22 +799,22 @@ class Client {
|
|
|
729
799
|
if (opts.image !== undefined)
|
|
730
800
|
body.image = normalizeImage(opts.image);
|
|
731
801
|
warnOnUnknownMetadata(metadata);
|
|
732
|
-
return this.
|
|
802
|
+
return this.write("/api/v1/memory/store", body, writeKey(opts, true));
|
|
733
803
|
}
|
|
734
804
|
async store(content, userId, metadata = {}, opts = {}) {
|
|
735
805
|
return this.add(content, userId, metadata, opts);
|
|
736
806
|
}
|
|
737
807
|
async addTurn(userMsg, assistantMsg, userId, opts = {}) {
|
|
738
|
-
return this.
|
|
808
|
+
return this.write("/api/v1/memory/store-turn", { user_id: this.uid(userId), user_msg: userMsg, assistant_msg: assistantMsg }, writeKey(opts));
|
|
739
809
|
}
|
|
740
810
|
async addBulk(content, userId, category = "general", timestamp, opts = {}) {
|
|
741
811
|
const body = { user_id: this.uid(userId), content, category: category ?? "general" };
|
|
742
812
|
if (timestamp)
|
|
743
813
|
body.timestamp = timestamp;
|
|
744
|
-
return this.
|
|
814
|
+
return this.write("/api/v1/memory/bulk-store", body, writeKey(opts));
|
|
745
815
|
}
|
|
746
816
|
async update(oldMemoryId, newContent, userId, opts = {}) {
|
|
747
|
-
return this.
|
|
817
|
+
return this.write("/api/v1/memory/supersede", { user_id: this.uid(userId), old_memory_id: oldMemoryId, new_content: newContent }, writeKey(opts));
|
|
748
818
|
}
|
|
749
819
|
async search(query, userId, limit = 10, opts = {}) {
|
|
750
820
|
checkSearchOpts(opts);
|
|
@@ -789,7 +859,7 @@ class Client {
|
|
|
789
859
|
checkContextLimit(opts.context_limit);
|
|
790
860
|
body.context_limit = opts.context_limit;
|
|
791
861
|
}
|
|
792
|
-
return this.post("/api/v1/memory/recall", withForm(body, opts.form, opts.tz));
|
|
862
|
+
return recallShape(await this.post("/api/v1/memory/recall", withForm(body, opts.form, opts.tz)));
|
|
793
863
|
}
|
|
794
864
|
async engram(name, query, userId, opts = {}) {
|
|
795
865
|
checkOpts(opts, KNOWN_ENGRAM_KEYS, "engram");
|
|
@@ -994,8 +1064,12 @@ class Client {
|
|
|
994
1064
|
warnIfStoreIdCollapses(userId);
|
|
995
1065
|
return this.post("/api/v1/memory/forget", { user_id: userId });
|
|
996
1066
|
}
|
|
997
|
-
post(path, body
|
|
998
|
-
return this.request("POST", path, body
|
|
1067
|
+
post(path, body) {
|
|
1068
|
+
return this.request("POST", path, body);
|
|
1069
|
+
}
|
|
1070
|
+
write(path, body, idempotencyKey) {
|
|
1071
|
+
const made = idempotencyKey == null;
|
|
1072
|
+
return this.request("POST", path, body, made ? newIdempotencyKey() : idempotencyKey, false, made);
|
|
999
1073
|
}
|
|
1000
1074
|
remove(path, body) {
|
|
1001
1075
|
return this.request("DELETE", path, body, undefined, true);
|
|
@@ -1115,6 +1189,7 @@ class Client {
|
|
|
1115
1189
|
const upper = method.toUpperCase();
|
|
1116
1190
|
const logPath = path.split("?")[0];
|
|
1117
1191
|
const idempotent = IDEMPOTENT_METHODS.has(upper) || reads || (upper === "POST" && READ_POSTS.has(logPath));
|
|
1192
|
+
const replaySafe = idempotent || extraHeaders?.["Idempotency-Key"] !== undefined;
|
|
1118
1193
|
const deadlineAt = this.deadlineAt();
|
|
1119
1194
|
const headers = extraHeaders ? { ...this.headers, ...extraHeaders } : this.headers;
|
|
1120
1195
|
let ambiguous = false;
|
|
@@ -1156,7 +1231,7 @@ class Client {
|
|
|
1156
1231
|
if (timedOut)
|
|
1157
1232
|
throw cutShort();
|
|
1158
1233
|
const connect = isConnectFailure(e);
|
|
1159
|
-
if ((
|
|
1234
|
+
if ((replaySafe || connect) && !last) {
|
|
1160
1235
|
if (!connect)
|
|
1161
1236
|
ambiguous = true;
|
|
1162
1237
|
await pauseWithoutAnswer(e?.code ?? e?.name ?? "network error");
|
|
@@ -1173,7 +1248,7 @@ class Client {
|
|
|
1173
1248
|
throw cutShort();
|
|
1174
1249
|
if (e instanceof WosError)
|
|
1175
1250
|
throw e;
|
|
1176
|
-
if (
|
|
1251
|
+
if (replaySafe && !last)
|
|
1177
1252
|
return null;
|
|
1178
1253
|
throw this.networkError(e, path);
|
|
1179
1254
|
}
|
|
@@ -1200,9 +1275,10 @@ class Client {
|
|
|
1200
1275
|
throw new WosError(res.status, "unexpected redirect — refused (the API key never follows a redirect). Check baseUrl: exact host, https://.");
|
|
1201
1276
|
}
|
|
1202
1277
|
this._rateLimit = parseRateLimit(res.headers) ?? this._rateLimit;
|
|
1278
|
+
warnIfRetiring(this.model, res.headers);
|
|
1203
1279
|
if (!res.ok) {
|
|
1204
1280
|
const ra = retryAfterMs(res.headers.get("Retry-After"));
|
|
1205
|
-
let wait = RETRY_ALWAYS.has(res.status) || (RETRY_IF_IDEMPOTENT.has(res.status) &&
|
|
1281
|
+
let wait = RETRY_ALWAYS.has(res.status) || (RETRY_IF_IDEMPOTENT.has(res.status) && replaySafe)
|
|
1206
1282
|
? ra ?? this.backoffMs(attempt)
|
|
1207
1283
|
: undefined;
|
|
1208
1284
|
const retrying = wait !== undefined && !last && wait <= MAX_RETRY_WAIT_MS && this.fits(wait, deadlineAt);
|
|
@@ -1329,7 +1405,7 @@ class Client {
|
|
|
1329
1405
|
clearTimeout(timer);
|
|
1330
1406
|
}
|
|
1331
1407
|
}
|
|
1332
|
-
async request(method, path, body, idempotencyKey, removes = false) {
|
|
1408
|
+
async request(method, path, body, idempotencyKey, removes = false, madeKey = false) {
|
|
1333
1409
|
const extraHeaders = this.idemHeader(idempotencyKey);
|
|
1334
1410
|
let payload;
|
|
1335
1411
|
try {
|
|
@@ -1357,7 +1433,7 @@ class Client {
|
|
|
1357
1433
|
const got = data === null ? "null" : Array.isArray(data) ? "array" : typeof data;
|
|
1358
1434
|
throw new WosError(res.status, `expected a JSON object in the response, got ${got}`);
|
|
1359
1435
|
}
|
|
1360
|
-
if (res.headers.get("Idempotent-Replayed") === "true" && !("replayed" in data)) {
|
|
1436
|
+
if (!madeKey && res.headers.get("Idempotent-Replayed") === "true" && !("replayed" in data)) {
|
|
1361
1437
|
data.replayed = true;
|
|
1362
1438
|
}
|
|
1363
1439
|
return data;
|
package/dist/wontopos.d.ts
CHANGED
|
@@ -28,9 +28,10 @@
|
|
|
28
28
|
* Reliability: every call retries transient failures with exponential backoff +
|
|
29
29
|
* jitter, honoring `Retry-After` up to 30s — 429 and a 409 that says another write
|
|
30
30
|
* to the store was in flight, always; 408/502/503/504 and connection errors only when
|
|
31
|
-
* a retry can never double-process a write (reads, the DELETE calls,
|
|
32
|
-
* connect time). Tune with `maxRetries`
|
|
33
|
-
*
|
|
31
|
+
* a retry can never double-process a write (reads, the DELETE calls, the memory writes,
|
|
32
|
+
* which carry an idempotency key, or a failure at connect time). Tune with `maxRetries`
|
|
33
|
+
* (0 disables) and `timeoutMs`, or per call site via the `withTimeout()` /
|
|
34
|
+
* `withRetries()` clones.
|
|
34
35
|
*
|
|
35
36
|
* Debugging: set `WONTOPOS_LOG=debug` to log method/path/status/timing/retries
|
|
36
37
|
* to stderr — never memory content, request bodies, or the API key.
|
|
@@ -48,6 +49,8 @@ export declare const SEARCH_LIMIT_MAX = 20;
|
|
|
48
49
|
/** `recall`'s surrounding-context count. 0 to 20 inclusive; 0 attaches none. */
|
|
49
50
|
export declare const CONTEXT_LIMIT_MIN = 0;
|
|
50
51
|
export declare const CONTEXT_LIMIT_MAX = 20;
|
|
52
|
+
/** @deprecated Test hook. The warn-once set is process-global by design. */
|
|
53
|
+
export declare function _resetRetirementWarnings(): void;
|
|
51
54
|
/** Quota from a response's `X-RateLimit-*` headers, or `null` if absent. */
|
|
52
55
|
export interface RateLimit {
|
|
53
56
|
limit: number | null;
|
|
@@ -100,8 +103,11 @@ export declare class PermissionDeniedError extends WosError {
|
|
|
100
103
|
/** 404 — the store or resource doesn't exist. */
|
|
101
104
|
export declare class NotFoundError extends WosError {
|
|
102
105
|
}
|
|
103
|
-
/** 409: another write to this store was in flight
|
|
104
|
-
*
|
|
106
|
+
/** 409: another write to this store was in flight, or the first request under this
|
|
107
|
+
* write's idempotency key is still running (both retried automatically; nothing was
|
|
108
|
+
* stored); the store id collides with an existing one (not retried); or the first
|
|
109
|
+
* request under the key did not come back with an answer, so whether it was stored is
|
|
110
|
+
* not known (not retried: check before sending it again under a new key). */
|
|
105
111
|
export declare class ConflictError extends WosError {
|
|
106
112
|
/** The existing store id this one collides with. Set only on a collision, which
|
|
107
113
|
* retrying cannot fix. */
|
|
@@ -184,7 +190,9 @@ export interface StoreResult {
|
|
|
184
190
|
note?: string;
|
|
185
191
|
/** `true` when this response was REPLAYED from a previous request with the same
|
|
186
192
|
* `idempotencyKey` — nothing new was stored. Absent on a fresh write. Read it to
|
|
187
|
-
* tell "my retry landed" from "my retry was a no-op".
|
|
193
|
+
* tell "my retry landed" from "my retry was a no-op". This client sets it only for
|
|
194
|
+
* a key you passed, and then the earlier request can also be an attempt of this same
|
|
195
|
+
* call that the client sent again after an answer that left the outcome open. */
|
|
188
196
|
replayed?: boolean;
|
|
189
197
|
[key: string]: unknown;
|
|
190
198
|
}
|
|
@@ -371,17 +379,25 @@ export interface SearchResult {
|
|
|
371
379
|
[key: string]: unknown;
|
|
372
380
|
}
|
|
373
381
|
export interface RecallResult {
|
|
382
|
+
/** The recent conversation, oldest first. */
|
|
374
383
|
short_term: {
|
|
375
|
-
turns:
|
|
384
|
+
turns: HistoryTurn[];
|
|
376
385
|
count: number;
|
|
386
|
+
[key: string]: unknown;
|
|
377
387
|
};
|
|
388
|
+
/** The stored memories that match the query, best first. */
|
|
378
389
|
long_term: {
|
|
379
390
|
memories: Memory[];
|
|
380
391
|
count: number;
|
|
392
|
+
[key: string]: unknown;
|
|
381
393
|
};
|
|
394
|
+
/** The conversation around the best memory, in the order it was said. Each row is
|
|
395
|
+
* text that starts with where it sits next to that memory: `[before] `, `[match] `
|
|
396
|
+
* or `[after] `. Empty when there is no best memory or `context_limit` is 0. */
|
|
382
397
|
context: {
|
|
383
|
-
around_top_memory:
|
|
398
|
+
around_top_memory: string[];
|
|
384
399
|
count: number;
|
|
400
|
+
[key: string]: unknown;
|
|
385
401
|
};
|
|
386
402
|
[key: string]: unknown;
|
|
387
403
|
}
|
|
@@ -396,7 +412,7 @@ export interface EngramResult {
|
|
|
396
412
|
usage?: Usage;
|
|
397
413
|
[key: string]: unknown;
|
|
398
414
|
}
|
|
399
|
-
/** One turn of the short-term window, as `history()`
|
|
415
|
+
/** One turn of the short-term window, as `history()` and `recall()` return it. */
|
|
400
416
|
export interface HistoryTurn {
|
|
401
417
|
/** `"user"` or `"assistant"`. */
|
|
402
418
|
role?: string;
|
|
@@ -636,25 +652,24 @@ export interface SearchOptions {
|
|
|
636
652
|
/**
|
|
637
653
|
* Per-call options for a write.
|
|
638
654
|
*
|
|
639
|
-
*
|
|
640
|
-
*
|
|
641
|
-
*
|
|
642
|
-
*
|
|
643
|
-
*
|
|
644
|
-
*
|
|
645
|
-
* on 408 / 502 / 503 / 504 or a dropped body, where the write may already have been
|
|
646
|
-
* applied — without a key the client cannot know whether it was.
|
|
655
|
+
* Every memory write (`add`, `addTurn`, `addBulk`, `update`) carries an idempotency key:
|
|
656
|
+
* `idempotencyKey` when you pass one, otherwise one this client makes for the call. With
|
|
657
|
+
* it the client retries the write on 408 / 502 / 503 / 504 and on a dropped connection,
|
|
658
|
+
* as it does a read: the service answers a repeat with the first attempt's result instead
|
|
659
|
+
* of storing again, and one that arrives while the first is still running with a 409 to
|
|
660
|
+
* wait, which is retried as well.
|
|
647
661
|
*
|
|
648
|
-
*
|
|
649
|
-
*
|
|
650
|
-
*
|
|
662
|
+
* Pass your own key when the retry is yours: a job that died and was re-run, a queue
|
|
663
|
+
* that redelivers. The service keeps a key for 24 hours and answers a repeat with the
|
|
664
|
+
* first successful response, marked `replayed: true`; the same key with a different body
|
|
665
|
+
* is refused (422). A first attempt that failed is not kept, so a repeat of it runs,
|
|
666
|
+
* unless it did not come back with an answer, so whether it was applied is not known:
|
|
667
|
+
* then the repeat is refused with a `ConflictError` (409) instead of storing it twice.
|
|
668
|
+
* Check before sending it again under a new key.
|
|
651
669
|
*
|
|
652
670
|
* The key must be UNIQUE PER LOGICAL WRITE — derive it from the thing being stored
|
|
653
671
|
* (`` `import:${row.id}` ``), never a constant, or the second write replays the first
|
|
654
672
|
* and is silently lost. Format: 1-128 chars of `[A-Za-z0-9._:-]`, checked locally.
|
|
655
|
-
*
|
|
656
|
-
* The window is best-effort and can be shorter than 10 minutes; it is not a durable
|
|
657
|
-
* de-duplication record.
|
|
658
673
|
*/
|
|
659
674
|
export interface WriteOptions {
|
|
660
675
|
idempotencyKey?: string;
|
|
@@ -782,6 +797,12 @@ export interface ClientOptions {
|
|
|
782
797
|
* (`res.redirected`) is refused. A fetch that sends the request to another URL
|
|
783
798
|
* on purpose, such as a proxy that rewrites it, is fine. */
|
|
784
799
|
fetch?: FetchLike;
|
|
800
|
+
/** Run in a browser page anyway. Off by default: an API key in a page is readable by
|
|
801
|
+
* everyone who opens it, and with it every memory in its workspace, so a client in a
|
|
802
|
+
* page that would send the key to the service itself is refused. A `baseUrl` of your
|
|
803
|
+
* own, a proxy that holds the real key, is not refused. Turn this on only for a local
|
|
804
|
+
* demo with a key you will revoke. */
|
|
805
|
+
dangerouslyAllowBrowser?: boolean;
|
|
785
806
|
}
|
|
786
807
|
/** Wontopos memory client. Store and recall memories, isolated per `userId`. */
|
|
787
808
|
export declare class Client {
|
|
@@ -803,6 +824,8 @@ export declare class Client {
|
|
|
803
824
|
private readonly pageRelative;
|
|
804
825
|
/** Whether the caller supplied `fetch`. */
|
|
805
826
|
private readonly customFetch;
|
|
827
|
+
/** Whether the caller asked to run in a browser page anyway. */
|
|
828
|
+
private readonly allowBrowser;
|
|
806
829
|
/** The store every call uses unless one passes `userId`. */
|
|
807
830
|
private readonly defaultUser;
|
|
808
831
|
private _rateLimit;
|
|
@@ -1022,10 +1045,10 @@ export declare class Client {
|
|
|
1022
1045
|
*/
|
|
1023
1046
|
getImage(userId: string | undefined, memoryId: string): Promise<ImageBytes>;
|
|
1024
1047
|
/**
|
|
1025
|
-
* Remove the PHOTO from a memory
|
|
1048
|
+
* Remove the PHOTO from a memory. It can take the whole memory with it.
|
|
1026
1049
|
*
|
|
1027
|
-
* `preview: true` reports what would happen, including `memory_kept
|
|
1028
|
-
* nothing. Retried like `deleteStore`; without `preview`, a 404 after an ambiguous
|
|
1050
|
+
* `preview: true` reports what would happen, including `memory_kept` (`false` when
|
|
1051
|
+
* the memory itself would go), and changes nothing. Retried like `deleteStore`; without `preview`, a 404 after an ambiguous
|
|
1029
1052
|
* failure carries the same note.
|
|
1030
1053
|
*
|
|
1031
1054
|
* const p = await mem.forgetImage(undefined, id, { preview: true });
|
|
@@ -1182,6 +1205,11 @@ export declare class Client {
|
|
|
1182
1205
|
* this is destructive, so it never falls back to the default store. */
|
|
1183
1206
|
deleteAll(userId: string): Promise<StatusResult>;
|
|
1184
1207
|
private post;
|
|
1208
|
+
/** A memory write. It always carries an `Idempotency-Key`, the caller's or one made
|
|
1209
|
+
* for this call, so an answer that leaves the outcome open (408/502/503/504, a
|
|
1210
|
+
* dropped connection) can be retried: the service answers the retry with the first
|
|
1211
|
+
* attempt's result instead of storing again. */
|
|
1212
|
+
private write;
|
|
1185
1213
|
/** A DELETE that removes something: a 404 after an ambiguous failure says an earlier
|
|
1186
1214
|
* attempt may already have removed it. */
|
|
1187
1215
|
private remove;
|
|
@@ -1223,7 +1251,8 @@ export declare class Client {
|
|
|
1223
1251
|
* Retried: 429, and a 409 whose error carries `retry_after_ms` (another write to the
|
|
1224
1252
|
* store was in flight; nothing was stored), on every method; 408/502/503/504 and
|
|
1225
1253
|
* dropped connections on a call that only reads (an idempotent method, or a POST
|
|
1226
|
-
* that only reads)
|
|
1254
|
+
* that only reads) or a write that carries an idempotency key; connect failures on
|
|
1255
|
+
* every method. A retry whose wait is over the
|
|
1227
1256
|
* cap, or does not fit the deadline, is not made: the call fails with the error of
|
|
1228
1257
|
* the response it has. So does a retry the deadline leaves no time to send, and the
|
|
1229
1258
|
* retry of a call that only reads that the deadline cuts short. Any other retry cut
|
package/dist/wontopos.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const VERSION = "2.2.
|
|
1
|
+
const VERSION = "2.2.47";
|
|
2
2
|
const RUNTIME = (() => {
|
|
3
3
|
const p = globalThis?.process;
|
|
4
4
|
return p?.version ? ` (node/${p.version}; ${p.platform}-${p.arch})` : "";
|
|
@@ -263,12 +263,7 @@ const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "
|
|
|
263
263
|
const IMF_FIXDATE = /^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), (\d{2}) ([A-Z][a-z]{2}) (\d{4}) (\d{2}):(\d{2}):(\d{2}) GMT$/;
|
|
264
264
|
const RFC850_DATE = /^(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (\d{2})-([A-Z][a-z]{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2}) GMT$/;
|
|
265
265
|
const ASCTIME_DATE = /^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) ([A-Z][a-z]{2}) ([ \d]\d) (\d{2}):(\d{2}):(\d{2}) (\d{4})$/;
|
|
266
|
-
function
|
|
267
|
-
if (v == null)
|
|
268
|
-
return undefined;
|
|
269
|
-
const s = v.trim();
|
|
270
|
-
if (/^\d+$/.test(s))
|
|
271
|
-
return Math.min(Number(s), Number.MAX_SAFE_INTEGER / 1000) * 1000;
|
|
266
|
+
function httpDateMs(s, now = Date.now()) {
|
|
272
267
|
let parts;
|
|
273
268
|
let m;
|
|
274
269
|
if ((m = IMF_FIXDATE.exec(s)))
|
|
@@ -288,7 +283,37 @@ function retryAfterMs(v, now = Date.now()) {
|
|
|
288
283
|
const d = Number(day), h = Number(hh), mi = Number(mm), se = Number(ss);
|
|
289
284
|
if (month < 0 || d < 1 || d > 31 || h > 23 || mi > 59 || se > 60)
|
|
290
285
|
return undefined;
|
|
291
|
-
return
|
|
286
|
+
return Date.UTC(year, month, d, h, mi, se);
|
|
287
|
+
}
|
|
288
|
+
function retryAfterMs(v, now = Date.now()) {
|
|
289
|
+
if (v == null)
|
|
290
|
+
return undefined;
|
|
291
|
+
const s = v.trim();
|
|
292
|
+
if (/^\d+$/.test(s))
|
|
293
|
+
return Math.min(Number(s), Number.MAX_SAFE_INTEGER / 1000) * 1000;
|
|
294
|
+
const at = httpDateMs(s, now);
|
|
295
|
+
return at === undefined ? undefined : Math.max(0, at - now);
|
|
296
|
+
}
|
|
297
|
+
const warnedRetirements = new Set();
|
|
298
|
+
const WARNED_RETIREMENTS_MAX = 64;
|
|
299
|
+
function warnIfRetiring(model, h) {
|
|
300
|
+
const v = h.get("Sunset");
|
|
301
|
+
if (!v)
|
|
302
|
+
return;
|
|
303
|
+
const at = httpDateMs(v.trim());
|
|
304
|
+
if (at === undefined)
|
|
305
|
+
return;
|
|
306
|
+
const day = new Date(at).toISOString().slice(0, 10);
|
|
307
|
+
const k = `${model}|${day}`;
|
|
308
|
+
if (warnedRetirements.has(k) || warnedRetirements.size >= WARNED_RETIREMENTS_MAX)
|
|
309
|
+
return;
|
|
310
|
+
warnedRetirements.add(k);
|
|
311
|
+
const which = model ? `model ${JSON.stringify(model)}` : "the model this client uses";
|
|
312
|
+
console.warn(`wontopos: ${which} retires on ${day} (UTC). From then on its calls answer 410, except ` +
|
|
313
|
+
"deleteStore. listModels() lists the models you can move to.");
|
|
314
|
+
}
|
|
315
|
+
export function _resetRetirementWarnings() {
|
|
316
|
+
warnedRetirements.clear();
|
|
292
317
|
}
|
|
293
318
|
const BRIEF_BODY_MS = 1_000;
|
|
294
319
|
class SlowBody extends Error {
|
|
@@ -313,6 +338,23 @@ function tryUrl(s, base) {
|
|
|
313
338
|
return undefined;
|
|
314
339
|
}
|
|
315
340
|
}
|
|
341
|
+
function inBrowserPage() {
|
|
342
|
+
try {
|
|
343
|
+
const g = globalThis;
|
|
344
|
+
return typeof g.window !== "undefined" && typeof g.window.document !== "undefined" && typeof g.navigator !== "undefined";
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
function isServiceHost(host) {
|
|
351
|
+
const h = host.toLowerCase();
|
|
352
|
+
return h === "wontopos.com" || h.endsWith(".wontopos.com");
|
|
353
|
+
}
|
|
354
|
+
const BROWSER_REFUSAL = "wontopos: refusing to run in a browser page. The API key would be readable by anyone who " +
|
|
355
|
+
"opens the page, and with it every memory in its workspace. Call Wontopos from your server, " +
|
|
356
|
+
"or put a proxy of your own in front of it and point baseUrl there. For a local demo with a " +
|
|
357
|
+
"key you will revoke, pass dangerouslyAllowBrowser: true.";
|
|
316
358
|
function pageHref() {
|
|
317
359
|
try {
|
|
318
360
|
const href = globalThis.location?.href;
|
|
@@ -389,6 +431,18 @@ const memoryFromGet = (r) => {
|
|
|
389
431
|
return o.memory;
|
|
390
432
|
return typeof o.id === "string" ? o : {};
|
|
391
433
|
};
|
|
434
|
+
function recallShape(r) {
|
|
435
|
+
const o = asObj(r);
|
|
436
|
+
const st = asObj(o.short_term);
|
|
437
|
+
const lt = asObj(o.long_term);
|
|
438
|
+
const cx = asObj(o.context);
|
|
439
|
+
return {
|
|
440
|
+
...o,
|
|
441
|
+
short_term: { ...st, turns: asRecords(st.turns) },
|
|
442
|
+
long_term: { ...lt, memories: asRecords(lt.memories) },
|
|
443
|
+
context: { ...cx, around_top_memory: asList(cx.around_top_memory).filter((x) => typeof x === "string") },
|
|
444
|
+
};
|
|
445
|
+
}
|
|
392
446
|
function mergeResults(r) {
|
|
393
447
|
const out = asRecords(r?.memories);
|
|
394
448
|
const seen = new Set(out.map((m) => m.id).filter((id) => typeof id === "string"));
|
|
@@ -553,6 +607,15 @@ function writeKey(opts, takesImage = false) {
|
|
|
553
607
|
(takesImage ? "add takes idempotencyKey and image." : "This call takes idempotencyKey only."));
|
|
554
608
|
return opts.idempotencyKey;
|
|
555
609
|
}
|
|
610
|
+
function newIdempotencyKey() {
|
|
611
|
+
const c = globalThis.crypto;
|
|
612
|
+
if (typeof c?.randomUUID === "function")
|
|
613
|
+
return `sdk-${String(c.randomUUID()).replace(/-/g, "")}`;
|
|
614
|
+
let hex = "";
|
|
615
|
+
for (let i = 0; i < 4; i++)
|
|
616
|
+
hex += Math.floor(Math.random() * 0x100000000).toString(16).padStart(8, "0");
|
|
617
|
+
return `sdk-${hex}`;
|
|
618
|
+
}
|
|
556
619
|
function warnOnUnknownFilters(filters) {
|
|
557
620
|
warnOnceUnknownKeys(filters, KNOWN_FILTER_KEYS, warnedFilterKeys, (k) => `wontopos: unknown search filter ${JSON.stringify(k)} — the API drops keys it does not know, ` +
|
|
558
621
|
`so this filter has NO effect and the search is wider than you think. ` +
|
|
@@ -581,6 +644,7 @@ export class Client {
|
|
|
581
644
|
userinfo;
|
|
582
645
|
pageRelative;
|
|
583
646
|
customFetch;
|
|
647
|
+
allowBrowser;
|
|
584
648
|
defaultUser;
|
|
585
649
|
_rateLimit = null;
|
|
586
650
|
get rateLimit() {
|
|
@@ -614,6 +678,10 @@ export class Client {
|
|
|
614
678
|
throw new Error(notAUrl(trimmed));
|
|
615
679
|
this.pageRelative = !url;
|
|
616
680
|
this.userinfo = Boolean(parsed.username || parsed.password);
|
|
681
|
+
this.allowBrowser = opts.dangerouslyAllowBrowser === true;
|
|
682
|
+
if (!this.allowBrowser && inBrowserPage() && isServiceHost(parsed.hostname)) {
|
|
683
|
+
throw new Error(BROWSER_REFUSAL);
|
|
684
|
+
}
|
|
617
685
|
this.timeoutMs = checkMs(opts.timeoutMs) ?? 30_000;
|
|
618
686
|
this.model = opts.model ?? DEFAULT_MODEL;
|
|
619
687
|
if (this.model && !MODEL_RE.test(this.model)) {
|
|
@@ -682,6 +750,7 @@ export class Client {
|
|
|
682
750
|
fetch: this.fetchImpl,
|
|
683
751
|
deadlineMs: this.deadlineMs,
|
|
684
752
|
signal: this.signal,
|
|
753
|
+
dangerouslyAllowBrowser: this.allowBrowser,
|
|
685
754
|
...overrides,
|
|
686
755
|
});
|
|
687
756
|
}
|
|
@@ -713,22 +782,22 @@ export class Client {
|
|
|
713
782
|
if (opts.image !== undefined)
|
|
714
783
|
body.image = normalizeImage(opts.image);
|
|
715
784
|
warnOnUnknownMetadata(metadata);
|
|
716
|
-
return this.
|
|
785
|
+
return this.write("/api/v1/memory/store", body, writeKey(opts, true));
|
|
717
786
|
}
|
|
718
787
|
async store(content, userId, metadata = {}, opts = {}) {
|
|
719
788
|
return this.add(content, userId, metadata, opts);
|
|
720
789
|
}
|
|
721
790
|
async addTurn(userMsg, assistantMsg, userId, opts = {}) {
|
|
722
|
-
return this.
|
|
791
|
+
return this.write("/api/v1/memory/store-turn", { user_id: this.uid(userId), user_msg: userMsg, assistant_msg: assistantMsg }, writeKey(opts));
|
|
723
792
|
}
|
|
724
793
|
async addBulk(content, userId, category = "general", timestamp, opts = {}) {
|
|
725
794
|
const body = { user_id: this.uid(userId), content, category: category ?? "general" };
|
|
726
795
|
if (timestamp)
|
|
727
796
|
body.timestamp = timestamp;
|
|
728
|
-
return this.
|
|
797
|
+
return this.write("/api/v1/memory/bulk-store", body, writeKey(opts));
|
|
729
798
|
}
|
|
730
799
|
async update(oldMemoryId, newContent, userId, opts = {}) {
|
|
731
|
-
return this.
|
|
800
|
+
return this.write("/api/v1/memory/supersede", { user_id: this.uid(userId), old_memory_id: oldMemoryId, new_content: newContent }, writeKey(opts));
|
|
732
801
|
}
|
|
733
802
|
async search(query, userId, limit = 10, opts = {}) {
|
|
734
803
|
checkSearchOpts(opts);
|
|
@@ -773,7 +842,7 @@ export class Client {
|
|
|
773
842
|
checkContextLimit(opts.context_limit);
|
|
774
843
|
body.context_limit = opts.context_limit;
|
|
775
844
|
}
|
|
776
|
-
return this.post("/api/v1/memory/recall", withForm(body, opts.form, opts.tz));
|
|
845
|
+
return recallShape(await this.post("/api/v1/memory/recall", withForm(body, opts.form, opts.tz)));
|
|
777
846
|
}
|
|
778
847
|
async engram(name, query, userId, opts = {}) {
|
|
779
848
|
checkOpts(opts, KNOWN_ENGRAM_KEYS, "engram");
|
|
@@ -978,8 +1047,12 @@ export class Client {
|
|
|
978
1047
|
warnIfStoreIdCollapses(userId);
|
|
979
1048
|
return this.post("/api/v1/memory/forget", { user_id: userId });
|
|
980
1049
|
}
|
|
981
|
-
post(path, body
|
|
982
|
-
return this.request("POST", path, body
|
|
1050
|
+
post(path, body) {
|
|
1051
|
+
return this.request("POST", path, body);
|
|
1052
|
+
}
|
|
1053
|
+
write(path, body, idempotencyKey) {
|
|
1054
|
+
const made = idempotencyKey == null;
|
|
1055
|
+
return this.request("POST", path, body, made ? newIdempotencyKey() : idempotencyKey, false, made);
|
|
983
1056
|
}
|
|
984
1057
|
remove(path, body) {
|
|
985
1058
|
return this.request("DELETE", path, body, undefined, true);
|
|
@@ -1099,6 +1172,7 @@ export class Client {
|
|
|
1099
1172
|
const upper = method.toUpperCase();
|
|
1100
1173
|
const logPath = path.split("?")[0];
|
|
1101
1174
|
const idempotent = IDEMPOTENT_METHODS.has(upper) || reads || (upper === "POST" && READ_POSTS.has(logPath));
|
|
1175
|
+
const replaySafe = idempotent || extraHeaders?.["Idempotency-Key"] !== undefined;
|
|
1102
1176
|
const deadlineAt = this.deadlineAt();
|
|
1103
1177
|
const headers = extraHeaders ? { ...this.headers, ...extraHeaders } : this.headers;
|
|
1104
1178
|
let ambiguous = false;
|
|
@@ -1140,7 +1214,7 @@ export class Client {
|
|
|
1140
1214
|
if (timedOut)
|
|
1141
1215
|
throw cutShort();
|
|
1142
1216
|
const connect = isConnectFailure(e);
|
|
1143
|
-
if ((
|
|
1217
|
+
if ((replaySafe || connect) && !last) {
|
|
1144
1218
|
if (!connect)
|
|
1145
1219
|
ambiguous = true;
|
|
1146
1220
|
await pauseWithoutAnswer(e?.code ?? e?.name ?? "network error");
|
|
@@ -1157,7 +1231,7 @@ export class Client {
|
|
|
1157
1231
|
throw cutShort();
|
|
1158
1232
|
if (e instanceof WosError)
|
|
1159
1233
|
throw e;
|
|
1160
|
-
if (
|
|
1234
|
+
if (replaySafe && !last)
|
|
1161
1235
|
return null;
|
|
1162
1236
|
throw this.networkError(e, path);
|
|
1163
1237
|
}
|
|
@@ -1184,9 +1258,10 @@ export class Client {
|
|
|
1184
1258
|
throw new WosError(res.status, "unexpected redirect — refused (the API key never follows a redirect). Check baseUrl: exact host, https://.");
|
|
1185
1259
|
}
|
|
1186
1260
|
this._rateLimit = parseRateLimit(res.headers) ?? this._rateLimit;
|
|
1261
|
+
warnIfRetiring(this.model, res.headers);
|
|
1187
1262
|
if (!res.ok) {
|
|
1188
1263
|
const ra = retryAfterMs(res.headers.get("Retry-After"));
|
|
1189
|
-
let wait = RETRY_ALWAYS.has(res.status) || (RETRY_IF_IDEMPOTENT.has(res.status) &&
|
|
1264
|
+
let wait = RETRY_ALWAYS.has(res.status) || (RETRY_IF_IDEMPOTENT.has(res.status) && replaySafe)
|
|
1190
1265
|
? ra ?? this.backoffMs(attempt)
|
|
1191
1266
|
: undefined;
|
|
1192
1267
|
const retrying = wait !== undefined && !last && wait <= MAX_RETRY_WAIT_MS && this.fits(wait, deadlineAt);
|
|
@@ -1313,7 +1388,7 @@ export class Client {
|
|
|
1313
1388
|
clearTimeout(timer);
|
|
1314
1389
|
}
|
|
1315
1390
|
}
|
|
1316
|
-
async request(method, path, body, idempotencyKey, removes = false) {
|
|
1391
|
+
async request(method, path, body, idempotencyKey, removes = false, madeKey = false) {
|
|
1317
1392
|
const extraHeaders = this.idemHeader(idempotencyKey);
|
|
1318
1393
|
let payload;
|
|
1319
1394
|
try {
|
|
@@ -1341,7 +1416,7 @@ export class Client {
|
|
|
1341
1416
|
const got = data === null ? "null" : Array.isArray(data) ? "array" : typeof data;
|
|
1342
1417
|
throw new WosError(res.status, `expected a JSON object in the response, got ${got}`);
|
|
1343
1418
|
}
|
|
1344
|
-
if (res.headers.get("Idempotent-Replayed") === "true" && !("replayed" in data)) {
|
|
1419
|
+
if (!madeKey && res.headers.get("Idempotent-Replayed") === "true" && !("replayed" in data)) {
|
|
1345
1420
|
data.replayed = true;
|
|
1346
1421
|
}
|
|
1347
1422
|
return data;
|