wontopos 2.2.45 → 2.2.47

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/dist/wontopos.js CHANGED
@@ -1,4 +1,4 @@
1
- const VERSION = "2.2.45";
1
+ const VERSION = "2.2.47";
2
2
  const RUNTIME = (() => {
3
3
  const p = globalThis?.process;
4
4
  return p?.version ? ` (node/${p.version}; ${p.platform}-${p.arch})` : "";
@@ -263,12 +263,7 @@ const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "
263
263
  const IMF_FIXDATE = /^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), (\d{2}) ([A-Z][a-z]{2}) (\d{4}) (\d{2}):(\d{2}):(\d{2}) GMT$/;
264
264
  const RFC850_DATE = /^(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (\d{2})-([A-Z][a-z]{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2}) GMT$/;
265
265
  const ASCTIME_DATE = /^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) ([A-Z][a-z]{2}) ([ \d]\d) (\d{2}):(\d{2}):(\d{2}) (\d{4})$/;
266
- function retryAfterMs(v, now = Date.now()) {
267
- if (v == null)
268
- return undefined;
269
- const s = v.trim();
270
- if (/^\d+$/.test(s))
271
- return Math.min(Number(s), Number.MAX_SAFE_INTEGER / 1000) * 1000;
266
+ function httpDateMs(s, now = Date.now()) {
272
267
  let parts;
273
268
  let m;
274
269
  if ((m = IMF_FIXDATE.exec(s)))
@@ -288,7 +283,37 @@ function retryAfterMs(v, now = Date.now()) {
288
283
  const d = Number(day), h = Number(hh), mi = Number(mm), se = Number(ss);
289
284
  if (month < 0 || d < 1 || d > 31 || h > 23 || mi > 59 || se > 60)
290
285
  return undefined;
291
- return Math.max(0, Date.UTC(year, month, d, h, mi, se) - now);
286
+ return Date.UTC(year, month, d, h, mi, se);
287
+ }
288
+ function retryAfterMs(v, now = Date.now()) {
289
+ if (v == null)
290
+ return undefined;
291
+ const s = v.trim();
292
+ if (/^\d+$/.test(s))
293
+ return Math.min(Number(s), Number.MAX_SAFE_INTEGER / 1000) * 1000;
294
+ const at = httpDateMs(s, now);
295
+ return at === undefined ? undefined : Math.max(0, at - now);
296
+ }
297
+ const warnedRetirements = new Set();
298
+ const WARNED_RETIREMENTS_MAX = 64;
299
+ function warnIfRetiring(model, h) {
300
+ const v = h.get("Sunset");
301
+ if (!v)
302
+ return;
303
+ const at = httpDateMs(v.trim());
304
+ if (at === undefined)
305
+ return;
306
+ const day = new Date(at).toISOString().slice(0, 10);
307
+ const k = `${model}|${day}`;
308
+ if (warnedRetirements.has(k) || warnedRetirements.size >= WARNED_RETIREMENTS_MAX)
309
+ return;
310
+ warnedRetirements.add(k);
311
+ const which = model ? `model ${JSON.stringify(model)}` : "the model this client uses";
312
+ console.warn(`wontopos: ${which} retires on ${day} (UTC). From then on its calls answer 410, except ` +
313
+ "deleteStore. listModels() lists the models you can move to.");
314
+ }
315
+ export function _resetRetirementWarnings() {
316
+ warnedRetirements.clear();
292
317
  }
293
318
  const BRIEF_BODY_MS = 1_000;
294
319
  class SlowBody extends Error {
@@ -313,6 +338,23 @@ function tryUrl(s, base) {
313
338
  return undefined;
314
339
  }
315
340
  }
341
+ function inBrowserPage() {
342
+ try {
343
+ const g = globalThis;
344
+ return typeof g.window !== "undefined" && typeof g.window.document !== "undefined" && typeof g.navigator !== "undefined";
345
+ }
346
+ catch {
347
+ return false;
348
+ }
349
+ }
350
+ function isServiceHost(host) {
351
+ const h = host.toLowerCase();
352
+ return h === "wontopos.com" || h.endsWith(".wontopos.com");
353
+ }
354
+ const BROWSER_REFUSAL = "wontopos: refusing to run in a browser page. The API key would be readable by anyone who " +
355
+ "opens the page, and with it every memory in its workspace. Call Wontopos from your server, " +
356
+ "or put a proxy of your own in front of it and point baseUrl there. For a local demo with a " +
357
+ "key you will revoke, pass dangerouslyAllowBrowser: true.";
316
358
  function pageHref() {
317
359
  try {
318
360
  const href = globalThis.location?.href;
@@ -389,6 +431,18 @@ const memoryFromGet = (r) => {
389
431
  return o.memory;
390
432
  return typeof o.id === "string" ? o : {};
391
433
  };
434
+ function recallShape(r) {
435
+ const o = asObj(r);
436
+ const st = asObj(o.short_term);
437
+ const lt = asObj(o.long_term);
438
+ const cx = asObj(o.context);
439
+ return {
440
+ ...o,
441
+ short_term: { ...st, turns: asRecords(st.turns) },
442
+ long_term: { ...lt, memories: asRecords(lt.memories) },
443
+ context: { ...cx, around_top_memory: asList(cx.around_top_memory).filter((x) => typeof x === "string") },
444
+ };
445
+ }
392
446
  function mergeResults(r) {
393
447
  const out = asRecords(r?.memories);
394
448
  const seen = new Set(out.map((m) => m.id).filter((id) => typeof id === "string"));
@@ -553,6 +607,15 @@ function writeKey(opts, takesImage = false) {
553
607
  (takesImage ? "add takes idempotencyKey and image." : "This call takes idempotencyKey only."));
554
608
  return opts.idempotencyKey;
555
609
  }
610
+ function newIdempotencyKey() {
611
+ const c = globalThis.crypto;
612
+ if (typeof c?.randomUUID === "function")
613
+ return `sdk-${String(c.randomUUID()).replace(/-/g, "")}`;
614
+ let hex = "";
615
+ for (let i = 0; i < 4; i++)
616
+ hex += Math.floor(Math.random() * 0x100000000).toString(16).padStart(8, "0");
617
+ return `sdk-${hex}`;
618
+ }
556
619
  function warnOnUnknownFilters(filters) {
557
620
  warnOnceUnknownKeys(filters, KNOWN_FILTER_KEYS, warnedFilterKeys, (k) => `wontopos: unknown search filter ${JSON.stringify(k)} — the API drops keys it does not know, ` +
558
621
  `so this filter has NO effect and the search is wider than you think. ` +
@@ -581,6 +644,7 @@ export class Client {
581
644
  userinfo;
582
645
  pageRelative;
583
646
  customFetch;
647
+ allowBrowser;
584
648
  defaultUser;
585
649
  _rateLimit = null;
586
650
  get rateLimit() {
@@ -614,6 +678,10 @@ export class Client {
614
678
  throw new Error(notAUrl(trimmed));
615
679
  this.pageRelative = !url;
616
680
  this.userinfo = Boolean(parsed.username || parsed.password);
681
+ this.allowBrowser = opts.dangerouslyAllowBrowser === true;
682
+ if (!this.allowBrowser && inBrowserPage() && isServiceHost(parsed.hostname)) {
683
+ throw new Error(BROWSER_REFUSAL);
684
+ }
617
685
  this.timeoutMs = checkMs(opts.timeoutMs) ?? 30_000;
618
686
  this.model = opts.model ?? DEFAULT_MODEL;
619
687
  if (this.model && !MODEL_RE.test(this.model)) {
@@ -682,6 +750,7 @@ export class Client {
682
750
  fetch: this.fetchImpl,
683
751
  deadlineMs: this.deadlineMs,
684
752
  signal: this.signal,
753
+ dangerouslyAllowBrowser: this.allowBrowser,
685
754
  ...overrides,
686
755
  });
687
756
  }
@@ -713,22 +782,22 @@ export class Client {
713
782
  if (opts.image !== undefined)
714
783
  body.image = normalizeImage(opts.image);
715
784
  warnOnUnknownMetadata(metadata);
716
- return this.post("/api/v1/memory/store", body, writeKey(opts, true));
785
+ return this.write("/api/v1/memory/store", body, writeKey(opts, true));
717
786
  }
718
787
  async store(content, userId, metadata = {}, opts = {}) {
719
788
  return this.add(content, userId, metadata, opts);
720
789
  }
721
790
  async addTurn(userMsg, assistantMsg, userId, opts = {}) {
722
- return this.post("/api/v1/memory/store-turn", { user_id: this.uid(userId), user_msg: userMsg, assistant_msg: assistantMsg }, writeKey(opts));
791
+ return this.write("/api/v1/memory/store-turn", { user_id: this.uid(userId), user_msg: userMsg, assistant_msg: assistantMsg }, writeKey(opts));
723
792
  }
724
793
  async addBulk(content, userId, category = "general", timestamp, opts = {}) {
725
794
  const body = { user_id: this.uid(userId), content, category: category ?? "general" };
726
795
  if (timestamp)
727
796
  body.timestamp = timestamp;
728
- return this.post("/api/v1/memory/bulk-store", body, writeKey(opts));
797
+ return this.write("/api/v1/memory/bulk-store", body, writeKey(opts));
729
798
  }
730
799
  async update(oldMemoryId, newContent, userId, opts = {}) {
731
- return this.post("/api/v1/memory/supersede", { user_id: this.uid(userId), old_memory_id: oldMemoryId, new_content: newContent }, writeKey(opts));
800
+ return this.write("/api/v1/memory/supersede", { user_id: this.uid(userId), old_memory_id: oldMemoryId, new_content: newContent }, writeKey(opts));
732
801
  }
733
802
  async search(query, userId, limit = 10, opts = {}) {
734
803
  checkSearchOpts(opts);
@@ -773,7 +842,7 @@ export class Client {
773
842
  checkContextLimit(opts.context_limit);
774
843
  body.context_limit = opts.context_limit;
775
844
  }
776
- return this.post("/api/v1/memory/recall", withForm(body, opts.form, opts.tz));
845
+ return recallShape(await this.post("/api/v1/memory/recall", withForm(body, opts.form, opts.tz)));
777
846
  }
778
847
  async engram(name, query, userId, opts = {}) {
779
848
  checkOpts(opts, KNOWN_ENGRAM_KEYS, "engram");
@@ -978,8 +1047,12 @@ export class Client {
978
1047
  warnIfStoreIdCollapses(userId);
979
1048
  return this.post("/api/v1/memory/forget", { user_id: userId });
980
1049
  }
981
- post(path, body, idempotencyKey) {
982
- return this.request("POST", path, body, idempotencyKey);
1050
+ post(path, body) {
1051
+ return this.request("POST", path, body);
1052
+ }
1053
+ write(path, body, idempotencyKey) {
1054
+ const made = idempotencyKey == null;
1055
+ return this.request("POST", path, body, made ? newIdempotencyKey() : idempotencyKey, false, made);
983
1056
  }
984
1057
  remove(path, body) {
985
1058
  return this.request("DELETE", path, body, undefined, true);
@@ -1097,8 +1170,9 @@ export class Client {
1097
1170
  }
1098
1171
  const attempts = this.maxRetries + 1;
1099
1172
  const upper = method.toUpperCase();
1100
- const idempotent = IDEMPOTENT_METHODS.has(upper);
1101
1173
  const logPath = path.split("?")[0];
1174
+ const idempotent = IDEMPOTENT_METHODS.has(upper) || reads || (upper === "POST" && READ_POSTS.has(logPath));
1175
+ const replaySafe = idempotent || extraHeaders?.["Idempotency-Key"] !== undefined;
1102
1176
  const deadlineAt = this.deadlineAt();
1103
1177
  const headers = extraHeaders ? { ...this.headers, ...extraHeaders } : this.headers;
1104
1178
  let ambiguous = false;
@@ -1119,7 +1193,7 @@ export class Client {
1119
1193
  };
1120
1194
  const cutShort = () => {
1121
1195
  const why = att.why();
1122
- if (why === "deadline" && (idempotent || reads || (upper === "POST" && READ_POSTS.has(logPath))) && previous) {
1196
+ if (why === "deadline" && idempotent && previous) {
1123
1197
  return previous;
1124
1198
  }
1125
1199
  return new APIConnectionError(0, this.abortMessage(why));
@@ -1140,7 +1214,7 @@ export class Client {
1140
1214
  if (timedOut)
1141
1215
  throw cutShort();
1142
1216
  const connect = isConnectFailure(e);
1143
- if ((idempotent || connect) && !last) {
1217
+ if ((replaySafe || connect) && !last) {
1144
1218
  if (!connect)
1145
1219
  ambiguous = true;
1146
1220
  await pauseWithoutAnswer(e?.code ?? e?.name ?? "network error");
@@ -1157,7 +1231,7 @@ export class Client {
1157
1231
  throw cutShort();
1158
1232
  if (e instanceof WosError)
1159
1233
  throw e;
1160
- if (idempotent && !last)
1234
+ if (replaySafe && !last)
1161
1235
  return null;
1162
1236
  throw this.networkError(e, path);
1163
1237
  }
@@ -1184,9 +1258,10 @@ export class Client {
1184
1258
  throw new WosError(res.status, "unexpected redirect — refused (the API key never follows a redirect). Check baseUrl: exact host, https://.");
1185
1259
  }
1186
1260
  this._rateLimit = parseRateLimit(res.headers) ?? this._rateLimit;
1261
+ warnIfRetiring(this.model, res.headers);
1187
1262
  if (!res.ok) {
1188
1263
  const ra = retryAfterMs(res.headers.get("Retry-After"));
1189
- let wait = RETRY_ALWAYS.has(res.status) || (RETRY_IF_IDEMPOTENT.has(res.status) && idempotent)
1264
+ let wait = RETRY_ALWAYS.has(res.status) || (RETRY_IF_IDEMPOTENT.has(res.status) && replaySafe)
1190
1265
  ? ra ?? this.backoffMs(attempt)
1191
1266
  : undefined;
1192
1267
  const retrying = wait !== undefined && !last && wait <= MAX_RETRY_WAIT_MS && this.fits(wait, deadlineAt);
@@ -1313,7 +1388,7 @@ export class Client {
1313
1388
  clearTimeout(timer);
1314
1389
  }
1315
1390
  }
1316
- async request(method, path, body, idempotencyKey, removes = false) {
1391
+ async request(method, path, body, idempotencyKey, removes = false, madeKey = false) {
1317
1392
  const extraHeaders = this.idemHeader(idempotencyKey);
1318
1393
  let payload;
1319
1394
  try {
@@ -1341,7 +1416,7 @@ export class Client {
1341
1416
  const got = data === null ? "null" : Array.isArray(data) ? "array" : typeof data;
1342
1417
  throw new WosError(res.status, `expected a JSON object in the response, got ${got}`);
1343
1418
  }
1344
- if (res.headers.get("Idempotent-Replayed") === "true" && !("replayed" in data)) {
1419
+ if (!madeKey && res.headers.get("Idempotent-Replayed") === "true" && !("replayed" in data)) {
1345
1420
  data.replayed = true;
1346
1421
  }
1347
1422
  return data;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wontopos",
3
- "version": "2.2.45",
3
+ "version": "2.2.47",
4
4
  "description": "Wontopos \u2014 long-term memory for AI agents. Identical recall in every language, ~100\u00d7 lower LLM bill.",
5
5
  "keywords": [
6
6
  "memory",