wontopos 2.2.14 → 2.2.16
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 +15 -1
- package/dist/wontopos.js +69 -10
- 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
|
@@ -25,7 +25,11 @@
|
|
|
25
25
|
* Reliability: every call retries transient failures with exponential backoff +
|
|
26
26
|
* jitter, honoring `Retry-After` — 429 always; 502/503 and connection errors only
|
|
27
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
|
|
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.
|
|
29
33
|
*
|
|
30
34
|
* Security posture (not configurable off): redirects are refused so the API key
|
|
31
35
|
* can never follow one to another host; responses over 64MB are refused; the key
|
|
@@ -264,6 +268,11 @@ export declare class Client {
|
|
|
264
268
|
withModel(model: string): Client;
|
|
265
269
|
/** A client bound to `userId` as its default store (everything else kept). */
|
|
266
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;
|
|
267
276
|
/** Store one memory. `metadata` optional (e.g. { event_date: "2026-03-01" }).
|
|
268
277
|
* `metadata.speaker`: "me" = the assistant's own words, or a person's name (up to 50 per store). */
|
|
269
278
|
add(content: string, userId?: string, metadata?: Record<string, unknown>): Promise<StoreResult>;
|
|
@@ -285,6 +294,11 @@ export declare class Client {
|
|
|
285
294
|
history(userId?: string): Promise<HistoryTurn[]>;
|
|
286
295
|
/** Memory counts for a store: { total_memories, short_term_turns }. */
|
|
287
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>;
|
|
288
302
|
/** List a store's stored memories — the original text plus metadata, never the
|
|
289
303
|
* vector. Paginated: pass the returned `next_cursor` back as `cursor` for the next
|
|
290
304
|
* page; a `null` cursor means the last page. Use it to browse or export a store.
|
package/dist/wontopos.js
CHANGED
|
@@ -25,7 +25,11 @@
|
|
|
25
25
|
* Reliability: every call retries transient failures with exponential backoff +
|
|
26
26
|
* jitter, honoring `Retry-After` — 429 always; 502/503 and connection errors only
|
|
27
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
|
|
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.
|
|
29
33
|
*
|
|
30
34
|
* Security posture (not configurable off): redirects are refused so the API key
|
|
31
35
|
* can never follow one to another host; responses over 64MB are refused; the key
|
|
@@ -33,8 +37,23 @@
|
|
|
33
37
|
* runtime's (never disabled by this SDK; Node 18+ floors TLS at 1.2). Prefer
|
|
34
38
|
* `Client.fromEnv()` over keys in source code.
|
|
35
39
|
*/
|
|
36
|
-
const VERSION = "2.2.
|
|
37
|
-
|
|
40
|
+
const VERSION = "2.2.16";
|
|
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
|
+
}
|
|
38
57
|
const DEFAULT_BASE_URL = "https://api.wontopos.com";
|
|
39
58
|
const DEFAULT_MODEL = "tablet-1";
|
|
40
59
|
/** 429 = rate-limited BEFORE processing → always safe to retry (no write, no bill). */
|
|
@@ -50,6 +69,10 @@ const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "0.0.0.0"]);
|
|
|
50
69
|
const MAX_RESPONSE_BYTES = 64 * 1024 * 1024;
|
|
51
70
|
/** Model names travel in a header — only header-safe characters. */
|
|
52
71
|
const MODEL_RE = /^[A-Za-z0-9._-]+$/;
|
|
72
|
+
/** Cap a server-controlled error message so a hostile body can't blow up a log. */
|
|
73
|
+
const MAX_ERR_MSG = 4096;
|
|
74
|
+
/** Paging backstop: bound the walk so a fresh-cursor-forever server can't loop us. */
|
|
75
|
+
const MAX_PAGES = 1_000_000;
|
|
53
76
|
/** Error codes that only occur while ESTABLISHING a connection — the request
|
|
54
77
|
* never reached the server, so a retry can't double-process a write. Mid-stream
|
|
55
78
|
* codes (ECONNRESET, EPIPE, UND_ERR_SOCKET, ETIMEDOUT) are ambiguous: the server
|
|
@@ -263,6 +286,15 @@ export class Client {
|
|
|
263
286
|
withUser(userId) {
|
|
264
287
|
return this.clone({ userId });
|
|
265
288
|
}
|
|
289
|
+
/** A client with a different per-request timeout in ms (everything else kept).
|
|
290
|
+
* `mem.withTimeout(120_000).addBulk(bigBlob)` — this slow call only. */
|
|
291
|
+
withTimeout(timeoutMs) {
|
|
292
|
+
return this.clone({ timeoutMs });
|
|
293
|
+
}
|
|
294
|
+
/** A client with a different retry budget (everything else kept). 0 disables retries. */
|
|
295
|
+
withRetries(maxRetries) {
|
|
296
|
+
return this.clone({ maxRetries });
|
|
297
|
+
}
|
|
266
298
|
// ----- write -----
|
|
267
299
|
// `userId` is optional in every call below - omit it to use the client's default
|
|
268
300
|
// store (set via `new Client({ apiKey, userId })` or `withUser`).
|
|
@@ -315,6 +347,18 @@ export class Client {
|
|
|
315
347
|
stats(userId) {
|
|
316
348
|
return this.post("/api/v1/memory/stats", { user_id: this.uid(userId) });
|
|
317
349
|
}
|
|
350
|
+
/** Fetch ONE memory by id — the original text + metadata, never the vector.
|
|
351
|
+
* The id is what `add`/`store` or `listMemories` returned. Same visibility as
|
|
352
|
+
* `listMemories`: an id from another store, a decomposed atom sentence, or an
|
|
353
|
+
* invalidated memory rejects with `NotFoundError`. `userId` optional (default store). */
|
|
354
|
+
get(userId, memoryId) {
|
|
355
|
+
// Sync throw (same shape as `delete`), so a missing id fails loudly even
|
|
356
|
+
// when the caller forgets to await.
|
|
357
|
+
if (!memoryId || typeof memoryId !== "string") {
|
|
358
|
+
throw new Error("memoryId is required — the id that add/store or listMemories returned.");
|
|
359
|
+
}
|
|
360
|
+
return this.post("/api/v1/memory/get", { user_id: this.uid(userId), memory_id: memoryId }).then((r) => (r.memory ?? {}));
|
|
361
|
+
}
|
|
318
362
|
/** List a store's stored memories — the original text plus metadata, never the
|
|
319
363
|
* vector. Paginated: pass the returned `next_cursor` back as `cursor` for the next
|
|
320
364
|
* page; a `null` cursor means the last page. Use it to browse or export a store.
|
|
@@ -341,12 +385,13 @@ export class Client {
|
|
|
341
385
|
async *iterMemories(userId, opts = {}) {
|
|
342
386
|
let cursor;
|
|
343
387
|
const seen = new Set();
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
388
|
+
// Backstop: the cursor-repeat guard catches a repeated cursor, but not a
|
|
389
|
+
// server that mints a FRESH cursor every page forever — bound the walk.
|
|
390
|
+
for (let page = 0; page < MAX_PAGES; page++) {
|
|
391
|
+
const p = await this.listMemories(userId, { limit: opts.pageSize ?? 100, cursor });
|
|
392
|
+
for (const m of p.memories ?? [])
|
|
347
393
|
yield m;
|
|
348
|
-
const next =
|
|
349
|
-
// Stop on last page OR a server that repeats a cursor (would loop forever).
|
|
394
|
+
const next = p.next_cursor ?? undefined;
|
|
350
395
|
if (!next || seen.has(next))
|
|
351
396
|
break;
|
|
352
397
|
seen.add(next);
|
|
@@ -475,7 +520,11 @@ export class Client {
|
|
|
475
520
|
}
|
|
476
521
|
async request(method, path, body) {
|
|
477
522
|
const attempts = this.maxRetries + 1;
|
|
523
|
+
// Debug logging shows the path WITHOUT its query string — a query can carry
|
|
524
|
+
// a store id (listSpeakers), and logs must never carry data.
|
|
525
|
+
const logPath = path.split("?")[0];
|
|
478
526
|
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
527
|
+
const start = Date.now();
|
|
479
528
|
const ctrl = new AbortController();
|
|
480
529
|
const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
|
|
481
530
|
let res;
|
|
@@ -500,7 +549,9 @@ export class Client {
|
|
|
500
549
|
// a POST may already have stored + billed server-side.
|
|
501
550
|
const safe = IDEMPOTENT_METHODS.has(method.toUpperCase()) || isConnectFailure(e);
|
|
502
551
|
if (!timedOut && safe && attempt + 1 < attempts) {
|
|
503
|
-
|
|
552
|
+
const delay = this.backoffMs(attempt);
|
|
553
|
+
logDebug(`${method} ${logPath}: ${e?.code ?? e?.name ?? "network error"} — retrying in ${delay}ms (attempt ${attempt + 1}/${attempts})`);
|
|
554
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
504
555
|
continue;
|
|
505
556
|
}
|
|
506
557
|
throw new APIConnectionError(0, timedOut ? `request timed out after ${this.timeoutMs}ms` : `network error: ${e?.message ?? e}`);
|
|
@@ -511,11 +562,14 @@ export class Client {
|
|
|
511
562
|
clearTimeout(timer);
|
|
512
563
|
const retryAfter = res.headers.get("Retry-After");
|
|
513
564
|
void res.body?.cancel();
|
|
514
|
-
|
|
565
|
+
const delay = this.backoffMs(attempt, retryAfter);
|
|
566
|
+
logDebug(`${method} ${logPath} -> ${res.status} — retrying in ${delay}ms (attempt ${attempt + 1}/${attempts})`);
|
|
567
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
515
568
|
continue;
|
|
516
569
|
}
|
|
517
570
|
if ((res.status >= 300 && res.status < 400) || res.type === "opaqueredirect") {
|
|
518
571
|
clearTimeout(timer);
|
|
572
|
+
void res.body?.cancel(); // release the stream — otherwise the connection leaks
|
|
519
573
|
throw new WosError(res.status, "unexpected redirect — refused (the API key never follows a redirect). Check baseUrl: exact host, https://.");
|
|
520
574
|
}
|
|
521
575
|
this._rateLimit = parseRateLimit(res.headers) ?? this._rateLimit;
|
|
@@ -539,6 +593,7 @@ export class Client {
|
|
|
539
593
|
finally {
|
|
540
594
|
clearTimeout(timer);
|
|
541
595
|
}
|
|
596
|
+
logDebug(`${method} ${logPath} -> ${res.status} in ${Date.now() - start}ms (attempt ${attempt + 1}/${attempts})`);
|
|
542
597
|
if (!res.ok) {
|
|
543
598
|
// Server returns either Anthropic-style envelope
|
|
544
599
|
// {"type":"error","error":{"type":...,"message":...,"request_id":...}}
|
|
@@ -563,6 +618,10 @@ export class Client {
|
|
|
563
618
|
catch {
|
|
564
619
|
/* keep raw text */
|
|
565
620
|
}
|
|
621
|
+
// Cap the FINAL message (a string `error` field can be as huge as the body),
|
|
622
|
+
// so a hostile server can't turn our exception/log into a giant string.
|
|
623
|
+
if (typeof msg === "string" && msg.length > MAX_ERR_MSG)
|
|
624
|
+
msg = msg.slice(0, MAX_ERR_MSG) + "…(truncated)";
|
|
566
625
|
throw errorFor(res.status, msg, requestId);
|
|
567
626
|
}
|
|
568
627
|
if (!text)
|
package/package.json
CHANGED