wontopos 2.2.24 → 2.2.26
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 +28 -1
- package/dist/wontopos.d.ts +51 -2
- package/dist/wontopos.js +106 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -52,7 +52,13 @@ registration and never counts against it.
|
|
|
52
52
|
## Recall caching
|
|
53
53
|
|
|
54
54
|
Opt in per search and repeated or extended queries reuse the previous result
|
|
55
|
-
at 10% of the normal rate (Tablet and Scroll models)
|
|
55
|
+
at 10% of the normal rate (Tablet and Scroll models).
|
|
56
|
+
|
|
57
|
+
It is not free to turn on: the FIRST call writes the cache and bills the query
|
|
58
|
+
tokens at 2x for a `5m` TTL, 3x for `1h`. Only hits inside the TTL bill at 0.1x.
|
|
59
|
+
So it pays for a query you repeat or extend, and costs more for one you issue
|
|
60
|
+
once — do not switch it on globally. Any write to the store invalidates its cache
|
|
61
|
+
at once, so a hit can never predate a new memory.
|
|
56
62
|
|
|
57
63
|
```ts
|
|
58
64
|
const hits = await mem.search("...the conversation so far...", "alice", 10,
|
|
@@ -74,6 +80,27 @@ Built in, no configuration needed:
|
|
|
74
80
|
meaning "delete everything"; wiping a store is only ever the explicit
|
|
75
81
|
`deleteAll(userId)` / `deleteStore(userId)`.
|
|
76
82
|
|
|
83
|
+
### Behind a corporate proxy
|
|
84
|
+
|
|
85
|
+
Node's global `fetch` (undici) ignores `HTTP_PROXY` / `HTTPS_PROXY`, so behind an
|
|
86
|
+
egress proxy this SDK could not connect at all — while the Python and Rust SDKs went
|
|
87
|
+
through, because `requests` and `reqwest` both read those variables. Pass a
|
|
88
|
+
proxy-aware `fetch` and the three behave the same:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import { ProxyAgent } from "undici";
|
|
92
|
+
|
|
93
|
+
const agent = new ProxyAgent(process.env.HTTPS_PROXY!);
|
|
94
|
+
const mem = new Client({
|
|
95
|
+
apiKey: process.env.WONTOPOS_API_KEY!,
|
|
96
|
+
fetch: (url, init) => fetch(url, { ...init, dispatcher: agent } as any),
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The same option is how you add instrumentation or drive the client without a
|
|
101
|
+
network in tests. It replaces the transport, not the client's rules — retries,
|
|
102
|
+
timeouts, redirect refusal and the response cap all still apply.
|
|
103
|
+
|
|
77
104
|
## Security
|
|
78
105
|
|
|
79
106
|
Built in, none of it configurable off:
|
package/dist/wontopos.d.ts
CHANGED
|
@@ -37,6 +37,8 @@
|
|
|
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
|
+
/** Test hook — the warn-once set is process-global by design. */
|
|
41
|
+
export declare function _resetStoreIdWarnings(): void;
|
|
40
42
|
/** Quota from a response's `X-RateLimit-*` headers, or `null` if absent. */
|
|
41
43
|
export interface RateLimit {
|
|
42
44
|
limit: number | null;
|
|
@@ -73,7 +75,18 @@ export declare class ConflictError extends WosError {
|
|
|
73
75
|
/** 429 — too many requests. Back off and retry (the client already retries these). */
|
|
74
76
|
export declare class RateLimitError extends WosError {
|
|
75
77
|
}
|
|
76
|
-
/**
|
|
78
|
+
/**
|
|
79
|
+
* 5xx — the service failed.
|
|
80
|
+
*
|
|
81
|
+
* `502` / `503` are transient: the client already retries them for calls where a
|
|
82
|
+
* retry cannot double-process a write, and retrying yourself is reasonable.
|
|
83
|
+
*
|
|
84
|
+
* `501` is NOT transient. It means the engine behind the model you selected does
|
|
85
|
+
* not implement that endpoint at all (the error carries `model` and `endpoint`).
|
|
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.
|
|
89
|
+
*/
|
|
77
90
|
export declare class ServerError extends WosError {
|
|
78
91
|
}
|
|
79
92
|
export interface Usage {
|
|
@@ -121,6 +134,10 @@ export interface StoreResult {
|
|
|
121
134
|
duplicate_of?: string;
|
|
122
135
|
/** Present when the store was a duplicate and something (e.g. a speaker tag) was dropped. */
|
|
123
136
|
note?: string;
|
|
137
|
+
/** `true` when this response was REPLAYED from a previous request with the same
|
|
138
|
+
* `idempotencyKey` — nothing new was stored. Absent on a fresh write. Read it to
|
|
139
|
+
* tell "my retry landed" from "my retry was a no-op". */
|
|
140
|
+
replayed?: boolean;
|
|
124
141
|
[key: string]: unknown;
|
|
125
142
|
}
|
|
126
143
|
/** `addTurn` / `addBulk` / `delete` / `deleteAll` → `{ status }`. */
|
|
@@ -209,8 +226,13 @@ export interface EngramCatalog {
|
|
|
209
226
|
}
|
|
210
227
|
/** `createStore` / `deleteStore` → `{ user_id, status }`. */
|
|
211
228
|
export interface StoreOpResult {
|
|
229
|
+
/** The store id the API actually used — NOT necessarily the one you sent.
|
|
230
|
+
* Ids are normalized (see the `note`), so compare this against your input. */
|
|
212
231
|
user_id?: string;
|
|
213
232
|
status?: string;
|
|
233
|
+
/** Present when the API changed your id, explaining how. Read it: two of your
|
|
234
|
+
* end users can land in one store if their ids differ only by punctuation. */
|
|
235
|
+
note?: string;
|
|
214
236
|
[key: string]: unknown;
|
|
215
237
|
}
|
|
216
238
|
export interface StoreInfo {
|
|
@@ -260,9 +282,13 @@ export interface SearchFilters {
|
|
|
260
282
|
/** Drop matches the engine scored below this importance (0–1). */
|
|
261
283
|
min_importance?: number;
|
|
262
284
|
}
|
|
285
|
+
/** Test hook — the warn-once set is process-global by design. */
|
|
286
|
+
export declare function _resetFilterWarnings(): void;
|
|
263
287
|
/** Known `search` options; extra fields pass through to the API untouched. */
|
|
264
288
|
export interface SearchOptions {
|
|
265
|
-
/** Recall caching
|
|
289
|
+
/** Recall caching. A hit inside the TTL bills at 0.1× — but the FIRST call
|
|
290
|
+
* writes the cache and bills the query tokens at 2× (`5m`) or 3× (`1h`), so
|
|
291
|
+
* this only pays for a query you repeat or extend. Do not enable it globally. */
|
|
266
292
|
cache_control?: {
|
|
267
293
|
ttl: "5m" | "1h";
|
|
268
294
|
};
|
|
@@ -312,6 +338,26 @@ export interface ClientOptions {
|
|
|
312
338
|
* key on an options object is not an error in TS at runtime). Both names work;
|
|
313
339
|
* `maxRetries` wins if somehow both are given. */
|
|
314
340
|
retries?: number;
|
|
341
|
+
/** The `fetch` used for every request (default: the global `fetch`).
|
|
342
|
+
*
|
|
343
|
+
* This is the seam for anything the runtime cannot express through options.
|
|
344
|
+
* The one that matters in practice is a corporate egress proxy: Node's global
|
|
345
|
+
* fetch (undici) IGNORES `HTTP_PROXY` / `HTTPS_PROXY`, so behind such a proxy
|
|
346
|
+
* this SDK could not connect at all — while the Python and Rust SDKs went
|
|
347
|
+
* through, because `requests` and `reqwest` both read those variables. Pass a
|
|
348
|
+
* proxy-aware fetch and the three behave the same again:
|
|
349
|
+
*
|
|
350
|
+
* import { ProxyAgent } from "undici";
|
|
351
|
+
* const agent = new ProxyAgent(process.env.HTTPS_PROXY!);
|
|
352
|
+
* const mem = new Client({
|
|
353
|
+
* apiKey,
|
|
354
|
+
* fetch: (url, init) => fetch(url, { ...init, dispatcher: agent } as any),
|
|
355
|
+
* });
|
|
356
|
+
*
|
|
357
|
+
* It is also how you add instrumentation or drive the client in a test without
|
|
358
|
+
* a network. Retries, timeouts, redirect refusal and the size cap all still
|
|
359
|
+
* apply — this replaces the transport, not the client's rules. */
|
|
360
|
+
fetch?: typeof fetch;
|
|
315
361
|
}
|
|
316
362
|
/** Wontopos memory client. Store and recall memories, isolated per `userId`. */
|
|
317
363
|
export declare class Client {
|
|
@@ -321,6 +367,9 @@ export declare class Client {
|
|
|
321
367
|
private readonly apiKey;
|
|
322
368
|
private readonly model;
|
|
323
369
|
private readonly maxRetries;
|
|
370
|
+
/** Transport. Held as a field (not read off `globalThis` per call) so a caller
|
|
371
|
+
* who passes one gets it for every request, including from cloned clients. */
|
|
372
|
+
private readonly fetchImpl;
|
|
324
373
|
/** The store every call uses unless one passes `userId`. */
|
|
325
374
|
private readonly defaultUser;
|
|
326
375
|
private _rateLimit;
|
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.26";
|
|
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). */
|
|
@@ -74,6 +74,40 @@ const MAX_ERR_MSG = 4096;
|
|
|
74
74
|
/** What the API accepts as an `Idempotency-Key`. Checked client-side so a bad key
|
|
75
75
|
* fails before the request instead of coming back as a 400 mid-retry. */
|
|
76
76
|
const IDEMPOTENCY_KEY_RE = /^[A-Za-z0-9._:\-]{1,128}$/;
|
|
77
|
+
/**
|
|
78
|
+
* The API normalizes a store id: lowercased, and every character outside
|
|
79
|
+
* `[a-z0-9_]` becomes `_`. So `Alice.Smith`, `alice-smith` and `alice_smith` are
|
|
80
|
+
* ALL the same store.
|
|
81
|
+
*
|
|
82
|
+
* That is a data-exposure hazard for the most common way this SDK is used — one
|
|
83
|
+
* store per end user. Two accounts whose ids differ only by punctuation or case
|
|
84
|
+
* (`bob.lee@x.com` / `bob-lee@x.com`) silently share every memory, and nothing in
|
|
85
|
+
* the response says so: the note only appears when `createStore` creates one, and
|
|
86
|
+
* an app that reuses an existing store never sees it.
|
|
87
|
+
*
|
|
88
|
+
* We cannot refuse the id — the API accepts it, and callers may have written it
|
|
89
|
+
* this way for a year. So we say it once, loudly, at the moment it happens.
|
|
90
|
+
*/
|
|
91
|
+
function normalizeStoreId(id) {
|
|
92
|
+
return id.toLowerCase().replace(/[^a-z0-9_]/g, "_");
|
|
93
|
+
}
|
|
94
|
+
const warnedStoreIds = new Set();
|
|
95
|
+
function warnIfStoreIdCollapses(id) {
|
|
96
|
+
if (!id || warnedStoreIds.has(id))
|
|
97
|
+
return;
|
|
98
|
+
const normalized = normalizeStoreId(id);
|
|
99
|
+
if (normalized === id)
|
|
100
|
+
return;
|
|
101
|
+
warnedStoreIds.add(id);
|
|
102
|
+
console.warn(`wontopos: store id ${JSON.stringify(id)} is stored as ${JSON.stringify(normalized)} ` +
|
|
103
|
+
`(lowercased, and anything outside [a-z0-9_] becomes "_"). Ids that differ only by case or ` +
|
|
104
|
+
`punctuation share ONE store and therefore one set of memories — if these ids come from your ` +
|
|
105
|
+
`end users, normalize them yourself first so two people can never collide.`);
|
|
106
|
+
}
|
|
107
|
+
/** Test hook — the warn-once set is process-global by design. */
|
|
108
|
+
export function _resetStoreIdWarnings() {
|
|
109
|
+
warnedStoreIds.clear();
|
|
110
|
+
}
|
|
77
111
|
/** Statuses that legitimately carry NO body (RFC 9110). Everything else must
|
|
78
112
|
* answer with a JSON object — see the empty-body check in `request`. */
|
|
79
113
|
const NO_BODY_STATUS = new Set([204, 205, 304]);
|
|
@@ -208,7 +242,18 @@ export class ConflictError extends WosError {
|
|
|
208
242
|
/** 429 — too many requests. Back off and retry (the client already retries these). */
|
|
209
243
|
export class RateLimitError extends WosError {
|
|
210
244
|
}
|
|
211
|
-
/**
|
|
245
|
+
/**
|
|
246
|
+
* 5xx — the service failed.
|
|
247
|
+
*
|
|
248
|
+
* `502` / `503` are transient: the client already retries them for calls where a
|
|
249
|
+
* retry cannot double-process a write, and retrying yourself is reasonable.
|
|
250
|
+
*
|
|
251
|
+
* `501` is NOT transient. It means the engine behind the model you selected does
|
|
252
|
+
* not implement that endpoint at all (the error carries `model` and `endpoint`).
|
|
253
|
+
* Retrying can never succeed — pick a model that supports it (`listModels`) or
|
|
254
|
+
* drop the call. This class used to say "safe to retry" for the whole 5xx range,
|
|
255
|
+
* which sent callers into a loop that could not end.
|
|
256
|
+
*/
|
|
212
257
|
export class ServerError extends WosError {
|
|
213
258
|
}
|
|
214
259
|
const STATUS_ERRORS = {
|
|
@@ -228,6 +273,34 @@ function errorFor(status, message, requestId) {
|
|
|
228
273
|
: STATUS_ERRORS[status] ?? (status >= 500 && status < 600 ? ServerError : WosError);
|
|
229
274
|
return new Cls(status, message, requestId);
|
|
230
275
|
}
|
|
276
|
+
/** The filter keys the API acts on. Anything else it DROPS silently, so a typo
|
|
277
|
+
* widens the search instead of failing — we warn rather than reject, because the
|
|
278
|
+
* API may grow a key before this package is updated. */
|
|
279
|
+
const KNOWN_FILTER_KEYS = new Set([
|
|
280
|
+
"categories",
|
|
281
|
+
"event_from",
|
|
282
|
+
"event_to",
|
|
283
|
+
"time_from",
|
|
284
|
+
"time_to",
|
|
285
|
+
"min_importance",
|
|
286
|
+
]);
|
|
287
|
+
const warnedFilterKeys = new Set();
|
|
288
|
+
function warnOnUnknownFilters(filters) {
|
|
289
|
+
if (!filters || typeof filters !== "object" || Array.isArray(filters))
|
|
290
|
+
return;
|
|
291
|
+
for (const k of Object.keys(filters)) {
|
|
292
|
+
if (KNOWN_FILTER_KEYS.has(k) || warnedFilterKeys.has(k))
|
|
293
|
+
continue;
|
|
294
|
+
warnedFilterKeys.add(k);
|
|
295
|
+
console.warn(`wontopos: unknown search filter ${JSON.stringify(k)} — the API drops keys it does not know, ` +
|
|
296
|
+
`so this filter has NO effect and the search is wider than you think. ` +
|
|
297
|
+
`Known keys: ${[...KNOWN_FILTER_KEYS].join(", ")}.`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/** Test hook — the warn-once set is process-global by design. */
|
|
301
|
+
export function _resetFilterWarnings() {
|
|
302
|
+
warnedFilterKeys.clear();
|
|
303
|
+
}
|
|
231
304
|
const DEFAULT_USER = "default";
|
|
232
305
|
/** Wontopos memory client. Store and recall memories, isolated per `userId`. */
|
|
233
306
|
export class Client {
|
|
@@ -237,6 +310,9 @@ export class Client {
|
|
|
237
310
|
apiKey;
|
|
238
311
|
model;
|
|
239
312
|
maxRetries;
|
|
313
|
+
/** Transport. Held as a field (not read off `globalThis` per call) so a caller
|
|
314
|
+
* who passes one gets it for every request, including from cloned clients. */
|
|
315
|
+
fetchImpl;
|
|
240
316
|
/** The store every call uses unless one passes `userId`. */
|
|
241
317
|
defaultUser;
|
|
242
318
|
_rateLimit = null;
|
|
@@ -271,6 +347,17 @@ export class Client {
|
|
|
271
347
|
// "retries exhausted"; Infinity would retry forever.
|
|
272
348
|
const mr = opts.maxRetries ?? opts.retries;
|
|
273
349
|
this.maxRetries = typeof mr === "number" && Number.isFinite(mr) ? Math.max(0, Math.floor(mr)) : 2;
|
|
350
|
+
// Bind the global so `fetch` is not called as a method of `globalThis`, which
|
|
351
|
+
// throws "Illegal invocation" on some runtimes. A caller-supplied fetch is
|
|
352
|
+
// taken as given — it is already whatever they meant to hand us.
|
|
353
|
+
const f = opts.fetch;
|
|
354
|
+
if (f !== undefined && typeof f !== "function") {
|
|
355
|
+
throw new Error("fetch must be a function (a fetch-compatible transport)");
|
|
356
|
+
}
|
|
357
|
+
this.fetchImpl = f ?? (globalThis.fetch ? globalThis.fetch.bind(globalThis) : undefined);
|
|
358
|
+
if (typeof this.fetchImpl !== "function") {
|
|
359
|
+
throw new Error("no fetch available in this runtime — pass one as `fetch` (Node 18+ has it built in; older Node needs undici)");
|
|
360
|
+
}
|
|
274
361
|
this.headers = {
|
|
275
362
|
"X-API-Key": this.apiKey,
|
|
276
363
|
"Content-Type": "application/json",
|
|
@@ -310,7 +397,9 @@ export class Client {
|
|
|
310
397
|
}
|
|
311
398
|
/** Resolve a call's store: the explicit userId, else the client default. */
|
|
312
399
|
uid(userId) {
|
|
313
|
-
|
|
400
|
+
const id = userId || this.defaultUser;
|
|
401
|
+
warnIfStoreIdCollapses(id);
|
|
402
|
+
return id;
|
|
314
403
|
}
|
|
315
404
|
clone(overrides) {
|
|
316
405
|
return new Client({
|
|
@@ -320,6 +409,10 @@ export class Client {
|
|
|
320
409
|
model: this.model,
|
|
321
410
|
userId: this.defaultUser,
|
|
322
411
|
maxRetries: this.maxRetries,
|
|
412
|
+
// Carry the transport across clones. Without this, `withModel(...)` and
|
|
413
|
+
// friends would silently fall back to the global fetch and a proxy-bound
|
|
414
|
+
// client would stop connecting the moment it was cloned.
|
|
415
|
+
fetch: this.fetchImpl,
|
|
323
416
|
...overrides,
|
|
324
417
|
});
|
|
325
418
|
}
|
|
@@ -384,6 +477,7 @@ export class Client {
|
|
|
384
477
|
async search(query, userId, limit = 10, opts = {}) {
|
|
385
478
|
// Reserved fields win over ...opts: an app that forwards untrusted input as
|
|
386
479
|
// opts must not be able to override the store (user_id), query, or limit.
|
|
480
|
+
warnOnUnknownFilters(opts.filters);
|
|
387
481
|
const r = await this.post("/api/v1/memory/search", { ...opts, user_id: this.uid(userId), query, max_results: limit });
|
|
388
482
|
return mergeLanes(r);
|
|
389
483
|
}
|
|
@@ -393,6 +487,7 @@ export class Client {
|
|
|
393
487
|
* speaker "me"), kept separate so the reader never confuses who said what. On a model
|
|
394
488
|
* without a self lane, `self_memories` is `[]`. `userId` optional (default store). */
|
|
395
489
|
async searchSelf(query, userId, limit = 10, opts = {}) {
|
|
490
|
+
warnOnUnknownFilters(opts.filters);
|
|
396
491
|
const r = await this.post("/api/v1/memory/search", { ...opts, user_id: this.uid(userId), query, max_results: limit });
|
|
397
492
|
return { memories: asRecords(r.memories), self_memories: asRecords(r.self_memories) };
|
|
398
493
|
}
|
|
@@ -633,7 +728,7 @@ export class Client {
|
|
|
633
728
|
const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
|
|
634
729
|
let res;
|
|
635
730
|
try {
|
|
636
|
-
res = await
|
|
731
|
+
res = await this.fetchImpl(`${this.base}${path}`, {
|
|
637
732
|
method,
|
|
638
733
|
headers: extraHeaders ? { ...this.headers, ...extraHeaders } : this.headers,
|
|
639
734
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
@@ -753,6 +848,13 @@ export class Client {
|
|
|
753
848
|
const got = data === null ? "null" : Array.isArray(data) ? "array" : typeof data;
|
|
754
849
|
throw new WosError(res.status, `expected a JSON object in the response, got ${got}`);
|
|
755
850
|
}
|
|
851
|
+
// 멱등키를 쓴 쓰기가 **저장된 것인지 재생된 것인지**를 호출자가 알아야 한다.
|
|
852
|
+
// 서버는 `Idempotent-Replayed: true` 로 말해주는데 지금까지 SDK 가 삼켰다 —
|
|
853
|
+
// 멱등키를 쓰는 사람이 가장 알고 싶은 사실이 그것이다(재시도가 실제로
|
|
854
|
+
// 저장을 일으켰나, 아니면 첫 응답이 되풀이된 것인가).
|
|
855
|
+
if (res.headers.get("Idempotent-Replayed") === "true") {
|
|
856
|
+
data.replayed = true;
|
|
857
|
+
}
|
|
756
858
|
return data;
|
|
757
859
|
}
|
|
758
860
|
throw new WosError(0, "retries exhausted"); // unreachable; keeps the compiler happy
|
package/package.json
CHANGED