wontopos 2.2.24 → 2.2.25

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.
@@ -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
- /** 5xx — the service failed. Safe to retry. */
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,6 +282,8 @@ 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
289
  /** Recall caching: repeated/extended queries within the TTL bill at 0.1×. */
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.24";
40
+ const VERSION = "2.2.25";
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
- /** 5xx — the service failed. Safe to retry. */
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 {
@@ -310,7 +383,9 @@ export class Client {
310
383
  }
311
384
  /** Resolve a call's store: the explicit userId, else the client default. */
312
385
  uid(userId) {
313
- return userId || this.defaultUser;
386
+ const id = userId || this.defaultUser;
387
+ warnIfStoreIdCollapses(id);
388
+ return id;
314
389
  }
315
390
  clone(overrides) {
316
391
  return new Client({
@@ -384,6 +459,7 @@ export class Client {
384
459
  async search(query, userId, limit = 10, opts = {}) {
385
460
  // Reserved fields win over ...opts: an app that forwards untrusted input as
386
461
  // opts must not be able to override the store (user_id), query, or limit.
462
+ warnOnUnknownFilters(opts.filters);
387
463
  const r = await this.post("/api/v1/memory/search", { ...opts, user_id: this.uid(userId), query, max_results: limit });
388
464
  return mergeLanes(r);
389
465
  }
@@ -393,6 +469,7 @@ export class Client {
393
469
  * speaker "me"), kept separate so the reader never confuses who said what. On a model
394
470
  * without a self lane, `self_memories` is `[]`. `userId` optional (default store). */
395
471
  async searchSelf(query, userId, limit = 10, opts = {}) {
472
+ warnOnUnknownFilters(opts.filters);
396
473
  const r = await this.post("/api/v1/memory/search", { ...opts, user_id: this.uid(userId), query, max_results: limit });
397
474
  return { memories: asRecords(r.memories), self_memories: asRecords(r.self_memories) };
398
475
  }
@@ -753,6 +830,13 @@ export class Client {
753
830
  const got = data === null ? "null" : Array.isArray(data) ? "array" : typeof data;
754
831
  throw new WosError(res.status, `expected a JSON object in the response, got ${got}`);
755
832
  }
833
+ // 멱등키를 쓴 쓰기가 **저장된 것인지 재생된 것인지**를 호출자가 알아야 한다.
834
+ // 서버는 `Idempotent-Replayed: true` 로 말해주는데 지금까지 SDK 가 삼켰다 —
835
+ // 멱등키를 쓰는 사람이 가장 알고 싶은 사실이 그것이다(재시도가 실제로
836
+ // 저장을 일으켰나, 아니면 첫 응답이 되풀이된 것인가).
837
+ if (res.headers.get("Idempotent-Replayed") === "true") {
838
+ data.replayed = true;
839
+ }
756
840
  return data;
757
841
  }
758
842
  throw new WosError(0, "retries exhausted"); // unreachable; keeps the compiler happy
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wontopos",
3
- "version": "2.2.24",
3
+ "version": "2.2.25",
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",