@maximem/synap-js-sdk 0.4.0 → 0.4.2

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/index.cjs CHANGED
@@ -32,7 +32,7 @@ var _chunkHR5Y3KXScjs = require('./chunk-HR5Y3KXS.cjs');
32
32
 
33
33
 
34
34
 
35
- var _chunkNIDPTBNCcjs = require('./chunk-NIDPTBNC.cjs');
35
+ var _chunkRB57I2IQcjs = require('./chunk-RB57I2IQ.cjs');
36
36
 
37
37
 
38
38
  var _chunkZKOJDUN4cjs = require('./chunk-ZKOJDUN4.cjs');
@@ -525,10 +525,19 @@ function buildFetchRequest(scope, options) {
525
525
  }
526
526
  return { endpoint: ENDPOINT_BY_SCOPE[scope], body };
527
527
  }
528
- async function fetchContext(transport, scope, options = {}) {
528
+ async function fetchContext(transport, scope, options = {}, hooks = {}) {
529
+ const startedAt = Date.now();
530
+ const anticipated = _nullishCoalesce(_optionalChain([hooks, 'access', _26 => _26.beforeFetch, 'optionalCall', _27 => _27(scope, options)]), () => ( null));
531
+ if (anticipated !== null) {
532
+ _optionalChain([hooks, 'access', _28 => _28.onServedFromCache, 'optionalCall', _29 => _29(scope, options, anticipated)]);
533
+ _optionalChain([hooks, 'access', _30 => _30.onAssembled, 'optionalCall', _31 => _31(scope, options, anticipated.response, startedAt)]);
534
+ return anticipated.response;
535
+ }
529
536
  const { endpoint, body } = buildFetchRequest(scope, options);
530
537
  const result = await transport.request(endpoint, { body });
531
- return _nullishCoalesce(_optionalChain([result, 'optionalAccess', _26 => _26["context"]]), () => ( {}));
538
+ const response = _nullishCoalesce(_optionalChain([result, 'optionalAccess', _32 => _32["context"]]), () => ( {}));
539
+ _optionalChain([hooks, 'access', _33 => _33.onAssembled, 'optionalCall', _34 => _34(scope, options, response, startedAt)]);
540
+ return response;
532
541
  }
533
542
 
534
543
  // src/context/normalize.ts
@@ -887,14 +896,14 @@ function sleep2(ms, signal) {
887
896
  if (ms <= 0) return Promise.resolve();
888
897
  return new Promise((resolve, reject) => {
889
898
  const timer = setTimeout(() => {
890
- _optionalChain([signal, 'optionalAccess', _27 => _27.removeEventListener, 'call', _28 => _28("abort", onAbort)]);
899
+ _optionalChain([signal, 'optionalAccess', _35 => _35.removeEventListener, 'call', _36 => _36("abort", onAbort)]);
891
900
  resolve();
892
901
  }, ms);
893
902
  function onAbort() {
894
903
  clearTimeout(timer);
895
- reject(_nullishCoalesce(_optionalChain([signal, 'optionalAccess', _29 => _29.reason]), () => ( new Error("Aborted"))));
904
+ reject(_nullishCoalesce(_optionalChain([signal, 'optionalAccess', _37 => _37.reason]), () => ( new Error("Aborted"))));
896
905
  }
897
- _optionalChain([signal, 'optionalAccess', _30 => _30.addEventListener, 'call', _31 => _31("abort", onAbort, { once: true })]);
906
+ _optionalChain([signal, 'optionalAccess', _38 => _38.addEventListener, 'call', _39 => _39("abort", onAbort, { once: true })]);
898
907
  });
899
908
  }
900
909
 
@@ -1031,6 +1040,8 @@ var CompactionSubscribers = class {
1031
1040
  }
1032
1041
  };
1033
1042
  function createConversationNamespace(transport, subscribers = new CompactionSubscribers(), onWrite = () => {
1043
+ }, onTurn = () => {
1044
+ }, onFetched = () => {
1034
1045
  }) {
1035
1046
  const convId = (v) => {
1036
1047
  const id = typeof v === "string" ? v : _nullishCoalesce(v.conversation_id, () => ( v.conversationId));
@@ -1042,6 +1053,12 @@ function createConversationNamespace(transport, subscribers = new CompactionSubs
1042
1053
  async record_message(options) {
1043
1054
  const body = buildRecordMessageBody(options);
1044
1055
  const result = await transport.request("conversations_messages", { body });
1056
+ onTurn({
1057
+ conversationId: String(_nullishCoalesce(body["conversation_id"], () => ( ""))),
1058
+ role: String(_nullishCoalesce(body["role"], () => ( "user"))),
1059
+ content: String(_nullishCoalesce(body["content"], () => ( ""))),
1060
+ timestamp: body["timestamp"]
1061
+ });
1045
1062
  onWrite({
1046
1063
  userId: body["user_id"],
1047
1064
  customerId: body["customer_id"]
@@ -1056,6 +1073,14 @@ function createConversationNamespace(transport, subscribers = new CompactionSubs
1056
1073
  const result = await transport.request("conversations_messages_batch", {
1057
1074
  body: { messages: bodies }
1058
1075
  });
1076
+ for (const body of bodies) {
1077
+ onTurn({
1078
+ conversationId: String(_nullishCoalesce(body["conversation_id"], () => ( ""))),
1079
+ role: String(_nullishCoalesce(body["role"], () => ( "user"))),
1080
+ content: String(_nullishCoalesce(body["content"], () => ( ""))),
1081
+ timestamp: body["timestamp"]
1082
+ });
1083
+ }
1059
1084
  for (const body of bodies) {
1060
1085
  onWrite({
1061
1086
  userId: body["user_id"],
@@ -1075,10 +1100,15 @@ function createConversationNamespace(transport, subscribers = new CompactionSubs
1075
1100
  },
1076
1101
  context: {
1077
1102
  async fetch(options = {}) {
1078
- validateConversationId(
1079
- _nullishCoalesce(options.conversation_id, () => ( options.conversationId))
1103
+ const id = _nullishCoalesce(options.conversation_id, () => ( options.conversationId));
1104
+ validateConversationId(id);
1105
+ const response = await fetchContext(transport, "conversation", options);
1106
+ onFetched(
1107
+ response,
1108
+ id,
1109
+ _nullishCoalesce(options.user_id, () => ( options.userId))
1080
1110
  );
1081
- return fetchContext(transport, "conversation", options);
1111
+ return response;
1082
1112
  },
1083
1113
  get_context_for_prompt: async (options = {}) => {
1084
1114
  const id = convId(options);
@@ -1086,7 +1116,7 @@ function createConversationNamespace(transport, subscribers = new CompactionSubs
1086
1116
  pathParams: { conversation_id: id },
1087
1117
  query: { style: _nullishCoalesce(options.style, () => ( "structured")) }
1088
1118
  });
1089
- return _nullishCoalesce(_nullishCoalesce(_optionalChain([result, 'optionalAccess', _32 => _32["context_for_prompt"]]), () => ( result)), () => ( {}));
1119
+ return _nullishCoalesce(_nullishCoalesce(_optionalChain([result, 'optionalAccess', _40 => _40["context_for_prompt"]]), () => ( result)), () => ( {}));
1090
1120
  },
1091
1121
  async compact(options) {
1092
1122
  return transport.request("conversations_compact", { body: buildCompactBody(options) });
@@ -1134,7 +1164,7 @@ function createConversationNamespace(transport, subscribers = new CompactionSubs
1134
1164
  }
1135
1165
 
1136
1166
  // src/user/interface.ts
1137
- function createUserNamespace(transport) {
1167
+ function createUserNamespace(transport, hooks = {}) {
1138
1168
  return {
1139
1169
  async get_profile(options) {
1140
1170
  const userId = typeof options === "string" ? options : _nullishCoalesce(options.user_id, () => ( options.userId));
@@ -1148,7 +1178,7 @@ function createUserNamespace(transport) {
1148
1178
  });
1149
1179
  },
1150
1180
  context: { async fetch(options = {}) {
1151
- return fetchContext(transport, "user", options);
1181
+ return fetchContext(transport, "user", options, hooks);
1152
1182
  } }
1153
1183
  };
1154
1184
  }
@@ -1208,16 +1238,27 @@ function envPort() {
1208
1238
  return Number.isFinite(parsed) ? parsed : void 0;
1209
1239
  }
1210
1240
  function envTls() {
1211
- const raw = _chunkZKOJDUN4cjs.getEnv.call(void 0, "SYNAP_GRPC_USE_TLS");
1241
+ const raw = _nullishCoalesce(_chunkZKOJDUN4cjs.getEnv.call(void 0, "SYNAP_GRPC_USE_TLS"), () => ( _chunkZKOJDUN4cjs.getEnv.call(void 0, "SYNAP_GRPC_TLS")));
1212
1242
  if (raw === void 0 || raw === "") return void 0;
1213
1243
  return !["0", "false", "no", "off"].includes(raw.trim().toLowerCase());
1214
1244
  }
1215
- function createInstanceNamespace(credentials, cache) {
1245
+ function createInstanceNamespace(credentials, cache, onTurn) {
1216
1246
  let client = null;
1217
1247
  let dispatchCompaction = null;
1248
+ let applyCompaction = null;
1218
1249
  const internals = {
1219
1250
  onCompactionUpdate(conversationId, bundle) {
1220
- _optionalChain([dispatchCompaction, 'optionalCall', _33 => _33(conversationId, bundle)]);
1251
+ _optionalChain([applyCompaction, 'optionalCall', _41 => _41(bundle)]);
1252
+ _optionalChain([dispatchCompaction, 'optionalCall', _42 => _42(conversationId, bundle)]);
1253
+ },
1254
+ setCompactionApplier(apply) {
1255
+ applyCompaction = apply;
1256
+ },
1257
+ sendContextUsed(event) {
1258
+ _optionalChain([client, 'optionalAccess', _43 => _43.sendContextUsed, 'call', _44 => _44(event)]);
1259
+ },
1260
+ sendContextAssembled(event) {
1261
+ _optionalChain([client, 'optionalAccess', _45 => _45.sendContextAssembled, 'call', _46 => _46(event)]);
1221
1262
  },
1222
1263
  setCompactionDispatcher(dispatch) {
1223
1264
  dispatchCompaction = dispatch;
@@ -1247,9 +1288,9 @@ function createInstanceNamespace(credentials, cache) {
1247
1288
  onStateChange: (state) => {
1248
1289
  if (state === "reconnecting") {
1249
1290
  reconnectAttempts += 1;
1250
- _optionalChain([onReconnect, 'optionalCall', _34 => _34(reconnectAttempts)]);
1291
+ _optionalChain([onReconnect, 'optionalCall', _47 => _47(reconnectAttempts)]);
1251
1292
  } else if (state === "disconnected") {
1252
- _optionalChain([onDisconnect, 'optionalCall', _35 => _35("stream disconnected")]);
1293
+ _optionalChain([onDisconnect, 'optionalCall', _48 => _48("stream disconnected")]);
1253
1294
  }
1254
1295
  }
1255
1296
  });
@@ -1258,7 +1299,7 @@ function createInstanceNamespace(credentials, cache) {
1258
1299
  } catch (error) {
1259
1300
  if (error instanceof _chunkHR5Y3KXScjs.SynapError) throw error;
1260
1301
  throw new (0, _chunkHR5Y3KXScjs.SynapError)(
1261
- `Could not open the Synap anticipation stream: ${_nullishCoalesce(_optionalChain([error, 'optionalAccess', _36 => _36.message]), () => ( String(error)))}`,
1302
+ `Could not open the Synap anticipation stream: ${_nullishCoalesce(_optionalChain([error, 'optionalAccess', _49 => _49.message]), () => ( String(error)))}`,
1262
1303
  { cause: error }
1263
1304
  );
1264
1305
  }
@@ -1275,6 +1316,12 @@ function createInstanceNamespace(credentials, cache) {
1275
1316
  if (options.content === void 0) {
1276
1317
  throw new (0, _chunkHR5Y3KXScjs.ListeningNotActiveError)("content is required");
1277
1318
  }
1319
+ const conversationId = _nullishCoalesce(_nullishCoalesce(options.conversation_id, () => ( options.conversationId)), () => ( ""));
1320
+ _optionalChain([onTurn, 'optionalCall', _50 => _50({
1321
+ conversationId,
1322
+ role: _nullishCoalesce(options.role, () => ( "user")),
1323
+ content: options.content
1324
+ })]);
1278
1325
  client.sendConversationEvent({
1279
1326
  event_type: _nullishCoalesce(_nullishCoalesce(options.event_type, () => ( options.eventType)), () => ( "user_message")),
1280
1327
  content: options.content,
@@ -1315,6 +1362,403 @@ function createInstanceNamespace(credentials, cache) {
1315
1362
  // src/version.ts
1316
1363
  var SDK_VERSION = "1.0.0-beta.0";
1317
1364
 
1365
+ // src/registry.ts
1366
+ var instances = /* @__PURE__ */ new Map();
1367
+ function buildRegistryKey(instanceId, apiKey) {
1368
+ if (instanceId) return instanceId;
1369
+ const key = _nullishCoalesce(apiKey, () => ( ""));
1370
+ if (!key) return "";
1371
+ return `apikey:${digest(key)}`;
1372
+ }
1373
+ function digest(value) {
1374
+ const PRIME = 1099511628211n;
1375
+ const MASK = 0xffffffffffffffffn;
1376
+ let hash = 14695981039346656037n;
1377
+ for (let i = 0; i < value.length; i += 1) {
1378
+ hash ^= BigInt(value.charCodeAt(i));
1379
+ hash = hash * PRIME & MASK;
1380
+ }
1381
+ return hash.toString(16).padStart(16, "0");
1382
+ }
1383
+ function get(key) {
1384
+ return instances.get(key);
1385
+ }
1386
+ function registerIfAbsent(key, client) {
1387
+ const existing = instances.get(key);
1388
+ if (existing !== void 0) return existing;
1389
+ instances.set(key, client);
1390
+ return void 0;
1391
+ }
1392
+ function aliasIfAbsent(key, client) {
1393
+ if (!key || instances.has(key)) return false;
1394
+ instances.set(key, client);
1395
+ return true;
1396
+ }
1397
+ function unregisterIfOwner(key, client) {
1398
+ if (instances.get(key) === client) instances.delete(key);
1399
+ }
1400
+
1401
+ // src/cache/short-term-store.ts
1402
+ var DEFAULT_MAX_CONVERSATIONS = 100;
1403
+ var DEFAULT_MAX_AGE_MS = 12 * 60 * 60 * 1e3;
1404
+ function parseIso(value) {
1405
+ if (value === null || value === void 0 || value === "") return null;
1406
+ if (value instanceof Date) return value.getTime();
1407
+ if (typeof value !== "string") return null;
1408
+ const parsed = Date.parse(value);
1409
+ return Number.isNaN(parsed) ? null : parsed;
1410
+ }
1411
+ function isAfter(turnTimestamp, cutoffMs) {
1412
+ const parsed = parseIso(turnTimestamp);
1413
+ if (parsed === null) return true;
1414
+ return parsed > cutoffMs;
1415
+ }
1416
+ var ShortTermStore = class {
1417
+ // Insertion order IS the LRU order: re-inserting on touch moves an entry to
1418
+ // the end, so the first key Map yields is the least recently used.
1419
+ #entries = /* @__PURE__ */ new Map();
1420
+ #maxConversations;
1421
+ #maxAgeMs;
1422
+ #now;
1423
+ constructor(options = {}) {
1424
+ this.#maxConversations = _nullishCoalesce(options.maxConversations, () => ( DEFAULT_MAX_CONVERSATIONS));
1425
+ this.#maxAgeMs = _nullishCoalesce(options.maxAgeMs, () => ( DEFAULT_MAX_AGE_MS));
1426
+ this.#now = _nullishCoalesce(options.now, () => ( (() => Date.now())));
1427
+ }
1428
+ /** The entry, or null. Age-evicts on read, as Python does. */
1429
+ get(conversationId) {
1430
+ if (!conversationId) return null;
1431
+ const entry = this.#entries.get(conversationId);
1432
+ if (entry === void 0) return null;
1433
+ if (this.#now() - entry.lastActivityAt > this.#maxAgeMs) {
1434
+ this.#entries.delete(conversationId);
1435
+ return null;
1436
+ }
1437
+ this.#touch(conversationId, entry);
1438
+ return entry;
1439
+ }
1440
+ has(conversationId) {
1441
+ return this.get(conversationId) !== null;
1442
+ }
1443
+ get size() {
1444
+ return this.#entries.size;
1445
+ }
1446
+ /**
1447
+ * Append a raw turn.
1448
+ *
1449
+ * Called from `record_message` and the stream's `send_message`. When no entry
1450
+ * exists yet (the first turn, before any compaction has arrived) a fresh one
1451
+ * is created with an empty summary; the next `compaction_update` fills the
1452
+ * summary fields in and prunes the turns it covers.
1453
+ */
1454
+ appendTurn(conversationId, role, content, timestamp) {
1455
+ if (!conversationId) return;
1456
+ const stamp = _nullishCoalesce(timestamp, () => ( new Date(this.#now()).toISOString()));
1457
+ const entry = _nullishCoalesce(this.#entries.get(conversationId), () => ( this.#blank(conversationId)));
1458
+ entry.recentTurns.push({ role, content, timestamp: stamp });
1459
+ entry.lastActivityAt = _nullishCoalesce(parseIso(stamp), () => ( this.#now()));
1460
+ this.#touch(conversationId, entry);
1461
+ this.#evict();
1462
+ }
1463
+ /**
1464
+ * Apply a `compaction_update` bundle.
1465
+ *
1466
+ * Replaces the summary fields from the bundle's `conversation_context` and
1467
+ * prunes locally buffered turns at or before its `end_timestamp`. Turns that
1468
+ * arrived DURING the compaction window are kept by design: the server has not
1469
+ * seen them yet, which is the whole reason the local tail exists.
1470
+ */
1471
+ applyCompaction(bundle) {
1472
+ if (typeof bundle !== "object" || bundle === null) return;
1473
+ const cc = _nullishCoalesce(bundle["conversation_context"], () => ( {}));
1474
+ const conversationId = String(
1475
+ _nullishCoalesce(_nullishCoalesce(_nullishCoalesce(cc["conversation_id"], () => ( bundle["anticipation_conversation_id"])), () => ( bundle["conversation_id"])), () => ( ""))
1476
+ );
1477
+ if (!conversationId) return;
1478
+ const endTs = parseIso(_nullishCoalesce(cc["end_timestamp"], () => ( cc["compacted_at"])));
1479
+ const entry = _nullishCoalesce(this.#entries.get(conversationId), () => ( this.#blank(conversationId)));
1480
+ entry.summary = cc["summary"] || entry.summary;
1481
+ entry.factualParagraph = cc["factual_paragraph"] || entry.factualParagraph;
1482
+ entry.conversationalParagraph = cc["conversational_paragraph"] || entry.conversationalParagraph;
1483
+ entry.currentState = { ..._nullishCoalesce(cc["current_state"], () => ( {})) };
1484
+ entry.keyExtractions = { ..._nullishCoalesce(cc["key_extractions"], () => ( {})) };
1485
+ entry.compactionId = cc["compaction_id"] || entry.compactionId;
1486
+ entry.compactedAt = cc["compacted_at"] || entry.compactedAt;
1487
+ entry.endTimestamp = cc["end_timestamp"] || entry.endTimestamp;
1488
+ if (endTs !== null && entry.recentTurns.length > 0) {
1489
+ entry.recentTurns = entry.recentTurns.filter((t) => isAfter(t.timestamp, endTs));
1490
+ }
1491
+ entry.lastActivityAt = this.#now();
1492
+ this.#touch(conversationId, entry);
1493
+ this.#evict();
1494
+ }
1495
+ invalidate(conversationId) {
1496
+ this.#entries.delete(conversationId);
1497
+ }
1498
+ clear() {
1499
+ this.#entries.clear();
1500
+ }
1501
+ #blank(conversationId) {
1502
+ return {
1503
+ conversationId,
1504
+ summary: null,
1505
+ factualParagraph: null,
1506
+ conversationalParagraph: null,
1507
+ currentState: {},
1508
+ keyExtractions: {},
1509
+ compactionId: null,
1510
+ compactedAt: null,
1511
+ endTimestamp: null,
1512
+ recentTurns: [],
1513
+ lastActivityAt: this.#now()
1514
+ };
1515
+ }
1516
+ /** Re-insert so this key becomes the most recently used. */
1517
+ #touch(conversationId, entry) {
1518
+ this.#entries.delete(conversationId);
1519
+ this.#entries.set(conversationId, entry);
1520
+ }
1521
+ #evict() {
1522
+ while (this.#entries.size > this.#maxConversations) {
1523
+ const oldest = this.#entries.keys().next();
1524
+ if (oldest.done === true) break;
1525
+ this.#entries.delete(oldest.value);
1526
+ }
1527
+ }
1528
+ };
1529
+
1530
+ // src/context/overlay.ts
1531
+ var MAX_TURNS = 20;
1532
+ var TOKEN_BUDGET = 2e3;
1533
+ function verbatimOverlayEnabled() {
1534
+ const raw = (_nullishCoalesce(_chunkZKOJDUN4cjs.getEnv.call(void 0, "SYNAP_ST_VERBATIM_OVERLAY"), () => ( ""))).trim().toLowerCase();
1535
+ if (["0", "false", "no", "off"].includes(raw)) return false;
1536
+ if (["1", "true", "yes", "on"].includes(raw)) return true;
1537
+ return true;
1538
+ }
1539
+ function estimateTokens(turn) {
1540
+ return Math.max(1, Math.floor(String(_nullishCoalesce(turn.content, () => ( ""))).length / 4));
1541
+ }
1542
+ function budgetRecentTurns(turns) {
1543
+ const windowed = turns.slice(-MAX_TURNS);
1544
+ let total = windowed.reduce((n2, t) => n2 + estimateTokens(t), 0);
1545
+ while (windowed.length > 0 && total > TOKEN_BUDGET) {
1546
+ const dropped = windowed.shift();
1547
+ total -= estimateTokens(dropped);
1548
+ }
1549
+ return windowed;
1550
+ }
1551
+ function overlayLocalRecentTurns(response, store, conversationId) {
1552
+ if (conversationId === void 0 || conversationId === "") return;
1553
+ try {
1554
+ const entry = store.get(conversationId);
1555
+ if (entry === null) return;
1556
+ const existing = _nullishCoalesce(response.conversation_context, () => ( null));
1557
+ if (existing !== null && !verbatimOverlayEnabled()) return;
1558
+ const localTurns = budgetRecentTurns(entry.recentTurns);
1559
+ const serverField = (name) => existing === null ? void 0 : existing[name];
1560
+ const orLocal = (name, local) => {
1561
+ const value = serverField(name);
1562
+ if (value === void 0 || value === null || value === "") return local;
1563
+ if (typeof value === "object" && Object.keys(value).length === 0) return local;
1564
+ return value;
1565
+ };
1566
+ response.conversation_context = {
1567
+ summary: orLocal("summary", entry.summary),
1568
+ current_state: orLocal("current_state", { ...entry.currentState }),
1569
+ key_extractions: orLocal("key_extractions", { ...entry.keyExtractions }),
1570
+ recent_turns: localTurns,
1571
+ compaction_id: orLocal("compaction_id", entry.compactionId),
1572
+ compacted_at: orLocal("compacted_at", entry.compactedAt),
1573
+ conversation_id: conversationId
1574
+ };
1575
+ } catch (e5) {
1576
+ }
1577
+ }
1578
+
1579
+ // src/context/user-summary.ts
1580
+ var USER_SUMMARY_INTERVAL = 5;
1581
+ var MAX_SUMMARY_ITEMS = 3;
1582
+ var COLLECTIONS = ["facts", "preferences", "episodes", "emotions", "temporal_events"];
1583
+ var TurnCounter = class {
1584
+ #counts = /* @__PURE__ */ new Map();
1585
+ /** Increment and return the new count. Python buckets a missing id under `_global`. */
1586
+ increment(conversationId) {
1587
+ const key = conversationId !== void 0 && conversationId !== "" ? conversationId : "_global";
1588
+ const next = (_nullishCoalesce(this.#counts.get(key), () => ( 0))) + 1;
1589
+ this.#counts.set(key, next);
1590
+ return next;
1591
+ }
1592
+ /**
1593
+ * True on every Nth turn, and never on the first.
1594
+ *
1595
+ * Python: `count > 0 and count % interval == 0`. Turn 5 injects, turn 1 does
1596
+ * not, which matters because the first turn is usually the one that just
1597
+ * triggered a fresh retrieval anyway.
1598
+ */
1599
+ shouldInject(conversationId) {
1600
+ const key = conversationId !== void 0 && conversationId !== "" ? conversationId : "_global";
1601
+ const count = _nullishCoalesce(this.#counts.get(key), () => ( 0));
1602
+ return count > 0 && count % USER_SUMMARY_INTERVAL === 0;
1603
+ }
1604
+ clear() {
1605
+ this.#counts.clear();
1606
+ }
1607
+ };
1608
+ function itemId(item) {
1609
+ return String(_nullishCoalesce(_nullishCoalesce(item["item_id"], () => ( item["id"])), () => ( "")));
1610
+ }
1611
+ function pick2(item, ...names) {
1612
+ for (const name of names) {
1613
+ const value = item[name];
1614
+ if (value !== void 0 && value !== null) return value;
1615
+ }
1616
+ return void 0;
1617
+ }
1618
+ function normalise(item) {
1619
+ const content = _nullishCoalesce(pick2(item, "content"), () => ( ""));
1620
+ const confidence = _nullishCoalesce(pick2(item, "confidence"), () => ( 0));
1621
+ const extractedAt = _nullishCoalesce(pick2(item, "extracted_at", "created_at"), () => ( null));
1622
+ return {
1623
+ id: itemId(item),
1624
+ content: String(content),
1625
+ confidence: Number(confidence),
1626
+ source: String(_nullishCoalesce(pick2(item, "source"), () => ( ""))),
1627
+ category: String(_nullishCoalesce(pick2(item, "context_type", "category"), () => ( ""))),
1628
+ emotion_type: String(_nullishCoalesce(pick2(item, "context_type", "emotion_type"), () => ( ""))),
1629
+ strength: Number(_nullishCoalesce(pick2(item, "strength", "confidence"), () => ( 0))),
1630
+ summary: String(_nullishCoalesce(pick2(item, "summary", "content"), () => ( ""))),
1631
+ significance: Number(_nullishCoalesce(pick2(item, "significance", "confidence"), () => ( 0))),
1632
+ intensity: Number(_nullishCoalesce(pick2(item, "intensity", "confidence"), () => ( 0))),
1633
+ context: String(_nullishCoalesce(pick2(item, "context", "content"), () => ( ""))),
1634
+ participants: _nullishCoalesce(pick2(item, "participants"), () => ( [])),
1635
+ extracted_at: extractedAt,
1636
+ occurred_at: _nullishCoalesce(pick2(item, "occurred_at", "extracted_at", "created_at"), () => ( null)),
1637
+ detected_at: _nullishCoalesce(pick2(item, "detected_at", "extracted_at", "created_at"), () => ( null)),
1638
+ metadata: _nullishCoalesce(pick2(item, "metadata"), () => ( {})),
1639
+ event_date: _nullishCoalesce(pick2(item, "event_date"), () => ( null)),
1640
+ valid_until: _nullishCoalesce(pick2(item, "valid_until"), () => ( null)),
1641
+ temporal_category: _nullishCoalesce(pick2(item, "temporal_category"), () => ( null)),
1642
+ temporal_confidence: Number(_nullishCoalesce(pick2(item, "temporal_confidence"), () => ( 0)))
1643
+ };
1644
+ }
1645
+ function mergeUserSummary(response, bundle) {
1646
+ const itemsByType = _nullishCoalesce(bundle["items_by_type"], () => ( {}));
1647
+ for (const collection of COLLECTIONS) {
1648
+ const items = itemsByType[collection];
1649
+ if (!Array.isArray(items)) continue;
1650
+ const existing = _nullishCoalesce(response[collection], () => ( []));
1651
+ const existingIds = new Set(existing.map((i) => String(_nullishCoalesce(_nullishCoalesce(i.id, () => ( i.item_id)), () => ( "")))));
1652
+ const fresh = items.filter((i) => typeof i === "object" && i !== null && !existingIds.has(itemId(i))).slice(0, MAX_SUMMARY_ITEMS).map(normalise);
1653
+ if (fresh.length === 0) continue;
1654
+ response[collection] = [...existing, ...fresh];
1655
+ }
1656
+ return response;
1657
+ }
1658
+
1659
+ // src/context/anticipated.ts
1660
+ function scopeLookupParams(scope, params) {
1661
+ const { searchQuery, entityId, customerId, clientId, conversationId } = params;
1662
+ switch (scope) {
1663
+ case "conversation":
1664
+ return { searchQuery, entityId, customerId, clientId, conversationId };
1665
+ case "user":
1666
+ return { searchQuery, entityId, customerId, clientId };
1667
+ case "customer":
1668
+ return { searchQuery, entityId: null, customerId, clientId };
1669
+ case "client":
1670
+ return { searchQuery, entityId: null, customerId: null, clientId };
1671
+ }
1672
+ }
1673
+ function buildAnticipationResponse(hit) {
1674
+ const response = {};
1675
+ for (const [type, items] of Object.entries(hit.itemsByType)) {
1676
+ response[type] = items;
1677
+ }
1678
+ response.metadata = {
1679
+ source: "anticipation",
1680
+ ttl_seconds: 0,
1681
+ cache_hit: true,
1682
+ retrieved_at: (/* @__PURE__ */ new Date()).toISOString()
1683
+ };
1684
+ return response;
1685
+ }
1686
+ function servedItemIds(hit) {
1687
+ const ids = [];
1688
+ for (const items of Object.values(hit.itemsByType)) {
1689
+ for (const item of items) {
1690
+ const id = _nullishCoalesce(item.item_id, () => ( item["id"]));
1691
+ if (typeof id === "string" && id !== "") ids.push(id);
1692
+ }
1693
+ }
1694
+ return ids;
1695
+ }
1696
+ function tryServeFromCache(cache, scope, params, maxItems) {
1697
+ try {
1698
+ const scoped = scopeLookupParams(scope, params);
1699
+ const hit = cache.lookup({
1700
+ searchQuery: _nullishCoalesce(scoped.searchQuery, () => ( null)),
1701
+ entityId: _nullishCoalesce(scoped.entityId, () => ( null)),
1702
+ customerId: _nullishCoalesce(scoped.customerId, () => ( null)),
1703
+ clientId: _nullishCoalesce(scoped.clientId, () => ( null)),
1704
+ conversationId: _nullishCoalesce(scoped.conversationId, () => ( null)),
1705
+ maxItems
1706
+ });
1707
+ if (hit === null) return { response: null, hit: null };
1708
+ return { response: buildAnticipationResponse(hit), hit };
1709
+ } catch (e6) {
1710
+ return { response: null, hit: null };
1711
+ }
1712
+ }
1713
+
1714
+ // src/context/assembled.ts
1715
+ var COLLECTIONS2 = ["facts", "preferences", "episodes", "emotions", "temporal_events"];
1716
+ function finalItemIds(response) {
1717
+ const ids = [];
1718
+ for (const collection of COLLECTIONS2) {
1719
+ const items = response[collection];
1720
+ if (!Array.isArray(items)) continue;
1721
+ for (const item of items) {
1722
+ const id = _nullishCoalesce(item["id"], () => ( item["item_id"]));
1723
+ if (typeof id === "string" && id !== "") ids.push(id);
1724
+ }
1725
+ }
1726
+ return ids;
1727
+ }
1728
+ function resolveSource(response, override) {
1729
+ const metadata = _nullishCoalesce(response.metadata, () => ( {}));
1730
+ let source = _nullishCoalesce(_nullishCoalesce(override, () => ( String(_nullishCoalesce(metadata["source"], () => ( ""))))), () => ( ""));
1731
+ if (source === "") source = "cloud";
1732
+ if (source === "cache") source = "http_cache";
1733
+ const impliesHit = ["anticipation", "anticipation_cache", "http_cache", "sdk_authoritative"];
1734
+ const cacheHit = impliesHit.includes(source) ? true : Boolean(metadata["cache_hit"]);
1735
+ return { source, cacheHit };
1736
+ }
1737
+ function buildAssembledEvent(params) {
1738
+ const { response } = params;
1739
+ const { source, cacheHit } = resolveSource(response, params.sourceOverride);
1740
+ const conversationContext = _nullishCoalesce(response.conversation_context, () => ( {}));
1741
+ const recentTurns = conversationContext["recent_turns"];
1742
+ const metadata = _nullishCoalesce(response.metadata, () => ( {}));
1743
+ return {
1744
+ correlation_id: params.correlationId,
1745
+ conversation_id: _nullishCoalesce(params.conversationId, () => ( "")),
1746
+ user_id: _nullishCoalesce(params.userId, () => ( "")),
1747
+ customer_id: _nullishCoalesce(params.customerId, () => ( "")),
1748
+ final_item_ids: finalItemIds(response),
1749
+ final_total_tokens: Number(_nullishCoalesce(metadata["total_tokens"], () => ( 0))),
1750
+ compaction_id: String(_nullishCoalesce(conversationContext["compaction_id"], () => ( ""))),
1751
+ recent_turn_count: Array.isArray(recentTurns) ? recentTurns.length : 0,
1752
+ compaction_end_timestamp: String(
1753
+ _nullishCoalesce(_nullishCoalesce(conversationContext["end_timestamp"], () => ( conversationContext["compacted_at"])), () => ( ""))
1754
+ ),
1755
+ assembly_source: source,
1756
+ assembly_duration_ms: Math.max(0, Date.now() - params.startedAt),
1757
+ cache_hit: cacheHit,
1758
+ sdk_version: params.sdkVersion
1759
+ };
1760
+ }
1761
+
1318
1762
  // src/context/unified.ts
1319
1763
  var ITEM_COLLECTIONS = [
1320
1764
  "facts",
@@ -1323,7 +1767,7 @@ var ITEM_COLLECTIONS = [
1323
1767
  "emotions",
1324
1768
  "temporal_events"
1325
1769
  ];
1326
- function itemId(item) {
1770
+ function itemId2(item) {
1327
1771
  const id = _nullishCoalesce(item.id, () => ( item.item_id));
1328
1772
  return typeof id === "string" && id !== "" ? id : void 0;
1329
1773
  }
@@ -1359,7 +1803,7 @@ function mergeScopeResults(scopeResults) {
1359
1803
  const items = response[collection];
1360
1804
  if (!Array.isArray(items)) continue;
1361
1805
  for (const item of items) {
1362
- const id = itemId(item);
1806
+ const id = itemId2(item);
1363
1807
  if (id !== void 0) {
1364
1808
  if (seen.has(id)) continue;
1365
1809
  seen.add(id);
@@ -1385,7 +1829,7 @@ function numberOf(value) {
1385
1829
  return typeof value === "number" && Number.isFinite(value) ? value : null;
1386
1830
  }
1387
1831
  function scopeOf(ctx, item) {
1388
- const id = itemId(item);
1832
+ const id = itemId2(item);
1389
1833
  return _nullishCoalesce((id !== void 0 ? ctx.scope_map[id] : void 0), () => ( "unknown"));
1390
1834
  }
1391
1835
  function bulletWithExtras(text, extras) {
@@ -1700,6 +2144,11 @@ var SynapClient = class {
1700
2144
  // can still reach it. These two were public and made `client.transport
1701
2145
  // .credentials.apiKey` and the raw cache's `now`/`fire`/`invalidateEntity`
1702
2146
  // reachable, which is exactly what `client.cache` exists to prevent.
2147
+ // Definite-assignment assertions (`!`) because the constructor can RETURN an
2148
+ // existing client from the registry before reaching any of these. TypeScript
2149
+ // cannot follow a constructor return, so it sees a path where the fields are
2150
+ // unassigned. On that path the object is discarded and the incumbent is
2151
+ // handed back instead, so nothing observes them.
1703
2152
  #transport;
1704
2153
  #anticipationCache;
1705
2154
 
@@ -1712,13 +2161,37 @@ var SynapClient = class {
1712
2161
 
1713
2162
  #closed = false;
1714
2163
  #initialized = false;
2164
+ #registryKeys;
1715
2165
  #compactionSubscribers;
2166
+ #shortTerm;
2167
+ #turns;
2168
+ #fetchHooks;
1716
2169
  constructor(options = {}) {
1717
2170
  const credentials = resolveCredentials(options);
2171
+ const resolvedBaseUrl = resolveBaseUrl(options);
1718
2172
  validateInstanceId(credentials.instanceId);
2173
+ const registryKey = buildRegistryKey(credentials.instanceId, credentials.apiKey);
2174
+ if (options._force_new !== true) {
2175
+ const existing = get(registryKey);
2176
+ if (existing !== void 0) {
2177
+ const incumbent = existing.#transport.currentCredentials().apiKey;
2178
+ if (credentials.apiKey && incumbent && credentials.apiKey !== incumbent) {
2179
+ existing.#warn(
2180
+ `Instance ${registryKey} is already running in this process on a different API key. The key passed here is ignored; this client will authenticate as the existing one. Rotating a key this way has no effect. Construct with apiKey and no instanceId to get a client on your own credential.`
2181
+ );
2182
+ }
2183
+ return existing;
2184
+ }
2185
+ }
2186
+ this.#registryKeys = options._force_new === true ? [] : [registryKey];
1719
2187
  this.#transport = new HttpTransport({
1720
2188
  credentials,
1721
- ...options.baseUrl !== void 0 ? { baseUrl: options.baseUrl } : {},
2189
+ // SYNAP_BASE_URL is honoured, as Python does (sdk.py) and as the 0.3.x
2190
+ // wrapper did. Ignoring it was a wrong-DESTINATION bug rather than a
2191
+ // missing convenience: anyone pointing at a self-hosted or staging
2192
+ // deployment through the environment would silently have started sending
2193
+ // their data to production on upgrade.
2194
+ ...resolvedBaseUrl !== void 0 ? { baseUrl: resolvedBaseUrl } : {},
1722
2195
  ...options.timeouts !== void 0 ? { timeouts: options.timeouts } : {},
1723
2196
  ...options.retryPolicy !== void 0 ? { retryPolicy: options.retryPolicy } : {},
1724
2197
  ...options.keepAlive !== void 0 ? { keepAlive: options.keepAlive } : {},
@@ -1726,11 +2199,42 @@ var SynapClient = class {
1726
2199
  userAgent: `@maximem/synap-js-sdk/${SDK_VERSION}`
1727
2200
  });
1728
2201
  if (options.heartbeat) this.#transport.startHeartbeat();
1729
- this.#anticipationCache = new (0, _chunkNIDPTBNCcjs.AnticipationCache)();
1730
- this.user = createUserNamespace(this.#transport);
1731
- this.customer = { context: { fetch: async (o2 = {}) => fetchContext(this.#transport, "customer", o2) } };
1732
- this.client = { context: { fetch: async (o2 = {}) => fetchContext(this.#transport, "client", o2) } };
2202
+ this.#anticipationCache = new (0, _chunkRB57I2IQcjs.AnticipationCache)();
2203
+ this.#fetchHooks = {
2204
+ beforeFetch: (scope, options2) => {
2205
+ const o2 = options2;
2206
+ const attempt = tryServeFromCache(
2207
+ this.#anticipationCache,
2208
+ scope,
2209
+ {
2210
+ searchQuery: _nullishCoalesce(o2["search_query"], () => ( o2["searchQuery"])),
2211
+ entityId: _nullishCoalesce(o2["user_id"], () => ( o2["userId"])),
2212
+ customerId: _nullishCoalesce(o2["customer_id"], () => ( o2["customerId"])),
2213
+ clientId: this.#transport.currentCredentials().clientId || null,
2214
+ conversationId: _nullishCoalesce(o2["conversation_id"], () => ( o2["conversationId"]))
2215
+ },
2216
+ Number(_nullishCoalesce(_nullishCoalesce(o2["max_results"], () => ( o2["maxResults"])), () => ( 10)))
2217
+ );
2218
+ if (attempt.response === null || attempt.hit === null) return null;
2219
+ return {
2220
+ response: attempt.response,
2221
+ servedItemIds: servedItemIds(attempt.hit),
2222
+ bundleId: _nullishCoalesce(attempt.hit.bundleIds[0], () => ( ""))
2223
+ };
2224
+ },
2225
+ onAssembled: (scope, options2, response, startedAt) => {
2226
+ this.#emitContextAssembled(scope, options2, response, startedAt);
2227
+ },
2228
+ onServedFromCache: (_scope, options2, served) => {
2229
+ this.#emitContextUsed(options2, _scope, served);
2230
+ }
2231
+ };
2232
+ this.user = createUserNamespace(this.#transport, this.#fetchHooks);
2233
+ this.customer = { context: { fetch: async (o2 = {}) => fetchContext(this.#transport, "customer", o2, this.#fetchHooks) } };
2234
+ this.client = { context: { fetch: async (o2 = {}) => fetchContext(this.#transport, "client", o2, this.#fetchHooks) } };
1733
2235
  this.#compactionSubscribers = new CompactionSubscribers();
2236
+ this.#shortTerm = new ShortTermStore();
2237
+ this.#turns = new TurnCounter();
1734
2238
  this.conversation = createConversationNamespace(
1735
2239
  this.#transport,
1736
2240
  this.#compactionSubscribers,
@@ -1743,6 +2247,15 @@ var SynapClient = class {
1743
2247
  for (const id of /* @__PURE__ */ new Set([userId, customerId])) {
1744
2248
  if (id) this.#anticipationCache.invalidateEntity(id);
1745
2249
  }
2250
+ },
2251
+ // Buffer the turn locally so the next fetch sees it, ahead of the server
2252
+ // having persisted and compacted it.
2253
+ ({ conversationId, role, content, timestamp }) => {
2254
+ this.#shortTerm.appendTurn(conversationId, role, content, timestamp);
2255
+ },
2256
+ (response, conversationId, userId) => {
2257
+ overlayLocalRecentTurns(response, this.#shortTerm, conversationId);
2258
+ this.#injectUserSummary(response, conversationId, userId);
1746
2259
  }
1747
2260
  );
1748
2261
  this.memories = createMemoriesInterface(this.#transport);
@@ -1750,8 +2263,18 @@ var SynapClient = class {
1750
2263
  this.cache = createCacheNamespace(this.#anticipationCache);
1751
2264
  this.instance = createInstanceNamespace(
1752
2265
  () => this.#transport.currentCredentials(),
1753
- this.#anticipationCache
2266
+ this.#anticipationCache,
2267
+ ({ conversationId, role, content }) => {
2268
+ this.#shortTerm.appendTurn(conversationId, role, content);
2269
+ }
1754
2270
  );
2271
+ this.instance._internal.setCompactionApplier((bundle) => {
2272
+ this.#shortTerm.applyCompaction(bundle);
2273
+ });
2274
+ if (options._force_new !== true) {
2275
+ const incumbent = registerIfAbsent(registryKey, this);
2276
+ if (incumbent !== void 0) return incumbent;
2277
+ }
1755
2278
  this.instance._internal.setCompactionDispatcher((conversationId, bundle) => {
1756
2279
  this.#compactionSubscribers.dispatch(conversationId, bundle, (error) => {
1757
2280
  this.#warn(`compaction subscriber threw: ${String(error)}`);
@@ -1789,10 +2312,10 @@ var SynapClient = class {
1789
2312
  }
1790
2313
  const context = await fetchContext(this.#transport, "user", {
1791
2314
  user_id: userId,
1792
- ...pick2(options, "customer_id", "customerId") !== void 0 ? { customer_id: pick2(options, "customer_id", "customerId") } : {},
1793
- ...pick2(options, "conversation_id", "conversationId") !== void 0 ? { conversation_id: pick2(options, "conversation_id", "conversationId") } : {},
2315
+ ...pick3(options, "customer_id", "customerId") !== void 0 ? { customer_id: pick3(options, "customer_id", "customerId") } : {},
2316
+ ...pick3(options, "conversation_id", "conversationId") !== void 0 ? { conversation_id: pick3(options, "conversation_id", "conversationId") } : {},
1794
2317
  search_query: [options.query],
1795
- max_results: _nullishCoalesce(pick2(options, "max_results", "maxResults"), () => ( 10)),
2318
+ max_results: _nullishCoalesce(pick3(options, "max_results", "maxResults"), () => ( 10)),
1796
2319
  types: _nullishCoalesce(options.types, () => ( ["all"])),
1797
2320
  ...options.mode !== void 0 ? { mode: options.mode } : {}
1798
2321
  });
@@ -1809,10 +2332,10 @@ var SynapClient = class {
1809
2332
  if (!userId) throw new (0, _chunkHR5Y3KXScjs.InvalidInputError)("user_id is required");
1810
2333
  const context = await fetchContext(this.#transport, "user", {
1811
2334
  user_id: userId,
1812
- ...pick2(options, "customer_id", "customerId") !== void 0 ? { customer_id: pick2(options, "customer_id", "customerId") } : {},
1813
- ...pick2(options, "conversation_id", "conversationId") !== void 0 ? { conversation_id: pick2(options, "conversation_id", "conversationId") } : {},
2335
+ ...pick3(options, "customer_id", "customerId") !== void 0 ? { customer_id: pick3(options, "customer_id", "customerId") } : {},
2336
+ ...pick3(options, "conversation_id", "conversationId") !== void 0 ? { conversation_id: pick3(options, "conversation_id", "conversationId") } : {},
1814
2337
  search_query: [],
1815
- max_results: _nullishCoalesce(pick2(options, "max_results", "maxResults"), () => ( 100)),
2338
+ max_results: _nullishCoalesce(pick3(options, "max_results", "maxResults"), () => ( 100)),
1816
2339
  types: _nullishCoalesce(options.types, () => ( ["all"])),
1817
2340
  ...options.mode !== void 0 ? { mode: options.mode } : {}
1818
2341
  });
@@ -1858,7 +2381,12 @@ var SynapClient = class {
1858
2381
  ...resolvedClientId !== "" && credentials.clientId === "" ? { clientId: resolvedClientId } : {},
1859
2382
  ...resolvedInstanceId !== "" && credentials.instanceId === "" ? { instanceId: resolvedInstanceId } : {}
1860
2383
  });
1861
- } catch (e5) {
2384
+ if (resolvedInstanceId !== "" && this.#registryKeys.length > 0) {
2385
+ if (aliasIfAbsent(resolvedInstanceId, this)) {
2386
+ this.#registryKeys.push(resolvedInstanceId);
2387
+ }
2388
+ }
2389
+ } catch (e7) {
1862
2390
  }
1863
2391
  }
1864
2392
  /**
@@ -1994,12 +2522,15 @@ var SynapClient = class {
1994
2522
  this.#warn(`Scope '${label}' fetch failed in unified fetch (non-fatal): ${String(result.reason)}`);
1995
2523
  }
1996
2524
  const merged = mergeScopeResults(successful);
2525
+ for (const [, response] of successful) {
2526
+ overlayLocalRecentTurns(response, this.#shortTerm, conversationId);
2527
+ }
1997
2528
  if (includeConversationContext && conversationId !== void 0 && conversationId !== "") {
1998
2529
  try {
1999
2530
  merged.conversation_context = await this.conversation.context.get_context_for_prompt({
2000
2531
  conversation_id: conversationId
2001
2532
  });
2002
- } catch (e6) {
2533
+ } catch (e8) {
2003
2534
  }
2004
2535
  }
2005
2536
  merged.formatted_context = formatForPrompt(merged, {
@@ -2026,6 +2557,69 @@ var SynapClient = class {
2026
2557
  anticipation_cache_snapshot() {
2027
2558
  return this.#anticipationCache.snapshot();
2028
2559
  }
2560
+ /**
2561
+ * Report a locally served fetch back to the server.
2562
+ *
2563
+ * Only while listening: the event rides the Listen stream, and there is
2564
+ * nowhere to put it otherwise. Never throws, because a telemetry failure
2565
+ * must not fail a retrieval that already succeeded.
2566
+ */
2567
+ #emitContextUsed(options, scope, served) {
2568
+ try {
2569
+ if (!this.instance.is_listening) return;
2570
+ this.instance._internal.sendContextUsed({
2571
+ bundle_id: served.bundleId,
2572
+ conversation_id: String(_nullishCoalesce(_nullishCoalesce(options["conversation_id"], () => ( options["conversationId"])), () => ( ""))),
2573
+ user_id: String(_nullishCoalesce(_nullishCoalesce(options["user_id"], () => ( options["userId"])), () => ( ""))),
2574
+ customer_id: String(_nullishCoalesce(_nullishCoalesce(options["customer_id"], () => ( options["customerId"])), () => ( ""))),
2575
+ served_item_ids: served.servedItemIds,
2576
+ scope,
2577
+ source_bundle_ids: served.bundleId ? [served.bundleId] : [],
2578
+ timestamp_ms: Date.now()
2579
+ });
2580
+ } catch (e9) {
2581
+ }
2582
+ }
2583
+ /**
2584
+ * Report what was actually composed for this turn.
2585
+ *
2586
+ * Fires whether the fetch was served from the cache or the network, because
2587
+ * the audit needs to distinguish them. Like the other stream events it is
2588
+ * fire-and-forget and only goes anywhere while listening.
2589
+ */
2590
+ #emitContextAssembled(scope, options, response, startedAt) {
2591
+ try {
2592
+ if (!this.instance.is_listening) return;
2593
+ this.instance._internal.sendContextAssembled({
2594
+ ...buildAssembledEvent({
2595
+ correlationId: newCorrelationId(),
2596
+ response,
2597
+ conversationId: _nullishCoalesce(options["conversation_id"], () => ( options["conversationId"])),
2598
+ userId: _nullishCoalesce(options["user_id"], () => ( options["userId"])),
2599
+ customerId: _nullishCoalesce(options["customer_id"], () => ( options["customerId"])),
2600
+ startedAt,
2601
+ sdkVersion: SDK_VERSION
2602
+ }),
2603
+ scope
2604
+ });
2605
+ } catch (e10) {
2606
+ }
2607
+ }
2608
+ /**
2609
+ * Fold a cached user summary in, every Nth turn.
2610
+ *
2611
+ * Only when the caller supplied a user_id. Without one the summary lookup
2612
+ * cannot be scoped, and an unscoped lookup is how user A's summary ends up in
2613
+ * user B's response.
2614
+ */
2615
+ #injectUserSummary(response, conversationId, userId) {
2616
+ this.#turns.increment(conversationId);
2617
+ if (userId === void 0 || userId === "") return;
2618
+ if (!this.#turns.shouldInject(conversationId)) return;
2619
+ const summary = this.#anticipationCache.lookupUserSummary(userId);
2620
+ if (summary === null) return;
2621
+ mergeUserSummary(response, summary);
2622
+ }
2029
2623
  /** Python logs via `logger.warning`; here it is one overridable seam. */
2030
2624
  #warn(message) {
2031
2625
  console.warn(`[synap] ${message}`);
@@ -2034,16 +2628,24 @@ var SynapClient = class {
2034
2628
  async shutdown() {
2035
2629
  if (this.#closed) return;
2036
2630
  this.#closed = true;
2631
+ this.#shortTerm.clear();
2632
+ this.#turns.clear();
2633
+ for (const key of this.#registryKeys) unregisterIfOwner(key, this);
2037
2634
  await this.instance.stop_listening();
2038
2635
  this.#compactionSubscribers.removeAll();
2039
2636
  this.#transport.close();
2040
2637
  this.#anticipationCache.clear();
2041
2638
  }
2042
2639
  };
2043
- function pick2(o2, snake, camel) {
2640
+ function pick3(o2, snake, camel) {
2044
2641
  const a = o2[snake];
2045
2642
  return a !== void 0 ? a : o2[camel];
2046
2643
  }
2644
+ function resolveBaseUrl(options) {
2645
+ if (options.baseUrl !== void 0) return options.baseUrl;
2646
+ const fromEnv = _optionalChain([_chunkZKOJDUN4cjs.getEnv.call(void 0, "SYNAP_BASE_URL"), 'optionalAccess', _51 => _51.trim, 'call', _52 => _52()]);
2647
+ return fromEnv !== void 0 && fromEnv !== "" ? fromEnv : void 0;
2648
+ }
2047
2649
  function resolveCredentials(options) {
2048
2650
  const apiKey = _nullishCoalesce(options.apiKey, () => ( _chunkZKOJDUN4cjs.getEnv.call(void 0, "SYNAP_API_KEY")));
2049
2651
  if (!apiKey) {
@@ -2107,5 +2709,5 @@ function resolveCredentials(options) {
2107
2709
 
2108
2710
 
2109
2711
 
2110
- exports.ANY_SCOPE = _chunkNIDPTBNCcjs.ANY_SCOPE; exports.AgentUnavailableError = _chunkHR5Y3KXScjs.AgentUnavailableError; exports.AnticipationCache = _chunkNIDPTBNCcjs.AnticipationCache; exports.AuthenticationError = _chunkHR5Y3KXScjs.AuthenticationError; exports.BM25 = _chunkNIDPTBNCcjs.BM25; exports.CONTRACT = _chunkNIDPTBNCcjs.CONTRACT; exports.ConflictError = _chunkHR5Y3KXScjs.ConflictError; exports.ConnectionError = _chunkHR5Y3KXScjs.ConnectionError; exports.ContextNotFoundError = _chunkHR5Y3KXScjs.ContextNotFoundError; exports.DEFAULT_BASE_URL = DEFAULT_BASE_URL; exports.DEFAULT_DOCUMENT_TYPE = DEFAULT_DOCUMENT_TYPE; exports.DEFAULT_INGEST_MODE = DEFAULT_INGEST_MODE; exports.DEFAULT_RETRY_POLICY = DEFAULT_RETRY_POLICY; exports.DEFAULT_TIMEOUTS = DEFAULT_TIMEOUTS; exports.DOCUMENT_TYPES = DOCUMENT_TYPES; exports.ENDPOINTS = ENDPOINTS; exports.HttpTransport = HttpTransport; exports.INGEST_MODES = INGEST_MODES; exports.ITEM_COLLECTIONS = ITEM_COLLECTIONS; exports.InsufficientCreditsError = _chunkHR5Y3KXScjs.InsufficientCreditsError; exports.InvalidConversationIdError = _chunkHR5Y3KXScjs.InvalidConversationIdError; exports.InvalidInputError = _chunkHR5Y3KXScjs.InvalidInputError; exports.InvalidInstanceIdError = _chunkHR5Y3KXScjs.InvalidInstanceIdError; exports.ListeningAlreadyActiveError = _chunkHR5Y3KXScjs.ListeningAlreadyActiveError; exports.ListeningNotActiveError = _chunkHR5Y3KXScjs.ListeningNotActiveError; exports.NetworkTimeoutError = _chunkHR5Y3KXScjs.NetworkTimeoutError; exports.PermanentError = _chunkHR5Y3KXScjs.PermanentError; exports.RateLimitError = _chunkHR5Y3KXScjs.RateLimitError; exports.SDKError = _chunkHR5Y3KXScjs.SDKError; exports.SDK_VERSION = SDK_VERSION; exports.ServiceUnavailableError = _chunkHR5Y3KXScjs.ServiceUnavailableError; exports.SessionExpiredError = _chunkHR5Y3KXScjs.SessionExpiredError; exports.SynapClient = SynapClient; exports.SynapError = _chunkHR5Y3KXScjs.SynapError; exports.SynapPermanentError = _chunkHR5Y3KXScjs.SynapPermanentError; exports.SynapTransientError = _chunkHR5Y3KXScjs.SynapTransientError; exports.THRESHOLDS = _chunkNIDPTBNCcjs.THRESHOLDS; exports.TranscriptConflictError = _chunkHR5Y3KXScjs.TranscriptConflictError; exports.TransientError = _chunkHR5Y3KXScjs.TransientError; exports.flattenContextItems = flattenContextItems; exports.formatForPrompt = formatForPrompt; exports.isOutcomeUnknown = isOutcomeUnknown; exports.isRecallQuery = _chunkNIDPTBNCcjs.isRecallQuery; exports.isSynapError = _chunkHR5Y3KXScjs.isSynapError; exports.mergeScopeResults = mergeScopeResults; exports.resolvePath = resolvePath; exports.shouldRetry = shouldRetry; exports.stem = _chunkNIDPTBNCcjs.stem; exports.tokenize = _chunkNIDPTBNCcjs.tokenize;
2712
+ exports.ANY_SCOPE = _chunkRB57I2IQcjs.ANY_SCOPE; exports.AgentUnavailableError = _chunkHR5Y3KXScjs.AgentUnavailableError; exports.AnticipationCache = _chunkRB57I2IQcjs.AnticipationCache; exports.AuthenticationError = _chunkHR5Y3KXScjs.AuthenticationError; exports.BM25 = _chunkRB57I2IQcjs.BM25; exports.CONTRACT = _chunkRB57I2IQcjs.CONTRACT; exports.ConflictError = _chunkHR5Y3KXScjs.ConflictError; exports.ConnectionError = _chunkHR5Y3KXScjs.ConnectionError; exports.ContextNotFoundError = _chunkHR5Y3KXScjs.ContextNotFoundError; exports.DEFAULT_BASE_URL = DEFAULT_BASE_URL; exports.DEFAULT_DOCUMENT_TYPE = DEFAULT_DOCUMENT_TYPE; exports.DEFAULT_INGEST_MODE = DEFAULT_INGEST_MODE; exports.DEFAULT_RETRY_POLICY = DEFAULT_RETRY_POLICY; exports.DEFAULT_TIMEOUTS = DEFAULT_TIMEOUTS; exports.DOCUMENT_TYPES = DOCUMENT_TYPES; exports.ENDPOINTS = ENDPOINTS; exports.HttpTransport = HttpTransport; exports.INGEST_MODES = INGEST_MODES; exports.ITEM_COLLECTIONS = ITEM_COLLECTIONS; exports.InsufficientCreditsError = _chunkHR5Y3KXScjs.InsufficientCreditsError; exports.InvalidConversationIdError = _chunkHR5Y3KXScjs.InvalidConversationIdError; exports.InvalidInputError = _chunkHR5Y3KXScjs.InvalidInputError; exports.InvalidInstanceIdError = _chunkHR5Y3KXScjs.InvalidInstanceIdError; exports.ListeningAlreadyActiveError = _chunkHR5Y3KXScjs.ListeningAlreadyActiveError; exports.ListeningNotActiveError = _chunkHR5Y3KXScjs.ListeningNotActiveError; exports.NetworkTimeoutError = _chunkHR5Y3KXScjs.NetworkTimeoutError; exports.PermanentError = _chunkHR5Y3KXScjs.PermanentError; exports.RateLimitError = _chunkHR5Y3KXScjs.RateLimitError; exports.SDKError = _chunkHR5Y3KXScjs.SDKError; exports.SDK_VERSION = SDK_VERSION; exports.ServiceUnavailableError = _chunkHR5Y3KXScjs.ServiceUnavailableError; exports.SessionExpiredError = _chunkHR5Y3KXScjs.SessionExpiredError; exports.SynapClient = SynapClient; exports.SynapError = _chunkHR5Y3KXScjs.SynapError; exports.SynapPermanentError = _chunkHR5Y3KXScjs.SynapPermanentError; exports.SynapTransientError = _chunkHR5Y3KXScjs.SynapTransientError; exports.THRESHOLDS = _chunkRB57I2IQcjs.THRESHOLDS; exports.TranscriptConflictError = _chunkHR5Y3KXScjs.TranscriptConflictError; exports.TransientError = _chunkHR5Y3KXScjs.TransientError; exports.flattenContextItems = flattenContextItems; exports.formatForPrompt = formatForPrompt; exports.isOutcomeUnknown = isOutcomeUnknown; exports.isRecallQuery = _chunkRB57I2IQcjs.isRecallQuery; exports.isSynapError = _chunkHR5Y3KXScjs.isSynapError; exports.mergeScopeResults = mergeScopeResults; exports.resolvePath = resolvePath; exports.shouldRetry = shouldRetry; exports.stem = _chunkRB57I2IQcjs.stem; exports.tokenize = _chunkRB57I2IQcjs.tokenize;
2111
2713
  //# sourceMappingURL=index.cjs.map