@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
@@ -1,11 +1,14 @@
1
- import { appendFileSync } from 'fs';
1
+ import { appendFileSync, mkdirSync, writeFileSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import { Agent } from '@mastra/core/agent';
4
+ import { coreFeatures } from '@mastra/core/features';
4
5
  import { resolveModelConfig } from '@mastra/core/llm';
5
6
  import { setThreadOMMetadata, getThreadOMMetadata, parseMemoryRequestContext } from '@mastra/core/memory';
6
7
  import { MessageHistory } from '@mastra/core/processors';
7
8
  import xxhash from 'xxhash-wasm';
8
- import { createHash } from 'crypto';
9
+ import { createHash, randomUUID } from 'crypto';
10
+ import { AsyncLocalStorage } from 'async_hooks';
11
+ import imageSize from 'image-size';
9
12
  import { Tiktoken } from 'js-tiktoken/lite';
10
13
  import o200k_base from 'js-tiktoken/ranks/o200k_base';
11
14
 
@@ -662,45 +665,177 @@ User messages are extremely important. If the user asks a question or gives a ne
662
665
  ${instruction}` : ""}`;
663
666
  }
664
667
  var OBSERVER_SYSTEM_PROMPT = buildObserverSystemPrompt();
665
- function formatMessagesForObserver(messages, options) {
668
+ var OBSERVER_IMAGE_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
669
+ "png",
670
+ "jpg",
671
+ "jpeg",
672
+ "webp",
673
+ "gif",
674
+ "bmp",
675
+ "tiff",
676
+ "tif",
677
+ "heic",
678
+ "heif",
679
+ "avif"
680
+ ]);
681
+ function formatObserverTimestamp(createdAt) {
682
+ return createdAt ? new Date(createdAt).toLocaleString("en-US", {
683
+ year: "numeric",
684
+ month: "short",
685
+ day: "numeric",
686
+ hour: "numeric",
687
+ minute: "2-digit",
688
+ hour12: true
689
+ }) : "";
690
+ }
691
+ function getObserverPathExtension(value) {
692
+ const normalized = value.split("#", 1)[0]?.split("?", 1)[0] ?? value;
693
+ const match = normalized.match(/\.([a-z0-9]+)$/i);
694
+ return match?.[1]?.toLowerCase();
695
+ }
696
+ function hasObserverImageFilenameExtension(filename) {
697
+ return typeof filename === "string" && OBSERVER_IMAGE_FILE_EXTENSIONS.has(getObserverPathExtension(filename) ?? "");
698
+ }
699
+ function isImageLikeObserverFilePart(part) {
700
+ if (part.type !== "file") {
701
+ return false;
702
+ }
703
+ if (typeof part.mimeType === "string" && part.mimeType.toLowerCase().startsWith("image/")) {
704
+ return true;
705
+ }
706
+ if (typeof part.data === "string" && part.data.startsWith("data:image/")) {
707
+ return true;
708
+ }
709
+ if (part.data instanceof URL && hasObserverImageFilenameExtension(part.data.pathname)) {
710
+ return true;
711
+ }
712
+ if (typeof part.data === "string") {
713
+ try {
714
+ const url = new URL(part.data);
715
+ if ((url.protocol === "http:" || url.protocol === "https:") && hasObserverImageFilenameExtension(url.pathname)) {
716
+ return true;
717
+ }
718
+ } catch {
719
+ }
720
+ }
721
+ return hasObserverImageFilenameExtension(part.filename);
722
+ }
723
+ function toObserverInputAttachmentPart(part) {
724
+ if (part.type === "image") {
725
+ return {
726
+ type: "image",
727
+ image: part.image,
728
+ mimeType: part.mimeType,
729
+ providerOptions: part.providerOptions,
730
+ providerMetadata: part.providerMetadata,
731
+ experimental_providerMetadata: part.experimental_providerMetadata
732
+ };
733
+ }
734
+ if (isImageLikeObserverFilePart(part)) {
735
+ return {
736
+ type: "image",
737
+ image: part.data,
738
+ mimeType: part.mimeType,
739
+ providerOptions: part.providerOptions,
740
+ providerMetadata: part.providerMetadata,
741
+ experimental_providerMetadata: part.experimental_providerMetadata
742
+ };
743
+ }
744
+ return {
745
+ type: "file",
746
+ data: part.data,
747
+ mimeType: part.mimeType,
748
+ filename: part.filename,
749
+ providerOptions: part.providerOptions,
750
+ providerMetadata: part.providerMetadata,
751
+ experimental_providerMetadata: part.experimental_providerMetadata
752
+ };
753
+ }
754
+ function resolveObserverAttachmentLabel(part) {
755
+ if (part.filename?.trim()) {
756
+ return part.filename.trim();
757
+ }
758
+ const asset = part.type === "image" ? part.image : part.data;
759
+ if (typeof asset !== "string" || asset.startsWith("data:")) {
760
+ return part.mimeType;
761
+ }
762
+ try {
763
+ const url = new URL(asset);
764
+ const basename = url.pathname.split("/").filter(Boolean).pop();
765
+ return basename ? decodeURIComponent(basename) : part.mimeType;
766
+ } catch {
767
+ return part.mimeType;
768
+ }
769
+ }
770
+ function formatObserverAttachmentPlaceholder(part, counter) {
771
+ const attachmentType = part.type === "image" || isImageLikeObserverFilePart(part) ? "Image" : "File";
772
+ const attachmentId = attachmentType === "Image" ? counter.nextImageId++ : counter.nextFileId++;
773
+ const label = resolveObserverAttachmentLabel(part);
774
+ return label ? `[${attachmentType} #${attachmentId}: ${label}]` : `[${attachmentType} #${attachmentId}]`;
775
+ }
776
+ function formatObserverMessage(msg, counter, options) {
666
777
  const maxLen = options?.maxPartLength;
667
- return messages.map((msg) => {
668
- const timestamp = msg.createdAt ? new Date(msg.createdAt).toLocaleString("en-US", {
669
- year: "numeric",
670
- month: "short",
671
- day: "numeric",
672
- hour: "numeric",
673
- minute: "2-digit",
674
- hour12: true
675
- }) : "";
676
- const role = msg.role.charAt(0).toUpperCase() + msg.role.slice(1);
677
- const timestampStr = timestamp ? ` (${timestamp})` : "";
678
- let content = "";
679
- if (typeof msg.content === "string") {
680
- content = maybeTruncate(msg.content, maxLen);
681
- } else if (msg.content?.parts && Array.isArray(msg.content.parts) && msg.content.parts.length > 0) {
682
- content = msg.content.parts.map((part) => {
683
- if (part.type === "text") return maybeTruncate(part.text, maxLen);
684
- if (part.type === "tool-invocation") {
685
- const inv = part.toolInvocation;
686
- if (inv.state === "result") {
687
- const resultStr = JSON.stringify(inv.result, null, 2);
688
- return `[Tool Result: ${inv.toolName}]
778
+ const timestamp = formatObserverTimestamp(msg.createdAt);
779
+ const role = msg.role.charAt(0).toUpperCase() + msg.role.slice(1);
780
+ const timestampStr = timestamp ? ` (${timestamp})` : "";
781
+ const attachments = [];
782
+ let content = "";
783
+ if (typeof msg.content === "string") {
784
+ content = maybeTruncate(msg.content, maxLen);
785
+ } else if (msg.content?.parts && Array.isArray(msg.content.parts) && msg.content.parts.length > 0) {
786
+ content = msg.content.parts.map((part) => {
787
+ if (part.type === "text") return maybeTruncate(part.text, maxLen);
788
+ if (part.type === "tool-invocation") {
789
+ const inv = part.toolInvocation;
790
+ if (inv.state === "result") {
791
+ const resultStr = JSON.stringify(inv.result, null, 2);
792
+ return `[Tool Result: ${inv.toolName}]
689
793
  ${maybeTruncate(resultStr, maxLen)}`;
690
- }
691
- const argsStr = JSON.stringify(inv.args, null, 2);
692
- return `[Tool Call: ${inv.toolName}]
794
+ }
795
+ const argsStr = JSON.stringify(inv.args, null, 2);
796
+ return `[Tool Call: ${inv.toolName}]
693
797
  ${maybeTruncate(argsStr, maxLen)}`;
798
+ }
799
+ const partType = part.type;
800
+ if (partType === "image" || partType === "file") {
801
+ const attachment = part;
802
+ const inputAttachment = toObserverInputAttachmentPart(attachment);
803
+ if (inputAttachment) {
804
+ attachments.push(inputAttachment);
694
805
  }
695
- if (part.type?.startsWith("data-")) return "";
696
- return "";
697
- }).filter(Boolean).join("\n");
698
- } else if (msg.content?.content) {
699
- content = maybeTruncate(msg.content.content, maxLen);
806
+ return formatObserverAttachmentPlaceholder(attachment, counter);
807
+ }
808
+ if (partType?.startsWith("data-")) return "";
809
+ return "";
810
+ }).filter(Boolean).join("\n");
811
+ } else if (msg.content?.content) {
812
+ content = maybeTruncate(msg.content.content, maxLen);
813
+ }
814
+ return {
815
+ text: `**${role}${timestampStr}:**
816
+ ${content}`,
817
+ attachments
818
+ };
819
+ }
820
+ function formatMessagesForObserver(messages, options) {
821
+ const counter = { nextImageId: 1, nextFileId: 1 };
822
+ return messages.map((msg) => formatObserverMessage(msg, counter, options).text).join("\n\n---\n\n");
823
+ }
824
+ function buildObserverHistoryMessage(messages) {
825
+ const counter = { nextImageId: 1, nextFileId: 1 };
826
+ const content = [{ type: "text", text: "## New Message History to Observe\n\n" }];
827
+ messages.forEach((message, index) => {
828
+ const formatted = formatObserverMessage(message, counter);
829
+ content.push({ type: "text", text: formatted.text });
830
+ content.push(...formatted.attachments);
831
+ if (index < messages.length - 1) {
832
+ content.push({ type: "text", text: "\n\n---\n\n" });
700
833
  }
701
- return `**${role}${timestampStr}:**
702
- ${content}`;
703
- }).join("\n\n---\n\n");
834
+ });
835
+ return {
836
+ role: "user",
837
+ content
838
+ };
704
839
  }
705
840
  function maybeTruncate(str, maxLen) {
706
841
  if (!maxLen || str.length <= maxLen) return str;
@@ -709,20 +844,42 @@ function maybeTruncate(str, maxLen) {
709
844
  return `${truncated}
710
845
  ... [truncated ${remaining} characters]`;
711
846
  }
712
- function formatMultiThreadMessagesForObserver(messagesByThread, threadOrder) {
713
- const sections = [];
714
- for (const threadId of threadOrder) {
847
+ function buildMultiThreadObserverHistoryMessage(messagesByThread, threadOrder) {
848
+ const counter = { nextImageId: 1, nextFileId: 1 };
849
+ const content = [
850
+ {
851
+ type: "text",
852
+ text: `## New Message History to Observe
853
+
854
+ The following messages are from ${threadOrder.length} different conversation threads. Each thread is wrapped in a <thread id="..."> tag.
855
+
856
+ `
857
+ }
858
+ ];
859
+ threadOrder.forEach((threadId, threadIndex) => {
715
860
  const messages = messagesByThread.get(threadId);
716
- if (!messages || messages.length === 0) continue;
717
- const formattedMessages = formatMessagesForObserver(messages);
718
- sections.push(`<thread id="${threadId}">
719
- ${formattedMessages}
720
- </thread>`);
721
- }
722
- return sections.join("\n\n");
861
+ if (!messages || messages.length === 0) return;
862
+ content.push({ type: "text", text: `<thread id="${threadId}">
863
+ ` });
864
+ messages.forEach((message, messageIndex) => {
865
+ const formatted = formatObserverMessage(message, counter);
866
+ content.push({ type: "text", text: formatted.text });
867
+ content.push(...formatted.attachments);
868
+ if (messageIndex < messages.length - 1) {
869
+ content.push({ type: "text", text: "\n\n---\n\n" });
870
+ }
871
+ });
872
+ content.push({ type: "text", text: "\n</thread>" });
873
+ if (threadIndex < threadOrder.length - 1) {
874
+ content.push({ type: "text", text: "\n\n" });
875
+ }
876
+ });
877
+ return {
878
+ role: "user",
879
+ content
880
+ };
723
881
  }
724
- function buildMultiThreadObserverPrompt(existingObservations, messagesByThread, threadOrder) {
725
- const formattedMessages = formatMultiThreadMessagesForObserver(messagesByThread, threadOrder);
882
+ function buildMultiThreadObserverTaskPrompt(existingObservations) {
726
883
  let prompt = "";
727
884
  if (existingObservations) {
728
885
  prompt += `## Previous Observations
@@ -734,15 +891,6 @@ ${existingObservations}
734
891
  `;
735
892
  prompt += "Do not repeat these existing observations. Your new observations will be appended to the existing observations.\n\n";
736
893
  }
737
- prompt += `## New Message History to Observe
738
-
739
- The following messages are from ${threadOrder.length} different conversation threads. Each thread is wrapped in a <thread id="..."> tag.
740
-
741
- ${formattedMessages}
742
-
743
- ---
744
-
745
- `;
746
894
  prompt += `## Your Task
747
895
 
748
896
  `;
@@ -819,8 +967,7 @@ function parseMultiThreadObserverOutput(output) {
819
967
  rawOutput: output
820
968
  };
821
969
  }
822
- function buildObserverPrompt(existingObservations, messagesToObserve, options) {
823
- const formattedMessages = formatMessagesForObserver(messagesToObserve);
970
+ function buildObserverTaskPrompt(existingObservations, options) {
824
971
  let prompt = "";
825
972
  if (existingObservations) {
826
973
  prompt += `## Previous Observations
@@ -832,13 +979,6 @@ ${existingObservations}
832
979
  `;
833
980
  prompt += "Do not repeat these existing observations. Your new observations will be appended to the existing observations.\n\n";
834
981
  }
835
- prompt += `## New Message History to Observe
836
-
837
- ${formattedMessages}
838
-
839
- ---
840
-
841
- `;
842
982
  prompt += `## Your Task
843
983
 
844
984
  `;
@@ -850,6 +990,16 @@ IMPORTANT: Do NOT include <current-task> or <suggested-response> sections in you
850
990
  }
851
991
  return prompt;
852
992
  }
993
+ function buildObserverPrompt(existingObservations, messagesToObserve, options) {
994
+ const formattedMessages = formatMessagesForObserver(messagesToObserve);
995
+ return `## New Message History to Observe
996
+
997
+ ${formattedMessages}
998
+
999
+ ---
1000
+
1001
+ ${buildObserverTaskPrompt(existingObservations, options)}`;
1002
+ }
853
1003
  function parseObserverOutput(output) {
854
1004
  if (detectDegenerateRepetition(output)) {
855
1005
  return {
@@ -1221,6 +1371,140 @@ function extractReflectorListItems(content) {
1221
1371
  function validateCompression(reflectedTokens, targetThreshold) {
1222
1372
  return reflectedTokens < targetThreshold;
1223
1373
  }
1374
+ var OM_REPRO_CAPTURE_DIR = process.env.OM_REPRO_CAPTURE_DIR ?? ".mastra-om-repro";
1375
+ function sanitizeCapturePathSegment(value) {
1376
+ const sanitized = value.replace(/[\\/]+/g, "_").replace(/\.{2,}/g, "_").trim();
1377
+ return sanitized.length > 0 ? sanitized : "unknown-thread";
1378
+ }
1379
+ function isOmReproCaptureEnabled() {
1380
+ return process.env.OM_REPRO_CAPTURE === "1";
1381
+ }
1382
+ function safeCaptureJson(value) {
1383
+ return JSON.parse(
1384
+ JSON.stringify(value, (_key, current) => {
1385
+ if (typeof current === "bigint") return current.toString();
1386
+ if (typeof current === "function") return "[function]";
1387
+ if (current instanceof Error) return { name: current.name, message: current.message, stack: current.stack };
1388
+ if (current instanceof Set) return { __type: "Set", values: Array.from(current.values()) };
1389
+ if (current instanceof Map) return { __type: "Map", entries: Array.from(current.entries()) };
1390
+ return current;
1391
+ })
1392
+ );
1393
+ }
1394
+ function buildReproMessageFingerprint(message) {
1395
+ const createdAt = message.createdAt instanceof Date ? message.createdAt.toISOString() : message.createdAt ? new Date(message.createdAt).toISOString() : "";
1396
+ return JSON.stringify({
1397
+ role: message.role,
1398
+ createdAt,
1399
+ content: message.content
1400
+ });
1401
+ }
1402
+ function inferReproIdRemap(preMessages, postMessages) {
1403
+ const preByFingerprint = /* @__PURE__ */ new Map();
1404
+ const postByFingerprint = /* @__PURE__ */ new Map();
1405
+ for (const message of preMessages) {
1406
+ if (!message.id) continue;
1407
+ const fingerprint = buildReproMessageFingerprint(message);
1408
+ const list = preByFingerprint.get(fingerprint) ?? [];
1409
+ list.push(message.id);
1410
+ preByFingerprint.set(fingerprint, list);
1411
+ }
1412
+ for (const message of postMessages) {
1413
+ if (!message.id) continue;
1414
+ const fingerprint = buildReproMessageFingerprint(message);
1415
+ const list = postByFingerprint.get(fingerprint) ?? [];
1416
+ list.push(message.id);
1417
+ postByFingerprint.set(fingerprint, list);
1418
+ }
1419
+ const remap = [];
1420
+ for (const [fingerprint, preIds] of preByFingerprint.entries()) {
1421
+ const postIds = postByFingerprint.get(fingerprint);
1422
+ if (!postIds || preIds.length !== 1 || postIds.length !== 1) continue;
1423
+ const fromId = preIds[0];
1424
+ const toId = postIds[0];
1425
+ if (!fromId || !toId || fromId === toId) {
1426
+ continue;
1427
+ }
1428
+ remap.push({ fromId, toId, fingerprint });
1429
+ }
1430
+ return remap;
1431
+ }
1432
+ function writeProcessInputStepReproCapture(params) {
1433
+ if (!isOmReproCaptureEnabled()) {
1434
+ return;
1435
+ }
1436
+ try {
1437
+ const sanitizedThreadId = sanitizeCapturePathSegment(params.threadId);
1438
+ const runId = `${Date.now()}-step-${params.stepNumber}-${randomUUID()}`;
1439
+ const captureDir = join(process.cwd(), OM_REPRO_CAPTURE_DIR, sanitizedThreadId, runId);
1440
+ mkdirSync(captureDir, { recursive: true });
1441
+ const contextMessages = params.messageList.get.all.db();
1442
+ const memoryContext = parseMemoryRequestContext(params.args.requestContext);
1443
+ const preMessageIds = new Set(params.preMessages.map((message) => message.id));
1444
+ const postMessageIds = new Set(contextMessages.map((message) => message.id));
1445
+ const removedMessageIds = params.preMessages.map((message) => message.id).filter((id) => Boolean(id) && !postMessageIds.has(id));
1446
+ const addedMessageIds = contextMessages.map((message) => message.id).filter((id) => Boolean(id) && !preMessageIds.has(id));
1447
+ const idRemap = inferReproIdRemap(params.preMessages, contextMessages);
1448
+ const rawState = params.args.state ?? {};
1449
+ const inputPayload = safeCaptureJson({
1450
+ stepNumber: params.stepNumber,
1451
+ threadId: params.threadId,
1452
+ resourceId: params.resourceId,
1453
+ readOnly: memoryContext?.memoryConfig?.readOnly,
1454
+ messageCount: contextMessages.length,
1455
+ messageIds: contextMessages.map((message) => message.id),
1456
+ stateKeys: Object.keys(rawState),
1457
+ state: rawState,
1458
+ args: {
1459
+ messages: params.args.messages,
1460
+ steps: params.args.steps,
1461
+ systemMessages: params.args.systemMessages,
1462
+ retryCount: params.args.retryCount,
1463
+ tools: params.args.tools,
1464
+ toolChoice: params.args.toolChoice,
1465
+ activeTools: params.args.activeTools,
1466
+ providerOptions: params.args.providerOptions,
1467
+ modelSettings: params.args.modelSettings,
1468
+ structuredOutput: params.args.structuredOutput
1469
+ }
1470
+ });
1471
+ const preStatePayload = safeCaptureJson({
1472
+ record: params.preRecord,
1473
+ bufferedChunks: params.preBufferedChunks,
1474
+ contextTokenCount: params.preContextTokenCount,
1475
+ messages: params.preMessages,
1476
+ messageList: params.preSerializedMessageList
1477
+ });
1478
+ const outputPayload = safeCaptureJson({
1479
+ details: params.details,
1480
+ messageDiff: {
1481
+ removedMessageIds,
1482
+ addedMessageIds,
1483
+ idRemap
1484
+ }
1485
+ });
1486
+ const postStatePayload = safeCaptureJson({
1487
+ record: params.postRecord,
1488
+ bufferedChunks: params.postBufferedChunks,
1489
+ contextTokenCount: params.postContextTokenCount,
1490
+ messageCount: contextMessages.length,
1491
+ messageIds: contextMessages.map((message) => message.id),
1492
+ messages: contextMessages,
1493
+ messageList: params.messageList.serialize()
1494
+ });
1495
+ writeFileSync(join(captureDir, "input.json"), `${JSON.stringify(inputPayload, null, 2)}
1496
+ `);
1497
+ writeFileSync(join(captureDir, "pre-state.json"), `${JSON.stringify(preStatePayload, null, 2)}
1498
+ `);
1499
+ writeFileSync(join(captureDir, "output.json"), `${JSON.stringify(outputPayload, null, 2)}
1500
+ `);
1501
+ writeFileSync(join(captureDir, "post-state.json"), `${JSON.stringify(postStatePayload, null, 2)}
1502
+ `);
1503
+ params.debug?.(`[OM:repro-capture] wrote processInputStep capture to ${captureDir}`);
1504
+ } catch (error) {
1505
+ params.debug?.(`[OM:repro-capture] failed to write processInputStep capture: ${String(error)}`);
1506
+ }
1507
+ }
1224
1508
 
1225
1509
  // src/processors/observational-memory/thresholds.ts
1226
1510
  function getMaxThreshold(threshold) {
@@ -1306,14 +1590,90 @@ function calculateProjectedMessageRemoval(chunks, bufferActivation, messageToken
1306
1590
  }
1307
1591
  return bestBoundaryMessageTokens;
1308
1592
  }
1309
- var sharedDefaultEncoder;
1593
+ var GLOBAL_TIKTOKEN_KEY = "__mastraTiktoken";
1310
1594
  function getDefaultEncoder() {
1311
- if (!sharedDefaultEncoder) {
1312
- sharedDefaultEncoder = new Tiktoken(o200k_base);
1595
+ const cached = globalThis[GLOBAL_TIKTOKEN_KEY];
1596
+ if (cached) return cached;
1597
+ const encoder = new Tiktoken(o200k_base);
1598
+ globalThis[GLOBAL_TIKTOKEN_KEY] = encoder;
1599
+ return encoder;
1600
+ }
1601
+ var IMAGE_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
1602
+ "png",
1603
+ "jpg",
1604
+ "jpeg",
1605
+ "webp",
1606
+ "gif",
1607
+ "bmp",
1608
+ "tiff",
1609
+ "tif",
1610
+ "heic",
1611
+ "heif",
1612
+ "avif"
1613
+ ]);
1614
+ var TOKEN_ESTIMATE_CACHE_VERSION = 5;
1615
+ var DEFAULT_IMAGE_ESTIMATOR = {
1616
+ baseTokens: 85,
1617
+ tileTokens: 170,
1618
+ fallbackTiles: 4
1619
+ };
1620
+ var GOOGLE_LEGACY_IMAGE_TOKENS_PER_TILE = 258;
1621
+ var GOOGLE_GEMINI_3_IMAGE_TOKENS_BY_RESOLUTION = {
1622
+ low: 280,
1623
+ medium: 560,
1624
+ high: 1120,
1625
+ ultra_high: 2240,
1626
+ unspecified: 1120
1627
+ };
1628
+ var ANTHROPIC_IMAGE_TOKENS_PER_PIXEL = 1 / 750;
1629
+ var ANTHROPIC_IMAGE_MAX_LONG_EDGE = 1568;
1630
+ var GOOGLE_MEDIA_RESOLUTION_VALUES = /* @__PURE__ */ new Set([
1631
+ "low",
1632
+ "medium",
1633
+ "high",
1634
+ "ultra_high",
1635
+ "unspecified"
1636
+ ]);
1637
+ var ATTACHMENT_COUNT_TIMEOUT_MS = 2e4;
1638
+ var REMOTE_IMAGE_PROBE_TIMEOUT_MS = 2500;
1639
+ var PROVIDER_API_KEY_ENV_VARS = {
1640
+ openai: ["OPENAI_API_KEY"],
1641
+ google: ["GOOGLE_GENERATIVE_AI_API_KEY", "GOOGLE_API_KEY"],
1642
+ anthropic: ["ANTHROPIC_API_KEY"]
1643
+ };
1644
+ function getPartMastraMetadata(part) {
1645
+ return part.providerMetadata?.mastra;
1646
+ }
1647
+ function ensurePartMastraMetadata(part) {
1648
+ const typedPart = part;
1649
+ typedPart.providerMetadata ??= {};
1650
+ typedPart.providerMetadata.mastra ??= {};
1651
+ return typedPart.providerMetadata.mastra;
1652
+ }
1653
+ function getContentMastraMetadata(content) {
1654
+ if (!content || typeof content !== "object") {
1655
+ return void 0;
1313
1656
  }
1314
- return sharedDefaultEncoder;
1657
+ return content.metadata?.mastra;
1658
+ }
1659
+ function ensureContentMastraMetadata(content) {
1660
+ if (!content || typeof content !== "object") {
1661
+ return void 0;
1662
+ }
1663
+ const typedContent = content;
1664
+ typedContent.metadata ??= {};
1665
+ typedContent.metadata.mastra ??= {};
1666
+ return typedContent.metadata.mastra;
1667
+ }
1668
+ function getMessageMastraMetadata(message) {
1669
+ return message.metadata?.mastra;
1670
+ }
1671
+ function ensureMessageMastraMetadata(message) {
1672
+ const typedMessage = message;
1673
+ typedMessage.metadata ??= {};
1674
+ typedMessage.metadata.mastra ??= {};
1675
+ return typedMessage.metadata.mastra;
1315
1676
  }
1316
- var TOKEN_ESTIMATE_CACHE_VERSION = 1;
1317
1677
  function buildEstimateKey(kind, text) {
1318
1678
  const payloadHash = createHash("sha1").update(text).digest("hex");
1319
1679
  return `${kind}:${payloadHash}`;
@@ -1336,51 +1696,60 @@ function getCacheEntry(cache, key) {
1336
1696
  if (isTokenEstimateEntry(cache)) {
1337
1697
  return cache.key === key ? cache : void 0;
1338
1698
  }
1339
- return void 0;
1699
+ const keyedEntry = cache[key];
1700
+ return isTokenEstimateEntry(keyedEntry) ? keyedEntry : void 0;
1701
+ }
1702
+ function mergeCacheEntry(cache, key, entry) {
1703
+ if (isTokenEstimateEntry(cache)) {
1704
+ if (cache.key === key) {
1705
+ return entry;
1706
+ }
1707
+ return {
1708
+ [cache.key]: cache,
1709
+ [key]: entry
1710
+ };
1711
+ }
1712
+ if (cache && typeof cache === "object") {
1713
+ return {
1714
+ ...cache,
1715
+ [key]: entry
1716
+ };
1717
+ }
1718
+ return entry;
1340
1719
  }
1341
1720
  function getPartCacheEntry(part, key) {
1342
- const cache = part?.providerMetadata?.mastra?.tokenEstimate;
1343
- return getCacheEntry(cache, key);
1721
+ return getCacheEntry(getPartMastraMetadata(part)?.tokenEstimate, key);
1344
1722
  }
1345
- function setPartCacheEntry(part, _key, entry) {
1346
- const mutablePart = part;
1347
- mutablePart.providerMetadata ??= {};
1348
- mutablePart.providerMetadata.mastra ??= {};
1349
- mutablePart.providerMetadata.mastra.tokenEstimate = entry;
1723
+ function setPartCacheEntry(part, key, entry) {
1724
+ const mastraMetadata = ensurePartMastraMetadata(part);
1725
+ mastraMetadata.tokenEstimate = mergeCacheEntry(mastraMetadata.tokenEstimate, key, entry);
1350
1726
  }
1351
1727
  function getMessageCacheEntry(message, key) {
1352
- const content = message.content;
1353
- if (content && typeof content === "object") {
1354
- const contentLevelCache = content.metadata?.mastra?.tokenEstimate;
1355
- const contentLevelEntry = getCacheEntry(contentLevelCache, key);
1356
- if (contentLevelEntry) return contentLevelEntry;
1357
- }
1358
- const messageLevelCache = message?.metadata?.mastra?.tokenEstimate;
1359
- return getCacheEntry(messageLevelCache, key);
1360
- }
1361
- function setMessageCacheEntry(message, _key, entry) {
1362
- const content = message.content;
1363
- if (content && typeof content === "object") {
1364
- content.metadata ??= {};
1365
- content.metadata.mastra ??= {};
1366
- content.metadata.mastra.tokenEstimate = entry;
1728
+ const contentLevelEntry = getCacheEntry(getContentMastraMetadata(message.content)?.tokenEstimate, key);
1729
+ if (contentLevelEntry) return contentLevelEntry;
1730
+ return getCacheEntry(getMessageMastraMetadata(message)?.tokenEstimate, key);
1731
+ }
1732
+ function setMessageCacheEntry(message, key, entry) {
1733
+ const contentMastraMetadata = ensureContentMastraMetadata(message.content);
1734
+ if (contentMastraMetadata) {
1735
+ contentMastraMetadata.tokenEstimate = mergeCacheEntry(contentMastraMetadata.tokenEstimate, key, entry);
1367
1736
  return;
1368
1737
  }
1369
- message.metadata ??= {};
1370
- message.metadata.mastra ??= {};
1371
- message.metadata.mastra.tokenEstimate = entry;
1738
+ const messageMastraMetadata = ensureMessageMastraMetadata(message);
1739
+ messageMastraMetadata.tokenEstimate = mergeCacheEntry(messageMastraMetadata.tokenEstimate, key, entry);
1372
1740
  }
1373
1741
  function serializePartForTokenCounting(part) {
1374
- const hasTokenEstimate = Boolean(part?.providerMetadata?.mastra?.tokenEstimate);
1742
+ const typedPart = part;
1743
+ const hasTokenEstimate = Boolean(typedPart.providerMetadata?.mastra?.tokenEstimate);
1375
1744
  if (!hasTokenEstimate) {
1376
1745
  return JSON.stringify(part);
1377
1746
  }
1378
1747
  const clonedPart = {
1379
- ...part,
1748
+ ...typedPart,
1380
1749
  providerMetadata: {
1381
- ...part.providerMetadata ?? {},
1750
+ ...typedPart.providerMetadata ?? {},
1382
1751
  mastra: {
1383
- ...part.providerMetadata?.mastra ?? {}
1752
+ ...typedPart.providerMetadata?.mastra ?? {}
1384
1753
  }
1385
1754
  }
1386
1755
  };
@@ -1393,23 +1762,648 @@ function serializePartForTokenCounting(part) {
1393
1762
  }
1394
1763
  return JSON.stringify(clonedPart);
1395
1764
  }
1765
+ function getFilenameFromAttachmentData(data) {
1766
+ const pathname = data instanceof URL ? data.pathname : typeof data === "string" && isHttpUrlString(data) ? (() => {
1767
+ try {
1768
+ return new URL(data).pathname;
1769
+ } catch {
1770
+ return void 0;
1771
+ }
1772
+ })() : void 0;
1773
+ const filename = pathname?.split("/").filter(Boolean).pop();
1774
+ return filename ? decodeURIComponent(filename) : void 0;
1775
+ }
1776
+ function serializeNonImageFilePartForTokenCounting(part) {
1777
+ const filename = getObjectValue(part, "filename");
1778
+ const inferredFilename = getFilenameFromAttachmentData(getObjectValue(part, "data"));
1779
+ return JSON.stringify({
1780
+ type: "file",
1781
+ mimeType: getObjectValue(part, "mimeType") ?? null,
1782
+ filename: typeof filename === "string" && filename.trim().length > 0 ? filename.trim() : inferredFilename ?? null
1783
+ });
1784
+ }
1396
1785
  function isValidCacheEntry(entry, expectedKey, expectedSource) {
1397
1786
  return Boolean(
1398
1787
  entry && entry.v === TOKEN_ESTIMATE_CACHE_VERSION && entry.source === expectedSource && entry.key === expectedKey && Number.isFinite(entry.tokens)
1399
1788
  );
1400
1789
  }
1790
+ function parseModelContext(model) {
1791
+ if (!model) return void 0;
1792
+ if (typeof model === "object") {
1793
+ return model.provider || model.modelId ? { provider: model.provider, modelId: model.modelId } : void 0;
1794
+ }
1795
+ const slashIndex = model.indexOf("/");
1796
+ if (slashIndex === -1) {
1797
+ return { modelId: model };
1798
+ }
1799
+ return {
1800
+ provider: model.slice(0, slashIndex),
1801
+ modelId: model.slice(slashIndex + 1)
1802
+ };
1803
+ }
1804
+ function normalizeImageDetail(detail) {
1805
+ if (detail === "low" || detail === "high") return detail;
1806
+ return "auto";
1807
+ }
1808
+ function getObjectValue(value, key) {
1809
+ if (!value || typeof value !== "object") return void 0;
1810
+ return value[key];
1811
+ }
1812
+ function resolveImageDetail(part) {
1813
+ const openAIProviderOptions = getObjectValue(getObjectValue(part, "providerOptions"), "openai");
1814
+ const openAIProviderMetadata = getObjectValue(getObjectValue(part, "providerMetadata"), "openai");
1815
+ const mastraMetadata = getObjectValue(getObjectValue(part, "providerMetadata"), "mastra");
1816
+ return normalizeImageDetail(
1817
+ getObjectValue(part, "detail") ?? getObjectValue(part, "imageDetail") ?? getObjectValue(openAIProviderOptions, "detail") ?? getObjectValue(openAIProviderOptions, "imageDetail") ?? getObjectValue(openAIProviderMetadata, "detail") ?? getObjectValue(openAIProviderMetadata, "imageDetail") ?? getObjectValue(mastraMetadata, "imageDetail")
1818
+ );
1819
+ }
1820
+ function normalizeGoogleMediaResolution(value) {
1821
+ return typeof value === "string" && GOOGLE_MEDIA_RESOLUTION_VALUES.has(value) ? value : void 0;
1822
+ }
1823
+ function resolveGoogleMediaResolution(part) {
1824
+ const providerOptions = getObjectValue(getObjectValue(part, "providerOptions"), "google");
1825
+ const providerMetadata = getObjectValue(getObjectValue(part, "providerMetadata"), "google");
1826
+ const mastraMetadata = getObjectValue(getObjectValue(part, "providerMetadata"), "mastra");
1827
+ return normalizeGoogleMediaResolution(getObjectValue(part, "mediaResolution")) ?? normalizeGoogleMediaResolution(getObjectValue(providerOptions, "mediaResolution")) ?? normalizeGoogleMediaResolution(getObjectValue(providerMetadata, "mediaResolution")) ?? normalizeGoogleMediaResolution(getObjectValue(mastraMetadata, "mediaResolution")) ?? "unspecified";
1828
+ }
1829
+ function getFiniteNumber(value) {
1830
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
1831
+ }
1832
+ function isHttpUrlString(value) {
1833
+ return typeof value === "string" && /^https?:\/\//i.test(value);
1834
+ }
1835
+ function isLikelyFilesystemPath(value) {
1836
+ return value.startsWith("/") || value.startsWith("./") || value.startsWith("../") || value.startsWith("~/") || /^[A-Za-z]:[\\/]/.test(value) || value.includes("\\");
1837
+ }
1838
+ function isLikelyBase64Content(value) {
1839
+ if (value.length < 16 || value.length % 4 !== 0 || /\s/.test(value) || isLikelyFilesystemPath(value)) {
1840
+ return false;
1841
+ }
1842
+ return /^[A-Za-z0-9+/]+={0,2}$/.test(value);
1843
+ }
1844
+ function decodeImageBuffer(value) {
1845
+ if (typeof Buffer !== "undefined" && Buffer.isBuffer(value)) {
1846
+ return value;
1847
+ }
1848
+ if (value instanceof Uint8Array) {
1849
+ return Buffer.from(value);
1850
+ }
1851
+ if (value instanceof ArrayBuffer) {
1852
+ return Buffer.from(value);
1853
+ }
1854
+ if (ArrayBuffer.isView(value)) {
1855
+ return Buffer.from(value.buffer, value.byteOffset, value.byteLength);
1856
+ }
1857
+ if (typeof value !== "string" || isHttpUrlString(value)) {
1858
+ return void 0;
1859
+ }
1860
+ if (value.startsWith("data:")) {
1861
+ const commaIndex = value.indexOf(",");
1862
+ if (commaIndex === -1) return void 0;
1863
+ const header = value.slice(0, commaIndex);
1864
+ const payload = value.slice(commaIndex + 1);
1865
+ if (/;base64/i.test(header)) {
1866
+ return Buffer.from(payload, "base64");
1867
+ }
1868
+ return Buffer.from(decodeURIComponent(payload), "utf8");
1869
+ }
1870
+ if (!isLikelyBase64Content(value)) {
1871
+ return void 0;
1872
+ }
1873
+ return Buffer.from(value, "base64");
1874
+ }
1875
+ function persistImageDimensions(part, dimensions) {
1876
+ const mastraMetadata = ensurePartMastraMetadata(part);
1877
+ mastraMetadata.imageDimensions = dimensions;
1878
+ }
1879
+ function resolveHttpAssetUrl(value) {
1880
+ if (value instanceof URL) {
1881
+ return value.toString();
1882
+ }
1883
+ if (typeof value === "string" && isHttpUrlString(value)) {
1884
+ return value;
1885
+ }
1886
+ return void 0;
1887
+ }
1888
+ async function resolveImageDimensionsAsync(part) {
1889
+ const existing = resolveImageDimensions(part);
1890
+ if (existing.width && existing.height) {
1891
+ return existing;
1892
+ }
1893
+ const asset = getObjectValue(part, "image") ?? getObjectValue(part, "data");
1894
+ const url = resolveHttpAssetUrl(asset);
1895
+ if (!url) {
1896
+ return existing;
1897
+ }
1898
+ try {
1899
+ const mod = await import('probe-image-size');
1900
+ const probeImageSize = mod.default;
1901
+ const probed = await probeImageSize(url, {
1902
+ open_timeout: REMOTE_IMAGE_PROBE_TIMEOUT_MS,
1903
+ response_timeout: REMOTE_IMAGE_PROBE_TIMEOUT_MS,
1904
+ read_timeout: REMOTE_IMAGE_PROBE_TIMEOUT_MS,
1905
+ follow_max: 2
1906
+ });
1907
+ const width = existing.width ?? getFiniteNumber(probed.width);
1908
+ const height = existing.height ?? getFiniteNumber(probed.height);
1909
+ if (!width || !height) {
1910
+ return existing;
1911
+ }
1912
+ const resolved = { width, height };
1913
+ persistImageDimensions(part, resolved);
1914
+ return resolved;
1915
+ } catch {
1916
+ return existing;
1917
+ }
1918
+ }
1919
+ function resolveImageDimensions(part) {
1920
+ const mastraMetadata = getObjectValue(getObjectValue(part, "providerMetadata"), "mastra");
1921
+ const dimensions = getObjectValue(mastraMetadata, "imageDimensions");
1922
+ const width = getFiniteNumber(getObjectValue(part, "width")) ?? getFiniteNumber(getObjectValue(part, "imageWidth")) ?? getFiniteNumber(getObjectValue(dimensions, "width"));
1923
+ const height = getFiniteNumber(getObjectValue(part, "height")) ?? getFiniteNumber(getObjectValue(part, "imageHeight")) ?? getFiniteNumber(getObjectValue(dimensions, "height"));
1924
+ if (width && height) {
1925
+ return { width, height };
1926
+ }
1927
+ const asset = getObjectValue(part, "image") ?? getObjectValue(part, "data");
1928
+ const buffer = decodeImageBuffer(asset);
1929
+ if (!buffer) {
1930
+ return { width, height };
1931
+ }
1932
+ try {
1933
+ const measured = imageSize(buffer);
1934
+ const measuredWidth = getFiniteNumber(measured.width);
1935
+ const measuredHeight = getFiniteNumber(measured.height);
1936
+ if (!measuredWidth || !measuredHeight) {
1937
+ return { width, height };
1938
+ }
1939
+ const resolved = {
1940
+ width: width ?? measuredWidth,
1941
+ height: height ?? measuredHeight
1942
+ };
1943
+ persistImageDimensions(part, resolved);
1944
+ return resolved;
1945
+ } catch {
1946
+ return { width, height };
1947
+ }
1948
+ }
1949
+ function getBase64Size(base64) {
1950
+ const sanitized = base64.replace(/\s+/g, "");
1951
+ const padding = sanitized.endsWith("==") ? 2 : sanitized.endsWith("=") ? 1 : 0;
1952
+ return Math.max(0, Math.floor(sanitized.length * 3 / 4) - padding);
1953
+ }
1954
+ function resolveImageSourceStats(image) {
1955
+ if (image instanceof URL) {
1956
+ return { source: "url" };
1957
+ }
1958
+ if (typeof image === "string") {
1959
+ if (isHttpUrlString(image)) {
1960
+ return { source: "url" };
1961
+ }
1962
+ if (image.startsWith("data:")) {
1963
+ const commaIndex = image.indexOf(",");
1964
+ const encoded = commaIndex === -1 ? "" : image.slice(commaIndex + 1);
1965
+ return {
1966
+ source: "data-uri",
1967
+ sizeBytes: getBase64Size(encoded)
1968
+ };
1969
+ }
1970
+ return {
1971
+ source: "binary",
1972
+ sizeBytes: getBase64Size(image)
1973
+ };
1974
+ }
1975
+ if (typeof Buffer !== "undefined" && Buffer.isBuffer(image)) {
1976
+ return { source: "binary", sizeBytes: image.length };
1977
+ }
1978
+ if (image instanceof Uint8Array) {
1979
+ return { source: "binary", sizeBytes: image.byteLength };
1980
+ }
1981
+ if (image instanceof ArrayBuffer) {
1982
+ return { source: "binary", sizeBytes: image.byteLength };
1983
+ }
1984
+ if (ArrayBuffer.isView(image)) {
1985
+ return { source: "binary", sizeBytes: image.byteLength };
1986
+ }
1987
+ return { source: "binary" };
1988
+ }
1989
+ function getPathnameExtension(value) {
1990
+ const normalized = value.split("#", 1)[0]?.split("?", 1)[0] ?? value;
1991
+ const match = normalized.match(/\.([a-z0-9]+)$/i);
1992
+ return match?.[1]?.toLowerCase();
1993
+ }
1994
+ function hasImageFilenameExtension(filename) {
1995
+ return typeof filename === "string" && IMAGE_FILE_EXTENSIONS.has(getPathnameExtension(filename) ?? "");
1996
+ }
1997
+ function isImageLikeFilePart(part) {
1998
+ if (getObjectValue(part, "type") !== "file") {
1999
+ return false;
2000
+ }
2001
+ const mimeType = getObjectValue(part, "mimeType");
2002
+ if (typeof mimeType === "string" && mimeType.toLowerCase().startsWith("image/")) {
2003
+ return true;
2004
+ }
2005
+ const data = getObjectValue(part, "data");
2006
+ if (typeof data === "string" && data.startsWith("data:image/")) {
2007
+ return true;
2008
+ }
2009
+ if (data instanceof URL && hasImageFilenameExtension(data.pathname)) {
2010
+ return true;
2011
+ }
2012
+ if (isHttpUrlString(data)) {
2013
+ try {
2014
+ const url = new URL(data);
2015
+ if (hasImageFilenameExtension(url.pathname)) {
2016
+ return true;
2017
+ }
2018
+ } catch {
2019
+ }
2020
+ }
2021
+ return hasImageFilenameExtension(getObjectValue(part, "filename"));
2022
+ }
2023
+ function resolveProviderId(modelContext) {
2024
+ return modelContext?.provider?.toLowerCase();
2025
+ }
2026
+ function resolveModelId(modelContext) {
2027
+ return modelContext?.modelId?.toLowerCase() ?? "";
2028
+ }
2029
+ function resolveOpenAIImageEstimatorConfig(modelContext) {
2030
+ const modelId = resolveModelId(modelContext);
2031
+ if (modelId.startsWith("gpt-5") || modelId === "gpt-5-chat-latest") {
2032
+ return { baseTokens: 70, tileTokens: 140, fallbackTiles: 4 };
2033
+ }
2034
+ if (modelId.startsWith("gpt-4o-mini")) {
2035
+ return { baseTokens: 2833, tileTokens: 5667, fallbackTiles: 1 };
2036
+ }
2037
+ if (modelId.startsWith("o1") || modelId.startsWith("o3")) {
2038
+ return { baseTokens: 75, tileTokens: 150, fallbackTiles: 4 };
2039
+ }
2040
+ if (modelId.includes("computer-use")) {
2041
+ return { baseTokens: 65, tileTokens: 129, fallbackTiles: 4 };
2042
+ }
2043
+ return DEFAULT_IMAGE_ESTIMATOR;
2044
+ }
2045
+ function isGoogleGemini3Model(modelContext) {
2046
+ return resolveProviderId(modelContext) === "google" && resolveModelId(modelContext).startsWith("gemini-3");
2047
+ }
2048
+ function scaleDimensionsForOpenAIHighDetail(width, height) {
2049
+ let scaledWidth = width;
2050
+ let scaledHeight = height;
2051
+ const largestSide = Math.max(scaledWidth, scaledHeight);
2052
+ if (largestSide > 2048) {
2053
+ const ratio = 2048 / largestSide;
2054
+ scaledWidth *= ratio;
2055
+ scaledHeight *= ratio;
2056
+ }
2057
+ const shortestSide = Math.min(scaledWidth, scaledHeight);
2058
+ if (shortestSide > 768) {
2059
+ const ratio = 768 / shortestSide;
2060
+ scaledWidth *= ratio;
2061
+ scaledHeight *= ratio;
2062
+ }
2063
+ return {
2064
+ width: Math.max(1, Math.round(scaledWidth)),
2065
+ height: Math.max(1, Math.round(scaledHeight))
2066
+ };
2067
+ }
2068
+ function scaleDimensionsForAnthropic(width, height) {
2069
+ const largestSide = Math.max(width, height);
2070
+ if (largestSide <= ANTHROPIC_IMAGE_MAX_LONG_EDGE) {
2071
+ return { width, height };
2072
+ }
2073
+ const ratio = ANTHROPIC_IMAGE_MAX_LONG_EDGE / largestSide;
2074
+ return {
2075
+ width: Math.max(1, Math.round(width * ratio)),
2076
+ height: Math.max(1, Math.round(height * ratio))
2077
+ };
2078
+ }
2079
+ function estimateOpenAIHighDetailTiles(dimensions, sourceStats, estimator) {
2080
+ if (dimensions.width && dimensions.height) {
2081
+ const scaled = scaleDimensionsForOpenAIHighDetail(dimensions.width, dimensions.height);
2082
+ return Math.max(1, Math.ceil(scaled.width / 512) * Math.ceil(scaled.height / 512));
2083
+ }
2084
+ if (sourceStats.sizeBytes !== void 0) {
2085
+ if (sourceStats.sizeBytes <= 512 * 1024) return 1;
2086
+ if (sourceStats.sizeBytes <= 2 * 1024 * 1024) return 4;
2087
+ if (sourceStats.sizeBytes <= 4 * 1024 * 1024) return 6;
2088
+ return 8;
2089
+ }
2090
+ return estimator.fallbackTiles;
2091
+ }
2092
+ function resolveEffectiveOpenAIImageDetail(detail, dimensions, sourceStats) {
2093
+ if (detail === "low" || detail === "high") return detail;
2094
+ if (dimensions.width && dimensions.height) {
2095
+ return Math.max(dimensions.width, dimensions.height) > 768 ? "high" : "low";
2096
+ }
2097
+ if (sourceStats.sizeBytes !== void 0) {
2098
+ return sourceStats.sizeBytes > 1024 * 1024 ? "high" : "low";
2099
+ }
2100
+ return "low";
2101
+ }
2102
+ function estimateLegacyGoogleImageTiles(dimensions) {
2103
+ if (!dimensions.width || !dimensions.height) return 1;
2104
+ return Math.max(1, Math.ceil(dimensions.width / 768) * Math.ceil(dimensions.height / 768));
2105
+ }
2106
+ function estimateAnthropicImageTokens(dimensions, sourceStats) {
2107
+ if (dimensions.width && dimensions.height) {
2108
+ const scaled = scaleDimensionsForAnthropic(dimensions.width, dimensions.height);
2109
+ return Math.max(1, Math.ceil(scaled.width * scaled.height * ANTHROPIC_IMAGE_TOKENS_PER_PIXEL));
2110
+ }
2111
+ if (sourceStats.sizeBytes !== void 0) {
2112
+ if (sourceStats.sizeBytes <= 512 * 1024) return 341;
2113
+ if (sourceStats.sizeBytes <= 2 * 1024 * 1024) return 1366;
2114
+ if (sourceStats.sizeBytes <= 4 * 1024 * 1024) return 2048;
2115
+ return 2731;
2116
+ }
2117
+ return 1600;
2118
+ }
2119
+ function estimateGoogleImageTokens(modelContext, part, dimensions) {
2120
+ if (isGoogleGemini3Model(modelContext)) {
2121
+ const mediaResolution = resolveGoogleMediaResolution(part);
2122
+ return {
2123
+ tokens: GOOGLE_GEMINI_3_IMAGE_TOKENS_BY_RESOLUTION[mediaResolution],
2124
+ mediaResolution
2125
+ };
2126
+ }
2127
+ return {
2128
+ tokens: estimateLegacyGoogleImageTiles(dimensions) * GOOGLE_LEGACY_IMAGE_TOKENS_PER_TILE,
2129
+ mediaResolution: "unspecified"
2130
+ };
2131
+ }
2132
+ function getProviderApiKey(provider) {
2133
+ for (const envVar of PROVIDER_API_KEY_ENV_VARS[provider] ?? []) {
2134
+ const value = process.env[envVar];
2135
+ if (typeof value === "string" && value.trim().length > 0) {
2136
+ return value.trim();
2137
+ }
2138
+ }
2139
+ return void 0;
2140
+ }
2141
+ function getAttachmentFilename(part) {
2142
+ const explicitFilename = getObjectValue(part, "filename");
2143
+ if (typeof explicitFilename === "string" && explicitFilename.trim().length > 0) {
2144
+ return explicitFilename.trim();
2145
+ }
2146
+ return getFilenameFromAttachmentData(getObjectValue(part, "data") ?? getObjectValue(part, "image"));
2147
+ }
2148
+ function getAttachmentMimeType(part, fallback) {
2149
+ const mimeType = getObjectValue(part, "mimeType");
2150
+ if (typeof mimeType === "string" && mimeType.trim().length > 0) {
2151
+ return mimeType.trim();
2152
+ }
2153
+ const asset = getObjectValue(part, "data") ?? getObjectValue(part, "image");
2154
+ if (typeof asset === "string" && asset.startsWith("data:")) {
2155
+ const semicolonIndex = asset.indexOf(";");
2156
+ const commaIndex = asset.indexOf(",");
2157
+ const endIndex = semicolonIndex === -1 ? commaIndex : Math.min(semicolonIndex, commaIndex);
2158
+ if (endIndex > 5) {
2159
+ return asset.slice(5, endIndex);
2160
+ }
2161
+ }
2162
+ return fallback;
2163
+ }
2164
+ function getAttachmentUrl(asset) {
2165
+ if (asset instanceof URL) {
2166
+ return asset.toString();
2167
+ }
2168
+ if (typeof asset === "string" && /^(https?:\/\/|data:)/i.test(asset)) {
2169
+ return asset;
2170
+ }
2171
+ return void 0;
2172
+ }
2173
+ function getAttachmentFingerprint(asset) {
2174
+ const url = getAttachmentUrl(asset);
2175
+ if (url) {
2176
+ return { url };
2177
+ }
2178
+ const base64 = encodeAttachmentBase64(asset);
2179
+ if (base64) {
2180
+ return { contentHash: createHash("sha1").update(base64).digest("hex") };
2181
+ }
2182
+ return {};
2183
+ }
2184
+ function encodeAttachmentBase64(asset) {
2185
+ if (typeof asset === "string") {
2186
+ if (asset.startsWith("data:")) {
2187
+ const commaIndex = asset.indexOf(",");
2188
+ return commaIndex === -1 ? void 0 : asset.slice(commaIndex + 1);
2189
+ }
2190
+ if (/^https?:\/\//i.test(asset)) {
2191
+ return void 0;
2192
+ }
2193
+ return asset;
2194
+ }
2195
+ if (typeof Buffer !== "undefined" && Buffer.isBuffer(asset)) {
2196
+ return asset.toString("base64");
2197
+ }
2198
+ if (asset instanceof Uint8Array) {
2199
+ return Buffer.from(asset).toString("base64");
2200
+ }
2201
+ if (asset instanceof ArrayBuffer) {
2202
+ return Buffer.from(asset).toString("base64");
2203
+ }
2204
+ if (ArrayBuffer.isView(asset)) {
2205
+ return Buffer.from(asset.buffer, asset.byteOffset, asset.byteLength).toString("base64");
2206
+ }
2207
+ return void 0;
2208
+ }
2209
+ function createTimeoutSignal(timeoutMs) {
2210
+ const controller = new AbortController();
2211
+ const timeout = setTimeout(
2212
+ () => controller.abort(new Error(`Attachment token counting timed out after ${timeoutMs}ms`)),
2213
+ timeoutMs
2214
+ );
2215
+ const cleanup = () => clearTimeout(timeout);
2216
+ controller.signal.addEventListener("abort", cleanup, { once: true });
2217
+ return { signal: controller.signal, cleanup };
2218
+ }
2219
+ function getNumericResponseField(value, paths) {
2220
+ for (const path of paths) {
2221
+ let current = value;
2222
+ for (const segment of path) {
2223
+ current = getObjectValue(current, segment);
2224
+ if (current === void 0) break;
2225
+ }
2226
+ if (typeof current === "number" && Number.isFinite(current)) {
2227
+ return current;
2228
+ }
2229
+ }
2230
+ return void 0;
2231
+ }
2232
+ function toOpenAIInputPart(part) {
2233
+ if (getObjectValue(part, "type") === "image" || isImageLikeFilePart(part)) {
2234
+ const asset = getObjectValue(part, "image") ?? getObjectValue(part, "data");
2235
+ const imageUrl = getAttachmentUrl(asset);
2236
+ if (imageUrl) {
2237
+ return { type: "input_image", image_url: imageUrl, detail: resolveImageDetail(part) };
2238
+ }
2239
+ const base64 = encodeAttachmentBase64(asset);
2240
+ if (!base64) return void 0;
2241
+ return {
2242
+ type: "input_image",
2243
+ image_url: `data:${getAttachmentMimeType(part, "image/png")};base64,${base64}`,
2244
+ detail: resolveImageDetail(part)
2245
+ };
2246
+ }
2247
+ if (getObjectValue(part, "type") === "file") {
2248
+ const asset = getObjectValue(part, "data");
2249
+ const fileUrl = getAttachmentUrl(asset);
2250
+ return fileUrl ? {
2251
+ type: "input_file",
2252
+ file_url: fileUrl,
2253
+ filename: getAttachmentFilename(part) ?? "attachment"
2254
+ } : (() => {
2255
+ const base64 = encodeAttachmentBase64(asset);
2256
+ if (!base64) return void 0;
2257
+ return {
2258
+ type: "input_file",
2259
+ file_data: `data:${getAttachmentMimeType(part, "application/octet-stream")};base64,${base64}`,
2260
+ filename: getAttachmentFilename(part) ?? "attachment"
2261
+ };
2262
+ })();
2263
+ }
2264
+ return void 0;
2265
+ }
2266
+ function toAnthropicContentPart(part) {
2267
+ const asset = getObjectValue(part, "image") ?? getObjectValue(part, "data");
2268
+ const url = getAttachmentUrl(asset);
2269
+ if (getObjectValue(part, "type") === "image" || isImageLikeFilePart(part)) {
2270
+ return url && /^https?:\/\//i.test(url) ? { type: "image", source: { type: "url", url } } : (() => {
2271
+ const base64 = encodeAttachmentBase64(asset);
2272
+ if (!base64) return void 0;
2273
+ return {
2274
+ type: "image",
2275
+ source: { type: "base64", media_type: getAttachmentMimeType(part, "image/png"), data: base64 }
2276
+ };
2277
+ })();
2278
+ }
2279
+ if (getObjectValue(part, "type") === "file") {
2280
+ return url && /^https?:\/\//i.test(url) ? { type: "document", source: { type: "url", url } } : (() => {
2281
+ const base64 = encodeAttachmentBase64(asset);
2282
+ if (!base64) return void 0;
2283
+ return {
2284
+ type: "document",
2285
+ source: { type: "base64", media_type: getAttachmentMimeType(part, "application/pdf"), data: base64 }
2286
+ };
2287
+ })();
2288
+ }
2289
+ return void 0;
2290
+ }
2291
+ function toGooglePart(part) {
2292
+ const asset = getObjectValue(part, "image") ?? getObjectValue(part, "data");
2293
+ const url = getAttachmentUrl(asset);
2294
+ const mimeType = getAttachmentMimeType(
2295
+ part,
2296
+ getObjectValue(part, "type") === "file" && !isImageLikeFilePart(part) ? "application/pdf" : "image/png"
2297
+ );
2298
+ if (url && !url.startsWith("data:")) {
2299
+ return { fileData: { mimeType, fileUri: url } };
2300
+ }
2301
+ const base64 = encodeAttachmentBase64(asset);
2302
+ if (!base64) return void 0;
2303
+ return { inlineData: { mimeType, data: base64 } };
2304
+ }
2305
+ async function fetchOpenAIAttachmentTokenEstimate(modelId, part) {
2306
+ const apiKey = getProviderApiKey("openai");
2307
+ const inputPart = toOpenAIInputPart(part);
2308
+ if (!apiKey || !inputPart) return void 0;
2309
+ const { signal, cleanup } = createTimeoutSignal(ATTACHMENT_COUNT_TIMEOUT_MS);
2310
+ try {
2311
+ const response = await fetch("https://api.openai.com/v1/responses/input_tokens", {
2312
+ method: "POST",
2313
+ headers: {
2314
+ Authorization: `Bearer ${apiKey}`,
2315
+ "Content-Type": "application/json"
2316
+ },
2317
+ body: JSON.stringify({
2318
+ model: modelId,
2319
+ input: [{ type: "message", role: "user", content: [inputPart] }]
2320
+ }),
2321
+ signal
2322
+ });
2323
+ if (!response.ok) return void 0;
2324
+ const body = await response.json();
2325
+ return getNumericResponseField(body, [
2326
+ ["input_tokens"],
2327
+ ["total_tokens"],
2328
+ ["usage", "input_tokens"],
2329
+ ["usage", "total_tokens"]
2330
+ ]);
2331
+ } finally {
2332
+ cleanup();
2333
+ }
2334
+ }
2335
+ async function fetchAnthropicAttachmentTokenEstimate(modelId, part) {
2336
+ const apiKey = getProviderApiKey("anthropic");
2337
+ const contentPart = toAnthropicContentPart(part);
2338
+ if (!apiKey || !contentPart) return void 0;
2339
+ const { signal, cleanup } = createTimeoutSignal(ATTACHMENT_COUNT_TIMEOUT_MS);
2340
+ try {
2341
+ const response = await fetch("https://api.anthropic.com/v1/messages/count_tokens", {
2342
+ method: "POST",
2343
+ headers: {
2344
+ "x-api-key": apiKey,
2345
+ "anthropic-version": "2023-06-01",
2346
+ "Content-Type": "application/json"
2347
+ },
2348
+ body: JSON.stringify({
2349
+ model: modelId,
2350
+ messages: [{ role: "user", content: [contentPart] }]
2351
+ }),
2352
+ signal
2353
+ });
2354
+ if (!response.ok) return void 0;
2355
+ const body = await response.json();
2356
+ return getNumericResponseField(body, [["input_tokens"]]);
2357
+ } finally {
2358
+ cleanup();
2359
+ }
2360
+ }
2361
+ async function fetchGoogleAttachmentTokenEstimate(modelId, part) {
2362
+ const apiKey = getProviderApiKey("google");
2363
+ const googlePart = toGooglePart(part);
2364
+ if (!apiKey || !googlePart) return void 0;
2365
+ const { signal, cleanup } = createTimeoutSignal(ATTACHMENT_COUNT_TIMEOUT_MS);
2366
+ try {
2367
+ const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${modelId}:countTokens`, {
2368
+ method: "POST",
2369
+ headers: {
2370
+ "x-goog-api-key": apiKey,
2371
+ "Content-Type": "application/json"
2372
+ },
2373
+ body: JSON.stringify({
2374
+ contents: [{ role: "user", parts: [googlePart] }]
2375
+ }),
2376
+ signal
2377
+ });
2378
+ if (!response.ok) return void 0;
2379
+ const body = await response.json();
2380
+ return getNumericResponseField(body, [["totalTokens"], ["total_tokens"]]);
2381
+ } finally {
2382
+ cleanup();
2383
+ }
2384
+ }
1401
2385
  var TokenCounter = class _TokenCounter {
1402
2386
  encoder;
1403
2387
  cacheSource;
2388
+ defaultModelContext;
2389
+ modelContextStorage = new AsyncLocalStorage();
2390
+ inFlightAttachmentCounts = /* @__PURE__ */ new Map();
1404
2391
  // Per-message overhead: accounts for role tokens, message framing, and separators.
1405
2392
  // Empirically derived from OpenAI's token counting guide (3 tokens per message base +
1406
2393
  // fractional overhead from name/role encoding). 3.8 is a practical average across models.
1407
2394
  static TOKENS_PER_MESSAGE = 3.8;
1408
2395
  // Conversation-level overhead: system prompt framing, reply priming tokens, etc.
1409
2396
  static TOKENS_PER_CONVERSATION = 24;
1410
- constructor(encoding) {
2397
+ constructor(encoding, options) {
1411
2398
  this.encoder = encoding ? new Tiktoken(encoding) : getDefaultEncoder();
1412
2399
  this.cacheSource = `v${TOKEN_ESTIMATE_CACHE_VERSION}:${resolveEncodingId(encoding)}`;
2400
+ this.defaultModelContext = parseModelContext(options?.model);
2401
+ }
2402
+ runWithModelContext(model, fn) {
2403
+ return this.modelContextStorage.run(parseModelContext(model), fn);
2404
+ }
2405
+ getModelContext() {
2406
+ return this.modelContextStorage.getStore() ?? this.defaultModelContext;
1413
2407
  }
1414
2408
  /**
1415
2409
  * Count tokens in a plain string
@@ -1433,6 +2427,20 @@ var TokenCounter = class _TokenCounter {
1433
2427
  });
1434
2428
  return tokens;
1435
2429
  }
2430
+ readOrPersistFixedPartEstimate(part, kind, payload, tokens) {
2431
+ const key = buildEstimateKey(kind, payload);
2432
+ const cached = getPartCacheEntry(part, key);
2433
+ if (isValidCacheEntry(cached, key, this.cacheSource)) {
2434
+ return cached.tokens;
2435
+ }
2436
+ setPartCacheEntry(part, key, {
2437
+ v: TOKEN_ESTIMATE_CACHE_VERSION,
2438
+ source: this.cacheSource,
2439
+ key,
2440
+ tokens
2441
+ });
2442
+ return tokens;
2443
+ }
1436
2444
  readOrPersistMessageEstimate(message, kind, payload) {
1437
2445
  const key = buildEstimateKey(kind, payload);
1438
2446
  const cached = getMessageCacheEntry(message, key);
@@ -1456,15 +2464,315 @@ var TokenCounter = class _TokenCounter {
1456
2464
  usingStoredModelOutput: true
1457
2465
  };
1458
2466
  }
1459
- return {
1460
- value: invocationResult,
1461
- usingStoredModelOutput: false
1462
- };
2467
+ return {
2468
+ value: invocationResult,
2469
+ usingStoredModelOutput: false
2470
+ };
2471
+ }
2472
+ estimateImageAssetTokens(part, asset, kind) {
2473
+ const modelContext = this.getModelContext();
2474
+ const provider = resolveProviderId(modelContext);
2475
+ const modelId = modelContext?.modelId ?? null;
2476
+ const detail = resolveImageDetail(part);
2477
+ const dimensions = resolveImageDimensions(part);
2478
+ const sourceStats = resolveImageSourceStats(asset);
2479
+ if (provider === "google") {
2480
+ const googleEstimate = estimateGoogleImageTokens(modelContext, part, dimensions);
2481
+ return {
2482
+ tokens: googleEstimate.tokens,
2483
+ cachePayload: JSON.stringify({
2484
+ kind,
2485
+ provider,
2486
+ modelId,
2487
+ estimator: isGoogleGemini3Model(modelContext) ? "google-gemini-3" : "google-legacy",
2488
+ mediaResolution: googleEstimate.mediaResolution,
2489
+ width: dimensions.width ?? null,
2490
+ height: dimensions.height ?? null,
2491
+ source: sourceStats.source,
2492
+ sizeBytes: sourceStats.sizeBytes ?? null,
2493
+ mimeType: getObjectValue(part, "mimeType") ?? null,
2494
+ filename: getObjectValue(part, "filename") ?? null
2495
+ })
2496
+ };
2497
+ }
2498
+ if (provider === "anthropic") {
2499
+ return {
2500
+ tokens: estimateAnthropicImageTokens(dimensions, sourceStats),
2501
+ cachePayload: JSON.stringify({
2502
+ kind,
2503
+ provider,
2504
+ modelId,
2505
+ estimator: "anthropic",
2506
+ width: dimensions.width ?? null,
2507
+ height: dimensions.height ?? null,
2508
+ source: sourceStats.source,
2509
+ sizeBytes: sourceStats.sizeBytes ?? null,
2510
+ mimeType: getObjectValue(part, "mimeType") ?? null,
2511
+ filename: getObjectValue(part, "filename") ?? null
2512
+ })
2513
+ };
2514
+ }
2515
+ const estimator = resolveOpenAIImageEstimatorConfig(modelContext);
2516
+ const effectiveDetail = resolveEffectiveOpenAIImageDetail(detail, dimensions, sourceStats);
2517
+ const tiles = effectiveDetail === "high" ? estimateOpenAIHighDetailTiles(dimensions, sourceStats, estimator) : 0;
2518
+ const tokens = estimator.baseTokens + tiles * estimator.tileTokens;
2519
+ return {
2520
+ tokens,
2521
+ cachePayload: JSON.stringify({
2522
+ kind,
2523
+ provider,
2524
+ modelId,
2525
+ estimator: provider === "openai" ? "openai" : "fallback",
2526
+ detail,
2527
+ effectiveDetail,
2528
+ width: dimensions.width ?? null,
2529
+ height: dimensions.height ?? null,
2530
+ source: sourceStats.source,
2531
+ sizeBytes: sourceStats.sizeBytes ?? null,
2532
+ mimeType: getObjectValue(part, "mimeType") ?? null,
2533
+ filename: getObjectValue(part, "filename") ?? null
2534
+ })
2535
+ };
2536
+ }
2537
+ estimateImageTokens(part) {
2538
+ return this.estimateImageAssetTokens(part, part.image, "image");
2539
+ }
2540
+ estimateImageLikeFileTokens(part) {
2541
+ return this.estimateImageAssetTokens(part, part.data, "file");
2542
+ }
2543
+ countAttachmentPartSync(part) {
2544
+ if (part.type === "image") {
2545
+ const estimate = this.estimateImageTokens(part);
2546
+ return this.readOrPersistFixedPartEstimate(part, "image", estimate.cachePayload, estimate.tokens);
2547
+ }
2548
+ if (part.type === "file" && isImageLikeFilePart(part)) {
2549
+ const estimate = this.estimateImageLikeFileTokens(part);
2550
+ return this.readOrPersistFixedPartEstimate(part, "image-like-file", estimate.cachePayload, estimate.tokens);
2551
+ }
2552
+ if (part.type === "file") {
2553
+ return this.readOrPersistPartEstimate(part, "file-descriptor", serializeNonImageFilePartForTokenCounting(part));
2554
+ }
2555
+ return void 0;
2556
+ }
2557
+ buildRemoteAttachmentCachePayload(part) {
2558
+ const isImageAttachment = part.type === "image" || part.type === "file" && isImageLikeFilePart(part);
2559
+ const isNonImageFileAttachment = part.type === "file" && !isImageAttachment;
2560
+ if (!isImageAttachment && !isNonImageFileAttachment) {
2561
+ return void 0;
2562
+ }
2563
+ const modelContext = this.getModelContext();
2564
+ const provider = resolveProviderId(modelContext);
2565
+ const modelId = modelContext?.modelId ?? null;
2566
+ if (!provider || !modelId || !["openai", "google", "anthropic"].includes(provider)) {
2567
+ return void 0;
2568
+ }
2569
+ const asset = getObjectValue(part, "image") ?? getObjectValue(part, "data");
2570
+ const sourceStats = resolveImageSourceStats(asset);
2571
+ const fingerprint = getAttachmentFingerprint(asset);
2572
+ return JSON.stringify({
2573
+ strategy: "provider-endpoint",
2574
+ provider,
2575
+ modelId,
2576
+ type: getObjectValue(part, "type") ?? null,
2577
+ detail: isImageAttachment ? resolveImageDetail(part) : null,
2578
+ mediaResolution: provider === "google" && isImageAttachment ? resolveGoogleMediaResolution(part) : null,
2579
+ mimeType: getAttachmentMimeType(part, isNonImageFileAttachment ? "application/pdf" : "image/png"),
2580
+ filename: getAttachmentFilename(part) ?? null,
2581
+ source: sourceStats.source,
2582
+ sizeBytes: sourceStats.sizeBytes ?? null,
2583
+ assetUrl: fingerprint.url ?? null,
2584
+ assetHash: fingerprint.contentHash ?? null
2585
+ });
2586
+ }
2587
+ async fetchProviderAttachmentTokenEstimate(part) {
2588
+ const modelContext = this.getModelContext();
2589
+ const provider = resolveProviderId(modelContext);
2590
+ const modelId = modelContext?.modelId;
2591
+ if (!provider || !modelId) return void 0;
2592
+ try {
2593
+ if (provider === "openai") {
2594
+ return await fetchOpenAIAttachmentTokenEstimate(modelId, part);
2595
+ }
2596
+ if (provider === "google") {
2597
+ return await fetchGoogleAttachmentTokenEstimate(modelId, part);
2598
+ }
2599
+ if (provider === "anthropic") {
2600
+ return await fetchAnthropicAttachmentTokenEstimate(modelId, part);
2601
+ }
2602
+ } catch {
2603
+ return void 0;
2604
+ }
2605
+ return void 0;
2606
+ }
2607
+ async countAttachmentPartAsync(part) {
2608
+ const isImageAttachment = part.type === "image" || part.type === "file" && isImageLikeFilePart(part);
2609
+ const remotePayload = this.buildRemoteAttachmentCachePayload(part);
2610
+ if (remotePayload) {
2611
+ const remoteKey = buildEstimateKey("attachment-provider", remotePayload);
2612
+ const cachedRemote = getPartCacheEntry(part, remoteKey);
2613
+ if (isValidCacheEntry(cachedRemote, remoteKey, this.cacheSource)) {
2614
+ return cachedRemote.tokens;
2615
+ }
2616
+ const existingRequest = this.inFlightAttachmentCounts.get(remoteKey);
2617
+ if (existingRequest) {
2618
+ const remoteTokens = await existingRequest;
2619
+ if (typeof remoteTokens === "number" && Number.isFinite(remoteTokens) && remoteTokens > 0) {
2620
+ setPartCacheEntry(part, remoteKey, {
2621
+ v: TOKEN_ESTIMATE_CACHE_VERSION,
2622
+ source: this.cacheSource,
2623
+ key: remoteKey,
2624
+ tokens: remoteTokens
2625
+ });
2626
+ return remoteTokens;
2627
+ }
2628
+ } else {
2629
+ const remoteRequest = this.fetchProviderAttachmentTokenEstimate(part);
2630
+ this.inFlightAttachmentCounts.set(remoteKey, remoteRequest);
2631
+ let remoteTokens;
2632
+ try {
2633
+ remoteTokens = await remoteRequest;
2634
+ } finally {
2635
+ this.inFlightAttachmentCounts.delete(remoteKey);
2636
+ }
2637
+ if (typeof remoteTokens === "number" && Number.isFinite(remoteTokens) && remoteTokens > 0) {
2638
+ setPartCacheEntry(part, remoteKey, {
2639
+ v: TOKEN_ESTIMATE_CACHE_VERSION,
2640
+ source: this.cacheSource,
2641
+ key: remoteKey,
2642
+ tokens: remoteTokens
2643
+ });
2644
+ return remoteTokens;
2645
+ }
2646
+ }
2647
+ if (isImageAttachment) {
2648
+ await resolveImageDimensionsAsync(part);
2649
+ }
2650
+ const fallbackPayload = JSON.stringify({
2651
+ ...JSON.parse(remotePayload),
2652
+ strategy: "local-fallback",
2653
+ ...isImageAttachment ? resolveImageDimensions(part) : {}
2654
+ });
2655
+ const fallbackKey = buildEstimateKey("attachment-provider", fallbackPayload);
2656
+ const cachedFallback = getPartCacheEntry(part, fallbackKey);
2657
+ if (isValidCacheEntry(cachedFallback, fallbackKey, this.cacheSource)) {
2658
+ return cachedFallback.tokens;
2659
+ }
2660
+ const localTokens2 = this.countAttachmentPartSync(part);
2661
+ if (localTokens2 === void 0) {
2662
+ return void 0;
2663
+ }
2664
+ setPartCacheEntry(part, fallbackKey, {
2665
+ v: TOKEN_ESTIMATE_CACHE_VERSION,
2666
+ source: this.cacheSource,
2667
+ key: fallbackKey,
2668
+ tokens: localTokens2
2669
+ });
2670
+ return localTokens2;
2671
+ }
2672
+ if (isImageAttachment) {
2673
+ await resolveImageDimensionsAsync(part);
2674
+ }
2675
+ const localTokens = this.countAttachmentPartSync(part);
2676
+ return localTokens;
2677
+ }
2678
+ countNonAttachmentPart(part) {
2679
+ let overheadDelta = 0;
2680
+ let toolResultDelta = 0;
2681
+ if (part.type === "text") {
2682
+ return { tokens: this.readOrPersistPartEstimate(part, "text", part.text), overheadDelta, toolResultDelta };
2683
+ }
2684
+ if (part.type === "tool-invocation") {
2685
+ const invocation = part.toolInvocation;
2686
+ let tokens = 0;
2687
+ if (invocation.state === "call" || invocation.state === "partial-call") {
2688
+ if (invocation.toolName) {
2689
+ tokens += this.readOrPersistPartEstimate(part, `tool-${invocation.state}-name`, invocation.toolName);
2690
+ }
2691
+ if (invocation.args) {
2692
+ if (typeof invocation.args === "string") {
2693
+ tokens += this.readOrPersistPartEstimate(part, `tool-${invocation.state}-args`, invocation.args);
2694
+ } else {
2695
+ const argsJson = JSON.stringify(invocation.args);
2696
+ tokens += this.readOrPersistPartEstimate(part, `tool-${invocation.state}-args-json`, argsJson);
2697
+ overheadDelta -= 12;
2698
+ }
2699
+ }
2700
+ return { tokens, overheadDelta, toolResultDelta };
2701
+ }
2702
+ if (invocation.state === "result") {
2703
+ toolResultDelta++;
2704
+ const { value: resultForCounting, usingStoredModelOutput } = this.resolveToolResultForTokenCounting(
2705
+ part,
2706
+ invocation.result
2707
+ );
2708
+ if (resultForCounting !== void 0) {
2709
+ if (typeof resultForCounting === "string") {
2710
+ tokens += this.readOrPersistPartEstimate(
2711
+ part,
2712
+ usingStoredModelOutput ? "tool-result-model-output" : "tool-result",
2713
+ resultForCounting
2714
+ );
2715
+ } else {
2716
+ const resultJson = JSON.stringify(resultForCounting);
2717
+ tokens += this.readOrPersistPartEstimate(
2718
+ part,
2719
+ usingStoredModelOutput ? "tool-result-model-output-json" : "tool-result-json",
2720
+ resultJson
2721
+ );
2722
+ overheadDelta -= 12;
2723
+ }
2724
+ }
2725
+ return { tokens, overheadDelta, toolResultDelta };
2726
+ }
2727
+ throw new Error(
2728
+ `Unhandled tool-invocation state '${part.toolInvocation?.state}' in token counting for part type '${part.type}'`
2729
+ );
2730
+ }
2731
+ if (typeof part.type === "string" && part.type.startsWith("data-")) {
2732
+ return { tokens: 0, overheadDelta, toolResultDelta };
2733
+ }
2734
+ if (part.type === "reasoning") {
2735
+ return { tokens: 0, overheadDelta, toolResultDelta };
2736
+ }
2737
+ const serialized = serializePartForTokenCounting(part);
2738
+ return {
2739
+ tokens: this.readOrPersistPartEstimate(part, `part-${part.type}`, serialized),
2740
+ overheadDelta,
2741
+ toolResultDelta
2742
+ };
2743
+ }
2744
+ /**
2745
+ * Count tokens in a single message
2746
+ */
2747
+ countMessage(message) {
2748
+ let payloadTokens = this.countString(message.role);
2749
+ let overhead = _TokenCounter.TOKENS_PER_MESSAGE;
2750
+ let toolResultCount = 0;
2751
+ if (typeof message.content === "string") {
2752
+ payloadTokens += this.readOrPersistMessageEstimate(message, "message-content", message.content);
2753
+ } else if (message.content && typeof message.content === "object") {
2754
+ if (message.content.content && !Array.isArray(message.content.parts)) {
2755
+ payloadTokens += this.readOrPersistMessageEstimate(message, "content-content", message.content.content);
2756
+ } else if (Array.isArray(message.content.parts)) {
2757
+ for (const part of message.content.parts) {
2758
+ const attachmentTokens = this.countAttachmentPartSync(part);
2759
+ if (attachmentTokens !== void 0) {
2760
+ payloadTokens += attachmentTokens;
2761
+ continue;
2762
+ }
2763
+ const result = this.countNonAttachmentPart(part);
2764
+ payloadTokens += result.tokens;
2765
+ overhead += result.overheadDelta;
2766
+ toolResultCount += result.toolResultDelta;
2767
+ }
2768
+ }
2769
+ }
2770
+ if (toolResultCount > 0) {
2771
+ overhead += toolResultCount * _TokenCounter.TOKENS_PER_MESSAGE;
2772
+ }
2773
+ return Math.round(payloadTokens + overhead);
1463
2774
  }
1464
- /**
1465
- * Count tokens in a single message
1466
- */
1467
- countMessage(message) {
2775
+ async countMessageAsync(message) {
1468
2776
  let payloadTokens = this.countString(message.role);
1469
2777
  let overhead = _TokenCounter.TOKENS_PER_MESSAGE;
1470
2778
  let toolResultCount = 0;
@@ -1475,63 +2783,15 @@ var TokenCounter = class _TokenCounter {
1475
2783
  payloadTokens += this.readOrPersistMessageEstimate(message, "content-content", message.content.content);
1476
2784
  } else if (Array.isArray(message.content.parts)) {
1477
2785
  for (const part of message.content.parts) {
1478
- if (part.type === "text") {
1479
- payloadTokens += this.readOrPersistPartEstimate(part, "text", part.text);
1480
- } else if (part.type === "tool-invocation") {
1481
- const invocation = part.toolInvocation;
1482
- if (invocation.state === "call" || invocation.state === "partial-call") {
1483
- if (invocation.toolName) {
1484
- payloadTokens += this.readOrPersistPartEstimate(
1485
- part,
1486
- `tool-${invocation.state}-name`,
1487
- invocation.toolName
1488
- );
1489
- }
1490
- if (invocation.args) {
1491
- if (typeof invocation.args === "string") {
1492
- payloadTokens += this.readOrPersistPartEstimate(
1493
- part,
1494
- `tool-${invocation.state}-args`,
1495
- invocation.args
1496
- );
1497
- } else {
1498
- const argsJson = JSON.stringify(invocation.args);
1499
- payloadTokens += this.readOrPersistPartEstimate(part, `tool-${invocation.state}-args-json`, argsJson);
1500
- overhead -= 12;
1501
- }
1502
- }
1503
- } else if (invocation.state === "result") {
1504
- toolResultCount++;
1505
- const { value: resultForCounting, usingStoredModelOutput } = this.resolveToolResultForTokenCounting(
1506
- part,
1507
- invocation.result
1508
- );
1509
- if (resultForCounting !== void 0) {
1510
- if (typeof resultForCounting === "string") {
1511
- payloadTokens += this.readOrPersistPartEstimate(
1512
- part,
1513
- usingStoredModelOutput ? "tool-result-model-output" : "tool-result",
1514
- resultForCounting
1515
- );
1516
- } else {
1517
- const resultJson = JSON.stringify(resultForCounting);
1518
- payloadTokens += this.readOrPersistPartEstimate(
1519
- part,
1520
- usingStoredModelOutput ? "tool-result-model-output-json" : "tool-result-json",
1521
- resultJson
1522
- );
1523
- overhead -= 12;
1524
- }
1525
- }
1526
- } else {
1527
- throw new Error(
1528
- `Unhandled tool-invocation state '${part.toolInvocation?.state}' in token counting for part type '${part.type}'`
1529
- );
1530
- }
1531
- } else if (typeof part.type === "string" && part.type.startsWith("data-")) ; else if (part.type === "reasoning") ; else {
1532
- const serialized = serializePartForTokenCounting(part);
1533
- payloadTokens += this.readOrPersistPartEstimate(part, `part-${part.type}`, serialized);
2786
+ const attachmentTokens = await this.countAttachmentPartAsync(part);
2787
+ if (attachmentTokens !== void 0) {
2788
+ payloadTokens += attachmentTokens;
2789
+ continue;
1534
2790
  }
2791
+ const result = this.countNonAttachmentPart(part);
2792
+ payloadTokens += result.tokens;
2793
+ overhead += result.overheadDelta;
2794
+ toolResultCount += result.toolResultDelta;
1535
2795
  }
1536
2796
  }
1537
2797
  }
@@ -1551,6 +2811,11 @@ var TokenCounter = class _TokenCounter {
1551
2811
  }
1552
2812
  return total;
1553
2813
  }
2814
+ async countMessagesAsync(messages) {
2815
+ if (!messages || messages.length === 0) return 0;
2816
+ const messageTotals = await Promise.all(messages.map((message) => this.countMessageAsync(message)));
2817
+ return _TokenCounter.TOKENS_PER_CONVERSATION + messageTotals.reduce((sum, count) => sum + count, 0);
2818
+ }
1554
2819
  /**
1555
2820
  * Count tokens in observations string
1556
2821
  */
@@ -1808,6 +3073,41 @@ var ObservationalMemory = class _ObservationalMemory {
1808
3073
  }
1809
3074
  return [];
1810
3075
  }
3076
+ /**
3077
+ * Refresh per-chunk messageTokens from the current in-memory message list.
3078
+ *
3079
+ * Buffered chunks store a messageTokens snapshot from when they were created,
3080
+ * but messages can be edited/sealed between buffering and activation, changing
3081
+ * their token weight. Using stale weights causes projected-removal math to
3082
+ * over- or under-estimate, leading to skipped activations or over-activation.
3083
+ *
3084
+ * Token recount only runs when the full chunk is present in the message list.
3085
+ * Partial recount is skipped because it would undercount and could cause
3086
+ * over-activation of buffered chunks.
3087
+ */
3088
+ refreshBufferedChunkMessageTokens(chunks, messageList) {
3089
+ const allMessages = messageList.get.all.db();
3090
+ const messageMap = new Map(allMessages.filter((m) => m?.id).map((m) => [m.id, m]));
3091
+ return chunks.map((chunk) => {
3092
+ const chunkMessages = chunk.messageIds.map((id) => messageMap.get(id)).filter((m) => !!m);
3093
+ if (chunkMessages.length !== chunk.messageIds.length) {
3094
+ return chunk;
3095
+ }
3096
+ const refreshedTokens = this.tokenCounter.countMessages(chunkMessages);
3097
+ const refreshedMessageTokens = chunk.messageIds.reduce((acc, id) => {
3098
+ const msg = messageMap.get(id);
3099
+ if (msg) {
3100
+ acc[id] = this.tokenCounter.countMessages([msg]);
3101
+ }
3102
+ return acc;
3103
+ }, {});
3104
+ return {
3105
+ ...chunk,
3106
+ messageTokens: refreshedTokens,
3107
+ messageTokenCounts: refreshedMessageTokens
3108
+ };
3109
+ });
3110
+ }
1811
3111
  /**
1812
3112
  * Check if we've crossed a new bufferTokens interval boundary.
1813
3113
  * Returns true if async buffering should be triggered.
@@ -1904,6 +3204,11 @@ var ObservationalMemory = class _ObservationalMemory {
1904
3204
  return `thread:${threadId ?? "unknown"}`;
1905
3205
  }
1906
3206
  constructor(config) {
3207
+ if (!coreFeatures.has("request-response-id-rotation")) {
3208
+ throw new Error(
3209
+ "Observational memory requires @mastra/core support for request-response-id-rotation. Please bump @mastra/core to a newer version."
3210
+ );
3211
+ }
1907
3212
  if (config.model && config.observation?.model) {
1908
3213
  throw new Error(
1909
3214
  "Cannot set both `model` and `observation.model`. Use `model` to set both agents, or set each individually."
@@ -2001,7 +3306,9 @@ Async buffering is enabled by default \u2014 this opt-out is only needed when us
2001
3306
  ),
2002
3307
  instruction: config.reflection?.instruction
2003
3308
  };
2004
- this.tokenCounter = new TokenCounter();
3309
+ this.tokenCounter = new TokenCounter(void 0, {
3310
+ model: typeof observationModel === "string" ? observationModel : void 0
3311
+ });
2005
3312
  this.onDebugEvent = config.onDebugEvent;
2006
3313
  this.messageHistory = new MessageHistory({ storage: this.storage });
2007
3314
  this.validateBufferConfig();
@@ -2031,25 +3338,59 @@ Async buffering is enabled by default \u2014 this opt-out is only needed when us
2031
3338
  async waitForBuffering(threadId, resourceId, timeoutMs = 3e4) {
2032
3339
  return _ObservationalMemory.awaitBuffering(threadId, resourceId, this.scope, timeoutMs);
2033
3340
  }
3341
+ getModelToResolve(model) {
3342
+ if (Array.isArray(model)) {
3343
+ return model[0]?.model ?? "unknown";
3344
+ }
3345
+ if (typeof model === "function") {
3346
+ return async (ctx) => {
3347
+ const result = await model(ctx);
3348
+ if (Array.isArray(result)) {
3349
+ return result[0]?.model ?? "unknown";
3350
+ }
3351
+ return result;
3352
+ };
3353
+ }
3354
+ return model;
3355
+ }
3356
+ formatModelName(model) {
3357
+ if (!model.modelId) {
3358
+ return "(unknown)";
3359
+ }
3360
+ return model.provider ? `${model.provider}/${model.modelId}` : model.modelId;
3361
+ }
3362
+ async resolveModelContext(modelConfig, requestContext) {
3363
+ const modelToResolve = this.getModelToResolve(modelConfig);
3364
+ if (!modelToResolve) {
3365
+ return void 0;
3366
+ }
3367
+ const resolved = await resolveModelConfig(modelToResolve, requestContext);
3368
+ return {
3369
+ provider: resolved.provider,
3370
+ modelId: resolved.modelId
3371
+ };
3372
+ }
3373
+ getRuntimeModelContext(model) {
3374
+ if (!model?.modelId) {
3375
+ return void 0;
3376
+ }
3377
+ return {
3378
+ provider: model.provider,
3379
+ modelId: model.modelId
3380
+ };
3381
+ }
3382
+ runWithTokenCounterModelContext(modelContext, fn) {
3383
+ return this.tokenCounter.runWithModelContext(modelContext, fn);
3384
+ }
2034
3385
  /**
2035
3386
  * Get the full config including resolved model names.
2036
3387
  * This is async because it needs to resolve the model configs.
2037
3388
  */
2038
3389
  async getResolvedConfig(requestContext) {
2039
- const getModelToResolve = (model) => {
2040
- if (Array.isArray(model)) {
2041
- return model[0]?.model ?? "unknown";
2042
- }
2043
- return model;
2044
- };
2045
- const formatModelName = (model) => {
2046
- return model.provider ? `${model.provider}/${model.modelId}` : model.modelId;
2047
- };
2048
3390
  const safeResolveModel = async (modelConfig) => {
2049
- const modelToResolve = getModelToResolve(modelConfig);
2050
3391
  try {
2051
- const resolved = await resolveModelConfig(modelToResolve, requestContext);
2052
- return formatModelName(resolved);
3392
+ const resolved = await this.resolveModelContext(modelConfig, requestContext);
3393
+ return resolved?.modelId ? this.formatModelName(resolved) : "(unknown)";
2053
3394
  } catch (error) {
2054
3395
  omError("[OM] Failed to resolve model config", error);
2055
3396
  return "(unknown)";
@@ -2530,10 +3871,16 @@ Async buffering is enabled by default \u2014 this opt-out is only needed when us
2530
3871
  */
2531
3872
  async callObserver(existingObservations, messagesToObserve, abortSignal, options) {
2532
3873
  const agent = this.getObserverAgent();
2533
- const prompt = buildObserverPrompt(existingObservations, messagesToObserve, options);
3874
+ const observerMessages = [
3875
+ {
3876
+ role: "user",
3877
+ content: buildObserverTaskPrompt(existingObservations, options)
3878
+ },
3879
+ buildObserverHistoryMessage(messagesToObserve)
3880
+ ];
2534
3881
  const doGenerate = async () => {
2535
3882
  return this.withAbortCheck(async () => {
2536
- const streamResult = await agent.stream(prompt, {
3883
+ const streamResult = await agent.stream(observerMessages, {
2537
3884
  modelSettings: {
2538
3885
  ...this.observationConfig.modelSettings
2539
3886
  },
@@ -2580,7 +3927,13 @@ Async buffering is enabled by default \u2014 this opt-out is only needed when us
2580
3927
  model: this.observationConfig.model,
2581
3928
  instructions: buildObserverSystemPrompt(true, this.observationConfig.instruction)
2582
3929
  });
2583
- const prompt = buildMultiThreadObserverPrompt(existingObservations, messagesByThread, threadOrder);
3930
+ const observerMessages = [
3931
+ {
3932
+ role: "user",
3933
+ content: buildMultiThreadObserverTaskPrompt(existingObservations)
3934
+ },
3935
+ buildMultiThreadObserverHistoryMessage(messagesByThread, threadOrder)
3936
+ ];
2584
3937
  const allMessages = [];
2585
3938
  for (const msgs of messagesByThread.values()) {
2586
3939
  allMessages.push(...msgs);
@@ -2590,7 +3943,7 @@ Async buffering is enabled by default \u2014 this opt-out is only needed when us
2590
3943
  }
2591
3944
  const doGenerate = async () => {
2592
3945
  return this.withAbortCheck(async () => {
2593
- const streamResult = await agent.stream(prompt, {
3946
+ const streamResult = await agent.stream(observerMessages, {
2594
3947
  modelSettings: {
2595
3948
  ...this.observationConfig.modelSettings
2596
3949
  },
@@ -2868,8 +4221,8 @@ ${suggestedResponse}
2868
4221
  /**
2869
4222
  * Calculate all threshold-related values for observation decision making.
2870
4223
  */
2871
- calculateObservationThresholds(_allMessages, unobservedMessages, _pendingTokens, otherThreadTokens, currentObservationTokens, _record) {
2872
- const contextWindowTokens = this.tokenCounter.countMessages(unobservedMessages);
4224
+ async calculateObservationThresholds(_allMessages, unobservedMessages, _pendingTokens, otherThreadTokens, currentObservationTokens, _record) {
4225
+ const contextWindowTokens = await this.tokenCounter.countMessagesAsync(unobservedMessages);
2873
4226
  const totalPendingTokens = Math.max(0, contextWindowTokens + otherThreadTokens);
2874
4227
  const threshold = calculateDynamicThreshold(this.observationConfig.messageTokens, currentObservationTokens);
2875
4228
  const baseReflectionThreshold = getMaxThreshold(this.reflectionConfig.observationTokens);
@@ -2979,7 +4332,7 @@ ${suggestedResponse}
2979
4332
  let freshRecord = await this.getOrCreateRecord(threadId, resourceId);
2980
4333
  const freshAllMessages = messageList.get.all.db();
2981
4334
  let freshUnobservedMessages = this.getUnobservedMessages(freshAllMessages, freshRecord);
2982
- const freshContextTokens = this.tokenCounter.countMessages(freshUnobservedMessages);
4335
+ const freshContextTokens = await this.tokenCounter.countMessagesAsync(freshUnobservedMessages);
2983
4336
  let freshOtherThreadTokens = 0;
2984
4337
  if (this.scope === "resource" && resourceId) {
2985
4338
  const freshOtherContext = await this.loadOtherThreadsContext(resourceId, threadId);
@@ -3121,17 +4474,25 @@ ${suggestedResponse}
3121
4474
  if (observedMessageIds && observedMessageIds.length > 0) {
3122
4475
  const observedSet = new Set(observedMessageIds);
3123
4476
  const idsToRemove = /* @__PURE__ */ new Set();
4477
+ const removalOrder = [];
3124
4478
  let skipped = 0;
3125
4479
  let backoffTriggered = false;
4480
+ const retentionCounter = typeof minRemaining === "number" ? new TokenCounter() : null;
3126
4481
  for (const msg of allMsgs) {
3127
4482
  if (!msg?.id || msg.id === "om-continuation" || !observedSet.has(msg.id)) {
3128
4483
  continue;
3129
4484
  }
3130
- if (typeof minRemaining === "number") {
4485
+ const unobservedParts = this.getUnobservedParts(msg);
4486
+ const totalParts = msg.content?.parts?.length ?? 0;
4487
+ if (unobservedParts.length > 0 && unobservedParts.length < totalParts) {
4488
+ msg.content.parts = unobservedParts;
4489
+ continue;
4490
+ }
4491
+ if (retentionCounter && typeof minRemaining === "number") {
3131
4492
  const nextRemainingMessages = allMsgs.filter(
3132
4493
  (m) => m?.id && m.id !== "om-continuation" && !idsToRemove.has(m.id) && m.id !== msg.id
3133
4494
  );
3134
- const remainingIfRemoved = this.tokenCounter.countMessages(nextRemainingMessages);
4495
+ const remainingIfRemoved = retentionCounter.countMessages(nextRemainingMessages);
3135
4496
  if (remainingIfRemoved < minRemaining) {
3136
4497
  skipped += 1;
3137
4498
  backoffTriggered = true;
@@ -3139,6 +4500,19 @@ ${suggestedResponse}
3139
4500
  }
3140
4501
  }
3141
4502
  idsToRemove.add(msg.id);
4503
+ removalOrder.push(msg.id);
4504
+ }
4505
+ if (retentionCounter && typeof minRemaining === "number" && idsToRemove.size > 0) {
4506
+ let remainingMessages = allMsgs.filter((m) => m?.id && m.id !== "om-continuation" && !idsToRemove.has(m.id));
4507
+ let remainingTokens = retentionCounter.countMessages(remainingMessages);
4508
+ while (remainingTokens < minRemaining && removalOrder.length > 0) {
4509
+ const restoreId = removalOrder.pop();
4510
+ idsToRemove.delete(restoreId);
4511
+ skipped += 1;
4512
+ backoffTriggered = true;
4513
+ remainingMessages = allMsgs.filter((m) => m?.id && m.id !== "om-continuation" && !idsToRemove.has(m.id));
4514
+ remainingTokens = retentionCounter.countMessages(remainingMessages);
4515
+ }
3142
4516
  }
3143
4517
  omDebug(
3144
4518
  `[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(",")}`
@@ -3176,8 +4550,8 @@ ${suggestedResponse}
3176
4550
  await this.saveMessagesWithSealedIdTracking(messagesToSave, sealedIds, threadId, resourceId, state);
3177
4551
  }
3178
4552
  } else {
3179
- const newInput = messageList.clear.input.db();
3180
- const newOutput = messageList.clear.response.db();
4553
+ const newInput = messageList.get.input.db();
4554
+ const newOutput = messageList.get.response.db();
3181
4555
  const messagesToSave = [...newInput, ...newOutput];
3182
4556
  if (messagesToSave.length > 0) {
3183
4557
  await this.saveMessagesWithSealedIdTracking(messagesToSave, sealedIds, threadId, resourceId, state);
@@ -3245,11 +4619,17 @@ ${suggestedResponse}
3245
4619
  messageList.add(continuationMessage, "memory");
3246
4620
  }
3247
4621
  /**
3248
- * Filter out already-observed messages from message list (step 0 only).
3249
- * Historical messages loaded from DB may contain observation markers from previous sessions.
4622
+ * Filter out already-observed messages from the in-memory context.
4623
+ *
4624
+ * Marker-boundary pruning is safest at step 0 (historical resume/rebuild), where
4625
+ * list ordering mirrors persisted history.
4626
+ * For step > 0, the list may include mid-loop mutations (sealing/splitting/trim),
4627
+ * so we prefer record-based fallback pruning over position-based marker pruning.
3250
4628
  */
3251
- filterAlreadyObservedMessages(messageList, record) {
4629
+ async filterAlreadyObservedMessages(messageList, record, options) {
3252
4630
  const allMessages = messageList.get.all.db();
4631
+ const useMarkerBoundaryPruning = options?.useMarkerBoundaryPruning ?? true;
4632
+ const fallbackCursor = record?.threadId ? getThreadOMMetadata((await this.storage.getThreadById({ threadId: record.threadId }))?.metadata)?.lastObservedMessageCursor : void 0;
3253
4633
  let markerMessageIndex = -1;
3254
4634
  let markerMessage = null;
3255
4635
  for (let i = allMessages.length - 1; i >= 0; i--) {
@@ -3261,7 +4641,7 @@ ${suggestedResponse}
3261
4641
  break;
3262
4642
  }
3263
4643
  }
3264
- if (markerMessage && markerMessageIndex !== -1) {
4644
+ if (useMarkerBoundaryPruning && markerMessage && markerMessageIndex !== -1) {
3265
4645
  const messagesToRemove = [];
3266
4646
  for (let i = 0; i < markerMessageIndex; i++) {
3267
4647
  const msg = allMessages[i];
@@ -3282,6 +4662,9 @@ ${suggestedResponse}
3282
4662
  }
3283
4663
  } else if (record) {
3284
4664
  const observedIds = new Set(Array.isArray(record.observedMessageIds) ? record.observedMessageIds : []);
4665
+ const derivedCursor = fallbackCursor ?? this.getLastObservedMessageCursor(
4666
+ allMessages.filter((msg) => !!msg?.id && observedIds.has(msg.id) && !!msg.createdAt)
4667
+ );
3285
4668
  const lastObservedAt = record.lastObservedAt;
3286
4669
  const messagesToRemove = [];
3287
4670
  for (const msg of allMessages) {
@@ -3290,6 +4673,10 @@ ${suggestedResponse}
3290
4673
  messagesToRemove.push(msg.id);
3291
4674
  continue;
3292
4675
  }
4676
+ if (derivedCursor && this.isMessageAtOrBeforeCursor(msg, derivedCursor)) {
4677
+ messagesToRemove.push(msg.id);
4678
+ continue;
4679
+ }
3293
4680
  if (lastObservedAt && msg.createdAt) {
3294
4681
  const msgDate = new Date(msg.createdAt);
3295
4682
  if (msgDate <= lastObservedAt) {
@@ -3314,7 +4701,7 @@ ${suggestedResponse}
3314
4701
  * 5. Filter out already-observed messages
3315
4702
  */
3316
4703
  async processInputStep(args) {
3317
- const { messageList, requestContext, stepNumber, state: _state, writer, abortSignal, abort } = args;
4704
+ const { messageList, requestContext, stepNumber, state: _state, writer, abortSignal, abort, model } = args;
3318
4705
  const state = _state ?? {};
3319
4706
  omDebug(
3320
4707
  `[OM:processInputStep:ENTER] step=${stepNumber}, hasMastraMemory=${!!requestContext?.get("MastraMemory")}, hasMemoryInfo=${!!messageList?.serialize()?.memoryInfo?.threadId}`
@@ -3327,262 +4714,322 @@ ${suggestedResponse}
3327
4714
  const { threadId, resourceId } = context;
3328
4715
  const memoryContext = parseMemoryRequestContext(requestContext);
3329
4716
  const readOnly = memoryContext?.memoryConfig?.readOnly;
3330
- let record = await this.getOrCreateRecord(threadId, resourceId);
3331
- omDebug(
3332
- `[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}`
3333
- );
3334
- await this.loadHistoricalMessagesIfNeeded(messageList, state, threadId, resourceId, record.lastObservedAt);
3335
- let unobservedContextBlocks;
3336
- if (this.scope === "resource" && resourceId) {
3337
- unobservedContextBlocks = await this.loadOtherThreadsContext(resourceId, threadId);
3338
- }
3339
- if (stepNumber === 0 && !readOnly && this.isAsyncObservationEnabled()) {
3340
- const lockKey = this.getLockKey(threadId, resourceId);
3341
- const bufferedChunks = this.getBufferedChunks(record);
4717
+ const actorModelContext = this.getRuntimeModelContext(model);
4718
+ state.__omActorModelContext = actorModelContext;
4719
+ return this.runWithTokenCounterModelContext(actorModelContext, async () => {
4720
+ let record = await this.getOrCreateRecord(threadId, resourceId);
4721
+ const reproCaptureEnabled = isOmReproCaptureEnabled();
4722
+ const preRecordSnapshot = reproCaptureEnabled ? safeCaptureJson(record) : null;
4723
+ const preMessagesSnapshot = reproCaptureEnabled ? safeCaptureJson(messageList.get.all.db()) : null;
4724
+ const preSerializedMessageList = reproCaptureEnabled ? safeCaptureJson(messageList.serialize()) : null;
4725
+ const reproCaptureDetails = {
4726
+ step0Activation: null,
4727
+ thresholdCleanup: null,
4728
+ thresholdReached: false
4729
+ };
3342
4730
  omDebug(
3343
- `[OM:step0-activation] asyncObsEnabled=true, bufferedChunks=${bufferedChunks.length}, isBufferingObs=${record.isBufferingObservation}`
4731
+ `[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}`
3344
4732
  );
3345
- {
3346
- const bufKey = this.getObservationBufferKey(lockKey);
3347
- const dbBoundary = record.lastBufferedAtTokens ?? 0;
3348
- const currentContextTokens = this.tokenCounter.countMessages(messageList.get.all.db());
3349
- if (dbBoundary > currentContextTokens) {
3350
- omDebug(
3351
- `[OM:step0-boundary-reset] dbBoundary=${dbBoundary} > currentContext=${currentContextTokens}, resetting to current`
3352
- );
3353
- _ObservationalMemory.lastBufferedBoundary.set(bufKey, currentContextTokens);
3354
- this.storage.setBufferingObservationFlag(record.id, false, currentContextTokens).catch(() => {
3355
- });
3356
- }
4733
+ await this.loadHistoricalMessagesIfNeeded(messageList, state, threadId, resourceId, record.lastObservedAt);
4734
+ let unobservedContextBlocks;
4735
+ if (this.scope === "resource" && resourceId) {
4736
+ unobservedContextBlocks = await this.loadOtherThreadsContext(resourceId, threadId);
3357
4737
  }
3358
- if (bufferedChunks.length > 0) {
3359
- const allMsgsForCheck = messageList.get.all.db();
3360
- const unobservedMsgsForCheck = this.getUnobservedMessages(allMsgsForCheck, record);
3361
- const otherThreadTokensForCheck = unobservedContextBlocks ? this.tokenCounter.countString(unobservedContextBlocks) : 0;
3362
- const currentObsTokensForCheck = record.observationTokenCount ?? 0;
3363
- const { totalPendingTokens: step0PendingTokens, threshold: step0Threshold } = this.calculateObservationThresholds(
3364
- allMsgsForCheck,
3365
- unobservedMsgsForCheck,
3366
- 0,
3367
- // pendingTokens not needed — allMessages covers context
3368
- otherThreadTokensForCheck,
3369
- currentObsTokensForCheck,
3370
- record
3371
- );
4738
+ if (stepNumber === 0 && !readOnly && this.isAsyncObservationEnabled()) {
4739
+ const lockKey = this.getLockKey(threadId, resourceId);
4740
+ const bufferedChunks = this.getBufferedChunks(record);
3372
4741
  omDebug(
3373
- `[OM:step0-activation] pendingTokens=${step0PendingTokens}, threshold=${step0Threshold}, blockAfter=${this.observationConfig.blockAfter}, shouldActivate=${step0PendingTokens >= step0Threshold}, allMsgs=${allMsgsForCheck.length}`
4742
+ `[OM:step0-activation] asyncObsEnabled=true, bufferedChunks=${bufferedChunks.length}, isBufferingObs=${record.isBufferingObservation}`
3374
4743
  );
3375
- if (step0PendingTokens >= step0Threshold) {
3376
- const activationResult = await this.tryActivateBufferedObservations(
3377
- record,
3378
- lockKey,
3379
- step0PendingTokens,
3380
- writer,
3381
- messageList
4744
+ {
4745
+ const bufKey = this.getObservationBufferKey(lockKey);
4746
+ const dbBoundary = record.lastBufferedAtTokens ?? 0;
4747
+ const currentContextTokens = this.tokenCounter.countMessages(messageList.get.all.db());
4748
+ if (dbBoundary > currentContextTokens) {
4749
+ omDebug(
4750
+ `[OM:step0-boundary-reset] dbBoundary=${dbBoundary} > currentContext=${currentContextTokens}, resetting to current`
4751
+ );
4752
+ _ObservationalMemory.lastBufferedBoundary.set(bufKey, currentContextTokens);
4753
+ this.storage.setBufferingObservationFlag(record.id, false, currentContextTokens).catch(() => {
4754
+ });
4755
+ }
4756
+ }
4757
+ if (bufferedChunks.length > 0) {
4758
+ const allMsgsForCheck = messageList.get.all.db();
4759
+ const unobservedMsgsForCheck = this.getUnobservedMessages(allMsgsForCheck, record);
4760
+ const otherThreadTokensForCheck = unobservedContextBlocks ? this.tokenCounter.countString(unobservedContextBlocks) : 0;
4761
+ const currentObsTokensForCheck = record.observationTokenCount ?? 0;
4762
+ const { totalPendingTokens: step0PendingTokens, threshold: step0Threshold } = await this.calculateObservationThresholds(
4763
+ allMsgsForCheck,
4764
+ unobservedMsgsForCheck,
4765
+ 0,
4766
+ // pendingTokens not needed — allMessages covers context
4767
+ otherThreadTokensForCheck,
4768
+ currentObsTokensForCheck,
4769
+ record
3382
4770
  );
3383
- if (activationResult.success && activationResult.updatedRecord) {
3384
- record = activationResult.updatedRecord;
3385
- const activatedIds = activationResult.activatedMessageIds ?? [];
3386
- if (activatedIds.length > 0) {
3387
- const activatedSet = new Set(activatedIds);
3388
- const allMsgs = messageList.get.all.db();
3389
- const idsToRemove = allMsgs.filter((msg) => msg?.id && msg.id !== "om-continuation" && activatedSet.has(msg.id)).map((msg) => msg.id);
3390
- if (idsToRemove.length > 0) {
3391
- messageList.removeByIds(idsToRemove);
4771
+ omDebug(
4772
+ `[OM:step0-activation] pendingTokens=${step0PendingTokens}, threshold=${step0Threshold}, blockAfter=${this.observationConfig.blockAfter}, shouldActivate=${step0PendingTokens >= step0Threshold}, allMsgs=${allMsgsForCheck.length}`
4773
+ );
4774
+ if (step0PendingTokens >= step0Threshold) {
4775
+ const activationResult = await this.tryActivateBufferedObservations(
4776
+ record,
4777
+ lockKey,
4778
+ step0PendingTokens,
4779
+ writer,
4780
+ messageList
4781
+ );
4782
+ reproCaptureDetails.step0Activation = {
4783
+ attempted: true,
4784
+ success: activationResult.success,
4785
+ messageTokensActivated: activationResult.messageTokensActivated ?? 0,
4786
+ activatedMessageIds: activationResult.activatedMessageIds ?? [],
4787
+ hadUpdatedRecord: !!activationResult.updatedRecord
4788
+ };
4789
+ if (activationResult.success && activationResult.updatedRecord) {
4790
+ record = activationResult.updatedRecord;
4791
+ const activatedIds = activationResult.activatedMessageIds ?? [];
4792
+ if (activatedIds.length > 0) {
4793
+ const activatedSet = new Set(activatedIds);
4794
+ const allMsgs = messageList.get.all.db();
4795
+ const idsToRemove = allMsgs.filter((msg) => msg?.id && msg.id !== "om-continuation" && activatedSet.has(msg.id)).map((msg) => msg.id);
4796
+ if (idsToRemove.length > 0) {
4797
+ messageList.removeByIds(idsToRemove);
4798
+ }
3392
4799
  }
3393
- }
3394
- this.cleanupStaticMaps(threadId, resourceId, activatedIds);
3395
- const bufKey = this.getObservationBufferKey(lockKey);
3396
- _ObservationalMemory.lastBufferedBoundary.set(bufKey, 0);
3397
- this.storage.setBufferingObservationFlag(record.id, false, 0).catch(() => {
3398
- });
3399
- const thread = await this.storage.getThreadById({ threadId });
3400
- if (thread) {
3401
- const newMetadata = setThreadOMMetadata(thread.metadata, {
3402
- suggestedResponse: activationResult.suggestedContinuation,
3403
- currentTask: activationResult.currentTask
4800
+ this.cleanupStaticMaps(threadId, resourceId, activatedIds);
4801
+ const bufKey = this.getObservationBufferKey(lockKey);
4802
+ _ObservationalMemory.lastBufferedBoundary.set(bufKey, 0);
4803
+ this.storage.setBufferingObservationFlag(record.id, false, 0).catch(() => {
3404
4804
  });
3405
- await this.storage.updateThread({
3406
- id: threadId,
3407
- title: thread.title ?? "",
3408
- metadata: newMetadata
4805
+ const thread = await this.storage.getThreadById({ threadId });
4806
+ if (thread) {
4807
+ const activatedSet = new Set(activationResult.activatedMessageIds ?? []);
4808
+ const activatedMessages = messageList.get.all.db().filter((msg) => msg?.id && activatedSet.has(msg.id));
4809
+ const newMetadata = setThreadOMMetadata(thread.metadata, {
4810
+ suggestedResponse: activationResult.suggestedContinuation,
4811
+ currentTask: activationResult.currentTask,
4812
+ lastObservedMessageCursor: this.getLastObservedMessageCursor(activatedMessages)
4813
+ });
4814
+ await this.storage.updateThread({
4815
+ id: threadId,
4816
+ title: thread.title ?? "",
4817
+ metadata: newMetadata
4818
+ });
4819
+ }
4820
+ await this.maybeReflect({
4821
+ record,
4822
+ observationTokens: record.observationTokenCount ?? 0,
4823
+ threadId,
4824
+ writer,
4825
+ messageList,
4826
+ requestContext
3409
4827
  });
4828
+ record = await this.getOrCreateRecord(threadId, resourceId);
3410
4829
  }
3411
- await this.maybeReflect({
3412
- record,
3413
- observationTokens: record.observationTokenCount ?? 0,
3414
- threadId,
3415
- writer,
3416
- messageList,
3417
- requestContext
3418
- });
3419
- record = await this.getOrCreateRecord(threadId, resourceId);
3420
4830
  }
3421
4831
  }
3422
4832
  }
3423
- }
3424
- if (stepNumber === 0 && !readOnly) {
3425
- const obsTokens = record.observationTokenCount ?? 0;
3426
- if (this.shouldReflect(obsTokens)) {
3427
- omDebug(`[OM:step0-reflect] obsTokens=${obsTokens} over reflectThreshold, triggering reflection`);
3428
- await this.maybeReflect({
3429
- record,
3430
- observationTokens: obsTokens,
3431
- threadId,
3432
- writer,
3433
- messageList,
3434
- requestContext
3435
- });
3436
- record = await this.getOrCreateRecord(threadId, resourceId);
3437
- } else if (this.isAsyncReflectionEnabled()) {
3438
- const lockKey = this.getLockKey(threadId, resourceId);
3439
- if (this.shouldTriggerAsyncReflection(obsTokens, lockKey, record)) {
3440
- omDebug(`[OM:step0-reflect] obsTokens=${obsTokens} above activation point, triggering async reflection`);
3441
- await this.maybeAsyncReflect(record, obsTokens, writer, messageList, requestContext);
3442
- record = await this.getOrCreateRecord(threadId, resourceId);
3443
- }
3444
- }
3445
- }
3446
- if (!readOnly) {
3447
- const allMessages = messageList.get.all.db();
3448
- const unobservedMessages = this.getUnobservedMessages(allMessages, record);
3449
- const otherThreadTokens = unobservedContextBlocks ? this.tokenCounter.countString(unobservedContextBlocks) : 0;
3450
- const currentObservationTokens = record.observationTokenCount ?? 0;
3451
- const thresholds = this.calculateObservationThresholds(
3452
- allMessages,
3453
- unobservedMessages,
3454
- 0,
3455
- // pendingTokens not needed — allMessages covers context
3456
- otherThreadTokens,
3457
- currentObservationTokens,
3458
- record
3459
- );
3460
- const { totalPendingTokens, threshold } = thresholds;
3461
- const bufferedChunkTokens = this.getBufferedChunks(record).reduce((sum, c) => sum + (c.messageTokens ?? 0), 0);
3462
- const unbufferedPendingTokens = Math.max(0, totalPendingTokens - bufferedChunkTokens);
3463
- const stateSealedIds = state.sealedIds ?? /* @__PURE__ */ new Set();
3464
- const staticSealedIds = _ObservationalMemory.sealedMessageIds.get(threadId) ?? /* @__PURE__ */ new Set();
3465
- const sealedIds = /* @__PURE__ */ new Set([...stateSealedIds, ...staticSealedIds]);
3466
- state.sealedIds = sealedIds;
3467
- const lockKey = this.getLockKey(threadId, resourceId);
3468
- if (this.isAsyncObservationEnabled() && totalPendingTokens < threshold) {
3469
- const shouldTrigger = this.shouldTriggerAsyncObservation(totalPendingTokens, lockKey, record, threshold);
3470
- omDebug(
3471
- `[OM:async-obs] belowThreshold: pending=${totalPendingTokens}, unbuffered=${unbufferedPendingTokens}, threshold=${threshold}, shouldTrigger=${shouldTrigger}, isBufferingObs=${record.isBufferingObservation}, lastBufferedAt=${record.lastBufferedAtTokens}`
3472
- );
3473
- if (shouldTrigger) {
3474
- this.startAsyncBufferedObservation(
4833
+ if (stepNumber === 0 && !readOnly) {
4834
+ const obsTokens = record.observationTokenCount ?? 0;
4835
+ if (this.shouldReflect(obsTokens)) {
4836
+ omDebug(`[OM:step0-reflect] obsTokens=${obsTokens} over reflectThreshold, triggering reflection`);
4837
+ await this.maybeReflect({
3475
4838
  record,
4839
+ observationTokens: obsTokens,
3476
4840
  threadId,
3477
- unobservedMessages,
3478
- lockKey,
3479
4841
  writer,
3480
- unbufferedPendingTokens,
4842
+ messageList,
3481
4843
  requestContext
3482
- );
4844
+ });
4845
+ record = await this.getOrCreateRecord(threadId, resourceId);
4846
+ } else if (this.isAsyncReflectionEnabled()) {
4847
+ const lockKey = this.getLockKey(threadId, resourceId);
4848
+ if (this.shouldTriggerAsyncReflection(obsTokens, lockKey, record)) {
4849
+ omDebug(`[OM:step0-reflect] obsTokens=${obsTokens} above activation point, triggering async reflection`);
4850
+ await this.maybeAsyncReflect(record, obsTokens, writer, messageList, requestContext);
4851
+ record = await this.getOrCreateRecord(threadId, resourceId);
4852
+ }
3483
4853
  }
3484
- } else if (this.isAsyncObservationEnabled()) {
3485
- const shouldTrigger = this.shouldTriggerAsyncObservation(totalPendingTokens, lockKey, record, threshold);
3486
- omDebug(
3487
- `[OM:async-obs] atOrAboveThreshold: pending=${totalPendingTokens}, unbuffered=${unbufferedPendingTokens}, threshold=${threshold}, step=${stepNumber}, shouldTrigger=${shouldTrigger}`
4854
+ }
4855
+ let didThresholdCleanup = false;
4856
+ if (!readOnly) {
4857
+ let allMessages = messageList.get.all.db();
4858
+ let unobservedMessages = this.getUnobservedMessages(allMessages, record);
4859
+ const otherThreadTokens = unobservedContextBlocks ? this.tokenCounter.countString(unobservedContextBlocks) : 0;
4860
+ let currentObservationTokens = record.observationTokenCount ?? 0;
4861
+ let thresholds = await this.calculateObservationThresholds(
4862
+ allMessages,
4863
+ unobservedMessages,
4864
+ 0,
4865
+ // pendingTokens not needed — allMessages covers context
4866
+ otherThreadTokens,
4867
+ currentObservationTokens,
4868
+ record
3488
4869
  );
3489
- if (shouldTrigger) {
3490
- this.startAsyncBufferedObservation(
4870
+ let { totalPendingTokens, threshold } = thresholds;
4871
+ let bufferedChunkTokens = this.getBufferedChunks(record).reduce((sum, c) => sum + (c.messageTokens ?? 0), 0);
4872
+ let unbufferedPendingTokens = Math.max(0, totalPendingTokens - bufferedChunkTokens);
4873
+ const stateSealedIds = state.sealedIds ?? /* @__PURE__ */ new Set();
4874
+ const staticSealedIds = _ObservationalMemory.sealedMessageIds.get(threadId) ?? /* @__PURE__ */ new Set();
4875
+ const sealedIds = /* @__PURE__ */ new Set([...stateSealedIds, ...staticSealedIds]);
4876
+ state.sealedIds = sealedIds;
4877
+ const lockKey = this.getLockKey(threadId, resourceId);
4878
+ if (this.isAsyncObservationEnabled() && totalPendingTokens < threshold) {
4879
+ const shouldTrigger = this.shouldTriggerAsyncObservation(totalPendingTokens, lockKey, record, threshold);
4880
+ omDebug(
4881
+ `[OM:async-obs] belowThreshold: pending=${totalPendingTokens}, unbuffered=${unbufferedPendingTokens}, threshold=${threshold}, shouldTrigger=${shouldTrigger}, isBufferingObs=${record.isBufferingObservation}, lastBufferedAt=${record.lastBufferedAtTokens}`
4882
+ );
4883
+ if (shouldTrigger) {
4884
+ void this.startAsyncBufferedObservation(
4885
+ record,
4886
+ threadId,
4887
+ unobservedMessages,
4888
+ lockKey,
4889
+ writer,
4890
+ unbufferedPendingTokens,
4891
+ requestContext
4892
+ );
4893
+ }
4894
+ } else if (this.isAsyncObservationEnabled()) {
4895
+ const shouldTrigger = this.shouldTriggerAsyncObservation(totalPendingTokens, lockKey, record, threshold);
4896
+ omDebug(
4897
+ `[OM:async-obs] atOrAboveThreshold: pending=${totalPendingTokens}, unbuffered=${unbufferedPendingTokens}, threshold=${threshold}, step=${stepNumber}, shouldTrigger=${shouldTrigger}`
4898
+ );
4899
+ if (shouldTrigger) {
4900
+ void this.startAsyncBufferedObservation(
4901
+ record,
4902
+ threadId,
4903
+ unobservedMessages,
4904
+ lockKey,
4905
+ writer,
4906
+ unbufferedPendingTokens,
4907
+ requestContext
4908
+ );
4909
+ }
4910
+ }
4911
+ if (stepNumber > 0) {
4912
+ await this.handlePerStepSave(messageList, sealedIds, threadId, resourceId, state);
4913
+ }
4914
+ if (stepNumber > 0 && totalPendingTokens >= threshold) {
4915
+ reproCaptureDetails.thresholdReached = true;
4916
+ const { observationSucceeded, updatedRecord, activatedMessageIds } = await this.handleThresholdReached(
4917
+ messageList,
3491
4918
  record,
3492
4919
  threadId,
3493
- unobservedMessages,
4920
+ resourceId,
4921
+ threshold,
3494
4922
  lockKey,
3495
4923
  writer,
3496
- unbufferedPendingTokens,
4924
+ abortSignal,
4925
+ abort,
3497
4926
  requestContext
3498
4927
  );
4928
+ if (observationSucceeded) {
4929
+ const observedIds = activatedMessageIds?.length ? activatedMessageIds : Array.isArray(updatedRecord.observedMessageIds) ? updatedRecord.observedMessageIds : void 0;
4930
+ const minRemaining = typeof this.observationConfig.bufferActivation === "number" ? resolveRetentionFloor(this.observationConfig.bufferActivation, threshold) : void 0;
4931
+ reproCaptureDetails.thresholdCleanup = {
4932
+ observationSucceeded,
4933
+ observedIdsCount: observedIds?.length ?? 0,
4934
+ observedIds,
4935
+ minRemaining,
4936
+ updatedRecordObservedIds: updatedRecord.observedMessageIds
4937
+ };
4938
+ omDebug(
4939
+ `[OM:cleanup] observedIds=${observedIds?.length ?? "undefined"}, ids=${observedIds?.join(",") ?? "none"}, updatedRecord.observedMessageIds=${JSON.stringify(updatedRecord.observedMessageIds)}, minRemaining=${minRemaining ?? "n/a"}`
4940
+ );
4941
+ await this.cleanupAfterObservation(
4942
+ messageList,
4943
+ sealedIds,
4944
+ threadId,
4945
+ resourceId,
4946
+ state,
4947
+ observedIds,
4948
+ minRemaining
4949
+ );
4950
+ didThresholdCleanup = true;
4951
+ if (activatedMessageIds?.length) {
4952
+ this.cleanupStaticMaps(threadId, resourceId, activatedMessageIds);
4953
+ }
4954
+ if (this.isAsyncObservationEnabled()) {
4955
+ const bufKey = this.getObservationBufferKey(lockKey);
4956
+ _ObservationalMemory.lastBufferedBoundary.set(bufKey, 0);
4957
+ this.storage.setBufferingObservationFlag(updatedRecord.id, false, 0).catch(() => {
4958
+ });
4959
+ omDebug(`[OM:threshold] post-activation boundary reset to 0`);
4960
+ }
4961
+ }
4962
+ record = updatedRecord;
3499
4963
  }
3500
4964
  }
3501
- if (stepNumber > 0) {
3502
- await this.handlePerStepSave(messageList, sealedIds, threadId, resourceId, state);
4965
+ await this.injectObservationsIntoContext(
4966
+ messageList,
4967
+ record,
4968
+ threadId,
4969
+ resourceId,
4970
+ unobservedContextBlocks,
4971
+ requestContext
4972
+ );
4973
+ if (!didThresholdCleanup) {
4974
+ await this.filterAlreadyObservedMessages(messageList, record, { useMarkerBoundaryPruning: stepNumber === 0 });
3503
4975
  }
3504
- if (stepNumber > 0 && totalPendingTokens >= threshold) {
3505
- const { observationSucceeded, updatedRecord, activatedMessageIds } = await this.handleThresholdReached(
3506
- messageList,
3507
- record,
4976
+ {
4977
+ const freshRecord = await this.getOrCreateRecord(threadId, resourceId);
4978
+ const contextMessages = messageList.get.all.db();
4979
+ const freshUnobservedTokens = await this.tokenCounter.countMessagesAsync(contextMessages);
4980
+ const otherThreadTokens = unobservedContextBlocks ? this.tokenCounter.countString(unobservedContextBlocks) : 0;
4981
+ const currentObservationTokens = freshRecord.observationTokenCount ?? 0;
4982
+ const threshold = calculateDynamicThreshold(this.observationConfig.messageTokens, currentObservationTokens);
4983
+ const baseReflectionThreshold = getMaxThreshold(this.reflectionConfig.observationTokens);
4984
+ const isSharedBudget = typeof this.observationConfig.messageTokens !== "number";
4985
+ const totalBudget = isSharedBudget ? this.observationConfig.messageTokens.max : 0;
4986
+ const effectiveObservationTokensThreshold = isSharedBudget ? Math.max(totalBudget - threshold, 1e3) : baseReflectionThreshold;
4987
+ const totalPendingTokens = freshUnobservedTokens + otherThreadTokens;
4988
+ await this.emitStepProgress(
4989
+ writer,
3508
4990
  threadId,
3509
4991
  resourceId,
3510
- threshold,
3511
- lockKey,
3512
- writer,
3513
- abortSignal,
3514
- abort,
3515
- requestContext
4992
+ stepNumber,
4993
+ freshRecord,
4994
+ {
4995
+ totalPendingTokens,
4996
+ threshold,
4997
+ effectiveObservationTokensThreshold
4998
+ },
4999
+ currentObservationTokens
3516
5000
  );
3517
- if (observationSucceeded) {
3518
- const observedIds = activatedMessageIds?.length ? activatedMessageIds : Array.isArray(updatedRecord.observedMessageIds) ? updatedRecord.observedMessageIds : void 0;
3519
- const minRemaining = typeof this.observationConfig.bufferActivation === "number" ? resolveRetentionFloor(this.observationConfig.bufferActivation, threshold) : void 0;
3520
- omDebug(
3521
- `[OM:cleanup] observedIds=${observedIds?.length ?? "undefined"}, ids=${observedIds?.join(",") ?? "none"}, updatedRecord.observedMessageIds=${JSON.stringify(updatedRecord.observedMessageIds)}, minRemaining=${minRemaining ?? "n/a"}`
3522
- );
3523
- await this.cleanupAfterObservation(
3524
- messageList,
3525
- sealedIds,
5001
+ this.storage.setPendingMessageTokens(freshRecord.id, totalPendingTokens).catch(() => {
5002
+ });
5003
+ if (reproCaptureEnabled && preRecordSnapshot && preMessagesSnapshot && preSerializedMessageList) {
5004
+ writeProcessInputStepReproCapture({
3526
5005
  threadId,
3527
5006
  resourceId,
3528
- state,
3529
- observedIds,
3530
- minRemaining
3531
- );
3532
- if (activatedMessageIds?.length) {
3533
- this.cleanupStaticMaps(threadId, resourceId, activatedMessageIds);
3534
- }
3535
- if (this.isAsyncObservationEnabled()) {
3536
- const bufKey = this.getObservationBufferKey(lockKey);
3537
- _ObservationalMemory.lastBufferedBoundary.set(bufKey, 0);
3538
- this.storage.setBufferingObservationFlag(updatedRecord.id, false, 0).catch(() => {
3539
- });
3540
- omDebug(`[OM:threshold] post-activation boundary reset to 0`);
3541
- }
5007
+ stepNumber,
5008
+ args,
5009
+ preRecord: preRecordSnapshot,
5010
+ postRecord: freshRecord,
5011
+ preMessages: preMessagesSnapshot,
5012
+ preBufferedChunks: this.getBufferedChunks(preRecordSnapshot),
5013
+ preContextTokenCount: this.tokenCounter.countMessages(preMessagesSnapshot),
5014
+ preSerializedMessageList,
5015
+ postBufferedChunks: this.getBufferedChunks(freshRecord),
5016
+ postContextTokenCount: this.tokenCounter.countMessages(contextMessages),
5017
+ messageList,
5018
+ details: {
5019
+ ...reproCaptureDetails,
5020
+ totalPendingTokens,
5021
+ threshold,
5022
+ effectiveObservationTokensThreshold,
5023
+ currentObservationTokens,
5024
+ otherThreadTokens,
5025
+ contextMessageCount: contextMessages.length
5026
+ },
5027
+ debug: omDebug
5028
+ });
3542
5029
  }
3543
- record = updatedRecord;
3544
5030
  }
3545
- }
3546
- await this.injectObservationsIntoContext(
3547
- messageList,
3548
- record,
3549
- threadId,
3550
- resourceId,
3551
- unobservedContextBlocks,
3552
- requestContext
3553
- );
3554
- if (stepNumber === 0) {
3555
- this.filterAlreadyObservedMessages(messageList, record);
3556
- }
3557
- {
3558
- const freshRecord = await this.getOrCreateRecord(threadId, resourceId);
3559
- const contextMessages = messageList.get.all.db();
3560
- const freshUnobservedTokens = this.tokenCounter.countMessages(contextMessages);
3561
- const otherThreadTokens = unobservedContextBlocks ? this.tokenCounter.countString(unobservedContextBlocks) : 0;
3562
- const currentObservationTokens = freshRecord.observationTokenCount ?? 0;
3563
- const threshold = calculateDynamicThreshold(this.observationConfig.messageTokens, currentObservationTokens);
3564
- const baseReflectionThreshold = getMaxThreshold(this.reflectionConfig.observationTokens);
3565
- const isSharedBudget = typeof this.observationConfig.messageTokens !== "number";
3566
- const totalBudget = isSharedBudget ? this.observationConfig.messageTokens.max : 0;
3567
- const effectiveObservationTokensThreshold = isSharedBudget ? Math.max(totalBudget - threshold, 1e3) : baseReflectionThreshold;
3568
- const totalPendingTokens = freshUnobservedTokens + otherThreadTokens;
3569
- await this.emitStepProgress(
3570
- writer,
3571
- threadId,
3572
- resourceId,
3573
- stepNumber,
3574
- freshRecord,
3575
- {
3576
- totalPendingTokens,
3577
- threshold,
3578
- effectiveObservationTokensThreshold
3579
- },
3580
- currentObservationTokens
3581
- );
3582
- this.storage.setPendingMessageTokens(freshRecord.id, totalPendingTokens).catch(() => {
3583
- });
3584
- }
3585
- return messageList;
5031
+ return messageList;
5032
+ });
3586
5033
  }
3587
5034
  /**
3588
5035
  * Save any unsaved messages at the end of the agent turn.
@@ -3599,30 +5046,35 @@ ${suggestedResponse}
3599
5046
  return messageList;
3600
5047
  }
3601
5048
  const { threadId, resourceId } = context;
3602
- const memoryContext = parseMemoryRequestContext(requestContext);
3603
- const readOnly = memoryContext?.memoryConfig?.readOnly;
3604
- if (readOnly) {
3605
- return messageList;
3606
- }
3607
- const newInput = messageList.get.input.db();
3608
- const newOutput = messageList.get.response.db();
3609
- const messagesToSave = [...newInput, ...newOutput];
3610
- omDebug(
3611
- `[OM:processOutputResult] threadId=${threadId}, inputMsgs=${newInput.length}, responseMsgs=${newOutput.length}, totalToSave=${messagesToSave.length}, allMsgsInList=${messageList.get.all.db().length}`
3612
- );
3613
- if (messagesToSave.length === 0) {
3614
- omDebug(`[OM:processOutputResult] nothing to save \u2014 all messages were already saved during per-step saves`);
3615
- return messageList;
3616
- }
3617
- const sealedIds = state.sealedIds ?? /* @__PURE__ */ new Set();
3618
- omDebug(
3619
- `[OM:processOutputResult] saving ${messagesToSave.length} messages, sealedIds=${sealedIds.size}, ids=${messagesToSave.map((m) => m.id?.slice(0, 8)).join(",")}`
3620
- );
3621
- await this.saveMessagesWithSealedIdTracking(messagesToSave, sealedIds, threadId, resourceId, state);
3622
- omDebug(
3623
- `[OM:processOutputResult] saved successfully, finalIds=${messagesToSave.map((m) => m.id?.slice(0, 8)).join(",")}`
5049
+ return this.runWithTokenCounterModelContext(
5050
+ state.__omActorModelContext,
5051
+ async () => {
5052
+ const memoryContext = parseMemoryRequestContext(requestContext);
5053
+ const readOnly = memoryContext?.memoryConfig?.readOnly;
5054
+ if (readOnly) {
5055
+ return messageList;
5056
+ }
5057
+ const newInput = messageList.get.input.db();
5058
+ const newOutput = messageList.get.response.db();
5059
+ const messagesToSave = [...newInput, ...newOutput];
5060
+ omDebug(
5061
+ `[OM:processOutputResult] threadId=${threadId}, inputMsgs=${newInput.length}, responseMsgs=${newOutput.length}, totalToSave=${messagesToSave.length}, allMsgsInList=${messageList.get.all.db().length}`
5062
+ );
5063
+ if (messagesToSave.length === 0) {
5064
+ omDebug(`[OM:processOutputResult] nothing to save \u2014 all messages were already saved during per-step saves`);
5065
+ return messageList;
5066
+ }
5067
+ const sealedIds = state.sealedIds ?? /* @__PURE__ */ new Set();
5068
+ omDebug(
5069
+ `[OM:processOutputResult] saving ${messagesToSave.length} messages, sealedIds=${sealedIds.size}, ids=${messagesToSave.map((m) => m.id?.slice(0, 8)).join(",")}`
5070
+ );
5071
+ await this.saveMessagesWithSealedIdTracking(messagesToSave, sealedIds, threadId, resourceId, state);
5072
+ omDebug(
5073
+ `[OM:processOutputResult] saved successfully, finalIds=${messagesToSave.map((m) => m.id?.slice(0, 8)).join(",")}`
5074
+ );
5075
+ return messageList;
5076
+ }
3624
5077
  );
3625
- return messageList;
3626
5078
  }
3627
5079
  /**
3628
5080
  * Save messages to storage while preventing duplicate inserts for sealed messages.
@@ -3776,6 +5228,30 @@ ${formattedMessages}
3776
5228
  }
3777
5229
  return maxTime > 0 ? new Date(maxTime) : /* @__PURE__ */ new Date();
3778
5230
  }
5231
+ /**
5232
+ * Compute a cursor pointing at the latest message by createdAt.
5233
+ * Used to derive a stable observation boundary for replay pruning.
5234
+ */
5235
+ getLastObservedMessageCursor(messages) {
5236
+ let latest;
5237
+ for (const msg of messages) {
5238
+ if (!msg?.id || !msg.createdAt) continue;
5239
+ if (!latest || new Date(msg.createdAt).getTime() > new Date(latest.createdAt).getTime()) {
5240
+ latest = msg;
5241
+ }
5242
+ }
5243
+ return latest ? { createdAt: new Date(latest.createdAt).toISOString(), id: latest.id } : void 0;
5244
+ }
5245
+ /**
5246
+ * Check if a message is at or before a cursor (by createdAt then id).
5247
+ */
5248
+ isMessageAtOrBeforeCursor(msg, cursor) {
5249
+ if (!msg.createdAt) return false;
5250
+ const msgIso = new Date(msg.createdAt).toISOString();
5251
+ if (msgIso < cursor.createdAt) return true;
5252
+ if (msgIso === cursor.createdAt && msg.id === cursor.id) return true;
5253
+ return false;
5254
+ }
3779
5255
  /**
3780
5256
  * Wrap observations in a thread attribution tag.
3781
5257
  * Used in resource scope to track which thread observations came from.
@@ -3874,7 +5350,7 @@ ${newThreadSection}`;
3874
5350
  await this.storage.setObservingFlag(record.id, true);
3875
5351
  registerOp(record.id, "observing");
3876
5352
  const cycleId = crypto.randomUUID();
3877
- const tokensToObserve = this.tokenCounter.countMessages(unobservedMessages);
5353
+ const tokensToObserve = await this.tokenCounter.countMessagesAsync(unobservedMessages);
3878
5354
  const lastMessage = unobservedMessages[unobservedMessages.length - 1];
3879
5355
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
3880
5356
  if (lastMessage?.id) {
@@ -3936,7 +5412,8 @@ ${result.observations}` : result.observations;
3936
5412
  if (thread) {
3937
5413
  const newMetadata = setThreadOMMetadata(thread.metadata, {
3938
5414
  suggestedResponse: result.suggestedContinuation,
3939
- currentTask: result.currentTask
5415
+ currentTask: result.currentTask,
5416
+ lastObservedMessageCursor: this.getLastObservedMessageCursor(messagesToObserve)
3940
5417
  });
3941
5418
  await this.storage.updateThread({
3942
5419
  id: threadId,
@@ -3951,7 +5428,7 @@ ${result.observations}` : result.observations;
3951
5428
  lastObservedAt,
3952
5429
  observedMessageIds: allObservedIds
3953
5430
  });
3954
- const actualTokensObserved = this.tokenCounter.countMessages(messagesToObserve);
5431
+ const actualTokensObserved = await this.tokenCounter.countMessagesAsync(messagesToObserve);
3955
5432
  if (lastMessage?.id) {
3956
5433
  const endMarker = createObservationEndMarker({
3957
5434
  cycleId,
@@ -4032,9 +5509,9 @@ ${result.observations}` : result.observations;
4032
5509
  * @param lockKey - Lock key for this scope
4033
5510
  * @param writer - Optional stream writer for emitting buffering markers
4034
5511
  */
4035
- startAsyncBufferedObservation(record, threadId, unobservedMessages, lockKey, writer, contextWindowTokens, requestContext) {
5512
+ async startAsyncBufferedObservation(record, threadId, unobservedMessages, lockKey, writer, contextWindowTokens, requestContext) {
4036
5513
  const bufferKey = this.getObservationBufferKey(lockKey);
4037
- const currentTokens = contextWindowTokens ?? this.tokenCounter.countMessages(unobservedMessages) + (record.pendingMessageTokens ?? 0);
5514
+ const currentTokens = contextWindowTokens ?? await this.tokenCounter.countMessagesAsync(unobservedMessages) + (record.pendingMessageTokens ?? 0);
4038
5515
  _ObservationalMemory.lastBufferedBoundary.set(bufferKey, currentTokens);
4039
5516
  registerOp(record.id, "bufferingObservation");
4040
5517
  this.storage.setBufferingObservationFlag(record.id, true, currentTokens).catch((err) => {
@@ -4094,7 +5571,7 @@ ${result.observations}` : result.observations;
4094
5571
  );
4095
5572
  const bufferTokens = this.observationConfig.bufferTokens ?? 5e3;
4096
5573
  const minNewTokens = bufferTokens / 2;
4097
- const newTokens = this.tokenCounter.countMessages(candidateMessages);
5574
+ const newTokens = await this.tokenCounter.countMessagesAsync(candidateMessages);
4098
5575
  if (newTokens < minNewTokens) {
4099
5576
  return;
4100
5577
  }
@@ -4115,7 +5592,7 @@ ${result.observations}` : result.observations;
4115
5592
  }
4116
5593
  const cycleId = `buffer-obs-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
4117
5594
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
4118
- const tokensToBuffer = this.tokenCounter.countMessages(messagesToBuffer);
5595
+ const tokensToBuffer = await this.tokenCounter.countMessagesAsync(messagesToBuffer);
4119
5596
  if (writer) {
4120
5597
  const startMarker = createBufferingStartMarker({
4121
5598
  cycleId,
@@ -4193,7 +5670,7 @@ ${result.observations}` : result.observations;
4193
5670
  }
4194
5671
  const newTokenCount = this.tokenCounter.countObservations(newObservations);
4195
5672
  const newMessageIds = messagesToBuffer.map((m) => m.id);
4196
- const messageTokens = this.tokenCounter.countMessages(messagesToBuffer);
5673
+ const messageTokens = await this.tokenCounter.countMessagesAsync(messagesToBuffer);
4197
5674
  const maxMessageTimestamp = this.getMaxMessageTimestamp(messagesToBuffer);
4198
5675
  const lastObservedAt = new Date(maxMessageTimestamp.getTime() + 1);
4199
5676
  await this.storage.updateBufferedObservations({
@@ -4211,7 +5688,7 @@ ${result.observations}` : result.observations;
4211
5688
  lastBufferedAtTime: lastObservedAt
4212
5689
  });
4213
5690
  if (writer) {
4214
- const tokensBuffered = this.tokenCounter.countMessages(messagesToBuffer);
5691
+ const tokensBuffered = await this.tokenCounter.countMessagesAsync(messagesToBuffer);
4215
5692
  const updatedRecord = await this.storage.getObservationalMemory(record.threadId, record.resourceId);
4216
5693
  const updatedChunks = this.getBufferedChunks(updatedRecord);
4217
5694
  const totalBufferedTokens = updatedChunks.reduce((sum, c) => sum + (c.tokenCount ?? 0), 0) || newTokenCount;
@@ -4280,14 +5757,14 @@ ${bufferedObservations}`;
4280
5757
  if (!freshRecord) {
4281
5758
  return { success: false };
4282
5759
  }
4283
- const freshChunks = this.getBufferedChunks(freshRecord);
4284
- if (!freshChunks.length) {
5760
+ const rawFreshChunks = this.getBufferedChunks(freshRecord);
5761
+ if (!rawFreshChunks.length) {
4285
5762
  return { success: false };
4286
5763
  }
4287
5764
  const messageTokensThreshold = getMaxThreshold(this.observationConfig.messageTokens);
4288
5765
  let effectivePendingTokens = currentPendingTokens;
4289
5766
  if (messageList) {
4290
- effectivePendingTokens = this.tokenCounter.countMessages(messageList.get.all.db());
5767
+ effectivePendingTokens = await this.tokenCounter.countMessagesAsync(messageList.get.all.db());
4291
5768
  if (effectivePendingTokens < messageTokensThreshold) {
4292
5769
  omDebug(
4293
5770
  `[OM:tryActivate] skipping activation: freshPendingTokens=${effectivePendingTokens} < threshold=${messageTokensThreshold}`
@@ -4295,6 +5772,7 @@ ${bufferedObservations}`;
4295
5772
  return { success: false };
4296
5773
  }
4297
5774
  }
5775
+ const freshChunks = messageList ? this.refreshBufferedChunkMessageTokens(rawFreshChunks, messageList) : rawFreshChunks;
4298
5776
  const bufferActivation = this.observationConfig.bufferActivation ?? 0.7;
4299
5777
  const activationRatio = resolveActivationRatio(bufferActivation, messageTokensThreshold);
4300
5778
  const forceMaxActivation = !!(this.observationConfig.blockAfter && effectivePendingTokens >= this.observationConfig.blockAfter);
@@ -4322,7 +5800,8 @@ ${bufferedObservations}`;
4322
5800
  activationRatio,
4323
5801
  messageTokensThreshold,
4324
5802
  currentPendingTokens: effectivePendingTokens,
4325
- forceMaxActivation
5803
+ forceMaxActivation,
5804
+ bufferedChunks: freshChunks
4326
5805
  });
4327
5806
  omDebug(
4328
5807
  `[OM:tryActivate] swapResult: chunksActivated=${activationResult.chunksActivated}, tokensActivated=${activationResult.messageTokensActivated}, obsTokensActivated=${activationResult.observationTokensActivated}, activatedCycleIds=${activationResult.activatedCycleIds.join(",")}`
@@ -4650,10 +6129,7 @@ ${unreflectedContent}` : freshRecord.bufferedReflection;
4650
6129
  const threshold = getMaxThreshold(this.observationConfig.messageTokens);
4651
6130
  const threadTokenCounts = /* @__PURE__ */ new Map();
4652
6131
  for (const [threadId, msgs] of messagesByThread) {
4653
- let tokens = 0;
4654
- for (const msg of msgs) {
4655
- tokens += this.tokenCounter.countMessage(msg);
4656
- }
6132
+ const tokens = await this.tokenCounter.countMessagesAsync(msgs);
4657
6133
  threadTokenCounts.set(threadId, tokens);
4658
6134
  }
4659
6135
  const threadsBySize = Array.from(messagesByThread.keys()).sort((a, b) => {
@@ -4710,7 +6186,7 @@ ${unreflectedContent}` : freshRecord.bufferedReflection;
4710
6186
  const allThreadIds = Array.from(threadsWithMessages.keys());
4711
6187
  for (const [threadId, msgs] of threadsWithMessages) {
4712
6188
  const lastMessage = msgs[msgs.length - 1];
4713
- const tokensToObserve = this.tokenCounter.countMessages(msgs);
6189
+ const tokensToObserve = await this.tokenCounter.countMessagesAsync(msgs);
4714
6190
  threadTokensToObserve.set(threadId, tokensToObserve);
4715
6191
  if (lastMessage?.id) {
4716
6192
  const startMarker = createObservationStartMarker({
@@ -4802,7 +6278,8 @@ ${unreflectedContent}` : freshRecord.bufferedReflection;
4802
6278
  const newMetadata = setThreadOMMetadata(thread.metadata, {
4803
6279
  lastObservedAt: threadLastObservedAt.toISOString(),
4804
6280
  suggestedResponse: result.suggestedContinuation,
4805
- currentTask: result.currentTask
6281
+ currentTask: result.currentTask,
6282
+ lastObservedMessageCursor: this.getLastObservedMessageCursor(threadMessages)
4806
6283
  });
4807
6284
  await this.storage.updateThread({
4808
6285
  id: threadId,
@@ -4845,7 +6322,7 @@ ${unreflectedContent}` : freshRecord.bufferedReflection;
4845
6322
  const { threadId, threadMessages, result } = obsResult;
4846
6323
  const lastMessage = threadMessages[threadMessages.length - 1];
4847
6324
  if (lastMessage?.id) {
4848
- const tokensObserved = threadTokensToObserve.get(threadId) ?? this.tokenCounter.countMessages(threadMessages);
6325
+ const tokensObserved = threadTokensToObserve.get(threadId) ?? await this.tokenCounter.countMessagesAsync(threadMessages);
4849
6326
  const endMarker = createObservationEndMarker({
4850
6327
  cycleId,
4851
6328
  operationType: "observation",
@@ -5096,7 +6573,7 @@ ${unreflectedContent}` : freshRecord.bufferedReflection;
5096
6573
  const currentMessages = messages ?? [];
5097
6574
  if (!this.meetsObservationThreshold({
5098
6575
  record: freshRecord,
5099
- unobservedTokens: this.tokenCounter.countMessages(currentMessages)
6576
+ unobservedTokens: await this.tokenCounter.countMessagesAsync(currentMessages)
5100
6577
  })) {
5101
6578
  return;
5102
6579
  }
@@ -5124,7 +6601,7 @@ ${unreflectedContent}` : freshRecord.bufferedReflection;
5124
6601
  }
5125
6602
  if (!this.meetsObservationThreshold({
5126
6603
  record: freshRecord,
5127
- unobservedTokens: this.tokenCounter.countMessages(unobservedMessages)
6604
+ unobservedTokens: await this.tokenCounter.countMessagesAsync(unobservedMessages)
5128
6605
  })) {
5129
6606
  return;
5130
6607
  }
@@ -5241,5 +6718,5 @@ ${unreflectedContent}` : freshRecord.bufferedReflection;
5241
6718
  };
5242
6719
 
5243
6720
  export { OBSERVATIONAL_MEMORY_DEFAULTS, OBSERVATION_CONTEXT_INSTRUCTIONS, OBSERVATION_CONTEXT_PROMPT, OBSERVATION_CONTINUATION_HINT, OBSERVER_SYSTEM_PROMPT, ObservationalMemory, TokenCounter, buildObserverPrompt, buildObserverSystemPrompt, extractCurrentTask, formatMessagesForObserver, hasCurrentTaskSection, optimizeObservationsForContext, parseObserverOutput };
5244
- //# sourceMappingURL=chunk-GBBQIJQF.js.map
5245
- //# sourceMappingURL=chunk-GBBQIJQF.js.map
6721
+ //# sourceMappingURL=chunk-3CM4XQJO.js.map
6722
+ //# sourceMappingURL=chunk-3CM4XQJO.js.map