wontopos 2.2.13 → 2.2.15
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 +1 -1
- package/dist/wontopos.d.ts +21 -5
- package/dist/wontopos.js +114 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ const ctx = await mem.recall("what does alice drink?", "alice");
|
|
|
22
22
|
|
|
23
23
|
## Methods
|
|
24
24
|
|
|
25
|
-
`add` · `addTurn` · `addBulk` · `update` · `search` · `recall` · `history` · `stats` · `delete` · `deleteAll`
|
|
25
|
+
`add` · `addTurn` · `addBulk` · `update` · `search` · `recall` · `history` · `stats` · `get` · `listMemories` · `delete` · `deleteAll`
|
|
26
26
|
|
|
27
27
|
All methods take `{ userId }` — it names the **store**: one isolated memory space per end-user, agent, or topic, then per account. WHO said each memory inside a store is the `speaker` tag below — storing the assistant's own words never needs a separate id.
|
|
28
28
|
|
package/dist/wontopos.d.ts
CHANGED
|
@@ -22,9 +22,14 @@
|
|
|
22
22
|
* Pure semantic retrieval — no keyword/BM25 matching, so it works the SAME in every
|
|
23
23
|
* language. Storing and searching call no LLM; you only pay for embeddings.
|
|
24
24
|
*
|
|
25
|
-
* Reliability: every call retries transient failures
|
|
26
|
-
*
|
|
27
|
-
*
|
|
25
|
+
* Reliability: every call retries transient failures with exponential backoff +
|
|
26
|
+
* jitter, honoring `Retry-After` — 429 always; 502/503 and connection errors only
|
|
27
|
+
* when a retry can never double-process a write (idempotent calls, or a failure
|
|
28
|
+
* at connect time). Tune with `maxRetries` (0 disables) and `timeoutMs`, or per
|
|
29
|
+
* call site via the `withTimeout()` / `withRetries()` clones.
|
|
30
|
+
*
|
|
31
|
+
* Debugging: set `WONTOPOS_LOG=debug` to log method/path/status/timing/retries
|
|
32
|
+
* to stderr — never memory content, request bodies, or the API key.
|
|
28
33
|
*
|
|
29
34
|
* Security posture (not configurable off): redirects are refused so the API key
|
|
30
35
|
* can never follow one to another host; responses over 64MB are refused; the key
|
|
@@ -226,8 +231,9 @@ export interface ClientOptions {
|
|
|
226
231
|
/** Default store for every call. Override per call by passing `userId`. Defaults
|
|
227
232
|
* to the account's built-in `default` store. */
|
|
228
233
|
userId?: string;
|
|
229
|
-
/** How many times to retry transient failures
|
|
230
|
-
* errors
|
|
234
|
+
/** How many times to retry transient failures before throwing — 429 always;
|
|
235
|
+
* 502/503 and connection errors only when a retry can never double-process a
|
|
236
|
+
* write. 0 disables retries (default 2). */
|
|
231
237
|
maxRetries?: number;
|
|
232
238
|
}
|
|
233
239
|
/** Wontopos memory client. Store and recall memories, isolated per `userId`. */
|
|
@@ -262,6 +268,11 @@ export declare class Client {
|
|
|
262
268
|
withModel(model: string): Client;
|
|
263
269
|
/** A client bound to `userId` as its default store (everything else kept). */
|
|
264
270
|
withUser(userId: string): Client;
|
|
271
|
+
/** A client with a different per-request timeout in ms (everything else kept).
|
|
272
|
+
* `mem.withTimeout(120_000).addBulk(bigBlob)` — this slow call only. */
|
|
273
|
+
withTimeout(timeoutMs: number): Client;
|
|
274
|
+
/** A client with a different retry budget (everything else kept). 0 disables retries. */
|
|
275
|
+
withRetries(maxRetries: number): Client;
|
|
265
276
|
/** Store one memory. `metadata` optional (e.g. { event_date: "2026-03-01" }).
|
|
266
277
|
* `metadata.speaker`: "me" = the assistant's own words, or a person's name (up to 50 per store). */
|
|
267
278
|
add(content: string, userId?: string, metadata?: Record<string, unknown>): Promise<StoreResult>;
|
|
@@ -283,6 +294,11 @@ export declare class Client {
|
|
|
283
294
|
history(userId?: string): Promise<HistoryTurn[]>;
|
|
284
295
|
/** Memory counts for a store: { total_memories, short_term_turns }. */
|
|
285
296
|
stats(userId?: string): Promise<StatsResult>;
|
|
297
|
+
/** Fetch ONE memory by id — the original text + metadata, never the vector.
|
|
298
|
+
* The id is what `add`/`store` or `listMemories` returned. Same visibility as
|
|
299
|
+
* `listMemories`: an id from another store, a decomposed atom sentence, or an
|
|
300
|
+
* invalidated memory rejects with `NotFoundError`. `userId` optional (default store). */
|
|
301
|
+
get(userId: string | undefined, memoryId: string): Promise<Memory>;
|
|
286
302
|
/** List a store's stored memories — the original text plus metadata, never the
|
|
287
303
|
* vector. Paginated: pass the returned `next_cursor` back as `cursor` for the next
|
|
288
304
|
* page; a `null` cursor means the last page. Use it to browse or export a store.
|
package/dist/wontopos.js
CHANGED
|
@@ -22,9 +22,14 @@
|
|
|
22
22
|
* Pure semantic retrieval — no keyword/BM25 matching, so it works the SAME in every
|
|
23
23
|
* language. Storing and searching call no LLM; you only pay for embeddings.
|
|
24
24
|
*
|
|
25
|
-
* Reliability: every call retries transient failures
|
|
26
|
-
*
|
|
27
|
-
*
|
|
25
|
+
* Reliability: every call retries transient failures with exponential backoff +
|
|
26
|
+
* jitter, honoring `Retry-After` — 429 always; 502/503 and connection errors only
|
|
27
|
+
* when a retry can never double-process a write (idempotent calls, or a failure
|
|
28
|
+
* at connect time). Tune with `maxRetries` (0 disables) and `timeoutMs`, or per
|
|
29
|
+
* call site via the `withTimeout()` / `withRetries()` clones.
|
|
30
|
+
*
|
|
31
|
+
* Debugging: set `WONTOPOS_LOG=debug` to log method/path/status/timing/retries
|
|
32
|
+
* to stderr — never memory content, request bodies, or the API key.
|
|
28
33
|
*
|
|
29
34
|
* Security posture (not configurable off): redirects are refused so the API key
|
|
30
35
|
* can never follow one to another host; responses over 64MB are refused; the key
|
|
@@ -32,8 +37,23 @@
|
|
|
32
37
|
* runtime's (never disabled by this SDK; Node 18+ floors TLS at 1.2). Prefer
|
|
33
38
|
* `Client.fromEnv()` over keys in source code.
|
|
34
39
|
*/
|
|
35
|
-
const VERSION = "2.2.
|
|
36
|
-
|
|
40
|
+
const VERSION = "2.2.15";
|
|
41
|
+
/** Runtime info helps support debug a report ("node 18 on Windows...") —
|
|
42
|
+
* platform only, never anything identifying. Browsers have no `process` (and
|
|
43
|
+
* silently drop the UA header anyway). */
|
|
44
|
+
const RUNTIME = (() => {
|
|
45
|
+
const p = globalThis?.process;
|
|
46
|
+
return p?.version ? ` (node/${p.version}; ${p.platform}-${p.arch})` : "";
|
|
47
|
+
})();
|
|
48
|
+
const USER_AGENT = `wontopos-node/${VERSION}${RUNTIME}`;
|
|
49
|
+
/** Wire-level debug logging: set WONTOPOS_LOG=debug. Logs method/path/status/
|
|
50
|
+
* timing/retries to stderr — NEVER memory content, request bodies, or the key. */
|
|
51
|
+
function logDebug(msg) {
|
|
52
|
+
const env = globalThis?.process?.env;
|
|
53
|
+
if (typeof env?.WONTOPOS_LOG === "string" && env.WONTOPOS_LOG.toLowerCase() === "debug") {
|
|
54
|
+
console.error(`wontopos: ${msg}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
37
57
|
const DEFAULT_BASE_URL = "https://api.wontopos.com";
|
|
38
58
|
const DEFAULT_MODEL = "tablet-1";
|
|
39
59
|
/** 429 = rate-limited BEFORE processing → always safe to retry (no write, no bill). */
|
|
@@ -49,6 +69,37 @@ const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "0.0.0.0"]);
|
|
|
49
69
|
const MAX_RESPONSE_BYTES = 64 * 1024 * 1024;
|
|
50
70
|
/** Model names travel in a header — only header-safe characters. */
|
|
51
71
|
const MODEL_RE = /^[A-Za-z0-9._-]+$/;
|
|
72
|
+
/** Error codes that only occur while ESTABLISHING a connection — the request
|
|
73
|
+
* never reached the server, so a retry can't double-process a write. Mid-stream
|
|
74
|
+
* codes (ECONNRESET, EPIPE, UND_ERR_SOCKET, ETIMEDOUT) are ambiguous: the server
|
|
75
|
+
* may already have processed (and billed) the request, so they're excluded. */
|
|
76
|
+
const CONNECT_FAIL_CODES = new Set([
|
|
77
|
+
"ECONNREFUSED",
|
|
78
|
+
"ENOTFOUND",
|
|
79
|
+
"EAI_AGAIN",
|
|
80
|
+
"EHOSTUNREACH",
|
|
81
|
+
"ENETUNREACH",
|
|
82
|
+
"ENETDOWN",
|
|
83
|
+
"EADDRNOTAVAIL",
|
|
84
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
85
|
+
]);
|
|
86
|
+
/** Walk an error's `cause` chain (and AggregateError members — happy-eyeballs
|
|
87
|
+
* connects) looking for a connect-level failure code. */
|
|
88
|
+
function isConnectFailure(e) {
|
|
89
|
+
const stack = [e];
|
|
90
|
+
for (let steps = 0; stack.length && steps < 24; steps++) {
|
|
91
|
+
const cur = stack.pop();
|
|
92
|
+
if (!cur || typeof cur !== "object")
|
|
93
|
+
continue;
|
|
94
|
+
if (typeof cur.code === "string" && CONNECT_FAIL_CODES.has(cur.code))
|
|
95
|
+
return true;
|
|
96
|
+
if (cur.cause)
|
|
97
|
+
stack.push(cur.cause);
|
|
98
|
+
if (Array.isArray(cur.errors))
|
|
99
|
+
stack.push(...cur.errors.slice(0, 8));
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
52
103
|
/** `wos-abc...wxyz` — enough to tell keys apart, never enough to use. */
|
|
53
104
|
function maskKey(key) {
|
|
54
105
|
return key.length > 12 ? `${key.slice(0, 4)}...${key.slice(-4)}` : "***";
|
|
@@ -152,7 +203,11 @@ export class Client {
|
|
|
152
203
|
throw new Error("apiKey contains whitespace - check for a stray newline or paste error");
|
|
153
204
|
this.apiKey = key;
|
|
154
205
|
this.base = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
155
|
-
|
|
206
|
+
// Coerce a non-finite or non-positive timeout back to the default — NaN
|
|
207
|
+
// (e.g. Number() of an unset env var) would otherwise make the abort timer
|
|
208
|
+
// fire immediately and every request "time out after NaNms".
|
|
209
|
+
const tm = opts.timeoutMs;
|
|
210
|
+
this.timeoutMs = typeof tm === "number" && Number.isFinite(tm) && tm > 0 ? tm : 30_000;
|
|
156
211
|
this.model = opts.model ?? DEFAULT_MODEL;
|
|
157
212
|
if (this.model && !MODEL_RE.test(this.model)) {
|
|
158
213
|
throw new Error(`invalid model name: ${JSON.stringify(this.model)} (letters, digits, '.', '_', '-' only)`);
|
|
@@ -227,6 +282,15 @@ export class Client {
|
|
|
227
282
|
withUser(userId) {
|
|
228
283
|
return this.clone({ userId });
|
|
229
284
|
}
|
|
285
|
+
/** A client with a different per-request timeout in ms (everything else kept).
|
|
286
|
+
* `mem.withTimeout(120_000).addBulk(bigBlob)` — this slow call only. */
|
|
287
|
+
withTimeout(timeoutMs) {
|
|
288
|
+
return this.clone({ timeoutMs });
|
|
289
|
+
}
|
|
290
|
+
/** A client with a different retry budget (everything else kept). 0 disables retries. */
|
|
291
|
+
withRetries(maxRetries) {
|
|
292
|
+
return this.clone({ maxRetries });
|
|
293
|
+
}
|
|
230
294
|
// ----- write -----
|
|
231
295
|
// `userId` is optional in every call below - omit it to use the client's default
|
|
232
296
|
// store (set via `new Client({ apiKey, userId })` or `withUser`).
|
|
@@ -279,6 +343,18 @@ export class Client {
|
|
|
279
343
|
stats(userId) {
|
|
280
344
|
return this.post("/api/v1/memory/stats", { user_id: this.uid(userId) });
|
|
281
345
|
}
|
|
346
|
+
/** Fetch ONE memory by id — the original text + metadata, never the vector.
|
|
347
|
+
* The id is what `add`/`store` or `listMemories` returned. Same visibility as
|
|
348
|
+
* `listMemories`: an id from another store, a decomposed atom sentence, or an
|
|
349
|
+
* invalidated memory rejects with `NotFoundError`. `userId` optional (default store). */
|
|
350
|
+
get(userId, memoryId) {
|
|
351
|
+
// Sync throw (same shape as `delete`), so a missing id fails loudly even
|
|
352
|
+
// when the caller forgets to await.
|
|
353
|
+
if (!memoryId || typeof memoryId !== "string") {
|
|
354
|
+
throw new Error("memoryId is required — the id that add/store or listMemories returned.");
|
|
355
|
+
}
|
|
356
|
+
return this.post("/api/v1/memory/get", { user_id: this.uid(userId), memory_id: memoryId }).then((r) => (r.memory ?? {}));
|
|
357
|
+
}
|
|
282
358
|
/** List a store's stored memories — the original text plus metadata, never the
|
|
283
359
|
* vector. Paginated: pass the returned `next_cursor` back as `cursor` for the next
|
|
284
360
|
* page; a `null` cursor means the last page. Use it to browse or export a store.
|
|
@@ -439,7 +515,11 @@ export class Client {
|
|
|
439
515
|
}
|
|
440
516
|
async request(method, path, body) {
|
|
441
517
|
const attempts = this.maxRetries + 1;
|
|
518
|
+
// Debug logging shows the path WITHOUT its query string — a query can carry
|
|
519
|
+
// a store id (listSpeakers), and logs must never carry data.
|
|
520
|
+
const logPath = path.split("?")[0];
|
|
442
521
|
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
522
|
+
const start = Date.now();
|
|
443
523
|
const ctrl = new AbortController();
|
|
444
524
|
const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
|
|
445
525
|
let res;
|
|
@@ -458,8 +538,15 @@ export class Client {
|
|
|
458
538
|
clearTimeout(timer);
|
|
459
539
|
// Timeouts are ambiguous (the write may have landed) — don't retry those.
|
|
460
540
|
const timedOut = e?.name === "AbortError" || ctrl.signal.aborted;
|
|
461
|
-
|
|
462
|
-
|
|
541
|
+
// Other network errors retry only when a retry can't double-process a
|
|
542
|
+
// write: idempotent methods always; writes only for connect-level
|
|
543
|
+
// failures (the request never reached the server). A mid-stream drop on
|
|
544
|
+
// a POST may already have stored + billed server-side.
|
|
545
|
+
const safe = IDEMPOTENT_METHODS.has(method.toUpperCase()) || isConnectFailure(e);
|
|
546
|
+
if (!timedOut && safe && attempt + 1 < attempts) {
|
|
547
|
+
const delay = this.backoffMs(attempt);
|
|
548
|
+
logDebug(`${method} ${logPath}: ${e?.code ?? e?.name ?? "network error"} — retrying in ${delay}ms (attempt ${attempt + 1}/${attempts})`);
|
|
549
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
463
550
|
continue;
|
|
464
551
|
}
|
|
465
552
|
throw new APIConnectionError(0, timedOut ? `request timed out after ${this.timeoutMs}ms` : `network error: ${e?.message ?? e}`);
|
|
@@ -470,7 +557,9 @@ export class Client {
|
|
|
470
557
|
clearTimeout(timer);
|
|
471
558
|
const retryAfter = res.headers.get("Retry-After");
|
|
472
559
|
void res.body?.cancel();
|
|
473
|
-
|
|
560
|
+
const delay = this.backoffMs(attempt, retryAfter);
|
|
561
|
+
logDebug(`${method} ${logPath} -> ${res.status} — retrying in ${delay}ms (attempt ${attempt + 1}/${attempts})`);
|
|
562
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
474
563
|
continue;
|
|
475
564
|
}
|
|
476
565
|
if ((res.status >= 300 && res.status < 400) || res.type === "opaqueredirect") {
|
|
@@ -489,11 +578,16 @@ export class Client {
|
|
|
489
578
|
if (e?.name === "AbortError" || ctrl.signal.aborted) {
|
|
490
579
|
throw new APIConnectionError(0, `request timed out after ${this.timeoutMs}ms`);
|
|
491
580
|
}
|
|
492
|
-
|
|
581
|
+
if (e instanceof WosError)
|
|
582
|
+
throw e; // the size cap — already the right error
|
|
583
|
+
// A drop while READING the body is a transport failure — surface it as
|
|
584
|
+
// APIConnectionError, not a raw fetch TypeError. Ambiguous, never retried.
|
|
585
|
+
throw new APIConnectionError(0, `network error: ${e?.message ?? e}`);
|
|
493
586
|
}
|
|
494
587
|
finally {
|
|
495
588
|
clearTimeout(timer);
|
|
496
589
|
}
|
|
590
|
+
logDebug(`${method} ${logPath} -> ${res.status} in ${Date.now() - start}ms (attempt ${attempt + 1}/${attempts})`);
|
|
497
591
|
if (!res.ok) {
|
|
498
592
|
// Server returns either Anthropic-style envelope
|
|
499
593
|
// {"type":"error","error":{"type":...,"message":...,"request_id":...}}
|
|
@@ -520,7 +614,16 @@ export class Client {
|
|
|
520
614
|
}
|
|
521
615
|
throw errorFor(res.status, msg, requestId);
|
|
522
616
|
}
|
|
523
|
-
|
|
617
|
+
if (!text)
|
|
618
|
+
return null;
|
|
619
|
+
try {
|
|
620
|
+
return JSON.parse(text);
|
|
621
|
+
}
|
|
622
|
+
catch (e) {
|
|
623
|
+
// A 2xx with a corrupt body is a real failure — surface it, don't leak
|
|
624
|
+
// a raw SyntaxError (parity with the Rust SDK).
|
|
625
|
+
throw new WosError(res.status, `invalid JSON in response: ${e?.message ?? e}`);
|
|
626
|
+
}
|
|
524
627
|
}
|
|
525
628
|
throw new WosError(0, "retries exhausted"); // unreachable; keeps the compiler happy
|
|
526
629
|
}
|
package/package.json
CHANGED