wontopos 2.2.20 → 2.2.22

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.
@@ -76,8 +76,6 @@ export declare class RateLimitError extends WosError {
76
76
  /** 5xx — the service failed. Safe to retry. */
77
77
  export declare class ServerError extends WosError {
78
78
  }
79
- /** Build the most specific WosError subclass for an HTTP status. */
80
- export declare function errorFor(status: number, message: string, requestId?: string): WosError;
81
79
  export interface Usage {
82
80
  input_tokens?: number;
83
81
  output_tokens?: number;
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.20";
40
+ const VERSION = "2.2.22";
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). */
@@ -129,8 +129,35 @@ function parseRateLimit(h) {
129
129
  // that isn't actually an array/object to the empty value.
130
130
  const asList = (x) => (Array.isArray(x) ? x : []);
131
131
  const asObj = (x) => (x != null && typeof x === "object" && !Array.isArray(x) ? x : {});
132
+ // A list of API records (memories, turns, models...) — every element is an object
133
+ // by contract. `asList` only fixes the CONTAINER: a hostile/broken server sending
134
+ // `{"memories": [null, 1, "x", {...}]}` still handed the caller those non-objects,
135
+ // typed as Memory[], and the first `m.content` threw a raw TypeError from inside
136
+ // user code. Drop elements that aren't objects and keep the valid records, so one
137
+ // bad element never poisons the batch (and never destroys it).
138
+ const asRecords = (x) => asList(x).filter((m) => m != null && typeof m === "object" && !Array.isArray(m));
132
139
  // Attach a delivery-form override (memoir/archive) + tz to a body, the way search
133
140
  // passes them; on Scroll 1.2+ this renders each returned memory's time in that form.
141
+ /** Every memory a search returned — main lane and self lane together.
142
+ *
143
+ * On a self-lane model the service splits the result: the assistant's OWN words
144
+ * come back in `self_memories` and are REMOVED from `memories`, so the two never
145
+ * double-count. `search()` read only `memories`, so on Scroll 1.2 an assistant
146
+ * turn stored with `addTurn` was silently missing from search while the same
147
+ * query on tablet-1 returned it — upgrading the model made search return LESS,
148
+ * and the dropped memories were already on the wire and already paid for.
149
+ *
150
+ * Both lanes, de-duplicated by id, each memory keeping its `speaker` so the
151
+ * caller can still tell who said what. Callers who want the lanes kept apart use
152
+ * `searchSelf()`, which exists for exactly that. */
153
+ function mergeLanes(r) {
154
+ const main = asRecords(r?.memories);
155
+ const mine = asRecords(r?.self_memories);
156
+ if (mine.length === 0)
157
+ return main;
158
+ const seen = new Set(main.map((m) => m.id).filter((id) => typeof id === "string"));
159
+ return main.concat(mine.filter((m) => !(typeof m.id === "string" && seen.has(m.id))));
160
+ }
134
161
  function withForm(body, form, tz) {
135
162
  if (form !== undefined)
136
163
  body.form = form;
@@ -188,7 +215,8 @@ const STATUS_ERRORS = {
188
215
  429: RateLimitError,
189
216
  };
190
217
  /** Build the most specific WosError subclass for an HTTP status. */
191
- export function errorFor(status, message, requestId) {
218
+ // Internal factory — the typed error CLASSES are the public surface.
219
+ function errorFor(status, message, requestId) {
192
220
  const Cls = status === 0
193
221
  ? APIConnectionError
194
222
  : STATUS_ERRORS[status] ?? (status >= 500 && status < 600 ? ServerError : WosError);
@@ -343,7 +371,7 @@ export class Client {
343
371
  // Reserved fields win over ...opts: an app that forwards untrusted input as
344
372
  // opts must not be able to override the store (user_id), query, or limit.
345
373
  const r = await this.post("/api/v1/memory/search", { ...opts, user_id: this.uid(userId), query, max_results: limit });
346
- return asList(r.memories);
374
+ return mergeLanes(r);
347
375
  }
348
376
  /** Search a self-memory model (Scroll 1.2+): both lanes from ONE call. Returns
349
377
  * `{ memories, self_memories }` — `memories` is the main lane (what others said /
@@ -352,7 +380,7 @@ export class Client {
352
380
  * without a self lane, `self_memories` is `[]`. `userId` optional (default store). */
353
381
  async searchSelf(query, userId, limit = 10, opts = {}) {
354
382
  const r = await this.post("/api/v1/memory/search", { ...opts, user_id: this.uid(userId), query, max_results: limit });
355
- return { memories: asList(r.memories), self_memories: asList(r.self_memories) };
383
+ return { memories: asRecords(r.memories), self_memories: asRecords(r.self_memories) };
356
384
  }
357
385
  /** One-call LLM context: short-term turns + long-term matches + surrounding context.
358
386
  * `form` ("memoir"/"archive", Scroll 1.2+) renders each long-term memory's time in
@@ -368,7 +396,7 @@ export class Client {
368
396
  /** Recent conversation turns (short-term memory). */
369
397
  async history(userId) {
370
398
  const r = await this.post("/api/v1/memory/history", { user_id: this.uid(userId) });
371
- return asList(r.turns);
399
+ return asRecords(r.turns);
372
400
  }
373
401
  /** Memory counts for a store: { total_memories, short_term_turns }. */
374
402
  stats(userId) {
@@ -416,7 +444,7 @@ export class Client {
416
444
  // server that mints a FRESH cursor every page forever — bound the walk.
417
445
  for (let page = 0; page < MAX_PAGES; page++) {
418
446
  const p = await this.listMemories(userId, { limit: opts.pageSize ?? 100, cursor });
419
- for (const m of asList(p.memories))
447
+ for (const m of asRecords(p.memories))
420
448
  yield m;
421
449
  const next = p.next_cursor ?? undefined;
422
450
  if (!next || seen.has(next))
@@ -443,7 +471,7 @@ export class Client {
443
471
  /** Available models: `[{ id, name, available, memory }, ...]`. Needs no API key. */
444
472
  async listModels() {
445
473
  const data = await this.request("GET", "/api/v1/models");
446
- return asList(data.models);
474
+ return asRecords(data.models);
447
475
  }
448
476
  // ----- stores -----
449
477
  /** Create a store — the `userId` you read and write under. Stores are explicit:
@@ -456,7 +484,7 @@ export class Client {
456
484
  /** List your stores: `[{ user_id, created_at }, ...]` (`default` first). */
457
485
  async listStores() {
458
486
  const r = await this.request("GET", "/api/v1/memory/collections");
459
- return asList(r.collections);
487
+ return asRecords(r.collections);
460
488
  }
461
489
  /** Delete a store and ALL its memories. Returns `{ user_id, status }`. */
462
490
  deleteStore(userId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wontopos",
3
- "version": "2.2.20",
3
+ "version": "2.2.22",
4
4
  "description": "Wontopos — long-term memory for AI agents. Pure semantic retrieval, identical recall in every language, ~100× lower LLM bill.",
5
5
  "keywords": [
6
6
  "memory",