@maximem/synap-js-sdk 0.5.8 → 0.5.10

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.
Files changed (47) hide show
  1. package/README.md +5 -1
  2. package/dist/anticipation-cache-LPNKMRBH.cjs +20 -0
  3. package/dist/anticipation-cache-LPNKMRBH.cjs.map +1 -0
  4. package/dist/anticipation-cache-RWKF55J4.js +20 -0
  5. package/dist/{chunk-FXGLT6AT.js → chunk-I7TWR4S6.js} +59 -3
  6. package/dist/chunk-I7TWR4S6.js.map +1 -0
  7. package/dist/{chunk-T6U5PVAD.cjs → chunk-ICUFS3B5.cjs} +449 -12
  8. package/dist/chunk-ICUFS3B5.cjs.map +1 -0
  9. package/dist/{chunk-ODVOQOSO.js → chunk-R72YL6Q4.js} +445 -8
  10. package/dist/chunk-R72YL6Q4.js.map +1 -0
  11. package/dist/{chunk-HCULAVJC.cjs → chunk-RE4DDIWO.cjs} +66 -10
  12. package/dist/chunk-RE4DDIWO.cjs.map +1 -0
  13. package/dist/chunk-T372OL3Q.cjs.map +1 -1
  14. package/dist/chunk-ZKOJDUN4.cjs.map +1 -1
  15. package/dist/env-55ULHNJZ.cjs.map +1 -1
  16. package/dist/{errors-Bl3vY1eb.d.cts → errors-Bdayqlzy.d.cts} +88 -2
  17. package/dist/{errors-Bl3vY1eb.d.ts → errors-Bdayqlzy.d.ts} +88 -2
  18. package/dist/grpc/index.cjs +4 -4
  19. package/dist/grpc/index.cjs.map +1 -1
  20. package/dist/grpc/index.d.cts +8 -3
  21. package/dist/grpc/index.d.ts +8 -3
  22. package/dist/grpc/index.js +3 -3
  23. package/dist/index.cjs +1357 -128
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.d.cts +277 -11
  26. package/dist/index.d.ts +277 -11
  27. package/dist/index.js +1309 -80
  28. package/dist/index.js.map +1 -1
  29. package/dist/onnx-encoder-4RSL6ID4.cjs +103 -0
  30. package/dist/onnx-encoder-4RSL6ID4.cjs.map +1 -0
  31. package/dist/onnx-encoder-NP35KH4H.js +103 -0
  32. package/dist/onnx-encoder-NP35KH4H.js.map +1 -0
  33. package/dist/{stream-client-VWKINE47.js → stream-client-3WHXGW4U.js} +2 -2
  34. package/dist/stream-client-L6OQPZN7.cjs +16 -0
  35. package/dist/stream-client-L6OQPZN7.cjs.map +1 -0
  36. package/package.json +4 -2
  37. package/dist/anticipation-cache-LRE3TLK4.cjs +0 -14
  38. package/dist/anticipation-cache-LRE3TLK4.cjs.map +0 -1
  39. package/dist/anticipation-cache-WLU4NMD5.js +0 -14
  40. package/dist/chunk-FXGLT6AT.js.map +0 -1
  41. package/dist/chunk-HCULAVJC.cjs.map +0 -1
  42. package/dist/chunk-ODVOQOSO.js.map +0 -1
  43. package/dist/chunk-T6U5PVAD.cjs.map +0 -1
  44. package/dist/stream-client-N46JBT5A.cjs +0 -16
  45. package/dist/stream-client-N46JBT5A.cjs.map +0 -1
  46. /package/dist/{anticipation-cache-WLU4NMD5.js.map → anticipation-cache-RWKF55J4.js.map} +0 -0
  47. /package/dist/{stream-client-VWKINE47.js.map → stream-client-3WHXGW4U.js.map} +0 -0
package/dist/index.js CHANGED
@@ -3,12 +3,14 @@ import {
3
3
  AnticipationCache,
4
4
  BM25,
5
5
  CONTRACT,
6
+ HISTORY_SEARCH,
7
+ SEMANTIC_MATCH,
6
8
  THRESHOLDS,
7
9
  isRecallQuery,
8
10
  rungKey,
9
11
  stem,
10
12
  tokenize
11
- } from "./chunk-ODVOQOSO.js";
13
+ } from "./chunk-R72YL6Q4.js";
12
14
  import {
13
15
  getEnv
14
16
  } from "./chunk-QBCVMPGK.js";
@@ -55,6 +57,7 @@ var ENDPOINTS = {
55
57
  context_fetch_client: { method: "POST", path: "/v1/context/client/fetch", idempotent: true },
56
58
  context_fetch_conversation: { method: "POST", path: "/v1/context/conversation/fetch", idempotent: true },
57
59
  context_for_prompt: { method: "GET", path: "/v1/conversations/{conversation_id}/context-for-prompt", idempotent: true },
60
+ conversation_turns: { method: "GET", path: "/v1/conversations/{conversation_id}/messages", idempotent: true },
58
61
  conversations_ingest: { method: "POST", path: "/v1/conversations/ingest", idempotent: false },
59
62
  conversations_messages: { method: "POST", path: "/v1/conversations/messages", idempotent: false },
60
63
  conversations_messages_batch: { method: "POST", path: "/v1/conversations/messages/batch", idempotent: false },
@@ -264,6 +267,17 @@ var HttpTransport = class {
264
267
  * deployment older than the field, and must mean "enforce nothing".
265
268
  */
266
269
  userContextIsolation = void 0;
270
+ /**
271
+ * Set by the client to its identity resolution (`initialize()`'s whoami).
272
+ * Python refuses every call before `initialize()`; here the first request
273
+ * runs it instead, so a caller who skips it gets the same identity-dependent
274
+ * behaviour (client-scope cache hits, the B2C check) rather than an error.
275
+ */
276
+ identityResolver = void 0;
277
+ /** Resolve identity once, before the first request. Never throws. */
278
+ async ready() {
279
+ if (this.identityResolver !== void 0) await this.identityResolver();
280
+ }
267
281
  /** Refuse a customer_id on a B2C instance, at the call site. */
268
282
  checkCustomerId(customerId, where) {
269
283
  checkCustomerId(
@@ -295,6 +309,7 @@ var HttpTransport = class {
295
309
  const spec = ENDPOINTS[name];
296
310
  const correlationId = options.correlationId ?? newCorrelationId();
297
311
  const idempotent = options.idempotent ?? spec.idempotent;
312
+ if (name !== "whoami") await this.ready();
298
313
  let attempt = 0;
299
314
  let last = null;
300
315
  while (attempt < this.retryPolicy.maxAttempts) {
@@ -517,6 +532,86 @@ function sleep(ms, signal) {
517
532
  });
518
533
  }
519
534
 
535
+ // src/context/ranked.ts
536
+ var RESPONSE_SHAPES = ["by_type", "ranked"];
537
+ var MEMORY_TYPES = [
538
+ "facts",
539
+ "preferences",
540
+ "episodes",
541
+ "emotions",
542
+ "temporal_events"
543
+ ];
544
+ var EXCERPT_CATEGORY = "excerpts";
545
+ function wireRankScore(item) {
546
+ const md = item.metadata;
547
+ let raw = md !== null && typeof md === "object" ? md["rank_score"] : void 0;
548
+ if (raw === void 0 || raw === null) raw = item["rank_score"];
549
+ return typeof raw === "number" && Number.isFinite(raw) ? raw : null;
550
+ }
551
+ function wireCategory(item, fallback = "") {
552
+ if (item.source === "chunks") return EXCERPT_CATEGORY;
553
+ const category = item["category"];
554
+ if (typeof category === "string" && category !== "") return category;
555
+ const contextType = item["context_type"];
556
+ if (typeof contextType === "string" && contextType !== "") return contextType;
557
+ return fallback;
558
+ }
559
+ function byRankScore(a, b) {
560
+ const sa = wireRankScore(a);
561
+ const sb = wireRankScore(b);
562
+ if (sa === null && sb === null) return 0;
563
+ if (sa === null) return 1;
564
+ if (sb === null) return -1;
565
+ return sb - sa;
566
+ }
567
+ function rankItems(byType) {
568
+ const flat = [];
569
+ for (const type of MEMORY_TYPES) {
570
+ const list = byType[type];
571
+ if (!Array.isArray(list)) continue;
572
+ for (const it of list) {
573
+ if (it === null || typeof it !== "object") continue;
574
+ flat.push({
575
+ ...it,
576
+ category: wireCategory(it, type),
577
+ id: it.id || it.item_id || ""
578
+ });
579
+ }
580
+ }
581
+ flat.sort(byRankScore);
582
+ return flat.map((it, n2) => ({ ...it, rank: n2 + 1 }));
583
+ }
584
+ function applyResponseShape(raw, shape = "by_type") {
585
+ const wire = raw["items"];
586
+ if (shape !== "ranked" && !Array.isArray(wire)) return raw;
587
+ const source = Array.isArray(wire) ? wire : rankItems(raw);
588
+ const items = source.map((it) => ({
589
+ ...it,
590
+ id: it.id || it.item_id || "",
591
+ category: wireCategory(it)
592
+ }));
593
+ const out = { ...raw, items };
594
+ for (const type of MEMORY_TYPES) {
595
+ out[type] = items.filter((it) => it["category"] === type);
596
+ }
597
+ out["excerpts"] = items.filter((it) => it["category"] === EXCERPT_CATEGORY);
598
+ return out;
599
+ }
600
+ var RANKED_LABELS = {
601
+ facts: "fact",
602
+ preferences: "preference",
603
+ episodes: "episode",
604
+ emotions: "emotion",
605
+ temporal_events: "temporal event",
606
+ [EXCERPT_CATEGORY]: "excerpt"
607
+ };
608
+ function rankedLabel(category) {
609
+ return RANKED_LABELS[category] ?? (category !== "" ? category : "item");
610
+ }
611
+ function sortByRankScore(items) {
612
+ return [...items].sort(byRankScore);
613
+ }
614
+
520
615
  // src/context/fetch.ts
521
616
  var ENDPOINT_BY_SCOPE = {
522
617
  user: "context_fetch_user",
@@ -536,10 +631,10 @@ function buildFetchRequest(scope, options) {
536
631
  const conversationId = opt(options, "conversation_id", "conversationId");
537
632
  const searchQuery = opt(options, "search_query", "searchQuery");
538
633
  const maxResults = opt(options, "max_results", "maxResults") ?? 10;
539
- const precisionLevel = opt(options, "precision_level", "precisionLevel") ?? "high";
540
634
  const queryDate = opt(options, "query_date", "queryDate");
541
635
  const perQuery = opt(options, "max_results_per_query", "maxResultsPerQuery");
542
- const mode = options.mode ?? "fast";
636
+ const responseShape = responseShapeOf(options);
637
+ const { mode, precisionLevel } = retrievalOptionsOf(options);
543
638
  if (searchQuery !== void 0 && !Array.isArray(searchQuery)) {
544
639
  throw new InvalidInputError("search_query must be an array when provided");
545
640
  }
@@ -556,16 +651,19 @@ function buildFetchRequest(scope, options) {
556
651
  if (precisionLevel !== "high") body["precision_level"] = precisionLevel;
557
652
  if (queryDate) body["query_date"] = queryDate;
558
653
  if (perQuery) body["max_results_per_query"] = perQuery;
654
+ if (responseShape !== "by_type") body["response_shape"] = responseShape;
559
655
  if (options.include_conversation_context === false) {
560
656
  body["include_conversation_context"] = false;
561
657
  }
658
+ const scopePath = opt(options, "scope_path", "scopePath");
659
+ if (scopePath !== void 0 && scopePath !== null && Object.keys(scopePath).length > 0) {
660
+ body["scope"] = scopePath;
661
+ }
562
662
  switch (scope) {
563
663
  case "user":
564
664
  {
565
665
  if (!userId) throw new InvalidInputError("user_id is required");
566
666
  body["user_id"] = userId;
567
- const scopePath = opt(options, "scope_path", "scopePath");
568
- if (scopePath !== void 0) body["scope"] = scopePath;
569
667
  const contextMode = opt(options, "context_mode", "contextMode") ?? "in-conversation";
570
668
  if (!VALID_CONTEXT_MODES.includes(contextMode)) {
571
669
  throw new InvalidInputError(
@@ -603,21 +701,67 @@ function buildFetchRequest(scope, options) {
603
701
  return { endpoint: ENDPOINT_BY_SCOPE[scope], body };
604
702
  }
605
703
  var VALID_CONTEXT_MODES = ["in-conversation", "conversation-summary"];
704
+ function responseShapeOf(options) {
705
+ const shape = opt(options, "response_shape", "responseShape") ?? "by_type";
706
+ if (!RESPONSE_SHAPES.includes(shape)) {
707
+ throw new InvalidInputError(
708
+ `Invalid response_shape '${shape}'. Must be one of: ${RESPONSE_SHAPES.join(", ")}`
709
+ );
710
+ }
711
+ return shape;
712
+ }
713
+ var VALID_MODES = ["fast", "accurate"];
714
+ var VALID_PRECISION = ["high", "medium"];
715
+ function retrievalOptionsOf(options) {
716
+ const mode = options.mode ?? "fast";
717
+ if (!VALID_MODES.includes(mode)) {
718
+ throw new InvalidInputError(
719
+ `Invalid mode '${mode}'. Must be one of: ${VALID_MODES.join(", ")}`
720
+ );
721
+ }
722
+ const precisionLevel = opt(options, "precision_level", "precisionLevel") ?? "high";
723
+ if (!VALID_PRECISION.includes(precisionLevel)) {
724
+ throw new InvalidInputError(
725
+ `Invalid precision_level '${precisionLevel}'. Must be one of: ${VALID_PRECISION.join(", ")}`
726
+ );
727
+ }
728
+ return { mode, precisionLevel };
729
+ }
606
730
  async function fetchContext(transport, scope, options = {}, hooks = {}) {
607
731
  const startedAt = Date.now();
732
+ await transport.ready?.();
608
733
  transport.checkCustomerId(
609
734
  opt(options, "customer_id", "customerId"),
610
735
  `${scope}.context.fetch`
611
736
  );
612
- const anticipated = hooks.beforeFetch?.(scope, options) ?? null;
737
+ const responseShape = responseShapeOf(options);
738
+ retrievalOptionsOf(options);
739
+ const network = async () => {
740
+ const { endpoint, body } = buildFetchRequest(scope, options);
741
+ const result = await transport.request(endpoint, { body });
742
+ return applyResponseShape(result?.["context"] ?? {}, responseShape);
743
+ };
744
+ const anticipated = await hooks.beforeFetch?.(scope, options) ?? null;
745
+ if (anticipated !== null && anticipated.borderline) {
746
+ const pending = network();
747
+ pending.catch(() => void 0);
748
+ let timer;
749
+ const late = new Promise((resolve) => {
750
+ timer = setTimeout(() => resolve(null), anticipated.raceDeadlineMs ?? 250);
751
+ });
752
+ const won = await Promise.race([pending.then((r) => r, () => null), late]);
753
+ if (timer !== void 0) clearTimeout(timer);
754
+ if (won !== null) {
755
+ hooks.onAssembled?.(scope, options, won, startedAt);
756
+ return won;
757
+ }
758
+ }
613
759
  if (anticipated !== null) {
614
760
  hooks.onServedFromCache?.(scope, options, anticipated);
615
761
  hooks.onAssembled?.(scope, options, anticipated.response, startedAt);
616
- return anticipated.response;
762
+ return applyResponseShape(anticipated.response, responseShape);
617
763
  }
618
- const { endpoint, body } = buildFetchRequest(scope, options);
619
- const result = await transport.request(endpoint, { body });
620
- const response = result?.["context"] ?? {};
764
+ const response = await network();
621
765
  hooks.onAssembled?.(scope, options, response, startedAt);
622
766
  return response;
623
767
  }
@@ -738,6 +882,17 @@ function normalizeConversationContext(value) {
738
882
  conversationId: pick(value["conversationId"], value.conversation_id, null)
739
883
  };
740
884
  }
885
+ function normalizeRetrievalHealth(raw) {
886
+ if (!raw || typeof raw !== "object") return null;
887
+ const r = raw;
888
+ const warnings = r["warnings"];
889
+ return {
890
+ degraded: r["degraded"] === void 0 ? true : Boolean(r["degraded"]),
891
+ level: r["level"] ?? null,
892
+ code: r["code"] ?? null,
893
+ warnings: Array.isArray(warnings) ? warnings.map((w) => String(w)) : []
894
+ };
895
+ }
741
896
  function normalizeContextResponse(result = {}) {
742
897
  const context = result["context"] ?? result;
743
898
  return {
@@ -754,7 +909,15 @@ function normalizeContextResponse(result = {}) {
754
909
  )
755
910
  ),
756
911
  metadata: normalizeContextMetadata(obj(context.metadata)),
757
- rawResponse: pick(context["rawResponse"], context["raw_response"], {})
912
+ // `retrieval` is a SIBLING of `context` in the body, so it is read off the
913
+ // envelope first and only then off a bare context. Reading it from
914
+ // `context` alone is how the Python SDK dropped it.
915
+ retrieval: normalizeRetrievalHealth(
916
+ pick(result["retrieval"], context["retrieval"], null)
917
+ ),
918
+ rawResponse: pick(context["rawResponse"], context["raw_response"], {}),
919
+ // Only for a ranked fetch, so a by-type result has exactly the keys it had.
920
+ ...Array.isArray(context.items) ? { items: context.items, excerpts: Array.isArray(context.excerpts) ? context.excerpts : [] } : {}
758
921
  };
759
922
  }
760
923
 
@@ -1069,6 +1232,27 @@ function validateInstanceId(instanceId) {
1069
1232
  }
1070
1233
  }
1071
1234
 
1235
+ // src/agent-model.ts
1236
+ var AGENT_MODEL_KEY = "agent_model";
1237
+ var AGENT_CONTEXT_WINDOW_KEY = "agent_context_window";
1238
+ function agentModelOf(options) {
1239
+ const o2 = options ?? {};
1240
+ const window = o2["context_window"] ?? o2["contextWindow"];
1241
+ return {
1242
+ model: typeof o2["model"] === "string" ? o2["model"] : void 0,
1243
+ contextWindow: typeof window === "number" ? window : void 0
1244
+ };
1245
+ }
1246
+ function withAgentModel(metadata, perCall, defaults, asStrings = false) {
1247
+ const model = perCall.model || defaults.model;
1248
+ const window = perCall.contextWindow || defaults.contextWindow;
1249
+ if (!model && !window) return metadata;
1250
+ const out = { ...metadata ?? {} };
1251
+ if (model) out[AGENT_MODEL_KEY] = String(model);
1252
+ if (window) out[AGENT_CONTEXT_WINDOW_KEY] = asStrings ? String(Math.trunc(window)) : Math.trunc(window);
1253
+ return out;
1254
+ }
1255
+
1072
1256
  // src/conversation/interface.ts
1073
1257
  var COMPACTION_LEVELS = [
1074
1258
  "low",
@@ -1197,10 +1381,50 @@ var CompactionSubscribers = class {
1197
1381
  }
1198
1382
  }
1199
1383
  };
1384
+ function storedBatchRows(bodies, result) {
1385
+ const r = result ?? {};
1386
+ const results = Array.isArray(r["results"]) ? r["results"] : [];
1387
+ const errors = Array.isArray(r["errors"]) ? r["errors"] : [];
1388
+ const failed = new Set(errors.map((e) => e?.["index"]));
1389
+ const stored = bodies.filter((_, index) => !failed.has(index));
1390
+ if (stored.length !== results.length) return [];
1391
+ const pairs = [];
1392
+ stored.forEach((body, i) => {
1393
+ const row = results[i];
1394
+ if (row && typeof row === "object" && row["message_id"]) pairs.push([body, row]);
1395
+ });
1396
+ return pairs;
1397
+ }
1200
1398
  function createConversationNamespace(transport, subscribers = new CompactionSubscribers(), onWrite = () => {
1201
1399
  }, onTurn = () => {
1202
1400
  }, onFetched = () => {
1203
- }, localShortTerm = { prompt: () => null, compacted: () => null }) {
1401
+ }, localShortTerm = { prompt: () => null, compacted: () => null }, agentModel = () => ({})) {
1402
+ const writesInFlight = /* @__PURE__ */ new Map();
1403
+ const withTurnModel = (body, options) => {
1404
+ const metadata = withAgentModel(
1405
+ body["metadata"],
1406
+ agentModelOf(options),
1407
+ agentModel()
1408
+ );
1409
+ return metadata === void 0 ? body : { ...body, metadata };
1410
+ };
1411
+ const writeStarted = (conversationId) => {
1412
+ writesInFlight.set(conversationId, (writesInFlight.get(conversationId) ?? 0) + 1);
1413
+ };
1414
+ const writeLanded = (conversationId) => {
1415
+ const remaining = (writesInFlight.get(conversationId) ?? 0) - 1;
1416
+ if (remaining > 0) writesInFlight.set(conversationId, remaining);
1417
+ else writesInFlight.delete(conversationId);
1418
+ };
1419
+ const confirmation = (result, conversationId) => {
1420
+ const r = result ?? {};
1421
+ const count = r["conversation_message_count"];
1422
+ return {
1423
+ ...typeof r["message_id"] === "string" ? { messageId: r["message_id"] } : {},
1424
+ serverMessageCount: typeof count === "number" ? count : null,
1425
+ inFlight: writesInFlight.get(conversationId) ?? 0
1426
+ };
1427
+ };
1204
1428
  const convId = (v) => {
1205
1429
  const id = typeof v === "string" ? v : v.conversation_id ?? v.conversationId;
1206
1430
  if (!id) throw new InvalidInputError("conversation_id is required");
@@ -1213,13 +1437,21 @@ function createConversationNamespace(transport, subscribers = new CompactionSubs
1213
1437
  options["customer_id"] ?? options["customerId"],
1214
1438
  "conversation.record_message"
1215
1439
  );
1216
- const body = buildRecordMessageBody(options);
1217
- const result = await transport.request("conversations_messages", { body });
1440
+ const body = withTurnModel(buildRecordMessageBody(options), options);
1441
+ const conversationId = String(body["conversation_id"] ?? "");
1442
+ writeStarted(conversationId);
1443
+ let result;
1444
+ try {
1445
+ result = await transport.request("conversations_messages", { body });
1446
+ } finally {
1447
+ writeLanded(conversationId);
1448
+ }
1218
1449
  onTurn({
1219
- conversationId: String(body["conversation_id"] ?? ""),
1450
+ conversationId,
1220
1451
  role: String(body["role"] ?? "user"),
1221
1452
  content: String(body["content"] ?? ""),
1222
- timestamp: body["timestamp"]
1453
+ timestamp: body["timestamp"],
1454
+ ...confirmation(result, conversationId)
1223
1455
  });
1224
1456
  onWrite({
1225
1457
  userId: body["user_id"],
@@ -1238,16 +1470,18 @@ function createConversationNamespace(transport, subscribers = new CompactionSubs
1238
1470
  "conversation.record_messages_batch"
1239
1471
  );
1240
1472
  }
1241
- const bodies = messages.map(buildRecordMessageBody);
1473
+ const bodies = messages.map((m) => withTurnModel(buildRecordMessageBody(m), m));
1242
1474
  const result = await transport.request("conversations_messages_batch", {
1243
1475
  body: { messages: bodies }
1244
1476
  });
1245
- for (const body of bodies) {
1477
+ for (const [body, row] of storedBatchRows(bodies, result)) {
1478
+ const conversationId = String(body["conversation_id"] ?? "");
1246
1479
  onTurn({
1247
- conversationId: String(body["conversation_id"] ?? ""),
1480
+ conversationId,
1248
1481
  role: String(body["role"] ?? "user"),
1249
1482
  content: String(body["content"] ?? ""),
1250
- timestamp: body["timestamp"]
1483
+ timestamp: body["timestamp"],
1484
+ ...confirmation(row, conversationId)
1251
1485
  });
1252
1486
  }
1253
1487
  for (const body of bodies) {
@@ -1282,13 +1516,24 @@ function createConversationNamespace(transport, subscribers = new CompactionSubs
1282
1516
  },
1283
1517
  get_context_for_prompt: async (options = {}) => {
1284
1518
  const id = convId(options);
1519
+ const search = (options.search_history ?? options.searchHistory ?? true) && localShortTerm.historyExcerpts;
1520
+ const withHistory = async (response2) => {
1521
+ if (!search) return response2;
1522
+ try {
1523
+ return await localShortTerm.historyExcerpts(id, response2, options.query);
1524
+ } catch {
1525
+ return response2;
1526
+ }
1527
+ };
1285
1528
  const local = localShortTerm.prompt(id, options.style ?? "structured");
1286
- if (local !== null) return local;
1529
+ if (local !== null) return withHistory(local);
1287
1530
  const result = await transport.request("context_for_prompt", {
1288
1531
  pathParams: { conversation_id: id },
1289
1532
  query: { style: options.style ?? "structured" }
1290
1533
  });
1291
- return result?.["context_for_prompt"] ?? result ?? {};
1534
+ const response = result?.["context_for_prompt"] ?? result ?? {};
1535
+ localShortTerm.reseed?.(id, response);
1536
+ return withHistory(response);
1292
1537
  },
1293
1538
  async compact(options) {
1294
1539
  return transport.request("conversations_compact", { body: buildCompactBody(options) });
@@ -1471,11 +1716,13 @@ function jsonOrText(value) {
1471
1716
  }
1472
1717
  }
1473
1718
  }
1474
- function createInstanceNamespace(credentials, cache, onTurn) {
1719
+ function createInstanceNamespace(credentials, cache, onTurn, agentModel = () => ({}), onToolResult) {
1475
1720
  let client = null;
1476
1721
  const openSessions = /* @__PURE__ */ new Map();
1477
1722
  let dispatchCompaction = null;
1478
1723
  let applyCompaction = null;
1724
+ let onListen = null;
1725
+ let onConversationState = null;
1479
1726
  const internals = {
1480
1727
  onCompactionUpdate(conversationId, bundle) {
1481
1728
  applyCompaction?.(bundle);
@@ -1484,6 +1731,12 @@ function createInstanceNamespace(credentials, cache, onTurn) {
1484
1731
  setCompactionApplier(apply) {
1485
1732
  applyCompaction = apply;
1486
1733
  },
1734
+ setOnListen(handle) {
1735
+ onListen = handle;
1736
+ },
1737
+ setConversationStateHandler(handle) {
1738
+ onConversationState = handle;
1739
+ },
1487
1740
  sendContextUsed(event) {
1488
1741
  client?.sendContextUsed(event);
1489
1742
  },
@@ -1518,7 +1771,11 @@ function createInstanceNamespace(credentials, cache, onTurn) {
1518
1771
  if (client !== null) {
1519
1772
  throw new ListeningAlreadyActiveError("Listening is already active on this instance");
1520
1773
  }
1521
- const { GrpcStreamClient } = await import("./stream-client-VWKINE47.js");
1774
+ try {
1775
+ onListen?.();
1776
+ } catch {
1777
+ }
1778
+ const { GrpcStreamClient } = await import("./stream-client-3WHXGW4U.js");
1522
1779
  const onContext = options.on_context ?? options.onContext;
1523
1780
  const onDisconnect = options.on_disconnect ?? options.onDisconnect;
1524
1781
  const onReconnect = options.on_reconnect ?? options.onReconnect;
@@ -1532,6 +1789,9 @@ function createInstanceNamespace(credentials, cache, onTurn) {
1532
1789
  onCompactionUpdate: (conversationId, bundle) => {
1533
1790
  internals.onCompactionUpdate(conversationId, bundle);
1534
1791
  },
1792
+ onConversationState: (conversationId, messageCount) => {
1793
+ onConversationState?.(conversationId, messageCount);
1794
+ },
1535
1795
  onStateChange: (state) => {
1536
1796
  if (state !== "connected") openSessions.clear();
1537
1797
  if (state === "reconnecting") {
@@ -1610,7 +1870,7 @@ function createInstanceNamespace(credentials, cache, onTurn) {
1610
1870
  user_id: options.user_id ?? options.userId ?? "",
1611
1871
  customer_id: options.customer_id ?? options.customerId ?? "",
1612
1872
  session_id: options.session_id ?? options.sessionId ?? openSessions.get(conversationId) ?? "",
1613
- metadata: options.metadata ?? {},
1873
+ metadata: withAgentModel(options.metadata, agentModelOf(options), agentModel(), true) ?? {},
1614
1874
  timestamp_ms: Date.now(),
1615
1875
  tool_name: options.tool_name ?? options.toolName ?? "",
1616
1876
  // Python serialises tool_args with json.dumps into tool_args_json.
@@ -1640,6 +1900,10 @@ function createInstanceNamespace(credentials, cache, onTurn) {
1640
1900
  });
1641
1901
  },
1642
1902
  async record_tool_result(options) {
1903
+ const conversationId = options.conversation_id ?? options.conversationId ?? "";
1904
+ if (conversationId) {
1905
+ onToolResult?.({ conversationId, result: options.result, toolName: options.tool_name ?? options.toolName ?? "" });
1906
+ }
1643
1907
  await namespace.send_message({
1644
1908
  content: "",
1645
1909
  role: "tool",
@@ -1690,7 +1954,7 @@ function createInstanceNamespace(credentials, cache, onTurn) {
1690
1954
  }
1691
1955
 
1692
1956
  // src/version.ts
1693
- var SDK_VERSION = "0.5.8";
1957
+ var SDK_VERSION = "0.5.10";
1694
1958
 
1695
1959
  // src/registry.ts
1696
1960
  var instances = /* @__PURE__ */ new Map();
@@ -1731,6 +1995,34 @@ function unregisterIfOwner(key, client) {
1731
1995
  // src/cache/short-term-store.ts
1732
1996
  var DEFAULT_MAX_CONVERSATIONS = 100;
1733
1997
  var DEFAULT_MAX_AGE_MS = 12 * 60 * 60 * 1e3;
1998
+ var CONTEXT_BLOCK_PREFIX = "## Conversation so far (compacted";
1999
+ function contextBlockOf(cc) {
2000
+ const block = cc["context_block"];
2001
+ if (typeof block === "string" && block) return block;
2002
+ const summary = cc["summary"];
2003
+ if (typeof summary === "string" && summary.startsWith(CONTEXT_BLOCK_PREFIX)) return summary;
2004
+ return null;
2005
+ }
2006
+ function localMessageCount(entry) {
2007
+ return entry.compactedMessageCount + entry.recentTurns.length;
2008
+ }
2009
+ function isComplete(entry) {
2010
+ return entry.participant && entry.synced && !entry.shared && entry.serverMessageCount !== null && entry.serverMessageCount <= localMessageCount(entry);
2011
+ }
2012
+ function heldIn(turn, turns) {
2013
+ return turns.some(
2014
+ (held) => turn.message_id && held.message_id ? held.message_id === turn.message_id : held.role === turn.role && held.content === turn.content
2015
+ );
2016
+ }
2017
+ function turnsAfter(localTurns, serverTurns) {
2018
+ const newer = [];
2019
+ for (let i = localTurns.length - 1; i >= 0; i -= 1) {
2020
+ const turn = localTurns[i];
2021
+ if (heldIn(turn, serverTurns)) break;
2022
+ newer.unshift(turn);
2023
+ }
2024
+ return newer;
2025
+ }
1734
2026
  function parseIso(value) {
1735
2027
  if (value === null || value === void 0 || value === "") return null;
1736
2028
  if (value instanceof Date) return value.getTime();
@@ -1750,11 +2042,23 @@ var ShortTermStore = class {
1750
2042
  #maxConversations;
1751
2043
  #maxAgeMs;
1752
2044
  #now;
2045
+ /**
2046
+ * Every turn appended here is also kept in the archive (a TurnStore), which
2047
+ * a compaction does not prune: the turns it covers stay searchable locally
2048
+ * (history-search.ts). Python: `ShortTermContextStore.attach_archive`.
2049
+ */
2050
+ #archive = null;
1753
2051
  constructor(options = {}) {
1754
2052
  this.#maxConversations = options.maxConversations ?? DEFAULT_MAX_CONVERSATIONS;
1755
2053
  this.#maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
1756
2054
  this.#now = options.now ?? (() => Date.now());
1757
2055
  }
2056
+ attachArchive(archive) {
2057
+ this.#archive = archive;
2058
+ }
2059
+ get archive() {
2060
+ return this.#archive;
2061
+ }
1758
2062
  /** The entry, or null. Age-evicts on read, as Python does. */
1759
2063
  get(conversationId) {
1760
2064
  if (!conversationId) return null;
@@ -1780,23 +2084,88 @@ var ShortTermStore = class {
1780
2084
  * exists yet (the first turn, before any compaction has arrived) a fresh one
1781
2085
  * is created with an empty summary; the next `compaction_update` fills the
1782
2086
  * summary fields in and prunes the turns it covers.
2087
+ *
2088
+ * `confirmation` is what the write learned from the server. See
2089
+ * `TurnConfirmation` and `noteServerCount`.
1783
2090
  */
1784
- appendTurn(conversationId, role, content, timestamp) {
2091
+ appendTurn(conversationId, role, content, timestamp, confirmation = {}) {
1785
2092
  if (!conversationId) return;
1786
2093
  const stamp = timestamp ?? new Date(this.#now()).toISOString();
2094
+ if (this.#archive !== null) {
2095
+ try {
2096
+ this.#archive.add(conversationId, role, content, stamp, { messageId: confirmation.messageId ?? null });
2097
+ } catch {
2098
+ }
2099
+ }
1787
2100
  const entry = this.#entries.get(conversationId) ?? this.#blank(conversationId);
1788
- entry.recentTurns.push({ role, content, timestamp: stamp });
2101
+ const messageId = confirmation.messageId;
2102
+ const alreadyHeld = Boolean(messageId) && entry.recentTurns.some((t) => t.message_id === messageId);
2103
+ if (!alreadyHeld) {
2104
+ const turn = { role, content, timestamp: stamp };
2105
+ if (messageId) turn.message_id = messageId;
2106
+ entry.recentTurns.push(turn);
2107
+ }
2108
+ const count = confirmation.serverMessageCount;
2109
+ if (count !== null && count !== void 0) {
2110
+ this.#noteCount(entry, Number(count), confirmation.inFlight ?? 0);
2111
+ }
1789
2112
  entry.lastActivityAt = parseIso(stamp) ?? this.#now();
1790
2113
  this.#touch(conversationId, entry);
1791
2114
  this.#evict();
1792
2115
  }
2116
+ /**
2117
+ * Record the server's message count for a conversation.
2118
+ *
2119
+ * The streamed write path's half of `appendTurn`'s confirmation: a streamed
2120
+ * turn is appended when it is sent, and the count arrives later, once the
2121
+ * server has written it. A count for a conversation this process holds
2122
+ * nothing for is ignored.
2123
+ */
2124
+ noteServerCount(conversationId, count) {
2125
+ if (!conversationId) return;
2126
+ const entry = this.#entries.get(conversationId);
2127
+ if (entry !== void 0) this.#noteCount(entry, Number(count), 0);
2128
+ }
2129
+ /**
2130
+ * Compare the server's count with what the entry holds.
2131
+ *
2132
+ * The count is taken after one of this process's own writes, so it proves
2133
+ * the process takes part in the conversation.
2134
+ *
2135
+ * Server ahead of an entry that was in sync: something else wrote, and the
2136
+ * entry is marked shared for good. Server ahead of an entry that was never
2137
+ * in sync: this process joined late (a restart, an eviction), and `reseed`
2138
+ * will fix it. `inFlight` covers this process's own writes that the server
2139
+ * has counted and the entry has not appended yet, so two concurrent writes
2140
+ * do not look like a second writer.
2141
+ */
2142
+ #noteCount(entry, count, inFlight) {
2143
+ entry.participant = true;
2144
+ if (count > localMessageCount(entry) + inFlight) {
2145
+ if (entry.synced) entry.shared = true;
2146
+ } else if (entry.serverMessageCount === null) {
2147
+ entry.synced = true;
2148
+ }
2149
+ entry.serverMessageCount = Math.max(entry.serverMessageCount ?? 0, count);
2150
+ }
1793
2151
  /**
1794
2152
  * Apply a `compaction_update` bundle.
1795
2153
  *
1796
2154
  * Replaces the summary fields from the bundle's `conversation_context` and
1797
- * prunes locally buffered turns at or before its `end_timestamp`. Turns that
1798
- * arrived DURING the compaction window are kept by design: the server has not
1799
- * seen them yet, which is the whole reason the local tail exists.
2155
+ * drops the buffered turns the new summary covers.
2156
+ *
2157
+ * Which turns those are is decided by count, not by clock. The bundle says
2158
+ * how many messages the summary covers and how many the conversation held
2159
+ * when the bundle was built, so the last `total - compacted` of its
2160
+ * `recent_turns` are exactly the uncovered messages. Turns this process
2161
+ * appended after the server took that count are kept behind them. Timestamps
2162
+ * cannot do this job: the REST write returns the time of the response and
2163
+ * not of the stored row, a caller may supply its own message time, and a
2164
+ * streamed turn is stamped when a background write runs.
2165
+ *
2166
+ * A bundle from a server that sends no counts falls back to the older
2167
+ * timestamp prune, and the entry is left out of sync so it is never used to
2168
+ * answer on its own.
1800
2169
  */
1801
2170
  applyCompaction(bundle) {
1802
2171
  if (typeof bundle !== "object" || bundle === null) return;
@@ -1806,22 +2175,84 @@ var ShortTermStore = class {
1806
2175
  );
1807
2176
  if (!conversationId) return;
1808
2177
  const endTs = parseIso(cc["end_timestamp"] ?? cc["compacted_at"]);
2178
+ const compactedAt = parseIso(cc["compacted_at"]);
2179
+ const compactedCount = Number(cc["compacted_message_count"] ?? 0) || 0;
2180
+ const totalCount = Number(cc["total_message_count"] ?? 0) || 0;
1809
2181
  const entry = this.#entries.get(conversationId) ?? this.#blank(conversationId);
1810
- entry.summary = cc["summary"] || entry.summary;
1811
- entry.factualParagraph = cc["factual_paragraph"] || entry.factualParagraph;
1812
- entry.conversationalParagraph = cc["conversational_paragraph"] || entry.conversationalParagraph;
2182
+ const heldAt = parseIso(entry.compactedAt);
2183
+ if (compactedAt !== null && heldAt !== null && compactedAt < heldAt) return;
2184
+ const block = contextBlockOf(cc);
2185
+ if (block) {
2186
+ entry.contextBlock = block;
2187
+ entry.summary = cc["summary"] || block;
2188
+ entry.factualParagraph = null;
2189
+ entry.conversationalParagraph = null;
2190
+ } else {
2191
+ entry.summary = cc["summary"] || entry.summary;
2192
+ entry.factualParagraph = cc["factual_paragraph"] || entry.factualParagraph;
2193
+ entry.conversationalParagraph = cc["conversational_paragraph"] || entry.conversationalParagraph;
2194
+ }
1813
2195
  entry.currentState = { ...cc["current_state"] ?? {} };
1814
2196
  entry.keyExtractions = { ...cc["key_extractions"] ?? {} };
1815
2197
  entry.compactionId = cc["compaction_id"] || entry.compactionId;
1816
2198
  entry.compactedAt = cc["compacted_at"] || entry.compactedAt;
1817
2199
  entry.endTimestamp = cc["end_timestamp"] || entry.endTimestamp;
1818
- if (endTs !== null && entry.recentTurns.length > 0) {
1819
- entry.recentTurns = entry.recentTurns.filter((t) => isAfter(t.timestamp, endTs));
2200
+ if (compactedCount > 0 && compactedCount <= totalCount) {
2201
+ const uncovered = totalCount - compactedCount;
2202
+ const serverTurns = Array.isArray(cc["recent_turns"]) ? cc["recent_turns"] : [];
2203
+ const tail = uncovered > 0 ? serverTurns.slice(-uncovered).map((t) => ({ ...t })) : [];
2204
+ const ahead = localMessageCount(entry) - totalCount;
2205
+ const newer = entry.synced && ahead > 0 ? entry.recentTurns.slice(-ahead).filter((t) => !heldIn(t, tail)) : [];
2206
+ entry.recentTurns = [...tail, ...newer];
2207
+ entry.compactedMessageCount = compactedCount;
2208
+ entry.serverMessageCount = Math.max(entry.serverMessageCount ?? 0, totalCount);
2209
+ entry.synced = tail.length === uncovered;
2210
+ } else {
2211
+ if (endTs !== null && entry.recentTurns.length > 0) {
2212
+ entry.recentTurns = entry.recentTurns.filter((t) => isAfter(t.timestamp, endTs));
2213
+ }
2214
+ entry.synced = false;
1820
2215
  }
1821
2216
  entry.lastActivityAt = this.#now();
1822
2217
  this.#touch(conversationId, entry);
1823
2218
  this.#evict();
1824
2219
  }
2220
+ /**
2221
+ * Bring an entry back in step from a `/context-for-prompt` response.
2222
+ *
2223
+ * Called after the server was asked because the entry was not complete. The
2224
+ * response lists every message the server's summary does not cover, which is
2225
+ * what `recentTurns` is meant to hold.
2226
+ *
2227
+ * Only an entry that already exists is reseeded. Creating one here would let
2228
+ * a process that never writes to the conversation answer from a snapshot it
2229
+ * has no way to refresh.
2230
+ *
2231
+ * If the server's summary is not the one cached here, the entry cannot
2232
+ * render it, so it stays out of sync until the next bundle brings the
2233
+ * summary itself.
2234
+ *
2235
+ * Turns held locally and absent from the response are kept when they come
2236
+ * after everything the response has: they were written or sent after the
2237
+ * server read its messages.
2238
+ */
2239
+ reseed(conversationId, state) {
2240
+ if (!conversationId) return;
2241
+ const entry = this.#entries.get(conversationId);
2242
+ if (entry === void 0 || entry.shared) return;
2243
+ entry.serverMessageCount = Math.max(
2244
+ entry.serverMessageCount ?? 0,
2245
+ Number(state.totalMessageCount) || 0
2246
+ );
2247
+ if ((state.compactionId || null) !== entry.compactionId) {
2248
+ entry.synced = false;
2249
+ return;
2250
+ }
2251
+ const serverTurns = state.recentMessages.map((m) => ({ ...m }));
2252
+ entry.recentTurns = [...serverTurns, ...turnsAfter(entry.recentTurns, serverTurns)];
2253
+ entry.compactedMessageCount = Number(state.compactedMessageCount) || 0;
2254
+ entry.synced = true;
2255
+ }
1825
2256
  invalidate(conversationId) {
1826
2257
  this.#entries.delete(conversationId);
1827
2258
  }
@@ -1832,6 +2263,7 @@ var ShortTermStore = class {
1832
2263
  return {
1833
2264
  conversationId,
1834
2265
  summary: null,
2266
+ contextBlock: null,
1835
2267
  factualParagraph: null,
1836
2268
  conversationalParagraph: null,
1837
2269
  currentState: {},
@@ -1840,7 +2272,12 @@ var ShortTermStore = class {
1840
2272
  compactedAt: null,
1841
2273
  endTimestamp: null,
1842
2274
  recentTurns: [],
1843
- lastActivityAt: this.#now()
2275
+ lastActivityAt: this.#now(),
2276
+ compactedMessageCount: 0,
2277
+ serverMessageCount: null,
2278
+ participant: false,
2279
+ synced: false,
2280
+ shared: false
1844
2281
  };
1845
2282
  }
1846
2283
  /** Re-insert so this key becomes the most recently used. */
@@ -1857,6 +2294,520 @@ var ShortTermStore = class {
1857
2294
  }
1858
2295
  };
1859
2296
 
2297
+ // src/cache/turn-store.ts
2298
+ var SWEEP_EVERY_WRITES = 50;
2299
+ function bytesOf(text) {
2300
+ return new TextEncoder().encode(text).length;
2301
+ }
2302
+ var TurnStore = class {
2303
+ limits;
2304
+ onDisk = false;
2305
+ #now;
2306
+ #seq = 0;
2307
+ #writes = 0;
2308
+ #conversations = /* @__PURE__ */ new Map();
2309
+ #vectors = /* @__PURE__ */ new Map();
2310
+ constructor(options = {}) {
2311
+ this.limits = options.limits ?? { ...HISTORY_SEARCH.store_limits };
2312
+ this.#now = options.now ?? (() => Date.now() / 1e3);
2313
+ }
2314
+ add(conversationId, role, content, timestamp, options = {}) {
2315
+ this.addMany(conversationId, [
2316
+ { role, content, timestamp: timestamp ?? "", message_id: options.messageId ?? null, position: options.position ?? null }
2317
+ ]);
2318
+ }
2319
+ /**
2320
+ * Insert turns not held yet; returns how many were new. A turn held without
2321
+ * an id or position (a streamed write) gets them when the same role and
2322
+ * text come back from the server.
2323
+ */
2324
+ addMany(conversationId, turns) {
2325
+ if (!conversationId) return 0;
2326
+ const conv = this.#conversations.get(conversationId) ?? { lastActivity: 0, turns: [] };
2327
+ let added = 0;
2328
+ for (const turn of turns) {
2329
+ const role = String(turn.role || "user");
2330
+ const content = String(turn.content ?? "");
2331
+ if (!content) continue;
2332
+ const messageId = turn.message_id ? String(turn.message_id) : null;
2333
+ const raw = turn.position;
2334
+ const position = typeof raw === "number" && Number.isInteger(raw) ? raw : /^\d+$/.test(String(raw ?? "")) ? Number(raw) : null;
2335
+ const ts = turn.timestamp instanceof Date ? turn.timestamp.toISOString() : String(turn.timestamp ?? "");
2336
+ if (messageId !== null) {
2337
+ const held = conv.turns.find((t) => t.message_id === messageId);
2338
+ if (held !== void 0) {
2339
+ if (position !== null) held.position = position;
2340
+ continue;
2341
+ }
2342
+ }
2343
+ const same = conv.turns.find(
2344
+ (t) => t.role === role && t.content === content && (t.message_id === null || t.position === null)
2345
+ );
2346
+ if (same !== void 0) {
2347
+ same.message_id = same.message_id ?? messageId;
2348
+ same.position = position ?? same.position;
2349
+ continue;
2350
+ }
2351
+ conv.turns.push({ seq: ++this.#seq, role, content, timestamp: ts, message_id: messageId, position });
2352
+ added += 1;
2353
+ }
2354
+ conv.lastActivity = this.#now();
2355
+ this.#conversations.set(conversationId, conv);
2356
+ this.#trim(conv);
2357
+ this.#writes += 1;
2358
+ if (this.#writes % SWEEP_EVERY_WRITES === 0) this.sweep();
2359
+ return added;
2360
+ }
2361
+ #trim(conv) {
2362
+ const limits = this.limits;
2363
+ let size = conv.turns.reduce((n2, t) => n2 + bytesOf(t.content), 0);
2364
+ while (conv.turns.length > limits.max_turns_per_conversation || size > limits.max_bytes_per_conversation) {
2365
+ const excess = Math.max(conv.turns.length - limits.max_turns_per_conversation, 1);
2366
+ const order = [...conv.turns].sort(
2367
+ (a, b) => (a.position ?? Number.MAX_SAFE_INTEGER) - (b.position ?? Number.MAX_SAFE_INTEGER) || a.seq - b.seq
2368
+ );
2369
+ const gone = new Set(order.slice(0, excess).map((t) => t.seq));
2370
+ if (gone.size === 0) break;
2371
+ for (const t of conv.turns) if (gone.has(t.seq)) size -= bytesOf(t.content);
2372
+ conv.turns = conv.turns.filter((t) => !gone.has(t.seq));
2373
+ for (const vectors of this.#vectors.values()) for (const seq of gone) vectors.delete(seq);
2374
+ }
2375
+ }
2376
+ /** Idle conversations, then the least recently used beyond the count, then more until the bytes fit. */
2377
+ sweep() {
2378
+ const limits = this.limits;
2379
+ const now = this.#now();
2380
+ for (const [id, conv] of this.#conversations) {
2381
+ if (conv.lastActivity < now - limits.idle_ttl_seconds) this.drop(id);
2382
+ }
2383
+ const byRecent = [...this.#conversations.entries()].sort((a, b) => b[1].lastActivity - a[1].lastActivity);
2384
+ for (const [id] of byRecent.slice(limits.max_conversations)) this.drop(id);
2385
+ while (this.sizeBytes() > limits.max_file_bytes) {
2386
+ const oldest = this.conversations()[0];
2387
+ if (oldest === void 0) break;
2388
+ this.drop(oldest);
2389
+ }
2390
+ }
2391
+ drop(conversationId) {
2392
+ const conv = this.#conversations.get(conversationId);
2393
+ if (conv === void 0) return;
2394
+ for (const vectors of this.#vectors.values()) for (const t of conv.turns) vectors.delete(t.seq);
2395
+ this.#conversations.delete(conversationId);
2396
+ }
2397
+ clear() {
2398
+ this.#conversations.clear();
2399
+ this.#vectors.clear();
2400
+ }
2401
+ close() {
2402
+ }
2403
+ /** In conversation order: by server position where known, else as written here. */
2404
+ turns(conversationId) {
2405
+ const conv = this.#conversations.get(conversationId);
2406
+ if (conv === void 0) return [];
2407
+ const bySeq = [...conv.turns].sort((a, b) => a.seq - b.seq);
2408
+ const key = /* @__PURE__ */ new Map();
2409
+ let top = 0;
2410
+ let step = 0;
2411
+ for (const t of bySeq) {
2412
+ if (t.position !== null) {
2413
+ key.set(t.seq, t.position);
2414
+ top = Math.max(top, t.position);
2415
+ step = 0;
2416
+ } else {
2417
+ step += 1;
2418
+ key.set(t.seq, top + step * 1e-6);
2419
+ }
2420
+ }
2421
+ return bySeq.sort((a, b) => (key.get(a.seq) ?? 0) - (key.get(b.seq) ?? 0) || a.seq - b.seq).map((t) => ({ ...t }));
2422
+ }
2423
+ /** Turns the server also holds: archived tool results are local only. Python: `count`. */
2424
+ count(conversationId) {
2425
+ return (this.#conversations.get(conversationId)?.turns ?? []).filter((t) => t.role !== "tool").length;
2426
+ }
2427
+ sizeBytes() {
2428
+ let n2 = 0;
2429
+ for (const conv of this.#conversations.values()) for (const t of conv.turns) n2 += bytesOf(t.content);
2430
+ return n2;
2431
+ }
2432
+ /** Conversation ids, least recently active first. */
2433
+ conversations() {
2434
+ return [...this.#conversations.entries()].sort((a, b) => a[1].lastActivity - b[1].lastActivity).map(([id]) => id);
2435
+ }
2436
+ vectors(seqs, model) {
2437
+ const held = this.#vectors.get(model);
2438
+ const out = /* @__PURE__ */ new Map();
2439
+ if (held === void 0) return out;
2440
+ for (const seq of seqs) {
2441
+ const v = held.get(seq);
2442
+ if (v !== void 0) out.set(seq, v);
2443
+ }
2444
+ return out;
2445
+ }
2446
+ putVectors(model, items) {
2447
+ const held = this.#vectors.get(model) ?? /* @__PURE__ */ new Map();
2448
+ for (const [seq, vec] of items) held.set(seq, vec);
2449
+ this.#vectors.set(model, held);
2450
+ }
2451
+ };
2452
+
2453
+ // src/cache/history-search.ts
2454
+ var WORD_RE = /[\p{L}\p{N}\p{M}]+/gu;
2455
+ var FORMS = HISTORY_SEARCH.irregular_forms ?? {};
2456
+ function tokenize2(text) {
2457
+ const out = [];
2458
+ for (const word of (text || "").toLowerCase().match(WORD_RE) ?? []) {
2459
+ if (/^[\x00-\x7f]+$/.test(word)) out.push(...tokenize(FORMS[word] ?? word));
2460
+ else out.push(word);
2461
+ }
2462
+ return out;
2463
+ }
2464
+ var LITERAL_SOURCE = String.raw`"([^"]{3,})"|\b([A-Z0-9][A-Z0-9-]{3,})\b|(\d[\d,.:/-]*\d|\d)`;
2465
+ var SENTENCE_RE = /(?<=[.!?])\s+/;
2466
+ function literalsOf(query) {
2467
+ const out = [];
2468
+ for (const m of (query || "").matchAll(new RegExp(LITERAL_SOURCE, "g"))) {
2469
+ const quoted = m[1];
2470
+ const value = quoted || m[2] || m[3];
2471
+ if (value && (quoted || /\d/.test(value))) out.push(value.toLowerCase());
2472
+ }
2473
+ return out;
2474
+ }
2475
+ function cosine(a, b) {
2476
+ let dot = 0;
2477
+ let na = 0;
2478
+ let nb = 0;
2479
+ const n2 = Math.min(a.length, b.length);
2480
+ for (let i = 0; i < n2; i++) {
2481
+ dot += a[i] * b[i];
2482
+ na += a[i] * a[i];
2483
+ nb += b[i] * b[i];
2484
+ }
2485
+ return na && nb ? dot / (Math.sqrt(na) * Math.sqrt(nb)) : 0;
2486
+ }
2487
+ function words(text) {
2488
+ return text.split(/\s+/).filter(Boolean);
2489
+ }
2490
+ function excerptOf(content, terms, literals, maxWords) {
2491
+ let sentences = content.trim().split(SENTENCE_RE).filter((s2) => s2.trim());
2492
+ if (sentences.length === 0) sentences = [content.trim()];
2493
+ const score = (sentence) => {
2494
+ const low = sentence.toLowerCase();
2495
+ const held = new Set(tokenize2(sentence));
2496
+ let n2 = 0;
2497
+ for (const t of terms) if (held.has(t)) n2 += 1;
2498
+ for (const lit of literals) if (low.includes(lit)) n2 += 2;
2499
+ return n2;
2500
+ };
2501
+ let best = 0;
2502
+ for (let i = 1; i < sentences.length; i++) if (score(sentences[i]) > score(sentences[best])) best = i;
2503
+ let lo = best;
2504
+ let hi = best;
2505
+ let count = words(sentences[best]).length;
2506
+ for (; ; ) {
2507
+ let grew = false;
2508
+ for (const j of [hi + 1, lo - 1]) {
2509
+ if (j >= 0 && j < sentences.length && count + words(sentences[j]).length <= maxWords) {
2510
+ count += words(sentences[j]).length;
2511
+ lo = Math.min(lo, j);
2512
+ hi = Math.max(hi, j);
2513
+ grew = true;
2514
+ }
2515
+ }
2516
+ if (!grew) break;
2517
+ }
2518
+ let text = sentences.slice(lo, hi + 1).join(" ");
2519
+ if (words(text).length > maxWords) text = words(text).slice(0, maxWords).join(" ") + " ...";
2520
+ return (lo > 0 ? "... " : "") + text + (hi < sentences.length - 1 && !text.endsWith("...") ? " ..." : "");
2521
+ }
2522
+ var FENCE = /^\s*(?:```|~~~)/;
2523
+ var TABLE_RULE = /^\s*\|?\s*:?-{2,}/;
2524
+ var SENTENCE = /(?<=[.!?])\s+/;
2525
+ function splitPassages(content, maxWords) {
2526
+ const text = (content ?? "").trim();
2527
+ if (words(text).length <= maxWords) return text ? [text] : [];
2528
+ const out = [];
2529
+ const lines = text.split("\n");
2530
+ let i = 0;
2531
+ while (i < lines.length) {
2532
+ const line = lines[i];
2533
+ if (FENCE.test(line)) {
2534
+ let j2 = i + 1;
2535
+ while (j2 < lines.length && !FENCE.test(lines[j2])) j2 += 1;
2536
+ out.push(...windowsOf(lines.slice(i + 1, j2), maxWords));
2537
+ i = j2 + 1;
2538
+ continue;
2539
+ }
2540
+ if (line.trim().startsWith("|")) {
2541
+ let j2 = i;
2542
+ while (j2 < lines.length && lines[j2].trim().startsWith("|")) j2 += 1;
2543
+ const rows = lines.slice(i, j2).filter((r) => !TABLE_RULE.test(r)).map((r) => r.trim());
2544
+ const [header, body] = rows.length > 1 ? [rows[0], rows.slice(1)] : ["", rows];
2545
+ for (const row of body) {
2546
+ const both = `${header} ${row}`.trim();
2547
+ out.push(words(both).length <= Math.trunc(maxWords * 3 / 2) ? both : row);
2548
+ }
2549
+ i = j2;
2550
+ continue;
2551
+ }
2552
+ let j = i;
2553
+ while (j < lines.length && lines[j].trim() && !FENCE.test(lines[j]) && !lines[j].trim().startsWith("|")) j += 1;
2554
+ if (j === i) {
2555
+ i += 1;
2556
+ continue;
2557
+ }
2558
+ const block = lines.slice(i, j);
2559
+ if (block.length > 1 && block.every((b) => words(b).length <= 25)) {
2560
+ out.push(...windowsOf(block, maxWords));
2561
+ } else {
2562
+ const sentences = block.map((b) => b.trim()).join(" ").split(SENTENCE).filter((x) => x.trim());
2563
+ out.push(...windowsOf(sentences, maxWords));
2564
+ }
2565
+ i = j;
2566
+ }
2567
+ const kept = out.filter((x) => x.trim());
2568
+ return kept.length ? kept : [text];
2569
+ }
2570
+ function windowsOf(pieces, maxWords) {
2571
+ const out = [];
2572
+ let cur = [];
2573
+ let used = 0;
2574
+ for (const raw of pieces) {
2575
+ const piece = raw.trim();
2576
+ if (!piece) continue;
2577
+ const n2 = words(piece).length;
2578
+ if (cur.length && used + n2 > maxWords) {
2579
+ out.push(cur.join(" "));
2580
+ cur = [];
2581
+ used = 0;
2582
+ }
2583
+ if (n2 > maxWords) {
2584
+ const w = words(piece);
2585
+ for (let k = 0; k < w.length; k += maxWords) out.push(w.slice(k, k + maxWords).join(" "));
2586
+ continue;
2587
+ }
2588
+ cur.push(piece);
2589
+ used += n2;
2590
+ }
2591
+ if (cur.length) out.push(cur.join(" "));
2592
+ return out;
2593
+ }
2594
+ function askedKinds(query) {
2595
+ const out = [];
2596
+ for (const [name, spec] of Object.entries(HISTORY_SEARCH.asked_kinds ?? {})) {
2597
+ if (name.startsWith("$") || typeof spec !== "object") continue;
2598
+ if (new RegExp(spec.query, "i").test(query ?? "")) out.push(new RegExp(spec.value));
2599
+ }
2600
+ return out;
2601
+ }
2602
+ function asksHowItStarted(query) {
2603
+ const spec = HISTORY_SEARCH.early_query;
2604
+ if (!spec?.pattern) return false;
2605
+ return new RegExp(spec.pattern, (spec.flags ?? "").includes("i") ? "i" : "").test(query ?? "");
2606
+ }
2607
+ async function searchTurns(turns, query, options = {}) {
2608
+ const params = HISTORY_SEARCH;
2609
+ const k = params.max_excerpts;
2610
+ const skip = new Set(options.exclude ?? []);
2611
+ const pool = turns.filter((t) => t.content && !skip.has(t.content) && !skip.has(t.message_id ?? ""));
2612
+ const terms = new Set(tokenize2(query || ""));
2613
+ const literals = literalsOf(query);
2614
+ if (pool.length === 0 || terms.size === 0 && literals.length === 0 && !options.encoder) return [];
2615
+ const maxWords = params.max_words_per_excerpt;
2616
+ const units = [];
2617
+ pool.forEach((t, ti) => {
2618
+ if (!params.passages) {
2619
+ units.push({ seq: t.seq, content: t.content, turn: ti, first: true, last: true, part: -1 });
2620
+ return;
2621
+ }
2622
+ const parts = splitPassages(t.content, maxWords);
2623
+ parts.forEach((part, pi) => {
2624
+ units.push({
2625
+ seq: t.seq,
2626
+ content: part,
2627
+ turn: ti,
2628
+ first: pi === 0,
2629
+ last: pi === parts.length - 1,
2630
+ part: parts.length > 1 ? pi : -1
2631
+ });
2632
+ });
2633
+ });
2634
+ const docs = units.map((u) => tokenize2(u.content));
2635
+ const bm = new BM25(docs);
2636
+ const termList = [...terms];
2637
+ const rows = units.map((turn, i) => {
2638
+ const held = new Set(docs[i]);
2639
+ let matched = 0;
2640
+ for (const t of terms) if (held.has(t)) matched += 1;
2641
+ const low = turn.content.toLowerCase();
2642
+ let literal = 0;
2643
+ for (const lit of literals) if (low.includes(lit)) literal += 1;
2644
+ return { i, bm25: matched ? bm.score(termList, i) : 0, matched, literal };
2645
+ });
2646
+ const need = Math.min(params.min_matched_terms, Math.max(1, terms.size));
2647
+ const rareCap = Math.max(1, Math.trunc((params.rare_term_max_share ?? 0) * units.length));
2648
+ const rare = /* @__PURE__ */ new Set();
2649
+ for (const t of terms) {
2650
+ let n3 = 0;
2651
+ for (const doc of docs) if (doc.includes(t)) n3 += 1;
2652
+ if (n3 > 0 && n3 <= rareCap) rare.add(t);
2653
+ }
2654
+ let eligible;
2655
+ if (params.passages) {
2656
+ const turnTerms = /* @__PURE__ */ new Map();
2657
+ for (const r of rows) {
2658
+ const held = turnTerms.get(units[r.i].turn) ?? /* @__PURE__ */ new Set();
2659
+ for (const t of docs[r.i]) if (terms.has(t)) held.add(t);
2660
+ turnTerms.set(units[r.i].turn, held);
2661
+ }
2662
+ eligible = rows.filter(
2663
+ (r) => r.literal > 0 || r.matched > 0 && (turnTerms.get(units[r.i].turn).size >= need || docs[r.i].some((t) => rare.has(t)))
2664
+ );
2665
+ } else {
2666
+ eligible = rows.filter(
2667
+ (r) => r.literal > 0 || r.matched >= need || r.matched > 0 && docs[r.i].some((t) => rare.has(t))
2668
+ );
2669
+ }
2670
+ const rrf = params.rrf_k;
2671
+ const fused = /* @__PURE__ */ new Map();
2672
+ const via = /* @__PURE__ */ new Map();
2673
+ for (const r of eligible) fused.set(r.i, 0);
2674
+ const add = (i, rank, how) => {
2675
+ fused.set(i, (fused.get(i) ?? 0) + 1 / (rrf + rank + 1));
2676
+ if (!via.has(i)) via.set(i, how);
2677
+ };
2678
+ eligible.filter((r) => r.literal > 0).sort((a, b) => b.literal - a.literal || a.i - b.i).forEach((r, rank) => add(r.i, rank, "literal"));
2679
+ eligible.filter((r) => r.bm25 > 0).sort((a, b) => b.bm25 - a.bm25 || a.i - b.i).forEach((r, rank) => add(r.i, rank, "bm25"));
2680
+ const kinds = askedKinds(query);
2681
+ if (kinds.length) {
2682
+ rows.filter((r) => r.matched > 0 && kinds.some((k2) => k2.test(units[r.i].content))).sort((a, b) => b.bm25 - a.bm25 || a.i - b.i).forEach((r, rank) => add(r.i, rank, "kind"));
2683
+ }
2684
+ if (options.encoder) {
2685
+ const ranked2 = await embeddingRanks(units, query, options.encoder, options.store ?? null, params.min_cosine ?? 0.4);
2686
+ ranked2.forEach(([i], rank) => add(i, rank, "embedding"));
2687
+ }
2688
+ if (fused.size === 0) return [];
2689
+ const n2 = Math.max(pool.length - 1, 1);
2690
+ const weight = asksHowItStarted(query) ? 0 : params.recency_weight;
2691
+ const top = /* @__PURE__ */ new Map();
2692
+ const ranked = /* @__PURE__ */ new Map();
2693
+ for (const [u, v] of fused) {
2694
+ const ti = units[u].turn;
2695
+ const score = v + weight * (ti / n2) / (rrf + 1);
2696
+ ranked.set(ti, [...ranked.get(ti) ?? [], [score, u]]);
2697
+ const held = top.get(ti);
2698
+ if (!held || score > held[0]) top.set(ti, [score, u]);
2699
+ }
2700
+ const best = [...top.keys()].sort((a, b) => top.get(b)[0] - top.get(a)[0] || a - b).slice(0, k);
2701
+ const perTurn = params.passages_per_turn ?? 1;
2702
+ const shown = (ti) => {
2703
+ const unit = units[top.get(ti)[1]];
2704
+ if (!params.passages || unit.first && unit.last) return excerptOf(pool[ti].content, terms, literals, maxWords);
2705
+ const picked = [...ranked.get(ti)].sort((a, b) => b[0] - a[0] || b[1] - a[1]).slice(0, perTurn).map(([, u]) => u).sort((a, b) => a - b);
2706
+ const parts = [];
2707
+ for (const u of picked) parts.push((units[u].first || parts.length ? "" : "... ") + units[u].content);
2708
+ return parts.join(" ... ") + (units[picked[picked.length - 1]].last ? "" : " ...");
2709
+ };
2710
+ const found = /* @__PURE__ */ new Map();
2711
+ for (const ti of best) {
2712
+ found.set(ti, {
2713
+ position: pool[ti].position,
2714
+ role: pool[ti].role,
2715
+ timestamp: pool[ti].timestamp,
2716
+ score: top.get(ti)[0],
2717
+ via: via.get(top.get(ti)[1]) ?? "",
2718
+ text: shown(ti)
2719
+ });
2720
+ }
2721
+ const before2 = params.previous_turns ?? 0;
2722
+ const tail = params.previous_turn_words ?? 40;
2723
+ for (const i of best) {
2724
+ for (let j = i - before2; j < i; j++) {
2725
+ if (j < 0 || found.has(j)) continue;
2726
+ const hit = pool[i].position;
2727
+ const prev = pool[j].position;
2728
+ if (hit && prev && hit - prev > before2) continue;
2729
+ const w = words(pool[j].content);
2730
+ found.set(j, {
2731
+ position: prev,
2732
+ role: pool[j].role,
2733
+ timestamp: pool[j].timestamp,
2734
+ score: 0,
2735
+ via: "previous",
2736
+ text: w.length <= tail ? w.join(" ") : "... " + w.slice(-tail).join(" ")
2737
+ });
2738
+ }
2739
+ }
2740
+ return [...found.keys()].sort((a, b) => a - b).map((i) => found.get(i));
2741
+ }
2742
+ async function embeddingRanks(pool, query, encoder, store, minCosine) {
2743
+ const keyOf = (u) => (u.part ?? -1) < 0 ? encoder.model : `${encoder.model}#p${u.part}`;
2744
+ const vectors = /* @__PURE__ */ new Map();
2745
+ if (store !== null) {
2746
+ const byKey = /* @__PURE__ */ new Map();
2747
+ pool.forEach((u, i) => byKey.set(keyOf(u), [...byKey.get(keyOf(u)) ?? [], i]));
2748
+ for (const [key, idx] of byKey) {
2749
+ const held = store.vectors(idx.map((i) => pool[i].seq), key);
2750
+ for (const i of idx) {
2751
+ const v = held.get(pool[i].seq);
2752
+ if (v) vectors.set(i, v);
2753
+ }
2754
+ }
2755
+ }
2756
+ const missing = pool.map((_, i) => i).filter((i) => !vectors.has(i));
2757
+ if (missing.length > 0) {
2758
+ const fresh = await encoder.encode(missing.map((i) => pool[i].content));
2759
+ const put = /* @__PURE__ */ new Map();
2760
+ missing.forEach((i, j) => {
2761
+ const v = Float32Array.from(fresh[j] ?? []);
2762
+ vectors.set(i, v);
2763
+ const key = keyOf(pool[i]);
2764
+ put.set(key, (put.get(key) ?? /* @__PURE__ */ new Map()).set(pool[i].seq, v));
2765
+ });
2766
+ if (store) for (const [key, items] of put) store.putVectors(key, items);
2767
+ }
2768
+ const q = (await (encoder.encodeQuery ?? encoder.encode).call(encoder, [query]))[0] ?? [];
2769
+ return pool.map((_, i) => [i, cosine(q, vectors.get(i) ?? [])]).filter(([, c]) => c >= minCosine).sort((a, b) => b[1] - a[1]);
2770
+ }
2771
+ function toolResultText(result, toolName) {
2772
+ let text = typeof result === "string" ? result : JSON.stringify(result) ?? String(result);
2773
+ const limit = HISTORY_SEARCH.tool_result_max_chars ?? 16e3;
2774
+ if (text.length > limit) text = text.slice(0, limit) + " ...[cut]";
2775
+ return `[tool result: ${toolName || "tool"}] ${text}`;
2776
+ }
2777
+ function renderExcerpts(excerpts, latest = "") {
2778
+ if (excerpts.length === 0) return "";
2779
+ const lines = [HISTORY_SEARCH.heading];
2780
+ for (const e of excerpts) {
2781
+ let where = e.position ? `turn ${e.position}, ${e.role}` : `${e.role}, earlier`;
2782
+ if (e.timestamp) {
2783
+ const ago = before(e.timestamp, latest);
2784
+ where += `, ${e.timestamp}` + (ago ? ` (${ago})` : "");
2785
+ }
2786
+ lines.push(`- ${where}: "${e.text}"`);
2787
+ }
2788
+ return lines.join("\n");
2789
+ }
2790
+ var ZONED = /(?:Z|[+-]\d{2}:\d{2})$/;
2791
+ function instant(stamp) {
2792
+ const text = (stamp ?? "").trim();
2793
+ if (!ZONED.test(text)) return null;
2794
+ const ms = Date.parse(text);
2795
+ return Number.isNaN(ms) ? null : ms / 1e3;
2796
+ }
2797
+ function before(stamp, latest) {
2798
+ const at = instant(stamp);
2799
+ const now = instant(latest);
2800
+ if (at === null || now === null || now < at) return "";
2801
+ const seconds = Math.trunc(now - at);
2802
+ for (const [unit, size] of [["day", 86400], ["hour", 3600], ["minute", 60]]) {
2803
+ if (seconds >= size) {
2804
+ const n2 = Math.trunc(seconds / size);
2805
+ return `${n2} ${unit}${n2 !== 1 ? "s" : ""} before the latest message`;
2806
+ }
2807
+ }
2808
+ return "less than a minute before the latest message";
2809
+ }
2810
+
1860
2811
  // src/context/overlay.ts
1861
2812
  var MAX_TURNS = 20;
1862
2813
  var TOKEN_BUDGET = 2e3;
@@ -1885,7 +2836,23 @@ function overlayLocalRecentTurns(response, store, conversationId, configured) {
1885
2836
  if (entry === null) return;
1886
2837
  const existing = response.conversation_context ?? null;
1887
2838
  if (existing !== null && !verbatimOverlayEnabled(configured)) return;
1888
- const localTurns = budgetRecentTurns(entry.recentTurns);
2839
+ const serverCompaction = existing === null ? null : existing["compaction_id"];
2840
+ const localIsWhole = isComplete(entry) && (!serverCompaction || serverCompaction === entry.compactionId);
2841
+ let sourceTurns = entry.recentTurns;
2842
+ if (existing !== null && !localIsWhole) {
2843
+ const raw = Array.isArray(existing["recent_turns"]) ? existing["recent_turns"] : [];
2844
+ const serverTurns = raw.map((t) => ({
2845
+ role: String(t["role"] ?? "user"),
2846
+ content: String(t["content"] ?? ""),
2847
+ timestamp: String(t["timestamp"] ?? "")
2848
+ }));
2849
+ sourceTurns = [...serverTurns, ...turnsAfter(entry.recentTurns, serverTurns)];
2850
+ }
2851
+ const localTurns = budgetRecentTurns(sourceTurns).map((t) => ({
2852
+ role: t.role,
2853
+ content: t.content,
2854
+ timestamp: t.timestamp
2855
+ }));
1889
2856
  const serverField = (name) => existing === null ? void 0 : existing[name];
1890
2857
  const orLocal = (name, local) => {
1891
2858
  const value = serverField(name);
@@ -1940,14 +2907,28 @@ function buildRecentMessages(turns) {
1940
2907
  role: t.role || "user",
1941
2908
  content: t.content ?? "",
1942
2909
  timestamp: t.timestamp,
1943
- message_id: `local-${idx}`
2910
+ message_id: t.message_id || `local-${idx}`
1944
2911
  }));
1945
2912
  }
1946
2913
  function compactedMessageCount(_entry) {
1947
2914
  return 0;
1948
2915
  }
2916
+ function blockOf(entry) {
2917
+ if (entry.contextBlock) return entry.contextBlock.trim();
2918
+ if (entry.summary && entry.summary.startsWith(CONTEXT_BLOCK_PREFIX)) return entry.summary.trim();
2919
+ return null;
2920
+ }
1949
2921
  function formatStructured(entry, recent) {
1950
2922
  const parts = [];
2923
+ const block = blockOf(entry);
2924
+ if (block) {
2925
+ parts.push(block);
2926
+ if (recent.length > 0) {
2927
+ parts.push(`## Recent Turns (${recent.length})`);
2928
+ for (const m of recent) parts.push(`**${m.role}**: ${m.content}`);
2929
+ }
2930
+ return parts.filter(Boolean).join("\n\n").trim();
2931
+ }
1951
2932
  if (entry.summary) {
1952
2933
  parts.push("## Summary");
1953
2934
  parts.push(entry.summary.trim());
@@ -1982,9 +2963,11 @@ function formatStructured(entry, recent) {
1982
2963
  }
1983
2964
  function formatNarrative(entry, recent) {
1984
2965
  const parts = [];
1985
- if (entry.summary) parts.push(entry.summary.trim());
1986
- if (entry.conversationalParagraph) parts.push(entry.conversationalParagraph.trim());
1987
- else if (entry.factualParagraph) parts.push(entry.factualParagraph.trim());
2966
+ const block = blockOf(entry);
2967
+ if (block) parts.push(block);
2968
+ else if (entry.summary) parts.push(entry.summary.trim());
2969
+ if (!block && entry.conversationalParagraph) parts.push(entry.conversationalParagraph.trim());
2970
+ else if (!block && entry.factualParagraph) parts.push(entry.factualParagraph.trim());
1988
2971
  if (recent.length > 0) {
1989
2972
  const tail = recent.map((m) => `${m.role}: ${m.content}`).join("\n");
1990
2973
  parts.push(`Recent exchanges:
@@ -1994,6 +2977,10 @@ ${tail}`);
1994
2977
  }
1995
2978
  function formatBullets(entry, recent) {
1996
2979
  const parts = [];
2980
+ const block = blockOf(entry);
2981
+ if (block) {
2982
+ return [block, ...recent.map((m) => `- ${m.role}: ${m.content}`)].join("\n").trim();
2983
+ }
1997
2984
  if (entry.summary) parts.push(`- Summary: ${entry.summary.trim()}`);
1998
2985
  for (const [k, v] of Object.entries(entry.currentState ?? {})) {
1999
2986
  parts.push(`- ${k}: ${stringify(v)}`);
@@ -2031,7 +3018,9 @@ function renderForPrompt(entry, style = DEFAULT_STYLE, now = Date.now) {
2031
3018
  recent_messages: recent,
2032
3019
  recent_message_count: recent.length,
2033
3020
  compacted_message_count: compacted,
2034
- total_message_count: recent.length + compacted
3021
+ total_message_count: recent.length + compacted,
3022
+ context_block: blockOf(entry),
3023
+ compaction_id: entry.compactionId
2035
3024
  };
2036
3025
  }
2037
3026
  function renderCompacted(entry, format = "structured", correlationId = "") {
@@ -2176,10 +3165,10 @@ function servedItemIds(hit) {
2176
3165
  }
2177
3166
  return ids;
2178
3167
  }
2179
- function tryServeFromCache(cache, scope, params, maxItems) {
3168
+ async function tryServeFromCache(cache, scope, params, maxItems) {
2180
3169
  try {
2181
3170
  const scoped = scopeLookupParams(scope, params);
2182
- const hit = cache.lookup({
3171
+ const hit = await cache.lookupAsync({
2183
3172
  searchQuery: scoped.searchQuery ?? null,
2184
3173
  entityId: scoped.entityId ?? null,
2185
3174
  customerId: scoped.customerId ?? null,
@@ -2267,6 +3256,8 @@ function mergeScopeResults(scopeResults) {
2267
3256
  episodes: [],
2268
3257
  emotions: [],
2269
3258
  temporal_events: [],
3259
+ items: [],
3260
+ excerpts: [],
2270
3261
  scope_map: {},
2271
3262
  conversation_context: null,
2272
3263
  profile: null,
@@ -2276,6 +3267,8 @@ function mergeScopeResults(scopeResults) {
2276
3267
  total_items: 0,
2277
3268
  metadata: null
2278
3269
  };
3270
+ const rankedSeen = /* @__PURE__ */ new Set();
3271
+ const rankedItems = [];
2279
3272
  for (const [scopeName, response] of scopeResults) {
2280
3273
  out.scopes_queried.push(scopeName);
2281
3274
  if (out.metadata === null && response.metadata !== void 0) {
@@ -2300,8 +3293,32 @@ function mergeScopeResults(scopeResults) {
2300
3293
  out[collection].push(item);
2301
3294
  }
2302
3295
  }
3296
+ const excerpts = response["excerpts"];
3297
+ if (Array.isArray(excerpts)) {
3298
+ for (const item of excerpts) {
3299
+ const id = itemId2(item);
3300
+ if (id !== void 0) {
3301
+ if (seen.has(id)) continue;
3302
+ seen.add(id);
3303
+ out.scope_map[id] = scopeName;
3304
+ }
3305
+ out.excerpts.push(item);
3306
+ }
3307
+ }
3308
+ const ranked = response["items"];
3309
+ if (Array.isArray(ranked)) {
3310
+ for (const item of ranked) {
3311
+ const id = itemId2(item);
3312
+ if (id !== void 0) {
3313
+ if (rankedSeen.has(id)) continue;
3314
+ rankedSeen.add(id);
3315
+ }
3316
+ rankedItems.push(item);
3317
+ }
3318
+ }
2303
3319
  }
2304
- out.total_items = ITEM_COLLECTIONS.reduce((n2, c) => n2 + out[c].length, 0);
3320
+ out.items = sortByRankScore(rankedItems).map((item, n2) => ({ ...item, rank: n2 + 1 }));
3321
+ out.total_items = ITEM_COLLECTIONS.reduce((n2, c) => n2 + out[c].length, 0) + out.excerpts.length;
2305
3322
  return out;
2306
3323
  }
2307
3324
  function oneDecimal(value) {
@@ -2327,7 +3344,18 @@ function formatForPrompt(ctx, options = {}) {
2327
3344
  const includeScope = options.includeScope ?? false;
2328
3345
  const includeConversationContext = options.includeConversationContext ?? true;
2329
3346
  const sections = [];
2330
- if (ctx.facts.length > 0) {
3347
+ const ranked = ctx.items ?? [];
3348
+ if (ranked.length > 0) {
3349
+ const lines = ranked.map((it, n2) => {
3350
+ let label = rankedLabel(String(it["category"] ?? ""));
3351
+ if (includeScope) label += `, scope: ${scopeOf(ctx, it)}`;
3352
+ return `${n2 + 1}. [${label}] ${String(it.content ?? "")}`;
3353
+ });
3354
+ sections.push(`### Memories
3355
+ ${lines.join("\n")}`);
3356
+ }
3357
+ const typed = ranked.length === 0;
3358
+ if (typed && ctx.facts.length > 0) {
2331
3359
  const lines = ctx.facts.map((f) => {
2332
3360
  let line = `- ${String(f.content ?? "")}`;
2333
3361
  if (includeScope) line += ` [scope: ${scopeOf(ctx, f)}]`;
@@ -2336,7 +3364,7 @@ function formatForPrompt(ctx, options = {}) {
2336
3364
  sections.push(`### Facts
2337
3365
  ${lines.join("\n")}`);
2338
3366
  }
2339
- if (ctx.preferences.length > 0) {
3367
+ if (typed && ctx.preferences.length > 0) {
2340
3368
  const lines = ctx.preferences.map((p) => {
2341
3369
  const extras = [];
2342
3370
  if (includeScope) extras.push(`scope: ${scopeOf(ctx, p)}`);
@@ -2349,7 +3377,7 @@ ${lines.join("\n")}`);
2349
3377
  sections.push(`### Preferences
2350
3378
  ${lines.join("\n")}`);
2351
3379
  }
2352
- if (ctx.episodes.length > 0) {
3380
+ if (typed && ctx.episodes.length > 0) {
2353
3381
  const lines = ctx.episodes.map((e) => {
2354
3382
  let line = `- ${String(e["summary"] ?? "")}`;
2355
3383
  if (includeScope) line += ` [scope: ${scopeOf(ctx, e)}]`;
@@ -2358,7 +3386,7 @@ ${lines.join("\n")}`);
2358
3386
  sections.push(`### Episodes
2359
3387
  ${lines.join("\n")}`);
2360
3388
  }
2361
- if (ctx.emotions.length > 0) {
3389
+ if (typed && ctx.emotions.length > 0) {
2362
3390
  const lines = ctx.emotions.map((em) => {
2363
3391
  const extras = [];
2364
3392
  if (includeScope) extras.push(`scope: ${scopeOf(ctx, em)}`);
@@ -2372,7 +3400,7 @@ ${lines.join("\n")}`);
2372
3400
  sections.push(`### Emotions
2373
3401
  ${lines.join("\n")}`);
2374
3402
  }
2375
- if (ctx.temporal_events.length > 0) {
3403
+ if (typed && ctx.temporal_events.length > 0) {
2376
3404
  const lines = ctx.temporal_events.map((te) => {
2377
3405
  const extras = [];
2378
3406
  if (includeScope) extras.push(`scope: ${scopeOf(ctx, te)}`);
@@ -2626,7 +3654,10 @@ function buildTool(target, options = {}, onWarning = () => {
2626
3654
  }
2627
3655
 
2628
3656
  // src/client.ts
2629
- var SynapClient = class {
3657
+ function localModelOff() {
3658
+ return ["off", "0", "false", "no", "none"].includes((getEnv("SYNAP_SDK_LOCAL_MODEL") ?? "").trim().toLowerCase());
3659
+ }
3660
+ var SynapClient = class _SynapClient {
2630
3661
  // Native-private (`#`), not TypeScript `private`. TS `private` is erased at
2631
3662
  // compile time, so the field stays enumerable at runtime and a JS consumer
2632
3663
  // can still reach it. These two were public and made `client.transport
@@ -2649,10 +3680,20 @@ var SynapClient = class {
2649
3680
  instance;
2650
3681
  #closed = false;
2651
3682
  #initialized = false;
3683
+ #identity;
2652
3684
  #registryKeys;
2653
3685
  #compactionSubscribers;
2654
3686
  #shortTerm;
3687
+ /** Every raw turn, past compaction, for local history search. */
3688
+ #turnStore;
3689
+ /** A local embedding model for history search; null = keywords only. */
3690
+ #localEncoder = null;
3691
+ /** `history_search_model`: "auto", an "onnx:..." name, or null for none. */
3692
+ #localModelName = "auto";
3693
+ #localModelStarted = false;
3694
+ #backfillAfter = /* @__PURE__ */ new Map();
2655
3695
  #stVerbatimOverlay;
3696
+ #agentModel = {};
2656
3697
  #logger;
2657
3698
  #stAuthoritative;
2658
3699
  #turns;
@@ -2690,13 +3731,14 @@ var SynapClient = class {
2690
3731
  userAgent: `@maximem/synap-js-sdk/${SDK_VERSION}`
2691
3732
  });
2692
3733
  if (options.heartbeat) this.#transport.startHeartbeat();
3734
+ this.#transport.identityResolver = () => this.#resolveIdentity();
2693
3735
  this.#anticipationCache = new AnticipationCache();
2694
3736
  this.#fetchHooks = {
2695
- beforeFetch: (scope, options2) => {
3737
+ beforeFetch: async (scope, options2) => {
2696
3738
  const o2 = options2;
2697
3739
  if (o2["query_date"] ?? o2["queryDate"]) return null;
2698
3740
  if (o2["max_results_per_query"] ?? o2["maxResultsPerQuery"]) return null;
2699
- const attempt = tryServeFromCache(
3741
+ const attempt = await tryServeFromCache(
2700
3742
  this.#anticipationCache,
2701
3743
  scope,
2702
3744
  {
@@ -2720,7 +3762,9 @@ var SynapClient = class {
2720
3762
  return {
2721
3763
  response: attempt.response,
2722
3764
  servedItemIds: servedItemIds(attempt.hit),
2723
- bundleId: attempt.hit.bundleIds[0] ?? ""
3765
+ bundleId: attempt.hit.bundleIds[0] ?? "",
3766
+ borderline: attempt.hit.band === "borderline",
3767
+ raceDeadlineMs: SEMANTIC_MATCH.race_deadline_ms
2724
3768
  };
2725
3769
  },
2726
3770
  onAssembled: (scope, options2, response, startedAt) => {
@@ -2735,9 +3779,19 @@ var SynapClient = class {
2735
3779
  this.client = { context: { fetch: async (o2 = {}) => fetchContext(this.#transport, "client", o2, this.#fetchHooks) } };
2736
3780
  this.#compactionSubscribers = new CompactionSubscribers();
2737
3781
  this.#shortTerm = new ShortTermStore();
3782
+ this.#turnStore = new TurnStore();
3783
+ this.#shortTerm.attachArchive(this.#turnStore);
2738
3784
  this.#stVerbatimOverlay = options.st_verbatim_overlay ?? options.stVerbatimOverlay;
3785
+ this.#agentModel = {
3786
+ model: options.agent_model ?? options.agentModel,
3787
+ contextWindow: options.agent_context_window ?? options.agentContextWindow
3788
+ };
2739
3789
  this.#logger = options.logger;
2740
3790
  this.#stAuthoritative = options.sdk_st_authoritative ?? options.sdkStAuthoritative;
3791
+ this.#localEncoder = options.history_search_encoder ?? options.historySearchEncoder ?? null;
3792
+ const modelOption = options.history_search_model !== void 0 ? options.history_search_model : options.historySearchModel;
3793
+ this.#localModelName = modelOption === void 0 ? "auto" : modelOption;
3794
+ if (this.#localEncoder?.model === SEMANTIC_MATCH.model) this.#anticipationCache.setEncoder(this.#localEncoder);
2741
3795
  this.#turns = new TurnCounter();
2742
3796
  this.conversation = createConversationNamespace(
2743
3797
  this.#transport,
@@ -2754,8 +3808,12 @@ var SynapClient = class {
2754
3808
  },
2755
3809
  // Buffer the turn locally so the next fetch sees it, ahead of the server
2756
3810
  // having persisted and compacted it.
2757
- ({ conversationId, role, content, timestamp }) => {
2758
- this.#shortTerm.appendTurn(conversationId, role, content, timestamp);
3811
+ ({ conversationId, role, content, timestamp, messageId, serverMessageCount, inFlight }) => {
3812
+ this.#shortTerm.appendTurn(conversationId, role, content, timestamp, {
3813
+ messageId,
3814
+ serverMessageCount,
3815
+ inFlight
3816
+ });
2759
3817
  },
2760
3818
  (response, conversationId, userId, scopePath) => {
2761
3819
  overlayLocalRecentTurns(response, this.#shortTerm, conversationId, this.#stVerbatimOverlay);
@@ -2763,8 +3821,11 @@ var SynapClient = class {
2763
3821
  },
2764
3822
  {
2765
3823
  prompt: (conversationId, style) => this.#localPrompt(conversationId, style),
2766
- compacted: (conversationId, format) => this.#localCompacted(conversationId, format)
2767
- }
3824
+ compacted: (conversationId, format) => this.#localCompacted(conversationId, format),
3825
+ reseed: (conversationId, response) => this.#reseedShortTerm(conversationId, response),
3826
+ historyExcerpts: (conversationId, response, query) => this.#withHistoryExcerpts(conversationId, response, query)
3827
+ },
3828
+ () => this.#agentModel
2768
3829
  );
2769
3830
  this.memories = createMemoriesInterface(this.#transport, ({ userId, customerId }) => {
2770
3831
  for (const id of /* @__PURE__ */ new Set([userId, customerId])) {
@@ -2778,11 +3839,17 @@ var SynapClient = class {
2778
3839
  this.#anticipationCache,
2779
3840
  ({ conversationId, role, content }) => {
2780
3841
  this.#shortTerm.appendTurn(conversationId, role, content);
2781
- }
3842
+ },
3843
+ () => this.#agentModel,
3844
+ ({ conversationId, result, toolName }) => this.archive_tool_result(conversationId, result, { tool_name: toolName })
2782
3845
  );
3846
+ this.instance._internal.setOnListen(() => this.#startLocalModel());
2783
3847
  this.instance._internal.setCompactionApplier((bundle) => {
2784
3848
  this.#shortTerm.applyCompaction(bundle);
2785
3849
  });
3850
+ this.instance._internal.setConversationStateHandler((conversationId, messageCount) => {
3851
+ this.#shortTerm.noteServerCount(conversationId, messageCount);
3852
+ });
2786
3853
  if (options._force_new !== true) {
2787
3854
  const incumbent = registerIfAbsent(registryKey, this);
2788
3855
  if (incumbent !== void 0) return incumbent;
@@ -2870,19 +3937,30 @@ var SynapClient = class {
2870
3937
  /**
2871
3938
  * Resolve `client_id` and `instance_id` from the API key.
2872
3939
  *
2873
- * Mirrors Python's `initialize()`. Calling it is optional here (unlike
2874
- * Python, which raises "SDK not initialized" from `_ensure_initialized`)
2875
- * because every HTTP route authenticates on the API key alone. It matters
2876
- * for the anticipation cache: a bundle shared at client scope is filtered on
2877
- * `client_id`, and an empty one fails that filter for every bundle. That was
2878
- * a silent cache-scope bug in Python before whoami bootstrapping existed.
3940
+ * Mirrors Python's `initialize()`. Python raises "SDK not initialized" on
3941
+ * any call made before it; here the first request runs the same resolution
3942
+ * instead (`identityResolver` on the transport), so skipping this call costs
3943
+ * nothing and breaks no existing caller. It matters for the anticipation
3944
+ * cache: a bundle shared at client scope is filtered on `client_id`, and an
3945
+ * empty one fails that filter for every bundle.
3946
+ *
3947
+ * Calling it explicitly still locks `configure()`, as in Python. Resolving
3948
+ * on first use does not, because a caller who never called it could always
3949
+ * configure after a request, and that keeps working.
2879
3950
  *
2880
3951
  * Best-effort and idempotent. A whoami failure leaves whatever the
2881
3952
  * constructor resolved and is not fatal.
2882
3953
  */
2883
3954
  async initialize() {
2884
- if (this.#initialized) return;
2885
3955
  this.#initialized = true;
3956
+ await this.#resolveIdentity();
3957
+ }
3958
+ /** Runs whoami at most once, however many calls arrive together. */
3959
+ #resolveIdentity() {
3960
+ this.#identity ??= this.#whoami();
3961
+ return this.#identity;
3962
+ }
3963
+ async #whoami() {
2886
3964
  const credentials = this.#transport.currentCredentials();
2887
3965
  if (credentials.clientId !== "" && credentials.instanceId !== "") return;
2888
3966
  try {
@@ -2942,6 +4020,14 @@ var SynapClient = class {
2942
4020
  this.#transport.setRetryPolicy(options.retryPolicy ?? options.retry_policy ?? null);
2943
4021
  }
2944
4022
  if (options.logger !== void 0) this.#logger = options.logger;
4023
+ const model = options.agent_model ?? options.agentModel;
4024
+ const window = options.agent_context_window ?? options.agentContextWindow;
4025
+ if (model !== void 0 || window !== void 0) {
4026
+ this.#agentModel = {
4027
+ model: model ?? this.#agentModel.model,
4028
+ contextWindow: window ?? this.#agentModel.contextWindow
4029
+ };
4030
+ }
2945
4031
  }
2946
4032
  /**
2947
4033
  * Fetch and merge context across every scope with an identifier, in one call.
@@ -2975,9 +4061,11 @@ var SynapClient = class {
2975
4061
  include_scope_labels: includeScopeLabels = false,
2976
4062
  context_mode: contextMode = "in-conversation",
2977
4063
  include_profile: includeProfile = true,
2978
- last_n_conversations: lastNConversations = 1
4064
+ last_n_conversations: lastNConversations = 1,
4065
+ response_shape: responseShape = "by_type"
2979
4066
  } = options;
2980
4067
  validateConversationId(conversationId);
4068
+ responseShapeOf({ response_shape: responseShape });
2981
4069
  const wants = (scope) => scopes === void 0 || scopes.includes(scope);
2982
4070
  const shared = {
2983
4071
  search_query: searchQuery,
@@ -2989,7 +4077,8 @@ var SynapClient = class {
2989
4077
  // threaded three call sites by hand and missed the client branch, which
2990
4078
  // sits one indent deeper; spreading removes that whole class of miss.
2991
4079
  query_date: queryDate,
2992
- max_results_per_query: maxResultsPerQuery
4080
+ max_results_per_query: maxResultsPerQuery,
4081
+ response_shape: responseShape
2993
4082
  };
2994
4083
  const labels = [];
2995
4084
  const tasks = [];
@@ -3152,25 +4241,139 @@ var SynapClient = class {
3152
4241
  /**
3153
4242
  * Whether the SDK is authoritative for short-term context.
3154
4243
  *
3155
- * Resolution order matches Python's `_is_st_authoritative`: the explicit
3156
- * option first, then `SYNAP_SDK_ST_AUTHORITATIVE` (truthy strings only).
3157
- * Defaults to false, so nothing changes for a caller who never sets it.
4244
+ * Resolution order matches Python's `_is_st_authoritative`:
4245
+ * `SYNAP_SDK_ST_AUTHORITATIVE` first, as an explicit override in either
4246
+ * direction, then the option. Off by default. Being enabled does not mean
4247
+ * the local store is used: that also takes an entry that holds the whole
4248
+ * conversation, see `isComplete`.
3158
4249
  */
3159
4250
  #isStAuthoritative() {
3160
- if (this.#stAuthoritative === true) return true;
3161
4251
  const raw = (getEnv("SYNAP_SDK_ST_AUTHORITATIVE") ?? "").trim().toLowerCase();
3162
- return ["1", "true", "yes", "on"].includes(raw);
4252
+ if (["0", "false", "no", "off"].includes(raw)) return false;
4253
+ if (["1", "true", "yes", "on"].includes(raw)) return true;
4254
+ return this.#stAuthoritative === true;
3163
4255
  }
3164
4256
  /**
3165
4257
  * Render `get_context_for_prompt` from the local store, or null to fall
3166
- * through to the network. Warm means a compaction landed OR raw turns are
3167
- * buffered: with neither there is nothing to render and the server's answer
3168
- * is the only real one.
4258
+ * through to the network.
4259
+ *
4260
+ * The store answers only when it holds the whole conversation: the server
4261
+ * has confirmed a count the entry accounts for. An entry that is merely
4262
+ * present is not enough. After a restart or an eviction it holds one turn,
4263
+ * and rendering that as the conversation drops everything said before it.
3169
4264
  */
4265
+ /**
4266
+ * `response` with the earlier turns that best match `query` (by default the
4267
+ * latest user message) appended, when a compaction exists. The search runs
4268
+ * here, over the turn archive. Python: `_with_history_excerpts`.
4269
+ */
4270
+ async #withHistoryExcerpts(conversationId, response, query) {
4271
+ const r = response ?? {};
4272
+ const store = this.#turnStore;
4273
+ const recent = Array.isArray(r["recent_messages"]) ? r["recent_messages"] : [];
4274
+ store.addMany(
4275
+ conversationId,
4276
+ recent.map((m) => ({
4277
+ role: String(m["role"] ?? "user"),
4278
+ content: String(m["content"] ?? ""),
4279
+ timestamp: String(m["timestamp"] ?? ""),
4280
+ message_id: typeof m["message_id"] === "string" ? m["message_id"] : null
4281
+ }))
4282
+ );
4283
+ const entry = this.#shortTerm.get(conversationId);
4284
+ const total = Math.max(Number(r["total_message_count"] ?? 0) || 0, Number(entry?.serverMessageCount ?? 0) || 0);
4285
+ this.#maybeBackfillTurns(conversationId, total);
4286
+ if (!r["context_block"]) return response;
4287
+ const turns = store.turns(conversationId);
4288
+ let q = query;
4289
+ if (!q) {
4290
+ const latest = [...recent].reverse().find((m) => m["role"] === "user");
4291
+ q = latest ? String(latest["content"] ?? "") : [...turns].reverse().find((t) => t.role === "user")?.content ?? "";
4292
+ }
4293
+ if (!q) return response;
4294
+ const exclude = /* @__PURE__ */ new Set();
4295
+ for (const m of recent) {
4296
+ if (typeof m["message_id"] === "string" && m["message_id"]) exclude.add(m["message_id"]);
4297
+ exclude.add(String(m["content"] ?? ""));
4298
+ }
4299
+ this.#startLocalModel();
4300
+ const hits = await searchTurns(turns, q, { exclude, encoder: this.#localEncoder, store });
4301
+ if (hits.length === 0) return response;
4302
+ const block = renderExcerpts(hits, turns.length ? turns[turns.length - 1].timestamp : "");
4303
+ return {
4304
+ ...r,
4305
+ history_excerpts: hits.map((h) => ({ position: h.position, role: h.role, text: h.text })),
4306
+ history_block: block,
4307
+ formatted_context: (String(r["formatted_context"] ?? "").trimEnd() + "\n\n" + block).trim()
4308
+ };
4309
+ }
4310
+ /**
4311
+ * Keep what a tool returned in this process's turn archive, so history search
4312
+ * can find it after a compaction. Never sent anywhere: the server's
4313
+ * conversation holds what people said, and a compaction is made from that.
4314
+ * `instance.record_tool_result` calls this too. Python: `archive_tool_result`.
4315
+ */
4316
+ archive_tool_result(conversationId, result, options = {}) {
4317
+ if (!conversationId || result === null || result === void 0) return;
4318
+ try {
4319
+ this.#turnStore.add(
4320
+ conversationId,
4321
+ "tool",
4322
+ toolResultText(result, options.tool_name ?? options.toolName),
4323
+ options.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
4324
+ );
4325
+ } catch {
4326
+ }
4327
+ }
4328
+ static BACKFILL_COOLDOWN_MS = 6e4;
4329
+ static BACKFILL_PAGE = 500;
4330
+ /** Fetch, in the background, turns the server holds and the archive does not. */
4331
+ /**
4332
+ * Load the local embedding model in the background, once. Searches and
4333
+ * lookups before it is ready match by words. Python: `_start_history_encoder`.
4334
+ */
4335
+ #startLocalModel() {
4336
+ if (this.#localModelStarted) return;
4337
+ this.#localModelStarted = true;
4338
+ if (localModelOff()) return;
4339
+ const name = this.#localModelName === "auto" ? SEMANTIC_MATCH.model : this.#localModelName;
4340
+ if (!name) return;
4341
+ const callerEncoder = this.#localEncoder;
4342
+ void import("./onnx-encoder-NP35KH4H.js").then(({ canRunLocalModel, loadOnnxEncoder }) => canRunLocalModel() ? loadOnnxEncoder(name) : null).then((encoder) => {
4343
+ if (encoder === null) return;
4344
+ if (callerEncoder === null) this.#localEncoder = encoder;
4345
+ if (encoder.model === SEMANTIC_MATCH.model) this.#anticipationCache.setEncoder(encoder);
4346
+ }).catch(() => {
4347
+ });
4348
+ }
4349
+ #maybeBackfillTurns(conversationId, total) {
4350
+ if (total <= this.#turnStore.count(conversationId)) return;
4351
+ const now = Date.now();
4352
+ if (now < (this.#backfillAfter.get(conversationId) ?? 0)) return;
4353
+ this.#backfillAfter.set(conversationId, now + _SynapClient.BACKFILL_COOLDOWN_MS);
4354
+ void this.#backfillTurns(conversationId, total);
4355
+ }
4356
+ async #backfillTurns(conversationId, total) {
4357
+ try {
4358
+ let start = 1;
4359
+ while (start <= total) {
4360
+ const result = await this.#transport.request("conversation_turns", {
4361
+ pathParams: { conversation_id: conversationId },
4362
+ query: { start, limit: _SynapClient.BACKFILL_PAGE }
4363
+ });
4364
+ const page = (result ?? {})["turns"] ?? [];
4365
+ this.#turnStore.addMany(conversationId, page);
4366
+ if (page.length < _SynapClient.BACKFILL_PAGE) break;
4367
+ start += _SynapClient.BACKFILL_PAGE;
4368
+ }
4369
+ } catch (error) {
4370
+ this.#warn(`Turn backfill for ${conversationId} failed: ${String(error)}`);
4371
+ }
4372
+ }
3170
4373
  #localPrompt(conversationId, style) {
3171
4374
  if (!this.#isStAuthoritative()) return null;
3172
4375
  const entry = this.#shortTerm.get(conversationId);
3173
- if (entry === null) return null;
4376
+ if (entry === null || !isComplete(entry)) return null;
3174
4377
  if (entry.compactionId === null && entry.recentTurns.length === 0) return null;
3175
4378
  return renderForPrompt(entry, style);
3176
4379
  }
@@ -3178,9 +4381,29 @@ var SynapClient = class {
3178
4381
  #localCompacted(conversationId, format) {
3179
4382
  if (!this.#isStAuthoritative()) return null;
3180
4383
  const entry = this.#shortTerm.get(conversationId);
3181
- if (entry === null || entry.compactionId === null) return null;
4384
+ if (entry === null || entry.compactionId === null || !isComplete(entry)) return null;
3182
4385
  return renderCompacted(entry, format);
3183
4386
  }
4387
+ /**
4388
+ * Bring the local store back in step from the server's answer.
4389
+ *
4390
+ * Only for a conversation this process writes to. The server's answer lists
4391
+ * every message its summary does not cover, which is exactly what the entry
4392
+ * was missing.
4393
+ */
4394
+ #reseedShortTerm(conversationId, response) {
4395
+ if (!this.#isStAuthoritative()) return;
4396
+ const entry = this.#shortTerm.get(conversationId);
4397
+ if (entry === null || !entry.participant) return;
4398
+ const r = response ?? {};
4399
+ const recent = Array.isArray(r["recent_messages"]) ? r["recent_messages"] : [];
4400
+ this.#shortTerm.reseed(conversationId, {
4401
+ compactionId: typeof r["compaction_id"] === "string" ? r["compaction_id"] : null,
4402
+ recentMessages: recent,
4403
+ compactedMessageCount: Number(r["compacted_message_count"] ?? 0) || 0,
4404
+ totalMessageCount: Number(r["total_message_count"] ?? 0) || 0
4405
+ });
4406
+ }
3184
4407
  /** Every diagnostic the SDK emits passes through here. */
3185
4408
  #warn(message) {
3186
4409
  if (this.#logger !== void 0) {
@@ -3243,6 +4466,7 @@ export {
3243
4466
  DEFAULT_TIMEOUTS,
3244
4467
  DOCUMENT_TYPES,
3245
4468
  ENDPOINTS,
4469
+ EXCERPT_CATEGORY,
3246
4470
  FORGET_MODES,
3247
4471
  HttpTransport,
3248
4472
  INGEST_MODES,
@@ -3253,8 +4477,10 @@ export {
3253
4477
  InvalidInstanceIdError,
3254
4478
  ListeningAlreadyActiveError,
3255
4479
  ListeningNotActiveError,
4480
+ MEMORY_TYPES,
3256
4481
  NetworkTimeoutError,
3257
4482
  PermanentError,
4483
+ RESPONSE_SHAPES,
3258
4484
  RateLimitError,
3259
4485
  SDKError,
3260
4486
  SDK_VERSION,
@@ -3267,15 +4493,18 @@ export {
3267
4493
  THRESHOLDS,
3268
4494
  TranscriptConflictError,
3269
4495
  TransientError,
4496
+ applyResponseShape,
3270
4497
  flattenContextItems,
3271
4498
  formatForPrompt,
3272
4499
  isOutcomeUnknown,
3273
4500
  isRecallQuery,
3274
4501
  isSynapError,
3275
4502
  mergeScopeResults,
4503
+ rankItems,
3276
4504
  resolvePath,
3277
4505
  shouldRetry,
3278
4506
  stem,
3279
- tokenize
4507
+ tokenize,
4508
+ wireRankScore
3280
4509
  };
3281
4510
  //# sourceMappingURL=index.js.map