@mastra/memory 1.6.1 → 1.6.2-alpha.0

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 (49) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/{chunk-GBBQIJQF.js → chunk-3CM4XQJO.js} +1940 -463
  3. package/dist/chunk-3CM4XQJO.js.map +1 -0
  4. package/dist/{chunk-D6II7EP4.cjs → chunk-5W5463NI.cjs} +1939 -461
  5. package/dist/chunk-5W5463NI.cjs.map +1 -0
  6. package/dist/docs/SKILL.md +1 -1
  7. package/dist/docs/assets/SOURCE_MAP.json +25 -25
  8. package/dist/docs/references/docs-agents-agent-memory.md +2 -2
  9. package/dist/docs/references/docs-agents-network-approval.md +4 -1
  10. package/dist/docs/references/docs-agents-networks.md +1 -1
  11. package/dist/docs/references/docs-memory-memory-processors.md +1 -1
  12. package/dist/docs/references/docs-memory-message-history.md +2 -2
  13. package/dist/docs/references/docs-memory-observational-memory.md +6 -2
  14. package/dist/docs/references/docs-memory-semantic-recall.md +2 -2
  15. package/dist/docs/references/docs-memory-storage.md +1 -1
  16. package/dist/docs/references/docs-memory-working-memory.md +6 -6
  17. package/dist/docs/references/reference-memory-cloneThread.md +1 -1
  18. package/dist/docs/references/reference-memory-observational-memory.md +7 -5
  19. package/dist/docs/references/reference-processors-token-limiter-processor.md +2 -2
  20. package/dist/docs/references/reference-storage-dynamodb.md +2 -2
  21. package/dist/docs/references/reference-storage-mongodb.md +1 -1
  22. package/dist/docs/references/reference-storage-postgresql.md +2 -2
  23. package/dist/docs/references/reference-storage-upstash.md +1 -1
  24. package/dist/docs/references/reference-vectors-pg.md +2 -0
  25. package/dist/index.cjs +48 -25
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.d.ts +14 -14
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +44 -21
  30. package/dist/index.js.map +1 -1
  31. package/dist/{observational-memory-AHVELJX4.cjs → observational-memory-C5LO7RBR.cjs} +17 -17
  32. package/dist/{observational-memory-AHVELJX4.cjs.map → observational-memory-C5LO7RBR.cjs.map} +1 -1
  33. package/dist/{observational-memory-QFQUF5EY.js → observational-memory-OYK4MEUD.js} +3 -3
  34. package/dist/{observational-memory-QFQUF5EY.js.map → observational-memory-OYK4MEUD.js.map} +1 -1
  35. package/dist/processors/index.cjs +15 -15
  36. package/dist/processors/index.js +1 -1
  37. package/dist/processors/observational-memory/observational-memory.d.ts +33 -2
  38. package/dist/processors/observational-memory/observational-memory.d.ts.map +1 -1
  39. package/dist/processors/observational-memory/observer-agent.d.ts +7 -4
  40. package/dist/processors/observational-memory/observer-agent.d.ts.map +1 -1
  41. package/dist/processors/observational-memory/repro-capture.d.ts +23 -0
  42. package/dist/processors/observational-memory/repro-capture.d.ts.map +1 -0
  43. package/dist/processors/observational-memory/token-counter.d.ts +28 -3
  44. package/dist/processors/observational-memory/token-counter.d.ts.map +1 -1
  45. package/dist/tools/working-memory.d.ts +6 -10
  46. package/dist/tools/working-memory.d.ts.map +1 -1
  47. package/package.json +13 -8
  48. package/dist/chunk-D6II7EP4.cjs.map +0 -1
  49. package/dist/chunk-GBBQIJQF.js.map +0 -1
@@ -3,17 +3,21 @@
3
3
  var fs = require('fs');
4
4
  var path = require('path');
5
5
  var agent = require('@mastra/core/agent');
6
+ var features = require('@mastra/core/features');
6
7
  var llm = require('@mastra/core/llm');
7
8
  var memory = require('@mastra/core/memory');
8
9
  var processors = require('@mastra/core/processors');
9
10
  var xxhash = require('xxhash-wasm');
10
11
  var crypto$1 = require('crypto');
12
+ var async_hooks = require('async_hooks');
13
+ var imageSize = require('image-size');
11
14
  var lite = require('js-tiktoken/lite');
12
15
  var o200k_base = require('js-tiktoken/ranks/o200k_base');
13
16
 
14
17
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
15
18
 
16
19
  var xxhash__default = /*#__PURE__*/_interopDefault(xxhash);
20
+ var imageSize__default = /*#__PURE__*/_interopDefault(imageSize);
17
21
  var o200k_base__default = /*#__PURE__*/_interopDefault(o200k_base);
18
22
 
19
23
  // src/processors/observational-memory/observational-memory.ts
@@ -669,45 +673,177 @@ User messages are extremely important. If the user asks a question or gives a ne
669
673
  ${instruction}` : ""}`;
670
674
  }
671
675
  var OBSERVER_SYSTEM_PROMPT = buildObserverSystemPrompt();
672
- function formatMessagesForObserver(messages, options) {
676
+ var OBSERVER_IMAGE_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
677
+ "png",
678
+ "jpg",
679
+ "jpeg",
680
+ "webp",
681
+ "gif",
682
+ "bmp",
683
+ "tiff",
684
+ "tif",
685
+ "heic",
686
+ "heif",
687
+ "avif"
688
+ ]);
689
+ function formatObserverTimestamp(createdAt) {
690
+ return createdAt ? new Date(createdAt).toLocaleString("en-US", {
691
+ year: "numeric",
692
+ month: "short",
693
+ day: "numeric",
694
+ hour: "numeric",
695
+ minute: "2-digit",
696
+ hour12: true
697
+ }) : "";
698
+ }
699
+ function getObserverPathExtension(value) {
700
+ const normalized = value.split("#", 1)[0]?.split("?", 1)[0] ?? value;
701
+ const match = normalized.match(/\.([a-z0-9]+)$/i);
702
+ return match?.[1]?.toLowerCase();
703
+ }
704
+ function hasObserverImageFilenameExtension(filename) {
705
+ return typeof filename === "string" && OBSERVER_IMAGE_FILE_EXTENSIONS.has(getObserverPathExtension(filename) ?? "");
706
+ }
707
+ function isImageLikeObserverFilePart(part) {
708
+ if (part.type !== "file") {
709
+ return false;
710
+ }
711
+ if (typeof part.mimeType === "string" && part.mimeType.toLowerCase().startsWith("image/")) {
712
+ return true;
713
+ }
714
+ if (typeof part.data === "string" && part.data.startsWith("data:image/")) {
715
+ return true;
716
+ }
717
+ if (part.data instanceof URL && hasObserverImageFilenameExtension(part.data.pathname)) {
718
+ return true;
719
+ }
720
+ if (typeof part.data === "string") {
721
+ try {
722
+ const url = new URL(part.data);
723
+ if ((url.protocol === "http:" || url.protocol === "https:") && hasObserverImageFilenameExtension(url.pathname)) {
724
+ return true;
725
+ }
726
+ } catch {
727
+ }
728
+ }
729
+ return hasObserverImageFilenameExtension(part.filename);
730
+ }
731
+ function toObserverInputAttachmentPart(part) {
732
+ if (part.type === "image") {
733
+ return {
734
+ type: "image",
735
+ image: part.image,
736
+ mimeType: part.mimeType,
737
+ providerOptions: part.providerOptions,
738
+ providerMetadata: part.providerMetadata,
739
+ experimental_providerMetadata: part.experimental_providerMetadata
740
+ };
741
+ }
742
+ if (isImageLikeObserverFilePart(part)) {
743
+ return {
744
+ type: "image",
745
+ image: part.data,
746
+ mimeType: part.mimeType,
747
+ providerOptions: part.providerOptions,
748
+ providerMetadata: part.providerMetadata,
749
+ experimental_providerMetadata: part.experimental_providerMetadata
750
+ };
751
+ }
752
+ return {
753
+ type: "file",
754
+ data: part.data,
755
+ mimeType: part.mimeType,
756
+ filename: part.filename,
757
+ providerOptions: part.providerOptions,
758
+ providerMetadata: part.providerMetadata,
759
+ experimental_providerMetadata: part.experimental_providerMetadata
760
+ };
761
+ }
762
+ function resolveObserverAttachmentLabel(part) {
763
+ if (part.filename?.trim()) {
764
+ return part.filename.trim();
765
+ }
766
+ const asset = part.type === "image" ? part.image : part.data;
767
+ if (typeof asset !== "string" || asset.startsWith("data:")) {
768
+ return part.mimeType;
769
+ }
770
+ try {
771
+ const url = new URL(asset);
772
+ const basename = url.pathname.split("/").filter(Boolean).pop();
773
+ return basename ? decodeURIComponent(basename) : part.mimeType;
774
+ } catch {
775
+ return part.mimeType;
776
+ }
777
+ }
778
+ function formatObserverAttachmentPlaceholder(part, counter) {
779
+ const attachmentType = part.type === "image" || isImageLikeObserverFilePart(part) ? "Image" : "File";
780
+ const attachmentId = attachmentType === "Image" ? counter.nextImageId++ : counter.nextFileId++;
781
+ const label = resolveObserverAttachmentLabel(part);
782
+ return label ? `[${attachmentType} #${attachmentId}: ${label}]` : `[${attachmentType} #${attachmentId}]`;
783
+ }
784
+ function formatObserverMessage(msg, counter, options) {
673
785
  const maxLen = options?.maxPartLength;
674
- return messages.map((msg) => {
675
- const timestamp = msg.createdAt ? new Date(msg.createdAt).toLocaleString("en-US", {
676
- year: "numeric",
677
- month: "short",
678
- day: "numeric",
679
- hour: "numeric",
680
- minute: "2-digit",
681
- hour12: true
682
- }) : "";
683
- const role = msg.role.charAt(0).toUpperCase() + msg.role.slice(1);
684
- const timestampStr = timestamp ? ` (${timestamp})` : "";
685
- let content = "";
686
- if (typeof msg.content === "string") {
687
- content = maybeTruncate(msg.content, maxLen);
688
- } else if (msg.content?.parts && Array.isArray(msg.content.parts) && msg.content.parts.length > 0) {
689
- content = msg.content.parts.map((part) => {
690
- if (part.type === "text") return maybeTruncate(part.text, maxLen);
691
- if (part.type === "tool-invocation") {
692
- const inv = part.toolInvocation;
693
- if (inv.state === "result") {
694
- const resultStr = JSON.stringify(inv.result, null, 2);
695
- return `[Tool Result: ${inv.toolName}]
786
+ const timestamp = formatObserverTimestamp(msg.createdAt);
787
+ const role = msg.role.charAt(0).toUpperCase() + msg.role.slice(1);
788
+ const timestampStr = timestamp ? ` (${timestamp})` : "";
789
+ const attachments = [];
790
+ let content = "";
791
+ if (typeof msg.content === "string") {
792
+ content = maybeTruncate(msg.content, maxLen);
793
+ } else if (msg.content?.parts && Array.isArray(msg.content.parts) && msg.content.parts.length > 0) {
794
+ content = msg.content.parts.map((part) => {
795
+ if (part.type === "text") return maybeTruncate(part.text, maxLen);
796
+ if (part.type === "tool-invocation") {
797
+ const inv = part.toolInvocation;
798
+ if (inv.state === "result") {
799
+ const resultStr = JSON.stringify(inv.result, null, 2);
800
+ return `[Tool Result: ${inv.toolName}]
696
801
  ${maybeTruncate(resultStr, maxLen)}`;
697
- }
698
- const argsStr = JSON.stringify(inv.args, null, 2);
699
- return `[Tool Call: ${inv.toolName}]
802
+ }
803
+ const argsStr = JSON.stringify(inv.args, null, 2);
804
+ return `[Tool Call: ${inv.toolName}]
700
805
  ${maybeTruncate(argsStr, maxLen)}`;
806
+ }
807
+ const partType = part.type;
808
+ if (partType === "image" || partType === "file") {
809
+ const attachment = part;
810
+ const inputAttachment = toObserverInputAttachmentPart(attachment);
811
+ if (inputAttachment) {
812
+ attachments.push(inputAttachment);
701
813
  }
702
- if (part.type?.startsWith("data-")) return "";
703
- return "";
704
- }).filter(Boolean).join("\n");
705
- } else if (msg.content?.content) {
706
- content = maybeTruncate(msg.content.content, maxLen);
814
+ return formatObserverAttachmentPlaceholder(attachment, counter);
815
+ }
816
+ if (partType?.startsWith("data-")) return "";
817
+ return "";
818
+ }).filter(Boolean).join("\n");
819
+ } else if (msg.content?.content) {
820
+ content = maybeTruncate(msg.content.content, maxLen);
821
+ }
822
+ return {
823
+ text: `**${role}${timestampStr}:**
824
+ ${content}`,
825
+ attachments
826
+ };
827
+ }
828
+ function formatMessagesForObserver(messages, options) {
829
+ const counter = { nextImageId: 1, nextFileId: 1 };
830
+ return messages.map((msg) => formatObserverMessage(msg, counter, options).text).join("\n\n---\n\n");
831
+ }
832
+ function buildObserverHistoryMessage(messages) {
833
+ const counter = { nextImageId: 1, nextFileId: 1 };
834
+ const content = [{ type: "text", text: "## New Message History to Observe\n\n" }];
835
+ messages.forEach((message, index) => {
836
+ const formatted = formatObserverMessage(message, counter);
837
+ content.push({ type: "text", text: formatted.text });
838
+ content.push(...formatted.attachments);
839
+ if (index < messages.length - 1) {
840
+ content.push({ type: "text", text: "\n\n---\n\n" });
707
841
  }
708
- return `**${role}${timestampStr}:**
709
- ${content}`;
710
- }).join("\n\n---\n\n");
842
+ });
843
+ return {
844
+ role: "user",
845
+ content
846
+ };
711
847
  }
712
848
  function maybeTruncate(str, maxLen) {
713
849
  if (!maxLen || str.length <= maxLen) return str;
@@ -716,20 +852,42 @@ function maybeTruncate(str, maxLen) {
716
852
  return `${truncated}
717
853
  ... [truncated ${remaining} characters]`;
718
854
  }
719
- function formatMultiThreadMessagesForObserver(messagesByThread, threadOrder) {
720
- const sections = [];
721
- for (const threadId of threadOrder) {
855
+ function buildMultiThreadObserverHistoryMessage(messagesByThread, threadOrder) {
856
+ const counter = { nextImageId: 1, nextFileId: 1 };
857
+ const content = [
858
+ {
859
+ type: "text",
860
+ text: `## New Message History to Observe
861
+
862
+ The following messages are from ${threadOrder.length} different conversation threads. Each thread is wrapped in a <thread id="..."> tag.
863
+
864
+ `
865
+ }
866
+ ];
867
+ threadOrder.forEach((threadId, threadIndex) => {
722
868
  const messages = messagesByThread.get(threadId);
723
- if (!messages || messages.length === 0) continue;
724
- const formattedMessages = formatMessagesForObserver(messages);
725
- sections.push(`<thread id="${threadId}">
726
- ${formattedMessages}
727
- </thread>`);
728
- }
729
- return sections.join("\n\n");
869
+ if (!messages || messages.length === 0) return;
870
+ content.push({ type: "text", text: `<thread id="${threadId}">
871
+ ` });
872
+ messages.forEach((message, messageIndex) => {
873
+ const formatted = formatObserverMessage(message, counter);
874
+ content.push({ type: "text", text: formatted.text });
875
+ content.push(...formatted.attachments);
876
+ if (messageIndex < messages.length - 1) {
877
+ content.push({ type: "text", text: "\n\n---\n\n" });
878
+ }
879
+ });
880
+ content.push({ type: "text", text: "\n</thread>" });
881
+ if (threadIndex < threadOrder.length - 1) {
882
+ content.push({ type: "text", text: "\n\n" });
883
+ }
884
+ });
885
+ return {
886
+ role: "user",
887
+ content
888
+ };
730
889
  }
731
- function buildMultiThreadObserverPrompt(existingObservations, messagesByThread, threadOrder) {
732
- const formattedMessages = formatMultiThreadMessagesForObserver(messagesByThread, threadOrder);
890
+ function buildMultiThreadObserverTaskPrompt(existingObservations) {
733
891
  let prompt = "";
734
892
  if (existingObservations) {
735
893
  prompt += `## Previous Observations
@@ -741,15 +899,6 @@ ${existingObservations}
741
899
  `;
742
900
  prompt += "Do not repeat these existing observations. Your new observations will be appended to the existing observations.\n\n";
743
901
  }
744
- prompt += `## New Message History to Observe
745
-
746
- The following messages are from ${threadOrder.length} different conversation threads. Each thread is wrapped in a <thread id="..."> tag.
747
-
748
- ${formattedMessages}
749
-
750
- ---
751
-
752
- `;
753
902
  prompt += `## Your Task
754
903
 
755
904
  `;
@@ -826,8 +975,7 @@ function parseMultiThreadObserverOutput(output) {
826
975
  rawOutput: output
827
976
  };
828
977
  }
829
- function buildObserverPrompt(existingObservations, messagesToObserve, options) {
830
- const formattedMessages = formatMessagesForObserver(messagesToObserve);
978
+ function buildObserverTaskPrompt(existingObservations, options) {
831
979
  let prompt = "";
832
980
  if (existingObservations) {
833
981
  prompt += `## Previous Observations
@@ -839,13 +987,6 @@ ${existingObservations}
839
987
  `;
840
988
  prompt += "Do not repeat these existing observations. Your new observations will be appended to the existing observations.\n\n";
841
989
  }
842
- prompt += `## New Message History to Observe
843
-
844
- ${formattedMessages}
845
-
846
- ---
847
-
848
- `;
849
990
  prompt += `## Your Task
850
991
 
851
992
  `;
@@ -857,6 +998,16 @@ IMPORTANT: Do NOT include <current-task> or <suggested-response> sections in you
857
998
  }
858
999
  return prompt;
859
1000
  }
1001
+ function buildObserverPrompt(existingObservations, messagesToObserve, options) {
1002
+ const formattedMessages = formatMessagesForObserver(messagesToObserve);
1003
+ return `## New Message History to Observe
1004
+
1005
+ ${formattedMessages}
1006
+
1007
+ ---
1008
+
1009
+ ${buildObserverTaskPrompt(existingObservations, options)}`;
1010
+ }
860
1011
  function parseObserverOutput(output) {
861
1012
  if (detectDegenerateRepetition(output)) {
862
1013
  return {
@@ -1228,6 +1379,140 @@ function extractReflectorListItems(content) {
1228
1379
  function validateCompression(reflectedTokens, targetThreshold) {
1229
1380
  return reflectedTokens < targetThreshold;
1230
1381
  }
1382
+ var OM_REPRO_CAPTURE_DIR = process.env.OM_REPRO_CAPTURE_DIR ?? ".mastra-om-repro";
1383
+ function sanitizeCapturePathSegment(value) {
1384
+ const sanitized = value.replace(/[\\/]+/g, "_").replace(/\.{2,}/g, "_").trim();
1385
+ return sanitized.length > 0 ? sanitized : "unknown-thread";
1386
+ }
1387
+ function isOmReproCaptureEnabled() {
1388
+ return process.env.OM_REPRO_CAPTURE === "1";
1389
+ }
1390
+ function safeCaptureJson(value) {
1391
+ return JSON.parse(
1392
+ JSON.stringify(value, (_key, current) => {
1393
+ if (typeof current === "bigint") return current.toString();
1394
+ if (typeof current === "function") return "[function]";
1395
+ if (current instanceof Error) return { name: current.name, message: current.message, stack: current.stack };
1396
+ if (current instanceof Set) return { __type: "Set", values: Array.from(current.values()) };
1397
+ if (current instanceof Map) return { __type: "Map", entries: Array.from(current.entries()) };
1398
+ return current;
1399
+ })
1400
+ );
1401
+ }
1402
+ function buildReproMessageFingerprint(message) {
1403
+ const createdAt = message.createdAt instanceof Date ? message.createdAt.toISOString() : message.createdAt ? new Date(message.createdAt).toISOString() : "";
1404
+ return JSON.stringify({
1405
+ role: message.role,
1406
+ createdAt,
1407
+ content: message.content
1408
+ });
1409
+ }
1410
+ function inferReproIdRemap(preMessages, postMessages) {
1411
+ const preByFingerprint = /* @__PURE__ */ new Map();
1412
+ const postByFingerprint = /* @__PURE__ */ new Map();
1413
+ for (const message of preMessages) {
1414
+ if (!message.id) continue;
1415
+ const fingerprint = buildReproMessageFingerprint(message);
1416
+ const list = preByFingerprint.get(fingerprint) ?? [];
1417
+ list.push(message.id);
1418
+ preByFingerprint.set(fingerprint, list);
1419
+ }
1420
+ for (const message of postMessages) {
1421
+ if (!message.id) continue;
1422
+ const fingerprint = buildReproMessageFingerprint(message);
1423
+ const list = postByFingerprint.get(fingerprint) ?? [];
1424
+ list.push(message.id);
1425
+ postByFingerprint.set(fingerprint, list);
1426
+ }
1427
+ const remap = [];
1428
+ for (const [fingerprint, preIds] of preByFingerprint.entries()) {
1429
+ const postIds = postByFingerprint.get(fingerprint);
1430
+ if (!postIds || preIds.length !== 1 || postIds.length !== 1) continue;
1431
+ const fromId = preIds[0];
1432
+ const toId = postIds[0];
1433
+ if (!fromId || !toId || fromId === toId) {
1434
+ continue;
1435
+ }
1436
+ remap.push({ fromId, toId, fingerprint });
1437
+ }
1438
+ return remap;
1439
+ }
1440
+ function writeProcessInputStepReproCapture(params) {
1441
+ if (!isOmReproCaptureEnabled()) {
1442
+ return;
1443
+ }
1444
+ try {
1445
+ const sanitizedThreadId = sanitizeCapturePathSegment(params.threadId);
1446
+ const runId = `${Date.now()}-step-${params.stepNumber}-${crypto$1.randomUUID()}`;
1447
+ const captureDir = path.join(process.cwd(), OM_REPRO_CAPTURE_DIR, sanitizedThreadId, runId);
1448
+ fs.mkdirSync(captureDir, { recursive: true });
1449
+ const contextMessages = params.messageList.get.all.db();
1450
+ const memoryContext = memory.parseMemoryRequestContext(params.args.requestContext);
1451
+ const preMessageIds = new Set(params.preMessages.map((message) => message.id));
1452
+ const postMessageIds = new Set(contextMessages.map((message) => message.id));
1453
+ const removedMessageIds = params.preMessages.map((message) => message.id).filter((id) => Boolean(id) && !postMessageIds.has(id));
1454
+ const addedMessageIds = contextMessages.map((message) => message.id).filter((id) => Boolean(id) && !preMessageIds.has(id));
1455
+ const idRemap = inferReproIdRemap(params.preMessages, contextMessages);
1456
+ const rawState = params.args.state ?? {};
1457
+ const inputPayload = safeCaptureJson({
1458
+ stepNumber: params.stepNumber,
1459
+ threadId: params.threadId,
1460
+ resourceId: params.resourceId,
1461
+ readOnly: memoryContext?.memoryConfig?.readOnly,
1462
+ messageCount: contextMessages.length,
1463
+ messageIds: contextMessages.map((message) => message.id),
1464
+ stateKeys: Object.keys(rawState),
1465
+ state: rawState,
1466
+ args: {
1467
+ messages: params.args.messages,
1468
+ steps: params.args.steps,
1469
+ systemMessages: params.args.systemMessages,
1470
+ retryCount: params.args.retryCount,
1471
+ tools: params.args.tools,
1472
+ toolChoice: params.args.toolChoice,
1473
+ activeTools: params.args.activeTools,
1474
+ providerOptions: params.args.providerOptions,
1475
+ modelSettings: params.args.modelSettings,
1476
+ structuredOutput: params.args.structuredOutput
1477
+ }
1478
+ });
1479
+ const preStatePayload = safeCaptureJson({
1480
+ record: params.preRecord,
1481
+ bufferedChunks: params.preBufferedChunks,
1482
+ contextTokenCount: params.preContextTokenCount,
1483
+ messages: params.preMessages,
1484
+ messageList: params.preSerializedMessageList
1485
+ });
1486
+ const outputPayload = safeCaptureJson({
1487
+ details: params.details,
1488
+ messageDiff: {
1489
+ removedMessageIds,
1490
+ addedMessageIds,
1491
+ idRemap
1492
+ }
1493
+ });
1494
+ const postStatePayload = safeCaptureJson({
1495
+ record: params.postRecord,
1496
+ bufferedChunks: params.postBufferedChunks,
1497
+ contextTokenCount: params.postContextTokenCount,
1498
+ messageCount: contextMessages.length,
1499
+ messageIds: contextMessages.map((message) => message.id),
1500
+ messages: contextMessages,
1501
+ messageList: params.messageList.serialize()
1502
+ });
1503
+ fs.writeFileSync(path.join(captureDir, "input.json"), `${JSON.stringify(inputPayload, null, 2)}
1504
+ `);
1505
+ fs.writeFileSync(path.join(captureDir, "pre-state.json"), `${JSON.stringify(preStatePayload, null, 2)}
1506
+ `);
1507
+ fs.writeFileSync(path.join(captureDir, "output.json"), `${JSON.stringify(outputPayload, null, 2)}
1508
+ `);
1509
+ fs.writeFileSync(path.join(captureDir, "post-state.json"), `${JSON.stringify(postStatePayload, null, 2)}
1510
+ `);
1511
+ params.debug?.(`[OM:repro-capture] wrote processInputStep capture to ${captureDir}`);
1512
+ } catch (error) {
1513
+ params.debug?.(`[OM:repro-capture] failed to write processInputStep capture: ${String(error)}`);
1514
+ }
1515
+ }
1231
1516
 
1232
1517
  // src/processors/observational-memory/thresholds.ts
1233
1518
  function getMaxThreshold(threshold) {
@@ -1313,14 +1598,90 @@ function calculateProjectedMessageRemoval(chunks, bufferActivation, messageToken
1313
1598
  }
1314
1599
  return bestBoundaryMessageTokens;
1315
1600
  }
1316
- var sharedDefaultEncoder;
1601
+ var GLOBAL_TIKTOKEN_KEY = "__mastraTiktoken";
1317
1602
  function getDefaultEncoder() {
1318
- if (!sharedDefaultEncoder) {
1319
- sharedDefaultEncoder = new lite.Tiktoken(o200k_base__default.default);
1603
+ const cached = globalThis[GLOBAL_TIKTOKEN_KEY];
1604
+ if (cached) return cached;
1605
+ const encoder = new lite.Tiktoken(o200k_base__default.default);
1606
+ globalThis[GLOBAL_TIKTOKEN_KEY] = encoder;
1607
+ return encoder;
1608
+ }
1609
+ var IMAGE_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
1610
+ "png",
1611
+ "jpg",
1612
+ "jpeg",
1613
+ "webp",
1614
+ "gif",
1615
+ "bmp",
1616
+ "tiff",
1617
+ "tif",
1618
+ "heic",
1619
+ "heif",
1620
+ "avif"
1621
+ ]);
1622
+ var TOKEN_ESTIMATE_CACHE_VERSION = 5;
1623
+ var DEFAULT_IMAGE_ESTIMATOR = {
1624
+ baseTokens: 85,
1625
+ tileTokens: 170,
1626
+ fallbackTiles: 4
1627
+ };
1628
+ var GOOGLE_LEGACY_IMAGE_TOKENS_PER_TILE = 258;
1629
+ var GOOGLE_GEMINI_3_IMAGE_TOKENS_BY_RESOLUTION = {
1630
+ low: 280,
1631
+ medium: 560,
1632
+ high: 1120,
1633
+ ultra_high: 2240,
1634
+ unspecified: 1120
1635
+ };
1636
+ var ANTHROPIC_IMAGE_TOKENS_PER_PIXEL = 1 / 750;
1637
+ var ANTHROPIC_IMAGE_MAX_LONG_EDGE = 1568;
1638
+ var GOOGLE_MEDIA_RESOLUTION_VALUES = /* @__PURE__ */ new Set([
1639
+ "low",
1640
+ "medium",
1641
+ "high",
1642
+ "ultra_high",
1643
+ "unspecified"
1644
+ ]);
1645
+ var ATTACHMENT_COUNT_TIMEOUT_MS = 2e4;
1646
+ var REMOTE_IMAGE_PROBE_TIMEOUT_MS = 2500;
1647
+ var PROVIDER_API_KEY_ENV_VARS = {
1648
+ openai: ["OPENAI_API_KEY"],
1649
+ google: ["GOOGLE_GENERATIVE_AI_API_KEY", "GOOGLE_API_KEY"],
1650
+ anthropic: ["ANTHROPIC_API_KEY"]
1651
+ };
1652
+ function getPartMastraMetadata(part) {
1653
+ return part.providerMetadata?.mastra;
1654
+ }
1655
+ function ensurePartMastraMetadata(part) {
1656
+ const typedPart = part;
1657
+ typedPart.providerMetadata ??= {};
1658
+ typedPart.providerMetadata.mastra ??= {};
1659
+ return typedPart.providerMetadata.mastra;
1660
+ }
1661
+ function getContentMastraMetadata(content) {
1662
+ if (!content || typeof content !== "object") {
1663
+ return void 0;
1320
1664
  }
1321
- return sharedDefaultEncoder;
1665
+ return content.metadata?.mastra;
1666
+ }
1667
+ function ensureContentMastraMetadata(content) {
1668
+ if (!content || typeof content !== "object") {
1669
+ return void 0;
1670
+ }
1671
+ const typedContent = content;
1672
+ typedContent.metadata ??= {};
1673
+ typedContent.metadata.mastra ??= {};
1674
+ return typedContent.metadata.mastra;
1675
+ }
1676
+ function getMessageMastraMetadata(message) {
1677
+ return message.metadata?.mastra;
1678
+ }
1679
+ function ensureMessageMastraMetadata(message) {
1680
+ const typedMessage = message;
1681
+ typedMessage.metadata ??= {};
1682
+ typedMessage.metadata.mastra ??= {};
1683
+ return typedMessage.metadata.mastra;
1322
1684
  }
1323
- var TOKEN_ESTIMATE_CACHE_VERSION = 1;
1324
1685
  function buildEstimateKey(kind, text) {
1325
1686
  const payloadHash = crypto$1.createHash("sha1").update(text).digest("hex");
1326
1687
  return `${kind}:${payloadHash}`;
@@ -1343,51 +1704,60 @@ function getCacheEntry(cache, key) {
1343
1704
  if (isTokenEstimateEntry(cache)) {
1344
1705
  return cache.key === key ? cache : void 0;
1345
1706
  }
1346
- return void 0;
1707
+ const keyedEntry = cache[key];
1708
+ return isTokenEstimateEntry(keyedEntry) ? keyedEntry : void 0;
1709
+ }
1710
+ function mergeCacheEntry(cache, key, entry) {
1711
+ if (isTokenEstimateEntry(cache)) {
1712
+ if (cache.key === key) {
1713
+ return entry;
1714
+ }
1715
+ return {
1716
+ [cache.key]: cache,
1717
+ [key]: entry
1718
+ };
1719
+ }
1720
+ if (cache && typeof cache === "object") {
1721
+ return {
1722
+ ...cache,
1723
+ [key]: entry
1724
+ };
1725
+ }
1726
+ return entry;
1347
1727
  }
1348
1728
  function getPartCacheEntry(part, key) {
1349
- const cache = part?.providerMetadata?.mastra?.tokenEstimate;
1350
- return getCacheEntry(cache, key);
1729
+ return getCacheEntry(getPartMastraMetadata(part)?.tokenEstimate, key);
1351
1730
  }
1352
- function setPartCacheEntry(part, _key, entry) {
1353
- const mutablePart = part;
1354
- mutablePart.providerMetadata ??= {};
1355
- mutablePart.providerMetadata.mastra ??= {};
1356
- mutablePart.providerMetadata.mastra.tokenEstimate = entry;
1731
+ function setPartCacheEntry(part, key, entry) {
1732
+ const mastraMetadata = ensurePartMastraMetadata(part);
1733
+ mastraMetadata.tokenEstimate = mergeCacheEntry(mastraMetadata.tokenEstimate, key, entry);
1357
1734
  }
1358
1735
  function getMessageCacheEntry(message, key) {
1359
- const content = message.content;
1360
- if (content && typeof content === "object") {
1361
- const contentLevelCache = content.metadata?.mastra?.tokenEstimate;
1362
- const contentLevelEntry = getCacheEntry(contentLevelCache, key);
1363
- if (contentLevelEntry) return contentLevelEntry;
1364
- }
1365
- const messageLevelCache = message?.metadata?.mastra?.tokenEstimate;
1366
- return getCacheEntry(messageLevelCache, key);
1367
- }
1368
- function setMessageCacheEntry(message, _key, entry) {
1369
- const content = message.content;
1370
- if (content && typeof content === "object") {
1371
- content.metadata ??= {};
1372
- content.metadata.mastra ??= {};
1373
- content.metadata.mastra.tokenEstimate = entry;
1736
+ const contentLevelEntry = getCacheEntry(getContentMastraMetadata(message.content)?.tokenEstimate, key);
1737
+ if (contentLevelEntry) return contentLevelEntry;
1738
+ return getCacheEntry(getMessageMastraMetadata(message)?.tokenEstimate, key);
1739
+ }
1740
+ function setMessageCacheEntry(message, key, entry) {
1741
+ const contentMastraMetadata = ensureContentMastraMetadata(message.content);
1742
+ if (contentMastraMetadata) {
1743
+ contentMastraMetadata.tokenEstimate = mergeCacheEntry(contentMastraMetadata.tokenEstimate, key, entry);
1374
1744
  return;
1375
1745
  }
1376
- message.metadata ??= {};
1377
- message.metadata.mastra ??= {};
1378
- message.metadata.mastra.tokenEstimate = entry;
1746
+ const messageMastraMetadata = ensureMessageMastraMetadata(message);
1747
+ messageMastraMetadata.tokenEstimate = mergeCacheEntry(messageMastraMetadata.tokenEstimate, key, entry);
1379
1748
  }
1380
1749
  function serializePartForTokenCounting(part) {
1381
- const hasTokenEstimate = Boolean(part?.providerMetadata?.mastra?.tokenEstimate);
1750
+ const typedPart = part;
1751
+ const hasTokenEstimate = Boolean(typedPart.providerMetadata?.mastra?.tokenEstimate);
1382
1752
  if (!hasTokenEstimate) {
1383
1753
  return JSON.stringify(part);
1384
1754
  }
1385
1755
  const clonedPart = {
1386
- ...part,
1756
+ ...typedPart,
1387
1757
  providerMetadata: {
1388
- ...part.providerMetadata ?? {},
1758
+ ...typedPart.providerMetadata ?? {},
1389
1759
  mastra: {
1390
- ...part.providerMetadata?.mastra ?? {}
1760
+ ...typedPart.providerMetadata?.mastra ?? {}
1391
1761
  }
1392
1762
  }
1393
1763
  };
@@ -1400,23 +1770,648 @@ function serializePartForTokenCounting(part) {
1400
1770
  }
1401
1771
  return JSON.stringify(clonedPart);
1402
1772
  }
1773
+ function getFilenameFromAttachmentData(data) {
1774
+ const pathname = data instanceof URL ? data.pathname : typeof data === "string" && isHttpUrlString(data) ? (() => {
1775
+ try {
1776
+ return new URL(data).pathname;
1777
+ } catch {
1778
+ return void 0;
1779
+ }
1780
+ })() : void 0;
1781
+ const filename = pathname?.split("/").filter(Boolean).pop();
1782
+ return filename ? decodeURIComponent(filename) : void 0;
1783
+ }
1784
+ function serializeNonImageFilePartForTokenCounting(part) {
1785
+ const filename = getObjectValue(part, "filename");
1786
+ const inferredFilename = getFilenameFromAttachmentData(getObjectValue(part, "data"));
1787
+ return JSON.stringify({
1788
+ type: "file",
1789
+ mimeType: getObjectValue(part, "mimeType") ?? null,
1790
+ filename: typeof filename === "string" && filename.trim().length > 0 ? filename.trim() : inferredFilename ?? null
1791
+ });
1792
+ }
1403
1793
  function isValidCacheEntry(entry, expectedKey, expectedSource) {
1404
1794
  return Boolean(
1405
1795
  entry && entry.v === TOKEN_ESTIMATE_CACHE_VERSION && entry.source === expectedSource && entry.key === expectedKey && Number.isFinite(entry.tokens)
1406
1796
  );
1407
1797
  }
1798
+ function parseModelContext(model) {
1799
+ if (!model) return void 0;
1800
+ if (typeof model === "object") {
1801
+ return model.provider || model.modelId ? { provider: model.provider, modelId: model.modelId } : void 0;
1802
+ }
1803
+ const slashIndex = model.indexOf("/");
1804
+ if (slashIndex === -1) {
1805
+ return { modelId: model };
1806
+ }
1807
+ return {
1808
+ provider: model.slice(0, slashIndex),
1809
+ modelId: model.slice(slashIndex + 1)
1810
+ };
1811
+ }
1812
+ function normalizeImageDetail(detail) {
1813
+ if (detail === "low" || detail === "high") return detail;
1814
+ return "auto";
1815
+ }
1816
+ function getObjectValue(value, key) {
1817
+ if (!value || typeof value !== "object") return void 0;
1818
+ return value[key];
1819
+ }
1820
+ function resolveImageDetail(part) {
1821
+ const openAIProviderOptions = getObjectValue(getObjectValue(part, "providerOptions"), "openai");
1822
+ const openAIProviderMetadata = getObjectValue(getObjectValue(part, "providerMetadata"), "openai");
1823
+ const mastraMetadata = getObjectValue(getObjectValue(part, "providerMetadata"), "mastra");
1824
+ return normalizeImageDetail(
1825
+ getObjectValue(part, "detail") ?? getObjectValue(part, "imageDetail") ?? getObjectValue(openAIProviderOptions, "detail") ?? getObjectValue(openAIProviderOptions, "imageDetail") ?? getObjectValue(openAIProviderMetadata, "detail") ?? getObjectValue(openAIProviderMetadata, "imageDetail") ?? getObjectValue(mastraMetadata, "imageDetail")
1826
+ );
1827
+ }
1828
+ function normalizeGoogleMediaResolution(value) {
1829
+ return typeof value === "string" && GOOGLE_MEDIA_RESOLUTION_VALUES.has(value) ? value : void 0;
1830
+ }
1831
+ function resolveGoogleMediaResolution(part) {
1832
+ const providerOptions = getObjectValue(getObjectValue(part, "providerOptions"), "google");
1833
+ const providerMetadata = getObjectValue(getObjectValue(part, "providerMetadata"), "google");
1834
+ const mastraMetadata = getObjectValue(getObjectValue(part, "providerMetadata"), "mastra");
1835
+ return normalizeGoogleMediaResolution(getObjectValue(part, "mediaResolution")) ?? normalizeGoogleMediaResolution(getObjectValue(providerOptions, "mediaResolution")) ?? normalizeGoogleMediaResolution(getObjectValue(providerMetadata, "mediaResolution")) ?? normalizeGoogleMediaResolution(getObjectValue(mastraMetadata, "mediaResolution")) ?? "unspecified";
1836
+ }
1837
+ function getFiniteNumber(value) {
1838
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
1839
+ }
1840
+ function isHttpUrlString(value) {
1841
+ return typeof value === "string" && /^https?:\/\//i.test(value);
1842
+ }
1843
+ function isLikelyFilesystemPath(value) {
1844
+ return value.startsWith("/") || value.startsWith("./") || value.startsWith("../") || value.startsWith("~/") || /^[A-Za-z]:[\\/]/.test(value) || value.includes("\\");
1845
+ }
1846
+ function isLikelyBase64Content(value) {
1847
+ if (value.length < 16 || value.length % 4 !== 0 || /\s/.test(value) || isLikelyFilesystemPath(value)) {
1848
+ return false;
1849
+ }
1850
+ return /^[A-Za-z0-9+/]+={0,2}$/.test(value);
1851
+ }
1852
+ function decodeImageBuffer(value) {
1853
+ if (typeof Buffer !== "undefined" && Buffer.isBuffer(value)) {
1854
+ return value;
1855
+ }
1856
+ if (value instanceof Uint8Array) {
1857
+ return Buffer.from(value);
1858
+ }
1859
+ if (value instanceof ArrayBuffer) {
1860
+ return Buffer.from(value);
1861
+ }
1862
+ if (ArrayBuffer.isView(value)) {
1863
+ return Buffer.from(value.buffer, value.byteOffset, value.byteLength);
1864
+ }
1865
+ if (typeof value !== "string" || isHttpUrlString(value)) {
1866
+ return void 0;
1867
+ }
1868
+ if (value.startsWith("data:")) {
1869
+ const commaIndex = value.indexOf(",");
1870
+ if (commaIndex === -1) return void 0;
1871
+ const header = value.slice(0, commaIndex);
1872
+ const payload = value.slice(commaIndex + 1);
1873
+ if (/;base64/i.test(header)) {
1874
+ return Buffer.from(payload, "base64");
1875
+ }
1876
+ return Buffer.from(decodeURIComponent(payload), "utf8");
1877
+ }
1878
+ if (!isLikelyBase64Content(value)) {
1879
+ return void 0;
1880
+ }
1881
+ return Buffer.from(value, "base64");
1882
+ }
1883
+ function persistImageDimensions(part, dimensions) {
1884
+ const mastraMetadata = ensurePartMastraMetadata(part);
1885
+ mastraMetadata.imageDimensions = dimensions;
1886
+ }
1887
+ function resolveHttpAssetUrl(value) {
1888
+ if (value instanceof URL) {
1889
+ return value.toString();
1890
+ }
1891
+ if (typeof value === "string" && isHttpUrlString(value)) {
1892
+ return value;
1893
+ }
1894
+ return void 0;
1895
+ }
1896
+ async function resolveImageDimensionsAsync(part) {
1897
+ const existing = resolveImageDimensions(part);
1898
+ if (existing.width && existing.height) {
1899
+ return existing;
1900
+ }
1901
+ const asset = getObjectValue(part, "image") ?? getObjectValue(part, "data");
1902
+ const url = resolveHttpAssetUrl(asset);
1903
+ if (!url) {
1904
+ return existing;
1905
+ }
1906
+ try {
1907
+ const mod = await import('probe-image-size');
1908
+ const probeImageSize = mod.default;
1909
+ const probed = await probeImageSize(url, {
1910
+ open_timeout: REMOTE_IMAGE_PROBE_TIMEOUT_MS,
1911
+ response_timeout: REMOTE_IMAGE_PROBE_TIMEOUT_MS,
1912
+ read_timeout: REMOTE_IMAGE_PROBE_TIMEOUT_MS,
1913
+ follow_max: 2
1914
+ });
1915
+ const width = existing.width ?? getFiniteNumber(probed.width);
1916
+ const height = existing.height ?? getFiniteNumber(probed.height);
1917
+ if (!width || !height) {
1918
+ return existing;
1919
+ }
1920
+ const resolved = { width, height };
1921
+ persistImageDimensions(part, resolved);
1922
+ return resolved;
1923
+ } catch {
1924
+ return existing;
1925
+ }
1926
+ }
1927
+ function resolveImageDimensions(part) {
1928
+ const mastraMetadata = getObjectValue(getObjectValue(part, "providerMetadata"), "mastra");
1929
+ const dimensions = getObjectValue(mastraMetadata, "imageDimensions");
1930
+ const width = getFiniteNumber(getObjectValue(part, "width")) ?? getFiniteNumber(getObjectValue(part, "imageWidth")) ?? getFiniteNumber(getObjectValue(dimensions, "width"));
1931
+ const height = getFiniteNumber(getObjectValue(part, "height")) ?? getFiniteNumber(getObjectValue(part, "imageHeight")) ?? getFiniteNumber(getObjectValue(dimensions, "height"));
1932
+ if (width && height) {
1933
+ return { width, height };
1934
+ }
1935
+ const asset = getObjectValue(part, "image") ?? getObjectValue(part, "data");
1936
+ const buffer = decodeImageBuffer(asset);
1937
+ if (!buffer) {
1938
+ return { width, height };
1939
+ }
1940
+ try {
1941
+ const measured = imageSize__default.default(buffer);
1942
+ const measuredWidth = getFiniteNumber(measured.width);
1943
+ const measuredHeight = getFiniteNumber(measured.height);
1944
+ if (!measuredWidth || !measuredHeight) {
1945
+ return { width, height };
1946
+ }
1947
+ const resolved = {
1948
+ width: width ?? measuredWidth,
1949
+ height: height ?? measuredHeight
1950
+ };
1951
+ persistImageDimensions(part, resolved);
1952
+ return resolved;
1953
+ } catch {
1954
+ return { width, height };
1955
+ }
1956
+ }
1957
+ function getBase64Size(base64) {
1958
+ const sanitized = base64.replace(/\s+/g, "");
1959
+ const padding = sanitized.endsWith("==") ? 2 : sanitized.endsWith("=") ? 1 : 0;
1960
+ return Math.max(0, Math.floor(sanitized.length * 3 / 4) - padding);
1961
+ }
1962
+ function resolveImageSourceStats(image) {
1963
+ if (image instanceof URL) {
1964
+ return { source: "url" };
1965
+ }
1966
+ if (typeof image === "string") {
1967
+ if (isHttpUrlString(image)) {
1968
+ return { source: "url" };
1969
+ }
1970
+ if (image.startsWith("data:")) {
1971
+ const commaIndex = image.indexOf(",");
1972
+ const encoded = commaIndex === -1 ? "" : image.slice(commaIndex + 1);
1973
+ return {
1974
+ source: "data-uri",
1975
+ sizeBytes: getBase64Size(encoded)
1976
+ };
1977
+ }
1978
+ return {
1979
+ source: "binary",
1980
+ sizeBytes: getBase64Size(image)
1981
+ };
1982
+ }
1983
+ if (typeof Buffer !== "undefined" && Buffer.isBuffer(image)) {
1984
+ return { source: "binary", sizeBytes: image.length };
1985
+ }
1986
+ if (image instanceof Uint8Array) {
1987
+ return { source: "binary", sizeBytes: image.byteLength };
1988
+ }
1989
+ if (image instanceof ArrayBuffer) {
1990
+ return { source: "binary", sizeBytes: image.byteLength };
1991
+ }
1992
+ if (ArrayBuffer.isView(image)) {
1993
+ return { source: "binary", sizeBytes: image.byteLength };
1994
+ }
1995
+ return { source: "binary" };
1996
+ }
1997
+ function getPathnameExtension(value) {
1998
+ const normalized = value.split("#", 1)[0]?.split("?", 1)[0] ?? value;
1999
+ const match = normalized.match(/\.([a-z0-9]+)$/i);
2000
+ return match?.[1]?.toLowerCase();
2001
+ }
2002
+ function hasImageFilenameExtension(filename) {
2003
+ return typeof filename === "string" && IMAGE_FILE_EXTENSIONS.has(getPathnameExtension(filename) ?? "");
2004
+ }
2005
+ function isImageLikeFilePart(part) {
2006
+ if (getObjectValue(part, "type") !== "file") {
2007
+ return false;
2008
+ }
2009
+ const mimeType = getObjectValue(part, "mimeType");
2010
+ if (typeof mimeType === "string" && mimeType.toLowerCase().startsWith("image/")) {
2011
+ return true;
2012
+ }
2013
+ const data = getObjectValue(part, "data");
2014
+ if (typeof data === "string" && data.startsWith("data:image/")) {
2015
+ return true;
2016
+ }
2017
+ if (data instanceof URL && hasImageFilenameExtension(data.pathname)) {
2018
+ return true;
2019
+ }
2020
+ if (isHttpUrlString(data)) {
2021
+ try {
2022
+ const url = new URL(data);
2023
+ if (hasImageFilenameExtension(url.pathname)) {
2024
+ return true;
2025
+ }
2026
+ } catch {
2027
+ }
2028
+ }
2029
+ return hasImageFilenameExtension(getObjectValue(part, "filename"));
2030
+ }
2031
+ function resolveProviderId(modelContext) {
2032
+ return modelContext?.provider?.toLowerCase();
2033
+ }
2034
+ function resolveModelId(modelContext) {
2035
+ return modelContext?.modelId?.toLowerCase() ?? "";
2036
+ }
2037
+ function resolveOpenAIImageEstimatorConfig(modelContext) {
2038
+ const modelId = resolveModelId(modelContext);
2039
+ if (modelId.startsWith("gpt-5") || modelId === "gpt-5-chat-latest") {
2040
+ return { baseTokens: 70, tileTokens: 140, fallbackTiles: 4 };
2041
+ }
2042
+ if (modelId.startsWith("gpt-4o-mini")) {
2043
+ return { baseTokens: 2833, tileTokens: 5667, fallbackTiles: 1 };
2044
+ }
2045
+ if (modelId.startsWith("o1") || modelId.startsWith("o3")) {
2046
+ return { baseTokens: 75, tileTokens: 150, fallbackTiles: 4 };
2047
+ }
2048
+ if (modelId.includes("computer-use")) {
2049
+ return { baseTokens: 65, tileTokens: 129, fallbackTiles: 4 };
2050
+ }
2051
+ return DEFAULT_IMAGE_ESTIMATOR;
2052
+ }
2053
+ function isGoogleGemini3Model(modelContext) {
2054
+ return resolveProviderId(modelContext) === "google" && resolveModelId(modelContext).startsWith("gemini-3");
2055
+ }
2056
+ function scaleDimensionsForOpenAIHighDetail(width, height) {
2057
+ let scaledWidth = width;
2058
+ let scaledHeight = height;
2059
+ const largestSide = Math.max(scaledWidth, scaledHeight);
2060
+ if (largestSide > 2048) {
2061
+ const ratio = 2048 / largestSide;
2062
+ scaledWidth *= ratio;
2063
+ scaledHeight *= ratio;
2064
+ }
2065
+ const shortestSide = Math.min(scaledWidth, scaledHeight);
2066
+ if (shortestSide > 768) {
2067
+ const ratio = 768 / shortestSide;
2068
+ scaledWidth *= ratio;
2069
+ scaledHeight *= ratio;
2070
+ }
2071
+ return {
2072
+ width: Math.max(1, Math.round(scaledWidth)),
2073
+ height: Math.max(1, Math.round(scaledHeight))
2074
+ };
2075
+ }
2076
+ function scaleDimensionsForAnthropic(width, height) {
2077
+ const largestSide = Math.max(width, height);
2078
+ if (largestSide <= ANTHROPIC_IMAGE_MAX_LONG_EDGE) {
2079
+ return { width, height };
2080
+ }
2081
+ const ratio = ANTHROPIC_IMAGE_MAX_LONG_EDGE / largestSide;
2082
+ return {
2083
+ width: Math.max(1, Math.round(width * ratio)),
2084
+ height: Math.max(1, Math.round(height * ratio))
2085
+ };
2086
+ }
2087
+ function estimateOpenAIHighDetailTiles(dimensions, sourceStats, estimator) {
2088
+ if (dimensions.width && dimensions.height) {
2089
+ const scaled = scaleDimensionsForOpenAIHighDetail(dimensions.width, dimensions.height);
2090
+ return Math.max(1, Math.ceil(scaled.width / 512) * Math.ceil(scaled.height / 512));
2091
+ }
2092
+ if (sourceStats.sizeBytes !== void 0) {
2093
+ if (sourceStats.sizeBytes <= 512 * 1024) return 1;
2094
+ if (sourceStats.sizeBytes <= 2 * 1024 * 1024) return 4;
2095
+ if (sourceStats.sizeBytes <= 4 * 1024 * 1024) return 6;
2096
+ return 8;
2097
+ }
2098
+ return estimator.fallbackTiles;
2099
+ }
2100
+ function resolveEffectiveOpenAIImageDetail(detail, dimensions, sourceStats) {
2101
+ if (detail === "low" || detail === "high") return detail;
2102
+ if (dimensions.width && dimensions.height) {
2103
+ return Math.max(dimensions.width, dimensions.height) > 768 ? "high" : "low";
2104
+ }
2105
+ if (sourceStats.sizeBytes !== void 0) {
2106
+ return sourceStats.sizeBytes > 1024 * 1024 ? "high" : "low";
2107
+ }
2108
+ return "low";
2109
+ }
2110
+ function estimateLegacyGoogleImageTiles(dimensions) {
2111
+ if (!dimensions.width || !dimensions.height) return 1;
2112
+ return Math.max(1, Math.ceil(dimensions.width / 768) * Math.ceil(dimensions.height / 768));
2113
+ }
2114
+ function estimateAnthropicImageTokens(dimensions, sourceStats) {
2115
+ if (dimensions.width && dimensions.height) {
2116
+ const scaled = scaleDimensionsForAnthropic(dimensions.width, dimensions.height);
2117
+ return Math.max(1, Math.ceil(scaled.width * scaled.height * ANTHROPIC_IMAGE_TOKENS_PER_PIXEL));
2118
+ }
2119
+ if (sourceStats.sizeBytes !== void 0) {
2120
+ if (sourceStats.sizeBytes <= 512 * 1024) return 341;
2121
+ if (sourceStats.sizeBytes <= 2 * 1024 * 1024) return 1366;
2122
+ if (sourceStats.sizeBytes <= 4 * 1024 * 1024) return 2048;
2123
+ return 2731;
2124
+ }
2125
+ return 1600;
2126
+ }
2127
+ function estimateGoogleImageTokens(modelContext, part, dimensions) {
2128
+ if (isGoogleGemini3Model(modelContext)) {
2129
+ const mediaResolution = resolveGoogleMediaResolution(part);
2130
+ return {
2131
+ tokens: GOOGLE_GEMINI_3_IMAGE_TOKENS_BY_RESOLUTION[mediaResolution],
2132
+ mediaResolution
2133
+ };
2134
+ }
2135
+ return {
2136
+ tokens: estimateLegacyGoogleImageTiles(dimensions) * GOOGLE_LEGACY_IMAGE_TOKENS_PER_TILE,
2137
+ mediaResolution: "unspecified"
2138
+ };
2139
+ }
2140
+ function getProviderApiKey(provider) {
2141
+ for (const envVar of PROVIDER_API_KEY_ENV_VARS[provider] ?? []) {
2142
+ const value = process.env[envVar];
2143
+ if (typeof value === "string" && value.trim().length > 0) {
2144
+ return value.trim();
2145
+ }
2146
+ }
2147
+ return void 0;
2148
+ }
2149
+ function getAttachmentFilename(part) {
2150
+ const explicitFilename = getObjectValue(part, "filename");
2151
+ if (typeof explicitFilename === "string" && explicitFilename.trim().length > 0) {
2152
+ return explicitFilename.trim();
2153
+ }
2154
+ return getFilenameFromAttachmentData(getObjectValue(part, "data") ?? getObjectValue(part, "image"));
2155
+ }
2156
+ function getAttachmentMimeType(part, fallback) {
2157
+ const mimeType = getObjectValue(part, "mimeType");
2158
+ if (typeof mimeType === "string" && mimeType.trim().length > 0) {
2159
+ return mimeType.trim();
2160
+ }
2161
+ const asset = getObjectValue(part, "data") ?? getObjectValue(part, "image");
2162
+ if (typeof asset === "string" && asset.startsWith("data:")) {
2163
+ const semicolonIndex = asset.indexOf(";");
2164
+ const commaIndex = asset.indexOf(",");
2165
+ const endIndex = semicolonIndex === -1 ? commaIndex : Math.min(semicolonIndex, commaIndex);
2166
+ if (endIndex > 5) {
2167
+ return asset.slice(5, endIndex);
2168
+ }
2169
+ }
2170
+ return fallback;
2171
+ }
2172
+ function getAttachmentUrl(asset) {
2173
+ if (asset instanceof URL) {
2174
+ return asset.toString();
2175
+ }
2176
+ if (typeof asset === "string" && /^(https?:\/\/|data:)/i.test(asset)) {
2177
+ return asset;
2178
+ }
2179
+ return void 0;
2180
+ }
2181
+ function getAttachmentFingerprint(asset) {
2182
+ const url = getAttachmentUrl(asset);
2183
+ if (url) {
2184
+ return { url };
2185
+ }
2186
+ const base64 = encodeAttachmentBase64(asset);
2187
+ if (base64) {
2188
+ return { contentHash: crypto$1.createHash("sha1").update(base64).digest("hex") };
2189
+ }
2190
+ return {};
2191
+ }
2192
+ function encodeAttachmentBase64(asset) {
2193
+ if (typeof asset === "string") {
2194
+ if (asset.startsWith("data:")) {
2195
+ const commaIndex = asset.indexOf(",");
2196
+ return commaIndex === -1 ? void 0 : asset.slice(commaIndex + 1);
2197
+ }
2198
+ if (/^https?:\/\//i.test(asset)) {
2199
+ return void 0;
2200
+ }
2201
+ return asset;
2202
+ }
2203
+ if (typeof Buffer !== "undefined" && Buffer.isBuffer(asset)) {
2204
+ return asset.toString("base64");
2205
+ }
2206
+ if (asset instanceof Uint8Array) {
2207
+ return Buffer.from(asset).toString("base64");
2208
+ }
2209
+ if (asset instanceof ArrayBuffer) {
2210
+ return Buffer.from(asset).toString("base64");
2211
+ }
2212
+ if (ArrayBuffer.isView(asset)) {
2213
+ return Buffer.from(asset.buffer, asset.byteOffset, asset.byteLength).toString("base64");
2214
+ }
2215
+ return void 0;
2216
+ }
2217
+ function createTimeoutSignal(timeoutMs) {
2218
+ const controller = new AbortController();
2219
+ const timeout = setTimeout(
2220
+ () => controller.abort(new Error(`Attachment token counting timed out after ${timeoutMs}ms`)),
2221
+ timeoutMs
2222
+ );
2223
+ const cleanup = () => clearTimeout(timeout);
2224
+ controller.signal.addEventListener("abort", cleanup, { once: true });
2225
+ return { signal: controller.signal, cleanup };
2226
+ }
2227
+ function getNumericResponseField(value, paths) {
2228
+ for (const path of paths) {
2229
+ let current = value;
2230
+ for (const segment of path) {
2231
+ current = getObjectValue(current, segment);
2232
+ if (current === void 0) break;
2233
+ }
2234
+ if (typeof current === "number" && Number.isFinite(current)) {
2235
+ return current;
2236
+ }
2237
+ }
2238
+ return void 0;
2239
+ }
2240
+ function toOpenAIInputPart(part) {
2241
+ if (getObjectValue(part, "type") === "image" || isImageLikeFilePart(part)) {
2242
+ const asset = getObjectValue(part, "image") ?? getObjectValue(part, "data");
2243
+ const imageUrl = getAttachmentUrl(asset);
2244
+ if (imageUrl) {
2245
+ return { type: "input_image", image_url: imageUrl, detail: resolveImageDetail(part) };
2246
+ }
2247
+ const base64 = encodeAttachmentBase64(asset);
2248
+ if (!base64) return void 0;
2249
+ return {
2250
+ type: "input_image",
2251
+ image_url: `data:${getAttachmentMimeType(part, "image/png")};base64,${base64}`,
2252
+ detail: resolveImageDetail(part)
2253
+ };
2254
+ }
2255
+ if (getObjectValue(part, "type") === "file") {
2256
+ const asset = getObjectValue(part, "data");
2257
+ const fileUrl = getAttachmentUrl(asset);
2258
+ return fileUrl ? {
2259
+ type: "input_file",
2260
+ file_url: fileUrl,
2261
+ filename: getAttachmentFilename(part) ?? "attachment"
2262
+ } : (() => {
2263
+ const base64 = encodeAttachmentBase64(asset);
2264
+ if (!base64) return void 0;
2265
+ return {
2266
+ type: "input_file",
2267
+ file_data: `data:${getAttachmentMimeType(part, "application/octet-stream")};base64,${base64}`,
2268
+ filename: getAttachmentFilename(part) ?? "attachment"
2269
+ };
2270
+ })();
2271
+ }
2272
+ return void 0;
2273
+ }
2274
+ function toAnthropicContentPart(part) {
2275
+ const asset = getObjectValue(part, "image") ?? getObjectValue(part, "data");
2276
+ const url = getAttachmentUrl(asset);
2277
+ if (getObjectValue(part, "type") === "image" || isImageLikeFilePart(part)) {
2278
+ return url && /^https?:\/\//i.test(url) ? { type: "image", source: { type: "url", url } } : (() => {
2279
+ const base64 = encodeAttachmentBase64(asset);
2280
+ if (!base64) return void 0;
2281
+ return {
2282
+ type: "image",
2283
+ source: { type: "base64", media_type: getAttachmentMimeType(part, "image/png"), data: base64 }
2284
+ };
2285
+ })();
2286
+ }
2287
+ if (getObjectValue(part, "type") === "file") {
2288
+ return url && /^https?:\/\//i.test(url) ? { type: "document", source: { type: "url", url } } : (() => {
2289
+ const base64 = encodeAttachmentBase64(asset);
2290
+ if (!base64) return void 0;
2291
+ return {
2292
+ type: "document",
2293
+ source: { type: "base64", media_type: getAttachmentMimeType(part, "application/pdf"), data: base64 }
2294
+ };
2295
+ })();
2296
+ }
2297
+ return void 0;
2298
+ }
2299
+ function toGooglePart(part) {
2300
+ const asset = getObjectValue(part, "image") ?? getObjectValue(part, "data");
2301
+ const url = getAttachmentUrl(asset);
2302
+ const mimeType = getAttachmentMimeType(
2303
+ part,
2304
+ getObjectValue(part, "type") === "file" && !isImageLikeFilePart(part) ? "application/pdf" : "image/png"
2305
+ );
2306
+ if (url && !url.startsWith("data:")) {
2307
+ return { fileData: { mimeType, fileUri: url } };
2308
+ }
2309
+ const base64 = encodeAttachmentBase64(asset);
2310
+ if (!base64) return void 0;
2311
+ return { inlineData: { mimeType, data: base64 } };
2312
+ }
2313
+ async function fetchOpenAIAttachmentTokenEstimate(modelId, part) {
2314
+ const apiKey = getProviderApiKey("openai");
2315
+ const inputPart = toOpenAIInputPart(part);
2316
+ if (!apiKey || !inputPart) return void 0;
2317
+ const { signal, cleanup } = createTimeoutSignal(ATTACHMENT_COUNT_TIMEOUT_MS);
2318
+ try {
2319
+ const response = await fetch("https://api.openai.com/v1/responses/input_tokens", {
2320
+ method: "POST",
2321
+ headers: {
2322
+ Authorization: `Bearer ${apiKey}`,
2323
+ "Content-Type": "application/json"
2324
+ },
2325
+ body: JSON.stringify({
2326
+ model: modelId,
2327
+ input: [{ type: "message", role: "user", content: [inputPart] }]
2328
+ }),
2329
+ signal
2330
+ });
2331
+ if (!response.ok) return void 0;
2332
+ const body = await response.json();
2333
+ return getNumericResponseField(body, [
2334
+ ["input_tokens"],
2335
+ ["total_tokens"],
2336
+ ["usage", "input_tokens"],
2337
+ ["usage", "total_tokens"]
2338
+ ]);
2339
+ } finally {
2340
+ cleanup();
2341
+ }
2342
+ }
2343
+ async function fetchAnthropicAttachmentTokenEstimate(modelId, part) {
2344
+ const apiKey = getProviderApiKey("anthropic");
2345
+ const contentPart = toAnthropicContentPart(part);
2346
+ if (!apiKey || !contentPart) return void 0;
2347
+ const { signal, cleanup } = createTimeoutSignal(ATTACHMENT_COUNT_TIMEOUT_MS);
2348
+ try {
2349
+ const response = await fetch("https://api.anthropic.com/v1/messages/count_tokens", {
2350
+ method: "POST",
2351
+ headers: {
2352
+ "x-api-key": apiKey,
2353
+ "anthropic-version": "2023-06-01",
2354
+ "Content-Type": "application/json"
2355
+ },
2356
+ body: JSON.stringify({
2357
+ model: modelId,
2358
+ messages: [{ role: "user", content: [contentPart] }]
2359
+ }),
2360
+ signal
2361
+ });
2362
+ if (!response.ok) return void 0;
2363
+ const body = await response.json();
2364
+ return getNumericResponseField(body, [["input_tokens"]]);
2365
+ } finally {
2366
+ cleanup();
2367
+ }
2368
+ }
2369
+ async function fetchGoogleAttachmentTokenEstimate(modelId, part) {
2370
+ const apiKey = getProviderApiKey("google");
2371
+ const googlePart = toGooglePart(part);
2372
+ if (!apiKey || !googlePart) return void 0;
2373
+ const { signal, cleanup } = createTimeoutSignal(ATTACHMENT_COUNT_TIMEOUT_MS);
2374
+ try {
2375
+ const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${modelId}:countTokens`, {
2376
+ method: "POST",
2377
+ headers: {
2378
+ "x-goog-api-key": apiKey,
2379
+ "Content-Type": "application/json"
2380
+ },
2381
+ body: JSON.stringify({
2382
+ contents: [{ role: "user", parts: [googlePart] }]
2383
+ }),
2384
+ signal
2385
+ });
2386
+ if (!response.ok) return void 0;
2387
+ const body = await response.json();
2388
+ return getNumericResponseField(body, [["totalTokens"], ["total_tokens"]]);
2389
+ } finally {
2390
+ cleanup();
2391
+ }
2392
+ }
1408
2393
  var TokenCounter = class _TokenCounter {
1409
2394
  encoder;
1410
2395
  cacheSource;
2396
+ defaultModelContext;
2397
+ modelContextStorage = new async_hooks.AsyncLocalStorage();
2398
+ inFlightAttachmentCounts = /* @__PURE__ */ new Map();
1411
2399
  // Per-message overhead: accounts for role tokens, message framing, and separators.
1412
2400
  // Empirically derived from OpenAI's token counting guide (3 tokens per message base +
1413
2401
  // fractional overhead from name/role encoding). 3.8 is a practical average across models.
1414
2402
  static TOKENS_PER_MESSAGE = 3.8;
1415
2403
  // Conversation-level overhead: system prompt framing, reply priming tokens, etc.
1416
2404
  static TOKENS_PER_CONVERSATION = 24;
1417
- constructor(encoding) {
2405
+ constructor(encoding, options) {
1418
2406
  this.encoder = encoding ? new lite.Tiktoken(encoding) : getDefaultEncoder();
1419
2407
  this.cacheSource = `v${TOKEN_ESTIMATE_CACHE_VERSION}:${resolveEncodingId(encoding)}`;
2408
+ this.defaultModelContext = parseModelContext(options?.model);
2409
+ }
2410
+ runWithModelContext(model, fn) {
2411
+ return this.modelContextStorage.run(parseModelContext(model), fn);
2412
+ }
2413
+ getModelContext() {
2414
+ return this.modelContextStorage.getStore() ?? this.defaultModelContext;
1420
2415
  }
1421
2416
  /**
1422
2417
  * Count tokens in a plain string
@@ -1440,6 +2435,20 @@ var TokenCounter = class _TokenCounter {
1440
2435
  });
1441
2436
  return tokens;
1442
2437
  }
2438
+ readOrPersistFixedPartEstimate(part, kind, payload, tokens) {
2439
+ const key = buildEstimateKey(kind, payload);
2440
+ const cached = getPartCacheEntry(part, key);
2441
+ if (isValidCacheEntry(cached, key, this.cacheSource)) {
2442
+ return cached.tokens;
2443
+ }
2444
+ setPartCacheEntry(part, key, {
2445
+ v: TOKEN_ESTIMATE_CACHE_VERSION,
2446
+ source: this.cacheSource,
2447
+ key,
2448
+ tokens
2449
+ });
2450
+ return tokens;
2451
+ }
1443
2452
  readOrPersistMessageEstimate(message, kind, payload) {
1444
2453
  const key = buildEstimateKey(kind, payload);
1445
2454
  const cached = getMessageCacheEntry(message, key);
@@ -1463,15 +2472,315 @@ var TokenCounter = class _TokenCounter {
1463
2472
  usingStoredModelOutput: true
1464
2473
  };
1465
2474
  }
1466
- return {
1467
- value: invocationResult,
1468
- usingStoredModelOutput: false
1469
- };
2475
+ return {
2476
+ value: invocationResult,
2477
+ usingStoredModelOutput: false
2478
+ };
2479
+ }
2480
+ estimateImageAssetTokens(part, asset, kind) {
2481
+ const modelContext = this.getModelContext();
2482
+ const provider = resolveProviderId(modelContext);
2483
+ const modelId = modelContext?.modelId ?? null;
2484
+ const detail = resolveImageDetail(part);
2485
+ const dimensions = resolveImageDimensions(part);
2486
+ const sourceStats = resolveImageSourceStats(asset);
2487
+ if (provider === "google") {
2488
+ const googleEstimate = estimateGoogleImageTokens(modelContext, part, dimensions);
2489
+ return {
2490
+ tokens: googleEstimate.tokens,
2491
+ cachePayload: JSON.stringify({
2492
+ kind,
2493
+ provider,
2494
+ modelId,
2495
+ estimator: isGoogleGemini3Model(modelContext) ? "google-gemini-3" : "google-legacy",
2496
+ mediaResolution: googleEstimate.mediaResolution,
2497
+ width: dimensions.width ?? null,
2498
+ height: dimensions.height ?? null,
2499
+ source: sourceStats.source,
2500
+ sizeBytes: sourceStats.sizeBytes ?? null,
2501
+ mimeType: getObjectValue(part, "mimeType") ?? null,
2502
+ filename: getObjectValue(part, "filename") ?? null
2503
+ })
2504
+ };
2505
+ }
2506
+ if (provider === "anthropic") {
2507
+ return {
2508
+ tokens: estimateAnthropicImageTokens(dimensions, sourceStats),
2509
+ cachePayload: JSON.stringify({
2510
+ kind,
2511
+ provider,
2512
+ modelId,
2513
+ estimator: "anthropic",
2514
+ width: dimensions.width ?? null,
2515
+ height: dimensions.height ?? null,
2516
+ source: sourceStats.source,
2517
+ sizeBytes: sourceStats.sizeBytes ?? null,
2518
+ mimeType: getObjectValue(part, "mimeType") ?? null,
2519
+ filename: getObjectValue(part, "filename") ?? null
2520
+ })
2521
+ };
2522
+ }
2523
+ const estimator = resolveOpenAIImageEstimatorConfig(modelContext);
2524
+ const effectiveDetail = resolveEffectiveOpenAIImageDetail(detail, dimensions, sourceStats);
2525
+ const tiles = effectiveDetail === "high" ? estimateOpenAIHighDetailTiles(dimensions, sourceStats, estimator) : 0;
2526
+ const tokens = estimator.baseTokens + tiles * estimator.tileTokens;
2527
+ return {
2528
+ tokens,
2529
+ cachePayload: JSON.stringify({
2530
+ kind,
2531
+ provider,
2532
+ modelId,
2533
+ estimator: provider === "openai" ? "openai" : "fallback",
2534
+ detail,
2535
+ effectiveDetail,
2536
+ width: dimensions.width ?? null,
2537
+ height: dimensions.height ?? null,
2538
+ source: sourceStats.source,
2539
+ sizeBytes: sourceStats.sizeBytes ?? null,
2540
+ mimeType: getObjectValue(part, "mimeType") ?? null,
2541
+ filename: getObjectValue(part, "filename") ?? null
2542
+ })
2543
+ };
2544
+ }
2545
+ estimateImageTokens(part) {
2546
+ return this.estimateImageAssetTokens(part, part.image, "image");
2547
+ }
2548
+ estimateImageLikeFileTokens(part) {
2549
+ return this.estimateImageAssetTokens(part, part.data, "file");
2550
+ }
2551
+ countAttachmentPartSync(part) {
2552
+ if (part.type === "image") {
2553
+ const estimate = this.estimateImageTokens(part);
2554
+ return this.readOrPersistFixedPartEstimate(part, "image", estimate.cachePayload, estimate.tokens);
2555
+ }
2556
+ if (part.type === "file" && isImageLikeFilePart(part)) {
2557
+ const estimate = this.estimateImageLikeFileTokens(part);
2558
+ return this.readOrPersistFixedPartEstimate(part, "image-like-file", estimate.cachePayload, estimate.tokens);
2559
+ }
2560
+ if (part.type === "file") {
2561
+ return this.readOrPersistPartEstimate(part, "file-descriptor", serializeNonImageFilePartForTokenCounting(part));
2562
+ }
2563
+ return void 0;
2564
+ }
2565
+ buildRemoteAttachmentCachePayload(part) {
2566
+ const isImageAttachment = part.type === "image" || part.type === "file" && isImageLikeFilePart(part);
2567
+ const isNonImageFileAttachment = part.type === "file" && !isImageAttachment;
2568
+ if (!isImageAttachment && !isNonImageFileAttachment) {
2569
+ return void 0;
2570
+ }
2571
+ const modelContext = this.getModelContext();
2572
+ const provider = resolveProviderId(modelContext);
2573
+ const modelId = modelContext?.modelId ?? null;
2574
+ if (!provider || !modelId || !["openai", "google", "anthropic"].includes(provider)) {
2575
+ return void 0;
2576
+ }
2577
+ const asset = getObjectValue(part, "image") ?? getObjectValue(part, "data");
2578
+ const sourceStats = resolveImageSourceStats(asset);
2579
+ const fingerprint = getAttachmentFingerprint(asset);
2580
+ return JSON.stringify({
2581
+ strategy: "provider-endpoint",
2582
+ provider,
2583
+ modelId,
2584
+ type: getObjectValue(part, "type") ?? null,
2585
+ detail: isImageAttachment ? resolveImageDetail(part) : null,
2586
+ mediaResolution: provider === "google" && isImageAttachment ? resolveGoogleMediaResolution(part) : null,
2587
+ mimeType: getAttachmentMimeType(part, isNonImageFileAttachment ? "application/pdf" : "image/png"),
2588
+ filename: getAttachmentFilename(part) ?? null,
2589
+ source: sourceStats.source,
2590
+ sizeBytes: sourceStats.sizeBytes ?? null,
2591
+ assetUrl: fingerprint.url ?? null,
2592
+ assetHash: fingerprint.contentHash ?? null
2593
+ });
2594
+ }
2595
+ async fetchProviderAttachmentTokenEstimate(part) {
2596
+ const modelContext = this.getModelContext();
2597
+ const provider = resolveProviderId(modelContext);
2598
+ const modelId = modelContext?.modelId;
2599
+ if (!provider || !modelId) return void 0;
2600
+ try {
2601
+ if (provider === "openai") {
2602
+ return await fetchOpenAIAttachmentTokenEstimate(modelId, part);
2603
+ }
2604
+ if (provider === "google") {
2605
+ return await fetchGoogleAttachmentTokenEstimate(modelId, part);
2606
+ }
2607
+ if (provider === "anthropic") {
2608
+ return await fetchAnthropicAttachmentTokenEstimate(modelId, part);
2609
+ }
2610
+ } catch {
2611
+ return void 0;
2612
+ }
2613
+ return void 0;
2614
+ }
2615
+ async countAttachmentPartAsync(part) {
2616
+ const isImageAttachment = part.type === "image" || part.type === "file" && isImageLikeFilePart(part);
2617
+ const remotePayload = this.buildRemoteAttachmentCachePayload(part);
2618
+ if (remotePayload) {
2619
+ const remoteKey = buildEstimateKey("attachment-provider", remotePayload);
2620
+ const cachedRemote = getPartCacheEntry(part, remoteKey);
2621
+ if (isValidCacheEntry(cachedRemote, remoteKey, this.cacheSource)) {
2622
+ return cachedRemote.tokens;
2623
+ }
2624
+ const existingRequest = this.inFlightAttachmentCounts.get(remoteKey);
2625
+ if (existingRequest) {
2626
+ const remoteTokens = await existingRequest;
2627
+ if (typeof remoteTokens === "number" && Number.isFinite(remoteTokens) && remoteTokens > 0) {
2628
+ setPartCacheEntry(part, remoteKey, {
2629
+ v: TOKEN_ESTIMATE_CACHE_VERSION,
2630
+ source: this.cacheSource,
2631
+ key: remoteKey,
2632
+ tokens: remoteTokens
2633
+ });
2634
+ return remoteTokens;
2635
+ }
2636
+ } else {
2637
+ const remoteRequest = this.fetchProviderAttachmentTokenEstimate(part);
2638
+ this.inFlightAttachmentCounts.set(remoteKey, remoteRequest);
2639
+ let remoteTokens;
2640
+ try {
2641
+ remoteTokens = await remoteRequest;
2642
+ } finally {
2643
+ this.inFlightAttachmentCounts.delete(remoteKey);
2644
+ }
2645
+ if (typeof remoteTokens === "number" && Number.isFinite(remoteTokens) && remoteTokens > 0) {
2646
+ setPartCacheEntry(part, remoteKey, {
2647
+ v: TOKEN_ESTIMATE_CACHE_VERSION,
2648
+ source: this.cacheSource,
2649
+ key: remoteKey,
2650
+ tokens: remoteTokens
2651
+ });
2652
+ return remoteTokens;
2653
+ }
2654
+ }
2655
+ if (isImageAttachment) {
2656
+ await resolveImageDimensionsAsync(part);
2657
+ }
2658
+ const fallbackPayload = JSON.stringify({
2659
+ ...JSON.parse(remotePayload),
2660
+ strategy: "local-fallback",
2661
+ ...isImageAttachment ? resolveImageDimensions(part) : {}
2662
+ });
2663
+ const fallbackKey = buildEstimateKey("attachment-provider", fallbackPayload);
2664
+ const cachedFallback = getPartCacheEntry(part, fallbackKey);
2665
+ if (isValidCacheEntry(cachedFallback, fallbackKey, this.cacheSource)) {
2666
+ return cachedFallback.tokens;
2667
+ }
2668
+ const localTokens2 = this.countAttachmentPartSync(part);
2669
+ if (localTokens2 === void 0) {
2670
+ return void 0;
2671
+ }
2672
+ setPartCacheEntry(part, fallbackKey, {
2673
+ v: TOKEN_ESTIMATE_CACHE_VERSION,
2674
+ source: this.cacheSource,
2675
+ key: fallbackKey,
2676
+ tokens: localTokens2
2677
+ });
2678
+ return localTokens2;
2679
+ }
2680
+ if (isImageAttachment) {
2681
+ await resolveImageDimensionsAsync(part);
2682
+ }
2683
+ const localTokens = this.countAttachmentPartSync(part);
2684
+ return localTokens;
2685
+ }
2686
+ countNonAttachmentPart(part) {
2687
+ let overheadDelta = 0;
2688
+ let toolResultDelta = 0;
2689
+ if (part.type === "text") {
2690
+ return { tokens: this.readOrPersistPartEstimate(part, "text", part.text), overheadDelta, toolResultDelta };
2691
+ }
2692
+ if (part.type === "tool-invocation") {
2693
+ const invocation = part.toolInvocation;
2694
+ let tokens = 0;
2695
+ if (invocation.state === "call" || invocation.state === "partial-call") {
2696
+ if (invocation.toolName) {
2697
+ tokens += this.readOrPersistPartEstimate(part, `tool-${invocation.state}-name`, invocation.toolName);
2698
+ }
2699
+ if (invocation.args) {
2700
+ if (typeof invocation.args === "string") {
2701
+ tokens += this.readOrPersistPartEstimate(part, `tool-${invocation.state}-args`, invocation.args);
2702
+ } else {
2703
+ const argsJson = JSON.stringify(invocation.args);
2704
+ tokens += this.readOrPersistPartEstimate(part, `tool-${invocation.state}-args-json`, argsJson);
2705
+ overheadDelta -= 12;
2706
+ }
2707
+ }
2708
+ return { tokens, overheadDelta, toolResultDelta };
2709
+ }
2710
+ if (invocation.state === "result") {
2711
+ toolResultDelta++;
2712
+ const { value: resultForCounting, usingStoredModelOutput } = this.resolveToolResultForTokenCounting(
2713
+ part,
2714
+ invocation.result
2715
+ );
2716
+ if (resultForCounting !== void 0) {
2717
+ if (typeof resultForCounting === "string") {
2718
+ tokens += this.readOrPersistPartEstimate(
2719
+ part,
2720
+ usingStoredModelOutput ? "tool-result-model-output" : "tool-result",
2721
+ resultForCounting
2722
+ );
2723
+ } else {
2724
+ const resultJson = JSON.stringify(resultForCounting);
2725
+ tokens += this.readOrPersistPartEstimate(
2726
+ part,
2727
+ usingStoredModelOutput ? "tool-result-model-output-json" : "tool-result-json",
2728
+ resultJson
2729
+ );
2730
+ overheadDelta -= 12;
2731
+ }
2732
+ }
2733
+ return { tokens, overheadDelta, toolResultDelta };
2734
+ }
2735
+ throw new Error(
2736
+ `Unhandled tool-invocation state '${part.toolInvocation?.state}' in token counting for part type '${part.type}'`
2737
+ );
2738
+ }
2739
+ if (typeof part.type === "string" && part.type.startsWith("data-")) {
2740
+ return { tokens: 0, overheadDelta, toolResultDelta };
2741
+ }
2742
+ if (part.type === "reasoning") {
2743
+ return { tokens: 0, overheadDelta, toolResultDelta };
2744
+ }
2745
+ const serialized = serializePartForTokenCounting(part);
2746
+ return {
2747
+ tokens: this.readOrPersistPartEstimate(part, `part-${part.type}`, serialized),
2748
+ overheadDelta,
2749
+ toolResultDelta
2750
+ };
2751
+ }
2752
+ /**
2753
+ * Count tokens in a single message
2754
+ */
2755
+ countMessage(message) {
2756
+ let payloadTokens = this.countString(message.role);
2757
+ let overhead = _TokenCounter.TOKENS_PER_MESSAGE;
2758
+ let toolResultCount = 0;
2759
+ if (typeof message.content === "string") {
2760
+ payloadTokens += this.readOrPersistMessageEstimate(message, "message-content", message.content);
2761
+ } else if (message.content && typeof message.content === "object") {
2762
+ if (message.content.content && !Array.isArray(message.content.parts)) {
2763
+ payloadTokens += this.readOrPersistMessageEstimate(message, "content-content", message.content.content);
2764
+ } else if (Array.isArray(message.content.parts)) {
2765
+ for (const part of message.content.parts) {
2766
+ const attachmentTokens = this.countAttachmentPartSync(part);
2767
+ if (attachmentTokens !== void 0) {
2768
+ payloadTokens += attachmentTokens;
2769
+ continue;
2770
+ }
2771
+ const result = this.countNonAttachmentPart(part);
2772
+ payloadTokens += result.tokens;
2773
+ overhead += result.overheadDelta;
2774
+ toolResultCount += result.toolResultDelta;
2775
+ }
2776
+ }
2777
+ }
2778
+ if (toolResultCount > 0) {
2779
+ overhead += toolResultCount * _TokenCounter.TOKENS_PER_MESSAGE;
2780
+ }
2781
+ return Math.round(payloadTokens + overhead);
1470
2782
  }
1471
- /**
1472
- * Count tokens in a single message
1473
- */
1474
- countMessage(message) {
2783
+ async countMessageAsync(message) {
1475
2784
  let payloadTokens = this.countString(message.role);
1476
2785
  let overhead = _TokenCounter.TOKENS_PER_MESSAGE;
1477
2786
  let toolResultCount = 0;
@@ -1482,63 +2791,15 @@ var TokenCounter = class _TokenCounter {
1482
2791
  payloadTokens += this.readOrPersistMessageEstimate(message, "content-content", message.content.content);
1483
2792
  } else if (Array.isArray(message.content.parts)) {
1484
2793
  for (const part of message.content.parts) {
1485
- if (part.type === "text") {
1486
- payloadTokens += this.readOrPersistPartEstimate(part, "text", part.text);
1487
- } else if (part.type === "tool-invocation") {
1488
- const invocation = part.toolInvocation;
1489
- if (invocation.state === "call" || invocation.state === "partial-call") {
1490
- if (invocation.toolName) {
1491
- payloadTokens += this.readOrPersistPartEstimate(
1492
- part,
1493
- `tool-${invocation.state}-name`,
1494
- invocation.toolName
1495
- );
1496
- }
1497
- if (invocation.args) {
1498
- if (typeof invocation.args === "string") {
1499
- payloadTokens += this.readOrPersistPartEstimate(
1500
- part,
1501
- `tool-${invocation.state}-args`,
1502
- invocation.args
1503
- );
1504
- } else {
1505
- const argsJson = JSON.stringify(invocation.args);
1506
- payloadTokens += this.readOrPersistPartEstimate(part, `tool-${invocation.state}-args-json`, argsJson);
1507
- overhead -= 12;
1508
- }
1509
- }
1510
- } else if (invocation.state === "result") {
1511
- toolResultCount++;
1512
- const { value: resultForCounting, usingStoredModelOutput } = this.resolveToolResultForTokenCounting(
1513
- part,
1514
- invocation.result
1515
- );
1516
- if (resultForCounting !== void 0) {
1517
- if (typeof resultForCounting === "string") {
1518
- payloadTokens += this.readOrPersistPartEstimate(
1519
- part,
1520
- usingStoredModelOutput ? "tool-result-model-output" : "tool-result",
1521
- resultForCounting
1522
- );
1523
- } else {
1524
- const resultJson = JSON.stringify(resultForCounting);
1525
- payloadTokens += this.readOrPersistPartEstimate(
1526
- part,
1527
- usingStoredModelOutput ? "tool-result-model-output-json" : "tool-result-json",
1528
- resultJson
1529
- );
1530
- overhead -= 12;
1531
- }
1532
- }
1533
- } else {
1534
- throw new Error(
1535
- `Unhandled tool-invocation state '${part.toolInvocation?.state}' in token counting for part type '${part.type}'`
1536
- );
1537
- }
1538
- } else if (typeof part.type === "string" && part.type.startsWith("data-")) ; else if (part.type === "reasoning") ; else {
1539
- const serialized = serializePartForTokenCounting(part);
1540
- payloadTokens += this.readOrPersistPartEstimate(part, `part-${part.type}`, serialized);
2794
+ const attachmentTokens = await this.countAttachmentPartAsync(part);
2795
+ if (attachmentTokens !== void 0) {
2796
+ payloadTokens += attachmentTokens;
2797
+ continue;
1541
2798
  }
2799
+ const result = this.countNonAttachmentPart(part);
2800
+ payloadTokens += result.tokens;
2801
+ overhead += result.overheadDelta;
2802
+ toolResultCount += result.toolResultDelta;
1542
2803
  }
1543
2804
  }
1544
2805
  }
@@ -1558,6 +2819,11 @@ var TokenCounter = class _TokenCounter {
1558
2819
  }
1559
2820
  return total;
1560
2821
  }
2822
+ async countMessagesAsync(messages) {
2823
+ if (!messages || messages.length === 0) return 0;
2824
+ const messageTotals = await Promise.all(messages.map((message) => this.countMessageAsync(message)));
2825
+ return _TokenCounter.TOKENS_PER_CONVERSATION + messageTotals.reduce((sum, count) => sum + count, 0);
2826
+ }
1561
2827
  /**
1562
2828
  * Count tokens in observations string
1563
2829
  */
@@ -1815,6 +3081,41 @@ var ObservationalMemory = class _ObservationalMemory {
1815
3081
  }
1816
3082
  return [];
1817
3083
  }
3084
+ /**
3085
+ * Refresh per-chunk messageTokens from the current in-memory message list.
3086
+ *
3087
+ * Buffered chunks store a messageTokens snapshot from when they were created,
3088
+ * but messages can be edited/sealed between buffering and activation, changing
3089
+ * their token weight. Using stale weights causes projected-removal math to
3090
+ * over- or under-estimate, leading to skipped activations or over-activation.
3091
+ *
3092
+ * Token recount only runs when the full chunk is present in the message list.
3093
+ * Partial recount is skipped because it would undercount and could cause
3094
+ * over-activation of buffered chunks.
3095
+ */
3096
+ refreshBufferedChunkMessageTokens(chunks, messageList) {
3097
+ const allMessages = messageList.get.all.db();
3098
+ const messageMap = new Map(allMessages.filter((m) => m?.id).map((m) => [m.id, m]));
3099
+ return chunks.map((chunk) => {
3100
+ const chunkMessages = chunk.messageIds.map((id) => messageMap.get(id)).filter((m) => !!m);
3101
+ if (chunkMessages.length !== chunk.messageIds.length) {
3102
+ return chunk;
3103
+ }
3104
+ const refreshedTokens = this.tokenCounter.countMessages(chunkMessages);
3105
+ const refreshedMessageTokens = chunk.messageIds.reduce((acc, id) => {
3106
+ const msg = messageMap.get(id);
3107
+ if (msg) {
3108
+ acc[id] = this.tokenCounter.countMessages([msg]);
3109
+ }
3110
+ return acc;
3111
+ }, {});
3112
+ return {
3113
+ ...chunk,
3114
+ messageTokens: refreshedTokens,
3115
+ messageTokenCounts: refreshedMessageTokens
3116
+ };
3117
+ });
3118
+ }
1818
3119
  /**
1819
3120
  * Check if we've crossed a new bufferTokens interval boundary.
1820
3121
  * Returns true if async buffering should be triggered.
@@ -1911,6 +3212,11 @@ var ObservationalMemory = class _ObservationalMemory {
1911
3212
  return `thread:${threadId ?? "unknown"}`;
1912
3213
  }
1913
3214
  constructor(config) {
3215
+ if (!features.coreFeatures.has("request-response-id-rotation")) {
3216
+ throw new Error(
3217
+ "Observational memory requires @mastra/core support for request-response-id-rotation. Please bump @mastra/core to a newer version."
3218
+ );
3219
+ }
1914
3220
  if (config.model && config.observation?.model) {
1915
3221
  throw new Error(
1916
3222
  "Cannot set both `model` and `observation.model`. Use `model` to set both agents, or set each individually."
@@ -2008,7 +3314,9 @@ Async buffering is enabled by default \u2014 this opt-out is only needed when us
2008
3314
  ),
2009
3315
  instruction: config.reflection?.instruction
2010
3316
  };
2011
- this.tokenCounter = new TokenCounter();
3317
+ this.tokenCounter = new TokenCounter(void 0, {
3318
+ model: typeof observationModel === "string" ? observationModel : void 0
3319
+ });
2012
3320
  this.onDebugEvent = config.onDebugEvent;
2013
3321
  this.messageHistory = new processors.MessageHistory({ storage: this.storage });
2014
3322
  this.validateBufferConfig();
@@ -2038,25 +3346,59 @@ Async buffering is enabled by default \u2014 this opt-out is only needed when us
2038
3346
  async waitForBuffering(threadId, resourceId, timeoutMs = 3e4) {
2039
3347
  return _ObservationalMemory.awaitBuffering(threadId, resourceId, this.scope, timeoutMs);
2040
3348
  }
3349
+ getModelToResolve(model) {
3350
+ if (Array.isArray(model)) {
3351
+ return model[0]?.model ?? "unknown";
3352
+ }
3353
+ if (typeof model === "function") {
3354
+ return async (ctx) => {
3355
+ const result = await model(ctx);
3356
+ if (Array.isArray(result)) {
3357
+ return result[0]?.model ?? "unknown";
3358
+ }
3359
+ return result;
3360
+ };
3361
+ }
3362
+ return model;
3363
+ }
3364
+ formatModelName(model) {
3365
+ if (!model.modelId) {
3366
+ return "(unknown)";
3367
+ }
3368
+ return model.provider ? `${model.provider}/${model.modelId}` : model.modelId;
3369
+ }
3370
+ async resolveModelContext(modelConfig, requestContext) {
3371
+ const modelToResolve = this.getModelToResolve(modelConfig);
3372
+ if (!modelToResolve) {
3373
+ return void 0;
3374
+ }
3375
+ const resolved = await llm.resolveModelConfig(modelToResolve, requestContext);
3376
+ return {
3377
+ provider: resolved.provider,
3378
+ modelId: resolved.modelId
3379
+ };
3380
+ }
3381
+ getRuntimeModelContext(model) {
3382
+ if (!model?.modelId) {
3383
+ return void 0;
3384
+ }
3385
+ return {
3386
+ provider: model.provider,
3387
+ modelId: model.modelId
3388
+ };
3389
+ }
3390
+ runWithTokenCounterModelContext(modelContext, fn) {
3391
+ return this.tokenCounter.runWithModelContext(modelContext, fn);
3392
+ }
2041
3393
  /**
2042
3394
  * Get the full config including resolved model names.
2043
3395
  * This is async because it needs to resolve the model configs.
2044
3396
  */
2045
3397
  async getResolvedConfig(requestContext) {
2046
- const getModelToResolve = (model) => {
2047
- if (Array.isArray(model)) {
2048
- return model[0]?.model ?? "unknown";
2049
- }
2050
- return model;
2051
- };
2052
- const formatModelName = (model) => {
2053
- return model.provider ? `${model.provider}/${model.modelId}` : model.modelId;
2054
- };
2055
3398
  const safeResolveModel = async (modelConfig) => {
2056
- const modelToResolve = getModelToResolve(modelConfig);
2057
3399
  try {
2058
- const resolved = await llm.resolveModelConfig(modelToResolve, requestContext);
2059
- return formatModelName(resolved);
3400
+ const resolved = await this.resolveModelContext(modelConfig, requestContext);
3401
+ return resolved?.modelId ? this.formatModelName(resolved) : "(unknown)";
2060
3402
  } catch (error) {
2061
3403
  omError("[OM] Failed to resolve model config", error);
2062
3404
  return "(unknown)";
@@ -2537,10 +3879,16 @@ Async buffering is enabled by default \u2014 this opt-out is only needed when us
2537
3879
  */
2538
3880
  async callObserver(existingObservations, messagesToObserve, abortSignal, options) {
2539
3881
  const agent = this.getObserverAgent();
2540
- const prompt = buildObserverPrompt(existingObservations, messagesToObserve, options);
3882
+ const observerMessages = [
3883
+ {
3884
+ role: "user",
3885
+ content: buildObserverTaskPrompt(existingObservations, options)
3886
+ },
3887
+ buildObserverHistoryMessage(messagesToObserve)
3888
+ ];
2541
3889
  const doGenerate = async () => {
2542
3890
  return this.withAbortCheck(async () => {
2543
- const streamResult = await agent.stream(prompt, {
3891
+ const streamResult = await agent.stream(observerMessages, {
2544
3892
  modelSettings: {
2545
3893
  ...this.observationConfig.modelSettings
2546
3894
  },
@@ -2587,7 +3935,13 @@ Async buffering is enabled by default \u2014 this opt-out is only needed when us
2587
3935
  model: this.observationConfig.model,
2588
3936
  instructions: buildObserverSystemPrompt(true, this.observationConfig.instruction)
2589
3937
  });
2590
- const prompt = buildMultiThreadObserverPrompt(existingObservations, messagesByThread, threadOrder);
3938
+ const observerMessages = [
3939
+ {
3940
+ role: "user",
3941
+ content: buildMultiThreadObserverTaskPrompt(existingObservations)
3942
+ },
3943
+ buildMultiThreadObserverHistoryMessage(messagesByThread, threadOrder)
3944
+ ];
2591
3945
  const allMessages = [];
2592
3946
  for (const msgs of messagesByThread.values()) {
2593
3947
  allMessages.push(...msgs);
@@ -2597,7 +3951,7 @@ Async buffering is enabled by default \u2014 this opt-out is only needed when us
2597
3951
  }
2598
3952
  const doGenerate = async () => {
2599
3953
  return this.withAbortCheck(async () => {
2600
- const streamResult = await agent$1.stream(prompt, {
3954
+ const streamResult = await agent$1.stream(observerMessages, {
2601
3955
  modelSettings: {
2602
3956
  ...this.observationConfig.modelSettings
2603
3957
  },
@@ -2875,8 +4229,8 @@ ${suggestedResponse}
2875
4229
  /**
2876
4230
  * Calculate all threshold-related values for observation decision making.
2877
4231
  */
2878
- calculateObservationThresholds(_allMessages, unobservedMessages, _pendingTokens, otherThreadTokens, currentObservationTokens, _record) {
2879
- const contextWindowTokens = this.tokenCounter.countMessages(unobservedMessages);
4232
+ async calculateObservationThresholds(_allMessages, unobservedMessages, _pendingTokens, otherThreadTokens, currentObservationTokens, _record) {
4233
+ const contextWindowTokens = await this.tokenCounter.countMessagesAsync(unobservedMessages);
2880
4234
  const totalPendingTokens = Math.max(0, contextWindowTokens + otherThreadTokens);
2881
4235
  const threshold = calculateDynamicThreshold(this.observationConfig.messageTokens, currentObservationTokens);
2882
4236
  const baseReflectionThreshold = getMaxThreshold(this.reflectionConfig.observationTokens);
@@ -2986,7 +4340,7 @@ ${suggestedResponse}
2986
4340
  let freshRecord = await this.getOrCreateRecord(threadId, resourceId);
2987
4341
  const freshAllMessages = messageList.get.all.db();
2988
4342
  let freshUnobservedMessages = this.getUnobservedMessages(freshAllMessages, freshRecord);
2989
- const freshContextTokens = this.tokenCounter.countMessages(freshUnobservedMessages);
4343
+ const freshContextTokens = await this.tokenCounter.countMessagesAsync(freshUnobservedMessages);
2990
4344
  let freshOtherThreadTokens = 0;
2991
4345
  if (this.scope === "resource" && resourceId) {
2992
4346
  const freshOtherContext = await this.loadOtherThreadsContext(resourceId, threadId);
@@ -3128,17 +4482,25 @@ ${suggestedResponse}
3128
4482
  if (observedMessageIds && observedMessageIds.length > 0) {
3129
4483
  const observedSet = new Set(observedMessageIds);
3130
4484
  const idsToRemove = /* @__PURE__ */ new Set();
4485
+ const removalOrder = [];
3131
4486
  let skipped = 0;
3132
4487
  let backoffTriggered = false;
4488
+ const retentionCounter = typeof minRemaining === "number" ? new TokenCounter() : null;
3133
4489
  for (const msg of allMsgs) {
3134
4490
  if (!msg?.id || msg.id === "om-continuation" || !observedSet.has(msg.id)) {
3135
4491
  continue;
3136
4492
  }
3137
- if (typeof minRemaining === "number") {
4493
+ const unobservedParts = this.getUnobservedParts(msg);
4494
+ const totalParts = msg.content?.parts?.length ?? 0;
4495
+ if (unobservedParts.length > 0 && unobservedParts.length < totalParts) {
4496
+ msg.content.parts = unobservedParts;
4497
+ continue;
4498
+ }
4499
+ if (retentionCounter && typeof minRemaining === "number") {
3138
4500
  const nextRemainingMessages = allMsgs.filter(
3139
4501
  (m) => m?.id && m.id !== "om-continuation" && !idsToRemove.has(m.id) && m.id !== msg.id
3140
4502
  );
3141
- const remainingIfRemoved = this.tokenCounter.countMessages(nextRemainingMessages);
4503
+ const remainingIfRemoved = retentionCounter.countMessages(nextRemainingMessages);
3142
4504
  if (remainingIfRemoved < minRemaining) {
3143
4505
  skipped += 1;
3144
4506
  backoffTriggered = true;
@@ -3146,6 +4508,19 @@ ${suggestedResponse}
3146
4508
  }
3147
4509
  }
3148
4510
  idsToRemove.add(msg.id);
4511
+ removalOrder.push(msg.id);
4512
+ }
4513
+ if (retentionCounter && typeof minRemaining === "number" && idsToRemove.size > 0) {
4514
+ let remainingMessages = allMsgs.filter((m) => m?.id && m.id !== "om-continuation" && !idsToRemove.has(m.id));
4515
+ let remainingTokens = retentionCounter.countMessages(remainingMessages);
4516
+ while (remainingTokens < minRemaining && removalOrder.length > 0) {
4517
+ const restoreId = removalOrder.pop();
4518
+ idsToRemove.delete(restoreId);
4519
+ skipped += 1;
4520
+ backoffTriggered = true;
4521
+ remainingMessages = allMsgs.filter((m) => m?.id && m.id !== "om-continuation" && !idsToRemove.has(m.id));
4522
+ remainingTokens = retentionCounter.countMessages(remainingMessages);
4523
+ }
3149
4524
  }
3150
4525
  omDebug(
3151
4526
  `[OM:cleanupActivation] observedSet=${[...observedSet].map((id) => id.slice(0, 8)).join(",")}, matched=${idsToRemove.size}, skipped=${skipped}, backoffTriggered=${backoffTriggered}, idsToRemove=${[...idsToRemove].map((id) => id.slice(0, 8)).join(",")}`
@@ -3183,8 +4558,8 @@ ${suggestedResponse}
3183
4558
  await this.saveMessagesWithSealedIdTracking(messagesToSave, sealedIds, threadId, resourceId, state);
3184
4559
  }
3185
4560
  } else {
3186
- const newInput = messageList.clear.input.db();
3187
- const newOutput = messageList.clear.response.db();
4561
+ const newInput = messageList.get.input.db();
4562
+ const newOutput = messageList.get.response.db();
3188
4563
  const messagesToSave = [...newInput, ...newOutput];
3189
4564
  if (messagesToSave.length > 0) {
3190
4565
  await this.saveMessagesWithSealedIdTracking(messagesToSave, sealedIds, threadId, resourceId, state);
@@ -3252,11 +4627,17 @@ ${suggestedResponse}
3252
4627
  messageList.add(continuationMessage, "memory");
3253
4628
  }
3254
4629
  /**
3255
- * Filter out already-observed messages from message list (step 0 only).
3256
- * Historical messages loaded from DB may contain observation markers from previous sessions.
4630
+ * Filter out already-observed messages from the in-memory context.
4631
+ *
4632
+ * Marker-boundary pruning is safest at step 0 (historical resume/rebuild), where
4633
+ * list ordering mirrors persisted history.
4634
+ * For step > 0, the list may include mid-loop mutations (sealing/splitting/trim),
4635
+ * so we prefer record-based fallback pruning over position-based marker pruning.
3257
4636
  */
3258
- filterAlreadyObservedMessages(messageList, record) {
4637
+ async filterAlreadyObservedMessages(messageList, record, options) {
3259
4638
  const allMessages = messageList.get.all.db();
4639
+ const useMarkerBoundaryPruning = options?.useMarkerBoundaryPruning ?? true;
4640
+ const fallbackCursor = record?.threadId ? memory.getThreadOMMetadata((await this.storage.getThreadById({ threadId: record.threadId }))?.metadata)?.lastObservedMessageCursor : void 0;
3260
4641
  let markerMessageIndex = -1;
3261
4642
  let markerMessage = null;
3262
4643
  for (let i = allMessages.length - 1; i >= 0; i--) {
@@ -3268,7 +4649,7 @@ ${suggestedResponse}
3268
4649
  break;
3269
4650
  }
3270
4651
  }
3271
- if (markerMessage && markerMessageIndex !== -1) {
4652
+ if (useMarkerBoundaryPruning && markerMessage && markerMessageIndex !== -1) {
3272
4653
  const messagesToRemove = [];
3273
4654
  for (let i = 0; i < markerMessageIndex; i++) {
3274
4655
  const msg = allMessages[i];
@@ -3289,6 +4670,9 @@ ${suggestedResponse}
3289
4670
  }
3290
4671
  } else if (record) {
3291
4672
  const observedIds = new Set(Array.isArray(record.observedMessageIds) ? record.observedMessageIds : []);
4673
+ const derivedCursor = fallbackCursor ?? this.getLastObservedMessageCursor(
4674
+ allMessages.filter((msg) => !!msg?.id && observedIds.has(msg.id) && !!msg.createdAt)
4675
+ );
3292
4676
  const lastObservedAt = record.lastObservedAt;
3293
4677
  const messagesToRemove = [];
3294
4678
  for (const msg of allMessages) {
@@ -3297,6 +4681,10 @@ ${suggestedResponse}
3297
4681
  messagesToRemove.push(msg.id);
3298
4682
  continue;
3299
4683
  }
4684
+ if (derivedCursor && this.isMessageAtOrBeforeCursor(msg, derivedCursor)) {
4685
+ messagesToRemove.push(msg.id);
4686
+ continue;
4687
+ }
3300
4688
  if (lastObservedAt && msg.createdAt) {
3301
4689
  const msgDate = new Date(msg.createdAt);
3302
4690
  if (msgDate <= lastObservedAt) {
@@ -3321,7 +4709,7 @@ ${suggestedResponse}
3321
4709
  * 5. Filter out already-observed messages
3322
4710
  */
3323
4711
  async processInputStep(args) {
3324
- const { messageList, requestContext, stepNumber, state: _state, writer, abortSignal, abort } = args;
4712
+ const { messageList, requestContext, stepNumber, state: _state, writer, abortSignal, abort, model } = args;
3325
4713
  const state = _state ?? {};
3326
4714
  omDebug(
3327
4715
  `[OM:processInputStep:ENTER] step=${stepNumber}, hasMastraMemory=${!!requestContext?.get("MastraMemory")}, hasMemoryInfo=${!!messageList?.serialize()?.memoryInfo?.threadId}`
@@ -3334,262 +4722,322 @@ ${suggestedResponse}
3334
4722
  const { threadId, resourceId } = context;
3335
4723
  const memoryContext = memory.parseMemoryRequestContext(requestContext);
3336
4724
  const readOnly = memoryContext?.memoryConfig?.readOnly;
3337
- let record = await this.getOrCreateRecord(threadId, resourceId);
3338
- omDebug(
3339
- `[OM:step] processInputStep step=${stepNumber}: recordId=${record.id}, genCount=${record.generationCount}, obsTokens=${record.observationTokenCount}, bufferedReflection=${record.bufferedReflection ? "present (" + record.bufferedReflection.length + " chars)" : "empty"}, activeObsLen=${record.activeObservations?.length}`
3340
- );
3341
- await this.loadHistoricalMessagesIfNeeded(messageList, state, threadId, resourceId, record.lastObservedAt);
3342
- let unobservedContextBlocks;
3343
- if (this.scope === "resource" && resourceId) {
3344
- unobservedContextBlocks = await this.loadOtherThreadsContext(resourceId, threadId);
3345
- }
3346
- if (stepNumber === 0 && !readOnly && this.isAsyncObservationEnabled()) {
3347
- const lockKey = this.getLockKey(threadId, resourceId);
3348
- const bufferedChunks = this.getBufferedChunks(record);
4725
+ const actorModelContext = this.getRuntimeModelContext(model);
4726
+ state.__omActorModelContext = actorModelContext;
4727
+ return this.runWithTokenCounterModelContext(actorModelContext, async () => {
4728
+ let record = await this.getOrCreateRecord(threadId, resourceId);
4729
+ const reproCaptureEnabled = isOmReproCaptureEnabled();
4730
+ const preRecordSnapshot = reproCaptureEnabled ? safeCaptureJson(record) : null;
4731
+ const preMessagesSnapshot = reproCaptureEnabled ? safeCaptureJson(messageList.get.all.db()) : null;
4732
+ const preSerializedMessageList = reproCaptureEnabled ? safeCaptureJson(messageList.serialize()) : null;
4733
+ const reproCaptureDetails = {
4734
+ step0Activation: null,
4735
+ thresholdCleanup: null,
4736
+ thresholdReached: false
4737
+ };
3349
4738
  omDebug(
3350
- `[OM:step0-activation] asyncObsEnabled=true, bufferedChunks=${bufferedChunks.length}, isBufferingObs=${record.isBufferingObservation}`
4739
+ `[OM:step] processInputStep step=${stepNumber}: recordId=${record.id}, genCount=${record.generationCount}, obsTokens=${record.observationTokenCount}, bufferedReflection=${record.bufferedReflection ? "present (" + record.bufferedReflection.length + " chars)" : "empty"}, activeObsLen=${record.activeObservations?.length}`
3351
4740
  );
3352
- {
3353
- const bufKey = this.getObservationBufferKey(lockKey);
3354
- const dbBoundary = record.lastBufferedAtTokens ?? 0;
3355
- const currentContextTokens = this.tokenCounter.countMessages(messageList.get.all.db());
3356
- if (dbBoundary > currentContextTokens) {
3357
- omDebug(
3358
- `[OM:step0-boundary-reset] dbBoundary=${dbBoundary} > currentContext=${currentContextTokens}, resetting to current`
3359
- );
3360
- _ObservationalMemory.lastBufferedBoundary.set(bufKey, currentContextTokens);
3361
- this.storage.setBufferingObservationFlag(record.id, false, currentContextTokens).catch(() => {
3362
- });
3363
- }
4741
+ await this.loadHistoricalMessagesIfNeeded(messageList, state, threadId, resourceId, record.lastObservedAt);
4742
+ let unobservedContextBlocks;
4743
+ if (this.scope === "resource" && resourceId) {
4744
+ unobservedContextBlocks = await this.loadOtherThreadsContext(resourceId, threadId);
3364
4745
  }
3365
- if (bufferedChunks.length > 0) {
3366
- const allMsgsForCheck = messageList.get.all.db();
3367
- const unobservedMsgsForCheck = this.getUnobservedMessages(allMsgsForCheck, record);
3368
- const otherThreadTokensForCheck = unobservedContextBlocks ? this.tokenCounter.countString(unobservedContextBlocks) : 0;
3369
- const currentObsTokensForCheck = record.observationTokenCount ?? 0;
3370
- const { totalPendingTokens: step0PendingTokens, threshold: step0Threshold } = this.calculateObservationThresholds(
3371
- allMsgsForCheck,
3372
- unobservedMsgsForCheck,
3373
- 0,
3374
- // pendingTokens not needed — allMessages covers context
3375
- otherThreadTokensForCheck,
3376
- currentObsTokensForCheck,
3377
- record
3378
- );
4746
+ if (stepNumber === 0 && !readOnly && this.isAsyncObservationEnabled()) {
4747
+ const lockKey = this.getLockKey(threadId, resourceId);
4748
+ const bufferedChunks = this.getBufferedChunks(record);
3379
4749
  omDebug(
3380
- `[OM:step0-activation] pendingTokens=${step0PendingTokens}, threshold=${step0Threshold}, blockAfter=${this.observationConfig.blockAfter}, shouldActivate=${step0PendingTokens >= step0Threshold}, allMsgs=${allMsgsForCheck.length}`
4750
+ `[OM:step0-activation] asyncObsEnabled=true, bufferedChunks=${bufferedChunks.length}, isBufferingObs=${record.isBufferingObservation}`
3381
4751
  );
3382
- if (step0PendingTokens >= step0Threshold) {
3383
- const activationResult = await this.tryActivateBufferedObservations(
3384
- record,
3385
- lockKey,
3386
- step0PendingTokens,
3387
- writer,
3388
- messageList
4752
+ {
4753
+ const bufKey = this.getObservationBufferKey(lockKey);
4754
+ const dbBoundary = record.lastBufferedAtTokens ?? 0;
4755
+ const currentContextTokens = this.tokenCounter.countMessages(messageList.get.all.db());
4756
+ if (dbBoundary > currentContextTokens) {
4757
+ omDebug(
4758
+ `[OM:step0-boundary-reset] dbBoundary=${dbBoundary} > currentContext=${currentContextTokens}, resetting to current`
4759
+ );
4760
+ _ObservationalMemory.lastBufferedBoundary.set(bufKey, currentContextTokens);
4761
+ this.storage.setBufferingObservationFlag(record.id, false, currentContextTokens).catch(() => {
4762
+ });
4763
+ }
4764
+ }
4765
+ if (bufferedChunks.length > 0) {
4766
+ const allMsgsForCheck = messageList.get.all.db();
4767
+ const unobservedMsgsForCheck = this.getUnobservedMessages(allMsgsForCheck, record);
4768
+ const otherThreadTokensForCheck = unobservedContextBlocks ? this.tokenCounter.countString(unobservedContextBlocks) : 0;
4769
+ const currentObsTokensForCheck = record.observationTokenCount ?? 0;
4770
+ const { totalPendingTokens: step0PendingTokens, threshold: step0Threshold } = await this.calculateObservationThresholds(
4771
+ allMsgsForCheck,
4772
+ unobservedMsgsForCheck,
4773
+ 0,
4774
+ // pendingTokens not needed — allMessages covers context
4775
+ otherThreadTokensForCheck,
4776
+ currentObsTokensForCheck,
4777
+ record
3389
4778
  );
3390
- if (activationResult.success && activationResult.updatedRecord) {
3391
- record = activationResult.updatedRecord;
3392
- const activatedIds = activationResult.activatedMessageIds ?? [];
3393
- if (activatedIds.length > 0) {
3394
- const activatedSet = new Set(activatedIds);
3395
- const allMsgs = messageList.get.all.db();
3396
- const idsToRemove = allMsgs.filter((msg) => msg?.id && msg.id !== "om-continuation" && activatedSet.has(msg.id)).map((msg) => msg.id);
3397
- if (idsToRemove.length > 0) {
3398
- messageList.removeByIds(idsToRemove);
4779
+ omDebug(
4780
+ `[OM:step0-activation] pendingTokens=${step0PendingTokens}, threshold=${step0Threshold}, blockAfter=${this.observationConfig.blockAfter}, shouldActivate=${step0PendingTokens >= step0Threshold}, allMsgs=${allMsgsForCheck.length}`
4781
+ );
4782
+ if (step0PendingTokens >= step0Threshold) {
4783
+ const activationResult = await this.tryActivateBufferedObservations(
4784
+ record,
4785
+ lockKey,
4786
+ step0PendingTokens,
4787
+ writer,
4788
+ messageList
4789
+ );
4790
+ reproCaptureDetails.step0Activation = {
4791
+ attempted: true,
4792
+ success: activationResult.success,
4793
+ messageTokensActivated: activationResult.messageTokensActivated ?? 0,
4794
+ activatedMessageIds: activationResult.activatedMessageIds ?? [],
4795
+ hadUpdatedRecord: !!activationResult.updatedRecord
4796
+ };
4797
+ if (activationResult.success && activationResult.updatedRecord) {
4798
+ record = activationResult.updatedRecord;
4799
+ const activatedIds = activationResult.activatedMessageIds ?? [];
4800
+ if (activatedIds.length > 0) {
4801
+ const activatedSet = new Set(activatedIds);
4802
+ const allMsgs = messageList.get.all.db();
4803
+ const idsToRemove = allMsgs.filter((msg) => msg?.id && msg.id !== "om-continuation" && activatedSet.has(msg.id)).map((msg) => msg.id);
4804
+ if (idsToRemove.length > 0) {
4805
+ messageList.removeByIds(idsToRemove);
4806
+ }
3399
4807
  }
3400
- }
3401
- this.cleanupStaticMaps(threadId, resourceId, activatedIds);
3402
- const bufKey = this.getObservationBufferKey(lockKey);
3403
- _ObservationalMemory.lastBufferedBoundary.set(bufKey, 0);
3404
- this.storage.setBufferingObservationFlag(record.id, false, 0).catch(() => {
3405
- });
3406
- const thread = await this.storage.getThreadById({ threadId });
3407
- if (thread) {
3408
- const newMetadata = memory.setThreadOMMetadata(thread.metadata, {
3409
- suggestedResponse: activationResult.suggestedContinuation,
3410
- currentTask: activationResult.currentTask
4808
+ this.cleanupStaticMaps(threadId, resourceId, activatedIds);
4809
+ const bufKey = this.getObservationBufferKey(lockKey);
4810
+ _ObservationalMemory.lastBufferedBoundary.set(bufKey, 0);
4811
+ this.storage.setBufferingObservationFlag(record.id, false, 0).catch(() => {
3411
4812
  });
3412
- await this.storage.updateThread({
3413
- id: threadId,
3414
- title: thread.title ?? "",
3415
- metadata: newMetadata
4813
+ const thread = await this.storage.getThreadById({ threadId });
4814
+ if (thread) {
4815
+ const activatedSet = new Set(activationResult.activatedMessageIds ?? []);
4816
+ const activatedMessages = messageList.get.all.db().filter((msg) => msg?.id && activatedSet.has(msg.id));
4817
+ const newMetadata = memory.setThreadOMMetadata(thread.metadata, {
4818
+ suggestedResponse: activationResult.suggestedContinuation,
4819
+ currentTask: activationResult.currentTask,
4820
+ lastObservedMessageCursor: this.getLastObservedMessageCursor(activatedMessages)
4821
+ });
4822
+ await this.storage.updateThread({
4823
+ id: threadId,
4824
+ title: thread.title ?? "",
4825
+ metadata: newMetadata
4826
+ });
4827
+ }
4828
+ await this.maybeReflect({
4829
+ record,
4830
+ observationTokens: record.observationTokenCount ?? 0,
4831
+ threadId,
4832
+ writer,
4833
+ messageList,
4834
+ requestContext
3416
4835
  });
4836
+ record = await this.getOrCreateRecord(threadId, resourceId);
3417
4837
  }
3418
- await this.maybeReflect({
3419
- record,
3420
- observationTokens: record.observationTokenCount ?? 0,
3421
- threadId,
3422
- writer,
3423
- messageList,
3424
- requestContext
3425
- });
3426
- record = await this.getOrCreateRecord(threadId, resourceId);
3427
4838
  }
3428
4839
  }
3429
4840
  }
3430
- }
3431
- if (stepNumber === 0 && !readOnly) {
3432
- const obsTokens = record.observationTokenCount ?? 0;
3433
- if (this.shouldReflect(obsTokens)) {
3434
- omDebug(`[OM:step0-reflect] obsTokens=${obsTokens} over reflectThreshold, triggering reflection`);
3435
- await this.maybeReflect({
3436
- record,
3437
- observationTokens: obsTokens,
3438
- threadId,
3439
- writer,
3440
- messageList,
3441
- requestContext
3442
- });
3443
- record = await this.getOrCreateRecord(threadId, resourceId);
3444
- } else if (this.isAsyncReflectionEnabled()) {
3445
- const lockKey = this.getLockKey(threadId, resourceId);
3446
- if (this.shouldTriggerAsyncReflection(obsTokens, lockKey, record)) {
3447
- omDebug(`[OM:step0-reflect] obsTokens=${obsTokens} above activation point, triggering async reflection`);
3448
- await this.maybeAsyncReflect(record, obsTokens, writer, messageList, requestContext);
3449
- record = await this.getOrCreateRecord(threadId, resourceId);
3450
- }
3451
- }
3452
- }
3453
- if (!readOnly) {
3454
- const allMessages = messageList.get.all.db();
3455
- const unobservedMessages = this.getUnobservedMessages(allMessages, record);
3456
- const otherThreadTokens = unobservedContextBlocks ? this.tokenCounter.countString(unobservedContextBlocks) : 0;
3457
- const currentObservationTokens = record.observationTokenCount ?? 0;
3458
- const thresholds = this.calculateObservationThresholds(
3459
- allMessages,
3460
- unobservedMessages,
3461
- 0,
3462
- // pendingTokens not needed — allMessages covers context
3463
- otherThreadTokens,
3464
- currentObservationTokens,
3465
- record
3466
- );
3467
- const { totalPendingTokens, threshold } = thresholds;
3468
- const bufferedChunkTokens = this.getBufferedChunks(record).reduce((sum, c) => sum + (c.messageTokens ?? 0), 0);
3469
- const unbufferedPendingTokens = Math.max(0, totalPendingTokens - bufferedChunkTokens);
3470
- const stateSealedIds = state.sealedIds ?? /* @__PURE__ */ new Set();
3471
- const staticSealedIds = _ObservationalMemory.sealedMessageIds.get(threadId) ?? /* @__PURE__ */ new Set();
3472
- const sealedIds = /* @__PURE__ */ new Set([...stateSealedIds, ...staticSealedIds]);
3473
- state.sealedIds = sealedIds;
3474
- const lockKey = this.getLockKey(threadId, resourceId);
3475
- if (this.isAsyncObservationEnabled() && totalPendingTokens < threshold) {
3476
- const shouldTrigger = this.shouldTriggerAsyncObservation(totalPendingTokens, lockKey, record, threshold);
3477
- omDebug(
3478
- `[OM:async-obs] belowThreshold: pending=${totalPendingTokens}, unbuffered=${unbufferedPendingTokens}, threshold=${threshold}, shouldTrigger=${shouldTrigger}, isBufferingObs=${record.isBufferingObservation}, lastBufferedAt=${record.lastBufferedAtTokens}`
3479
- );
3480
- if (shouldTrigger) {
3481
- this.startAsyncBufferedObservation(
4841
+ if (stepNumber === 0 && !readOnly) {
4842
+ const obsTokens = record.observationTokenCount ?? 0;
4843
+ if (this.shouldReflect(obsTokens)) {
4844
+ omDebug(`[OM:step0-reflect] obsTokens=${obsTokens} over reflectThreshold, triggering reflection`);
4845
+ await this.maybeReflect({
3482
4846
  record,
4847
+ observationTokens: obsTokens,
3483
4848
  threadId,
3484
- unobservedMessages,
3485
- lockKey,
3486
4849
  writer,
3487
- unbufferedPendingTokens,
4850
+ messageList,
3488
4851
  requestContext
3489
- );
4852
+ });
4853
+ record = await this.getOrCreateRecord(threadId, resourceId);
4854
+ } else if (this.isAsyncReflectionEnabled()) {
4855
+ const lockKey = this.getLockKey(threadId, resourceId);
4856
+ if (this.shouldTriggerAsyncReflection(obsTokens, lockKey, record)) {
4857
+ omDebug(`[OM:step0-reflect] obsTokens=${obsTokens} above activation point, triggering async reflection`);
4858
+ await this.maybeAsyncReflect(record, obsTokens, writer, messageList, requestContext);
4859
+ record = await this.getOrCreateRecord(threadId, resourceId);
4860
+ }
3490
4861
  }
3491
- } else if (this.isAsyncObservationEnabled()) {
3492
- const shouldTrigger = this.shouldTriggerAsyncObservation(totalPendingTokens, lockKey, record, threshold);
3493
- omDebug(
3494
- `[OM:async-obs] atOrAboveThreshold: pending=${totalPendingTokens}, unbuffered=${unbufferedPendingTokens}, threshold=${threshold}, step=${stepNumber}, shouldTrigger=${shouldTrigger}`
4862
+ }
4863
+ let didThresholdCleanup = false;
4864
+ if (!readOnly) {
4865
+ let allMessages = messageList.get.all.db();
4866
+ let unobservedMessages = this.getUnobservedMessages(allMessages, record);
4867
+ const otherThreadTokens = unobservedContextBlocks ? this.tokenCounter.countString(unobservedContextBlocks) : 0;
4868
+ let currentObservationTokens = record.observationTokenCount ?? 0;
4869
+ let thresholds = await this.calculateObservationThresholds(
4870
+ allMessages,
4871
+ unobservedMessages,
4872
+ 0,
4873
+ // pendingTokens not needed — allMessages covers context
4874
+ otherThreadTokens,
4875
+ currentObservationTokens,
4876
+ record
3495
4877
  );
3496
- if (shouldTrigger) {
3497
- this.startAsyncBufferedObservation(
4878
+ let { totalPendingTokens, threshold } = thresholds;
4879
+ let bufferedChunkTokens = this.getBufferedChunks(record).reduce((sum, c) => sum + (c.messageTokens ?? 0), 0);
4880
+ let unbufferedPendingTokens = Math.max(0, totalPendingTokens - bufferedChunkTokens);
4881
+ const stateSealedIds = state.sealedIds ?? /* @__PURE__ */ new Set();
4882
+ const staticSealedIds = _ObservationalMemory.sealedMessageIds.get(threadId) ?? /* @__PURE__ */ new Set();
4883
+ const sealedIds = /* @__PURE__ */ new Set([...stateSealedIds, ...staticSealedIds]);
4884
+ state.sealedIds = sealedIds;
4885
+ const lockKey = this.getLockKey(threadId, resourceId);
4886
+ if (this.isAsyncObservationEnabled() && totalPendingTokens < threshold) {
4887
+ const shouldTrigger = this.shouldTriggerAsyncObservation(totalPendingTokens, lockKey, record, threshold);
4888
+ omDebug(
4889
+ `[OM:async-obs] belowThreshold: pending=${totalPendingTokens}, unbuffered=${unbufferedPendingTokens}, threshold=${threshold}, shouldTrigger=${shouldTrigger}, isBufferingObs=${record.isBufferingObservation}, lastBufferedAt=${record.lastBufferedAtTokens}`
4890
+ );
4891
+ if (shouldTrigger) {
4892
+ void this.startAsyncBufferedObservation(
4893
+ record,
4894
+ threadId,
4895
+ unobservedMessages,
4896
+ lockKey,
4897
+ writer,
4898
+ unbufferedPendingTokens,
4899
+ requestContext
4900
+ );
4901
+ }
4902
+ } else if (this.isAsyncObservationEnabled()) {
4903
+ const shouldTrigger = this.shouldTriggerAsyncObservation(totalPendingTokens, lockKey, record, threshold);
4904
+ omDebug(
4905
+ `[OM:async-obs] atOrAboveThreshold: pending=${totalPendingTokens}, unbuffered=${unbufferedPendingTokens}, threshold=${threshold}, step=${stepNumber}, shouldTrigger=${shouldTrigger}`
4906
+ );
4907
+ if (shouldTrigger) {
4908
+ void this.startAsyncBufferedObservation(
4909
+ record,
4910
+ threadId,
4911
+ unobservedMessages,
4912
+ lockKey,
4913
+ writer,
4914
+ unbufferedPendingTokens,
4915
+ requestContext
4916
+ );
4917
+ }
4918
+ }
4919
+ if (stepNumber > 0) {
4920
+ await this.handlePerStepSave(messageList, sealedIds, threadId, resourceId, state);
4921
+ }
4922
+ if (stepNumber > 0 && totalPendingTokens >= threshold) {
4923
+ reproCaptureDetails.thresholdReached = true;
4924
+ const { observationSucceeded, updatedRecord, activatedMessageIds } = await this.handleThresholdReached(
4925
+ messageList,
3498
4926
  record,
3499
4927
  threadId,
3500
- unobservedMessages,
4928
+ resourceId,
4929
+ threshold,
3501
4930
  lockKey,
3502
4931
  writer,
3503
- unbufferedPendingTokens,
4932
+ abortSignal,
4933
+ abort,
3504
4934
  requestContext
3505
4935
  );
4936
+ if (observationSucceeded) {
4937
+ const observedIds = activatedMessageIds?.length ? activatedMessageIds : Array.isArray(updatedRecord.observedMessageIds) ? updatedRecord.observedMessageIds : void 0;
4938
+ const minRemaining = typeof this.observationConfig.bufferActivation === "number" ? resolveRetentionFloor(this.observationConfig.bufferActivation, threshold) : void 0;
4939
+ reproCaptureDetails.thresholdCleanup = {
4940
+ observationSucceeded,
4941
+ observedIdsCount: observedIds?.length ?? 0,
4942
+ observedIds,
4943
+ minRemaining,
4944
+ updatedRecordObservedIds: updatedRecord.observedMessageIds
4945
+ };
4946
+ omDebug(
4947
+ `[OM:cleanup] observedIds=${observedIds?.length ?? "undefined"}, ids=${observedIds?.join(",") ?? "none"}, updatedRecord.observedMessageIds=${JSON.stringify(updatedRecord.observedMessageIds)}, minRemaining=${minRemaining ?? "n/a"}`
4948
+ );
4949
+ await this.cleanupAfterObservation(
4950
+ messageList,
4951
+ sealedIds,
4952
+ threadId,
4953
+ resourceId,
4954
+ state,
4955
+ observedIds,
4956
+ minRemaining
4957
+ );
4958
+ didThresholdCleanup = true;
4959
+ if (activatedMessageIds?.length) {
4960
+ this.cleanupStaticMaps(threadId, resourceId, activatedMessageIds);
4961
+ }
4962
+ if (this.isAsyncObservationEnabled()) {
4963
+ const bufKey = this.getObservationBufferKey(lockKey);
4964
+ _ObservationalMemory.lastBufferedBoundary.set(bufKey, 0);
4965
+ this.storage.setBufferingObservationFlag(updatedRecord.id, false, 0).catch(() => {
4966
+ });
4967
+ omDebug(`[OM:threshold] post-activation boundary reset to 0`);
4968
+ }
4969
+ }
4970
+ record = updatedRecord;
3506
4971
  }
3507
4972
  }
3508
- if (stepNumber > 0) {
3509
- await this.handlePerStepSave(messageList, sealedIds, threadId, resourceId, state);
4973
+ await this.injectObservationsIntoContext(
4974
+ messageList,
4975
+ record,
4976
+ threadId,
4977
+ resourceId,
4978
+ unobservedContextBlocks,
4979
+ requestContext
4980
+ );
4981
+ if (!didThresholdCleanup) {
4982
+ await this.filterAlreadyObservedMessages(messageList, record, { useMarkerBoundaryPruning: stepNumber === 0 });
3510
4983
  }
3511
- if (stepNumber > 0 && totalPendingTokens >= threshold) {
3512
- const { observationSucceeded, updatedRecord, activatedMessageIds } = await this.handleThresholdReached(
3513
- messageList,
3514
- record,
4984
+ {
4985
+ const freshRecord = await this.getOrCreateRecord(threadId, resourceId);
4986
+ const contextMessages = messageList.get.all.db();
4987
+ const freshUnobservedTokens = await this.tokenCounter.countMessagesAsync(contextMessages);
4988
+ const otherThreadTokens = unobservedContextBlocks ? this.tokenCounter.countString(unobservedContextBlocks) : 0;
4989
+ const currentObservationTokens = freshRecord.observationTokenCount ?? 0;
4990
+ const threshold = calculateDynamicThreshold(this.observationConfig.messageTokens, currentObservationTokens);
4991
+ const baseReflectionThreshold = getMaxThreshold(this.reflectionConfig.observationTokens);
4992
+ const isSharedBudget = typeof this.observationConfig.messageTokens !== "number";
4993
+ const totalBudget = isSharedBudget ? this.observationConfig.messageTokens.max : 0;
4994
+ const effectiveObservationTokensThreshold = isSharedBudget ? Math.max(totalBudget - threshold, 1e3) : baseReflectionThreshold;
4995
+ const totalPendingTokens = freshUnobservedTokens + otherThreadTokens;
4996
+ await this.emitStepProgress(
4997
+ writer,
3515
4998
  threadId,
3516
4999
  resourceId,
3517
- threshold,
3518
- lockKey,
3519
- writer,
3520
- abortSignal,
3521
- abort,
3522
- requestContext
5000
+ stepNumber,
5001
+ freshRecord,
5002
+ {
5003
+ totalPendingTokens,
5004
+ threshold,
5005
+ effectiveObservationTokensThreshold
5006
+ },
5007
+ currentObservationTokens
3523
5008
  );
3524
- if (observationSucceeded) {
3525
- const observedIds = activatedMessageIds?.length ? activatedMessageIds : Array.isArray(updatedRecord.observedMessageIds) ? updatedRecord.observedMessageIds : void 0;
3526
- const minRemaining = typeof this.observationConfig.bufferActivation === "number" ? resolveRetentionFloor(this.observationConfig.bufferActivation, threshold) : void 0;
3527
- omDebug(
3528
- `[OM:cleanup] observedIds=${observedIds?.length ?? "undefined"}, ids=${observedIds?.join(",") ?? "none"}, updatedRecord.observedMessageIds=${JSON.stringify(updatedRecord.observedMessageIds)}, minRemaining=${minRemaining ?? "n/a"}`
3529
- );
3530
- await this.cleanupAfterObservation(
3531
- messageList,
3532
- sealedIds,
5009
+ this.storage.setPendingMessageTokens(freshRecord.id, totalPendingTokens).catch(() => {
5010
+ });
5011
+ if (reproCaptureEnabled && preRecordSnapshot && preMessagesSnapshot && preSerializedMessageList) {
5012
+ writeProcessInputStepReproCapture({
3533
5013
  threadId,
3534
5014
  resourceId,
3535
- state,
3536
- observedIds,
3537
- minRemaining
3538
- );
3539
- if (activatedMessageIds?.length) {
3540
- this.cleanupStaticMaps(threadId, resourceId, activatedMessageIds);
3541
- }
3542
- if (this.isAsyncObservationEnabled()) {
3543
- const bufKey = this.getObservationBufferKey(lockKey);
3544
- _ObservationalMemory.lastBufferedBoundary.set(bufKey, 0);
3545
- this.storage.setBufferingObservationFlag(updatedRecord.id, false, 0).catch(() => {
3546
- });
3547
- omDebug(`[OM:threshold] post-activation boundary reset to 0`);
3548
- }
5015
+ stepNumber,
5016
+ args,
5017
+ preRecord: preRecordSnapshot,
5018
+ postRecord: freshRecord,
5019
+ preMessages: preMessagesSnapshot,
5020
+ preBufferedChunks: this.getBufferedChunks(preRecordSnapshot),
5021
+ preContextTokenCount: this.tokenCounter.countMessages(preMessagesSnapshot),
5022
+ preSerializedMessageList,
5023
+ postBufferedChunks: this.getBufferedChunks(freshRecord),
5024
+ postContextTokenCount: this.tokenCounter.countMessages(contextMessages),
5025
+ messageList,
5026
+ details: {
5027
+ ...reproCaptureDetails,
5028
+ totalPendingTokens,
5029
+ threshold,
5030
+ effectiveObservationTokensThreshold,
5031
+ currentObservationTokens,
5032
+ otherThreadTokens,
5033
+ contextMessageCount: contextMessages.length
5034
+ },
5035
+ debug: omDebug
5036
+ });
3549
5037
  }
3550
- record = updatedRecord;
3551
5038
  }
3552
- }
3553
- await this.injectObservationsIntoContext(
3554
- messageList,
3555
- record,
3556
- threadId,
3557
- resourceId,
3558
- unobservedContextBlocks,
3559
- requestContext
3560
- );
3561
- if (stepNumber === 0) {
3562
- this.filterAlreadyObservedMessages(messageList, record);
3563
- }
3564
- {
3565
- const freshRecord = await this.getOrCreateRecord(threadId, resourceId);
3566
- const contextMessages = messageList.get.all.db();
3567
- const freshUnobservedTokens = this.tokenCounter.countMessages(contextMessages);
3568
- const otherThreadTokens = unobservedContextBlocks ? this.tokenCounter.countString(unobservedContextBlocks) : 0;
3569
- const currentObservationTokens = freshRecord.observationTokenCount ?? 0;
3570
- const threshold = calculateDynamicThreshold(this.observationConfig.messageTokens, currentObservationTokens);
3571
- const baseReflectionThreshold = getMaxThreshold(this.reflectionConfig.observationTokens);
3572
- const isSharedBudget = typeof this.observationConfig.messageTokens !== "number";
3573
- const totalBudget = isSharedBudget ? this.observationConfig.messageTokens.max : 0;
3574
- const effectiveObservationTokensThreshold = isSharedBudget ? Math.max(totalBudget - threshold, 1e3) : baseReflectionThreshold;
3575
- const totalPendingTokens = freshUnobservedTokens + otherThreadTokens;
3576
- await this.emitStepProgress(
3577
- writer,
3578
- threadId,
3579
- resourceId,
3580
- stepNumber,
3581
- freshRecord,
3582
- {
3583
- totalPendingTokens,
3584
- threshold,
3585
- effectiveObservationTokensThreshold
3586
- },
3587
- currentObservationTokens
3588
- );
3589
- this.storage.setPendingMessageTokens(freshRecord.id, totalPendingTokens).catch(() => {
3590
- });
3591
- }
3592
- return messageList;
5039
+ return messageList;
5040
+ });
3593
5041
  }
3594
5042
  /**
3595
5043
  * Save any unsaved messages at the end of the agent turn.
@@ -3606,30 +5054,35 @@ ${suggestedResponse}
3606
5054
  return messageList;
3607
5055
  }
3608
5056
  const { threadId, resourceId } = context;
3609
- const memoryContext = memory.parseMemoryRequestContext(requestContext);
3610
- const readOnly = memoryContext?.memoryConfig?.readOnly;
3611
- if (readOnly) {
3612
- return messageList;
3613
- }
3614
- const newInput = messageList.get.input.db();
3615
- const newOutput = messageList.get.response.db();
3616
- const messagesToSave = [...newInput, ...newOutput];
3617
- omDebug(
3618
- `[OM:processOutputResult] threadId=${threadId}, inputMsgs=${newInput.length}, responseMsgs=${newOutput.length}, totalToSave=${messagesToSave.length}, allMsgsInList=${messageList.get.all.db().length}`
3619
- );
3620
- if (messagesToSave.length === 0) {
3621
- omDebug(`[OM:processOutputResult] nothing to save \u2014 all messages were already saved during per-step saves`);
3622
- return messageList;
3623
- }
3624
- const sealedIds = state.sealedIds ?? /* @__PURE__ */ new Set();
3625
- omDebug(
3626
- `[OM:processOutputResult] saving ${messagesToSave.length} messages, sealedIds=${sealedIds.size}, ids=${messagesToSave.map((m) => m.id?.slice(0, 8)).join(",")}`
3627
- );
3628
- await this.saveMessagesWithSealedIdTracking(messagesToSave, sealedIds, threadId, resourceId, state);
3629
- omDebug(
3630
- `[OM:processOutputResult] saved successfully, finalIds=${messagesToSave.map((m) => m.id?.slice(0, 8)).join(",")}`
5057
+ return this.runWithTokenCounterModelContext(
5058
+ state.__omActorModelContext,
5059
+ async () => {
5060
+ const memoryContext = memory.parseMemoryRequestContext(requestContext);
5061
+ const readOnly = memoryContext?.memoryConfig?.readOnly;
5062
+ if (readOnly) {
5063
+ return messageList;
5064
+ }
5065
+ const newInput = messageList.get.input.db();
5066
+ const newOutput = messageList.get.response.db();
5067
+ const messagesToSave = [...newInput, ...newOutput];
5068
+ omDebug(
5069
+ `[OM:processOutputResult] threadId=${threadId}, inputMsgs=${newInput.length}, responseMsgs=${newOutput.length}, totalToSave=${messagesToSave.length}, allMsgsInList=${messageList.get.all.db().length}`
5070
+ );
5071
+ if (messagesToSave.length === 0) {
5072
+ omDebug(`[OM:processOutputResult] nothing to save \u2014 all messages were already saved during per-step saves`);
5073
+ return messageList;
5074
+ }
5075
+ const sealedIds = state.sealedIds ?? /* @__PURE__ */ new Set();
5076
+ omDebug(
5077
+ `[OM:processOutputResult] saving ${messagesToSave.length} messages, sealedIds=${sealedIds.size}, ids=${messagesToSave.map((m) => m.id?.slice(0, 8)).join(",")}`
5078
+ );
5079
+ await this.saveMessagesWithSealedIdTracking(messagesToSave, sealedIds, threadId, resourceId, state);
5080
+ omDebug(
5081
+ `[OM:processOutputResult] saved successfully, finalIds=${messagesToSave.map((m) => m.id?.slice(0, 8)).join(",")}`
5082
+ );
5083
+ return messageList;
5084
+ }
3631
5085
  );
3632
- return messageList;
3633
5086
  }
3634
5087
  /**
3635
5088
  * Save messages to storage while preventing duplicate inserts for sealed messages.
@@ -3783,6 +5236,30 @@ ${formattedMessages}
3783
5236
  }
3784
5237
  return maxTime > 0 ? new Date(maxTime) : /* @__PURE__ */ new Date();
3785
5238
  }
5239
+ /**
5240
+ * Compute a cursor pointing at the latest message by createdAt.
5241
+ * Used to derive a stable observation boundary for replay pruning.
5242
+ */
5243
+ getLastObservedMessageCursor(messages) {
5244
+ let latest;
5245
+ for (const msg of messages) {
5246
+ if (!msg?.id || !msg.createdAt) continue;
5247
+ if (!latest || new Date(msg.createdAt).getTime() > new Date(latest.createdAt).getTime()) {
5248
+ latest = msg;
5249
+ }
5250
+ }
5251
+ return latest ? { createdAt: new Date(latest.createdAt).toISOString(), id: latest.id } : void 0;
5252
+ }
5253
+ /**
5254
+ * Check if a message is at or before a cursor (by createdAt then id).
5255
+ */
5256
+ isMessageAtOrBeforeCursor(msg, cursor) {
5257
+ if (!msg.createdAt) return false;
5258
+ const msgIso = new Date(msg.createdAt).toISOString();
5259
+ if (msgIso < cursor.createdAt) return true;
5260
+ if (msgIso === cursor.createdAt && msg.id === cursor.id) return true;
5261
+ return false;
5262
+ }
3786
5263
  /**
3787
5264
  * Wrap observations in a thread attribution tag.
3788
5265
  * Used in resource scope to track which thread observations came from.
@@ -3881,7 +5358,7 @@ ${newThreadSection}`;
3881
5358
  await this.storage.setObservingFlag(record.id, true);
3882
5359
  registerOp(record.id, "observing");
3883
5360
  const cycleId = crypto.randomUUID();
3884
- const tokensToObserve = this.tokenCounter.countMessages(unobservedMessages);
5361
+ const tokensToObserve = await this.tokenCounter.countMessagesAsync(unobservedMessages);
3885
5362
  const lastMessage = unobservedMessages[unobservedMessages.length - 1];
3886
5363
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
3887
5364
  if (lastMessage?.id) {
@@ -3943,7 +5420,8 @@ ${result.observations}` : result.observations;
3943
5420
  if (thread) {
3944
5421
  const newMetadata = memory.setThreadOMMetadata(thread.metadata, {
3945
5422
  suggestedResponse: result.suggestedContinuation,
3946
- currentTask: result.currentTask
5423
+ currentTask: result.currentTask,
5424
+ lastObservedMessageCursor: this.getLastObservedMessageCursor(messagesToObserve)
3947
5425
  });
3948
5426
  await this.storage.updateThread({
3949
5427
  id: threadId,
@@ -3958,7 +5436,7 @@ ${result.observations}` : result.observations;
3958
5436
  lastObservedAt,
3959
5437
  observedMessageIds: allObservedIds
3960
5438
  });
3961
- const actualTokensObserved = this.tokenCounter.countMessages(messagesToObserve);
5439
+ const actualTokensObserved = await this.tokenCounter.countMessagesAsync(messagesToObserve);
3962
5440
  if (lastMessage?.id) {
3963
5441
  const endMarker = createObservationEndMarker({
3964
5442
  cycleId,
@@ -4039,9 +5517,9 @@ ${result.observations}` : result.observations;
4039
5517
  * @param lockKey - Lock key for this scope
4040
5518
  * @param writer - Optional stream writer for emitting buffering markers
4041
5519
  */
4042
- startAsyncBufferedObservation(record, threadId, unobservedMessages, lockKey, writer, contextWindowTokens, requestContext) {
5520
+ async startAsyncBufferedObservation(record, threadId, unobservedMessages, lockKey, writer, contextWindowTokens, requestContext) {
4043
5521
  const bufferKey = this.getObservationBufferKey(lockKey);
4044
- const currentTokens = contextWindowTokens ?? this.tokenCounter.countMessages(unobservedMessages) + (record.pendingMessageTokens ?? 0);
5522
+ const currentTokens = contextWindowTokens ?? await this.tokenCounter.countMessagesAsync(unobservedMessages) + (record.pendingMessageTokens ?? 0);
4045
5523
  _ObservationalMemory.lastBufferedBoundary.set(bufferKey, currentTokens);
4046
5524
  registerOp(record.id, "bufferingObservation");
4047
5525
  this.storage.setBufferingObservationFlag(record.id, true, currentTokens).catch((err) => {
@@ -4101,7 +5579,7 @@ ${result.observations}` : result.observations;
4101
5579
  );
4102
5580
  const bufferTokens = this.observationConfig.bufferTokens ?? 5e3;
4103
5581
  const minNewTokens = bufferTokens / 2;
4104
- const newTokens = this.tokenCounter.countMessages(candidateMessages);
5582
+ const newTokens = await this.tokenCounter.countMessagesAsync(candidateMessages);
4105
5583
  if (newTokens < minNewTokens) {
4106
5584
  return;
4107
5585
  }
@@ -4122,7 +5600,7 @@ ${result.observations}` : result.observations;
4122
5600
  }
4123
5601
  const cycleId = `buffer-obs-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
4124
5602
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
4125
- const tokensToBuffer = this.tokenCounter.countMessages(messagesToBuffer);
5603
+ const tokensToBuffer = await this.tokenCounter.countMessagesAsync(messagesToBuffer);
4126
5604
  if (writer) {
4127
5605
  const startMarker = createBufferingStartMarker({
4128
5606
  cycleId,
@@ -4200,7 +5678,7 @@ ${result.observations}` : result.observations;
4200
5678
  }
4201
5679
  const newTokenCount = this.tokenCounter.countObservations(newObservations);
4202
5680
  const newMessageIds = messagesToBuffer.map((m) => m.id);
4203
- const messageTokens = this.tokenCounter.countMessages(messagesToBuffer);
5681
+ const messageTokens = await this.tokenCounter.countMessagesAsync(messagesToBuffer);
4204
5682
  const maxMessageTimestamp = this.getMaxMessageTimestamp(messagesToBuffer);
4205
5683
  const lastObservedAt = new Date(maxMessageTimestamp.getTime() + 1);
4206
5684
  await this.storage.updateBufferedObservations({
@@ -4218,7 +5696,7 @@ ${result.observations}` : result.observations;
4218
5696
  lastBufferedAtTime: lastObservedAt
4219
5697
  });
4220
5698
  if (writer) {
4221
- const tokensBuffered = this.tokenCounter.countMessages(messagesToBuffer);
5699
+ const tokensBuffered = await this.tokenCounter.countMessagesAsync(messagesToBuffer);
4222
5700
  const updatedRecord = await this.storage.getObservationalMemory(record.threadId, record.resourceId);
4223
5701
  const updatedChunks = this.getBufferedChunks(updatedRecord);
4224
5702
  const totalBufferedTokens = updatedChunks.reduce((sum, c) => sum + (c.tokenCount ?? 0), 0) || newTokenCount;
@@ -4287,14 +5765,14 @@ ${bufferedObservations}`;
4287
5765
  if (!freshRecord) {
4288
5766
  return { success: false };
4289
5767
  }
4290
- const freshChunks = this.getBufferedChunks(freshRecord);
4291
- if (!freshChunks.length) {
5768
+ const rawFreshChunks = this.getBufferedChunks(freshRecord);
5769
+ if (!rawFreshChunks.length) {
4292
5770
  return { success: false };
4293
5771
  }
4294
5772
  const messageTokensThreshold = getMaxThreshold(this.observationConfig.messageTokens);
4295
5773
  let effectivePendingTokens = currentPendingTokens;
4296
5774
  if (messageList) {
4297
- effectivePendingTokens = this.tokenCounter.countMessages(messageList.get.all.db());
5775
+ effectivePendingTokens = await this.tokenCounter.countMessagesAsync(messageList.get.all.db());
4298
5776
  if (effectivePendingTokens < messageTokensThreshold) {
4299
5777
  omDebug(
4300
5778
  `[OM:tryActivate] skipping activation: freshPendingTokens=${effectivePendingTokens} < threshold=${messageTokensThreshold}`
@@ -4302,6 +5780,7 @@ ${bufferedObservations}`;
4302
5780
  return { success: false };
4303
5781
  }
4304
5782
  }
5783
+ const freshChunks = messageList ? this.refreshBufferedChunkMessageTokens(rawFreshChunks, messageList) : rawFreshChunks;
4305
5784
  const bufferActivation = this.observationConfig.bufferActivation ?? 0.7;
4306
5785
  const activationRatio = resolveActivationRatio(bufferActivation, messageTokensThreshold);
4307
5786
  const forceMaxActivation = !!(this.observationConfig.blockAfter && effectivePendingTokens >= this.observationConfig.blockAfter);
@@ -4329,7 +5808,8 @@ ${bufferedObservations}`;
4329
5808
  activationRatio,
4330
5809
  messageTokensThreshold,
4331
5810
  currentPendingTokens: effectivePendingTokens,
4332
- forceMaxActivation
5811
+ forceMaxActivation,
5812
+ bufferedChunks: freshChunks
4333
5813
  });
4334
5814
  omDebug(
4335
5815
  `[OM:tryActivate] swapResult: chunksActivated=${activationResult.chunksActivated}, tokensActivated=${activationResult.messageTokensActivated}, obsTokensActivated=${activationResult.observationTokensActivated}, activatedCycleIds=${activationResult.activatedCycleIds.join(",")}`
@@ -4657,10 +6137,7 @@ ${unreflectedContent}` : freshRecord.bufferedReflection;
4657
6137
  const threshold = getMaxThreshold(this.observationConfig.messageTokens);
4658
6138
  const threadTokenCounts = /* @__PURE__ */ new Map();
4659
6139
  for (const [threadId, msgs] of messagesByThread) {
4660
- let tokens = 0;
4661
- for (const msg of msgs) {
4662
- tokens += this.tokenCounter.countMessage(msg);
4663
- }
6140
+ const tokens = await this.tokenCounter.countMessagesAsync(msgs);
4664
6141
  threadTokenCounts.set(threadId, tokens);
4665
6142
  }
4666
6143
  const threadsBySize = Array.from(messagesByThread.keys()).sort((a, b) => {
@@ -4717,7 +6194,7 @@ ${unreflectedContent}` : freshRecord.bufferedReflection;
4717
6194
  const allThreadIds = Array.from(threadsWithMessages.keys());
4718
6195
  for (const [threadId, msgs] of threadsWithMessages) {
4719
6196
  const lastMessage = msgs[msgs.length - 1];
4720
- const tokensToObserve = this.tokenCounter.countMessages(msgs);
6197
+ const tokensToObserve = await this.tokenCounter.countMessagesAsync(msgs);
4721
6198
  threadTokensToObserve.set(threadId, tokensToObserve);
4722
6199
  if (lastMessage?.id) {
4723
6200
  const startMarker = createObservationStartMarker({
@@ -4809,7 +6286,8 @@ ${unreflectedContent}` : freshRecord.bufferedReflection;
4809
6286
  const newMetadata = memory.setThreadOMMetadata(thread.metadata, {
4810
6287
  lastObservedAt: threadLastObservedAt.toISOString(),
4811
6288
  suggestedResponse: result.suggestedContinuation,
4812
- currentTask: result.currentTask
6289
+ currentTask: result.currentTask,
6290
+ lastObservedMessageCursor: this.getLastObservedMessageCursor(threadMessages)
4813
6291
  });
4814
6292
  await this.storage.updateThread({
4815
6293
  id: threadId,
@@ -4852,7 +6330,7 @@ ${unreflectedContent}` : freshRecord.bufferedReflection;
4852
6330
  const { threadId, threadMessages, result } = obsResult;
4853
6331
  const lastMessage = threadMessages[threadMessages.length - 1];
4854
6332
  if (lastMessage?.id) {
4855
- const tokensObserved = threadTokensToObserve.get(threadId) ?? this.tokenCounter.countMessages(threadMessages);
6333
+ const tokensObserved = threadTokensToObserve.get(threadId) ?? await this.tokenCounter.countMessagesAsync(threadMessages);
4856
6334
  const endMarker = createObservationEndMarker({
4857
6335
  cycleId,
4858
6336
  operationType: "observation",
@@ -5103,7 +6581,7 @@ ${unreflectedContent}` : freshRecord.bufferedReflection;
5103
6581
  const currentMessages = messages ?? [];
5104
6582
  if (!this.meetsObservationThreshold({
5105
6583
  record: freshRecord,
5106
- unobservedTokens: this.tokenCounter.countMessages(currentMessages)
6584
+ unobservedTokens: await this.tokenCounter.countMessagesAsync(currentMessages)
5107
6585
  })) {
5108
6586
  return;
5109
6587
  }
@@ -5131,7 +6609,7 @@ ${unreflectedContent}` : freshRecord.bufferedReflection;
5131
6609
  }
5132
6610
  if (!this.meetsObservationThreshold({
5133
6611
  record: freshRecord,
5134
- unobservedTokens: this.tokenCounter.countMessages(unobservedMessages)
6612
+ unobservedTokens: await this.tokenCounter.countMessagesAsync(unobservedMessages)
5135
6613
  })) {
5136
6614
  return;
5137
6615
  }
@@ -5261,5 +6739,5 @@ exports.formatMessagesForObserver = formatMessagesForObserver;
5261
6739
  exports.hasCurrentTaskSection = hasCurrentTaskSection;
5262
6740
  exports.optimizeObservationsForContext = optimizeObservationsForContext;
5263
6741
  exports.parseObserverOutput = parseObserverOutput;
5264
- //# sourceMappingURL=chunk-D6II7EP4.cjs.map
5265
- //# sourceMappingURL=chunk-D6II7EP4.cjs.map
6742
+ //# sourceMappingURL=chunk-5W5463NI.cjs.map
6743
+ //# sourceMappingURL=chunk-5W5463NI.cjs.map