@mastra/memory 1.1.0-alpha.1 → 1.2.0-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 (95) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/dist/_types/@internal_ai-sdk-v4/dist/index.d.ts +30 -17
  3. package/dist/{chunk-FQJWVCDF.cjs → chunk-AWE2QQPI.cjs} +1884 -312
  4. package/dist/chunk-AWE2QQPI.cjs.map +1 -0
  5. package/dist/chunk-EQ4M72KU.js +439 -0
  6. package/dist/chunk-EQ4M72KU.js.map +1 -0
  7. package/dist/{chunk-O3CS4UGX.cjs → chunk-IDRQZVB4.cjs} +4 -4
  8. package/dist/{chunk-O3CS4UGX.cjs.map → chunk-IDRQZVB4.cjs.map} +1 -1
  9. package/dist/{chunk-YF4R74L2.js → chunk-RC6RZVYE.js} +4 -4
  10. package/dist/{chunk-YF4R74L2.js.map → chunk-RC6RZVYE.js.map} +1 -1
  11. package/dist/{chunk-6TXUWFIU.js → chunk-TYVPTNCP.js} +1885 -313
  12. package/dist/chunk-TYVPTNCP.js.map +1 -0
  13. package/dist/chunk-ZD3BKU5O.cjs +441 -0
  14. package/dist/chunk-ZD3BKU5O.cjs.map +1 -0
  15. package/dist/docs/SKILL.md +51 -50
  16. package/dist/docs/{SOURCE_MAP.json → assets/SOURCE_MAP.json} +22 -22
  17. package/dist/docs/{agents/03-agent-approval.md → references/docs-agents-agent-approval.md} +19 -19
  18. package/dist/docs/references/docs-agents-agent-memory.md +212 -0
  19. package/dist/docs/{agents/04-network-approval.md → references/docs-agents-network-approval.md} +13 -12
  20. package/dist/docs/{agents/02-networks.md → references/docs-agents-networks.md} +10 -12
  21. package/dist/docs/{memory/06-memory-processors.md → references/docs-memory-memory-processors.md} +6 -8
  22. package/dist/docs/{memory/03-message-history.md → references/docs-memory-message-history.md} +31 -20
  23. package/dist/docs/references/docs-memory-observational-memory.md +238 -0
  24. package/dist/docs/{memory/01-overview.md → references/docs-memory-overview.md} +8 -8
  25. package/dist/docs/{memory/05-semantic-recall.md → references/docs-memory-semantic-recall.md} +33 -17
  26. package/dist/docs/{memory/02-storage.md → references/docs-memory-storage.md} +29 -39
  27. package/dist/docs/{memory/04-working-memory.md → references/docs-memory-working-memory.md} +16 -27
  28. package/dist/docs/references/reference-core-getMemory.md +50 -0
  29. package/dist/docs/references/reference-core-listMemory.md +56 -0
  30. package/dist/docs/references/reference-memory-clone-utilities.md +199 -0
  31. package/dist/docs/references/reference-memory-cloneThread.md +130 -0
  32. package/dist/docs/references/reference-memory-createThread.md +68 -0
  33. package/dist/docs/references/reference-memory-getThreadById.md +24 -0
  34. package/dist/docs/references/reference-memory-listThreads.md +145 -0
  35. package/dist/docs/references/reference-memory-memory-class.md +147 -0
  36. package/dist/docs/references/reference-memory-observational-memory.md +528 -0
  37. package/dist/docs/{processors/01-reference.md → references/reference-processors-token-limiter-processor.md} +25 -12
  38. package/dist/docs/references/reference-storage-dynamodb.md +282 -0
  39. package/dist/docs/references/reference-storage-libsql.md +135 -0
  40. package/dist/docs/references/reference-storage-mongodb.md +262 -0
  41. package/dist/docs/references/reference-storage-postgresql.md +529 -0
  42. package/dist/docs/references/reference-storage-upstash.md +160 -0
  43. package/dist/docs/references/reference-vectors-libsql.md +305 -0
  44. package/dist/docs/references/reference-vectors-mongodb.md +295 -0
  45. package/dist/docs/references/reference-vectors-pg.md +408 -0
  46. package/dist/docs/references/reference-vectors-upstash.md +294 -0
  47. package/dist/index.cjs +919 -507
  48. package/dist/index.cjs.map +1 -1
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +914 -502
  51. package/dist/index.js.map +1 -1
  52. package/dist/{observational-memory-3Q42SITP.cjs → observational-memory-3UO64HYD.cjs} +14 -14
  53. package/dist/{observational-memory-3Q42SITP.cjs.map → observational-memory-3UO64HYD.cjs.map} +1 -1
  54. package/dist/{observational-memory-VXLHOSDZ.js → observational-memory-TVHT3HP4.js} +3 -3
  55. package/dist/{observational-memory-VXLHOSDZ.js.map → observational-memory-TVHT3HP4.js.map} +1 -1
  56. package/dist/processors/index.cjs +12 -12
  57. package/dist/processors/index.js +1 -1
  58. package/dist/processors/observational-memory/index.d.ts +1 -1
  59. package/dist/processors/observational-memory/index.d.ts.map +1 -1
  60. package/dist/processors/observational-memory/observational-memory.d.ts +267 -1
  61. package/dist/processors/observational-memory/observational-memory.d.ts.map +1 -1
  62. package/dist/processors/observational-memory/observer-agent.d.ts +3 -1
  63. package/dist/processors/observational-memory/observer-agent.d.ts.map +1 -1
  64. package/dist/processors/observational-memory/reflector-agent.d.ts +10 -3
  65. package/dist/processors/observational-memory/reflector-agent.d.ts.map +1 -1
  66. package/dist/processors/observational-memory/types.d.ts +243 -19
  67. package/dist/processors/observational-memory/types.d.ts.map +1 -1
  68. package/dist/{token-6GSAFR2W-WGTMOPEU.js → token-APYSY3BW-2DN6RAUY.js} +11 -11
  69. package/dist/token-APYSY3BW-2DN6RAUY.js.map +1 -0
  70. package/dist/{token-6GSAFR2W-2B4WM6AQ.cjs → token-APYSY3BW-ZQ7TMBY7.cjs} +14 -14
  71. package/dist/token-APYSY3BW-ZQ7TMBY7.cjs.map +1 -0
  72. package/dist/token-util-RMHT2CPJ-6TGPE335.cjs +10 -0
  73. package/dist/token-util-RMHT2CPJ-6TGPE335.cjs.map +1 -0
  74. package/dist/token-util-RMHT2CPJ-RJEA3FAN.js +8 -0
  75. package/dist/token-util-RMHT2CPJ-RJEA3FAN.js.map +1 -0
  76. package/dist/tools/working-memory.d.ts.map +1 -1
  77. package/package.json +9 -10
  78. package/dist/chunk-6TXUWFIU.js.map +0 -1
  79. package/dist/chunk-FQJWVCDF.cjs.map +0 -1
  80. package/dist/chunk-WM6IIUQW.js +0 -250
  81. package/dist/chunk-WM6IIUQW.js.map +0 -1
  82. package/dist/chunk-ZSBBXHNM.cjs +0 -252
  83. package/dist/chunk-ZSBBXHNM.cjs.map +0 -1
  84. package/dist/docs/README.md +0 -36
  85. package/dist/docs/agents/01-agent-memory.md +0 -166
  86. package/dist/docs/core/01-reference.md +0 -114
  87. package/dist/docs/memory/07-reference.md +0 -687
  88. package/dist/docs/storage/01-reference.md +0 -1218
  89. package/dist/docs/vectors/01-reference.md +0 -942
  90. package/dist/token-6GSAFR2W-2B4WM6AQ.cjs.map +0 -1
  91. package/dist/token-6GSAFR2W-WGTMOPEU.js.map +0 -1
  92. package/dist/token-util-NEHG7TUY-TV2H7N56.js +0 -8
  93. package/dist/token-util-NEHG7TUY-TV2H7N56.js.map +0 -1
  94. package/dist/token-util-NEHG7TUY-WJZIPNNX.cjs +0 -10
  95. package/dist/token-util-NEHG7TUY-WJZIPNNX.cjs.map +0 -1
@@ -1,6 +1,8 @@
1
+ import { appendFileSync } from 'fs';
2
+ import { join } from 'path';
1
3
  import { Agent } from '@mastra/core/agent';
2
4
  import { resolveModelConfig } from '@mastra/core/llm';
3
- import { parseMemoryRequestContext, getThreadOMMetadata, setThreadOMMetadata } from '@mastra/core/memory';
5
+ import { getThreadOMMetadata, parseMemoryRequestContext, setThreadOMMetadata } from '@mastra/core/memory';
4
6
  import { MessageHistory } from '@mastra/core/processors';
5
7
  import xxhash from 'xxhash-wasm';
6
8
  import { Tiktoken } from 'js-tiktoken/lite';
@@ -641,7 +643,7 @@ function parseMultiThreadObserverOutput(output) {
641
643
  rawOutput: output
642
644
  };
643
645
  }
644
- function buildObserverPrompt(existingObservations, messagesToObserve) {
646
+ function buildObserverPrompt(existingObservations, messagesToObserve, options) {
645
647
  const formattedMessages = formatMessagesForObserver(messagesToObserve);
646
648
  let prompt = "";
647
649
  if (existingObservations) {
@@ -665,6 +667,11 @@ ${formattedMessages}
665
667
 
666
668
  `;
667
669
  prompt += `Extract new observations from the message history above. Do not repeat observations that are already in the previous observations. Add your new observations in the format specified in your instructions.`;
670
+ if (options?.skipContinuationHints) {
671
+ prompt += `
672
+
673
+ IMPORTANT: Do NOT include <current-task> or <suggested-response> sections in your output. Only output <observations>.`;
674
+ }
668
675
  return prompt;
669
676
  }
670
677
  function parseObserverOutput(output) {
@@ -840,7 +847,9 @@ Hint for the agent's immediate next message. Examples:
840
847
 
841
848
  User messages are extremely important. If the user asks a question or gives a new task, make it clear in <current-task> that this is the priority. If the assistant needs to respond to the user, indicate in <suggested-response> that it should pause for user reply before continuing other tasks.`;
842
849
  }
843
- var COMPRESSION_RETRY_PROMPT = `
850
+ var COMPRESSION_GUIDANCE = {
851
+ 0: "",
852
+ 1: `
844
853
  ## COMPRESSION REQUIRED
845
854
 
846
855
  Your previous reflection was the same size or larger than the original observations.
@@ -853,8 +862,25 @@ Please re-process with slightly more compression:
853
862
  - For example if there is a long nested observation list about repeated tool calls, you can combine those into a single line and observe that the tool was called multiple times for x reason, and finally y outcome happened.
854
863
 
855
864
  Your current detail level was a 10/10, lets aim for a 8/10 detail level.
856
- `;
857
- function buildReflectorPrompt(observations, manualPrompt, compressionRetry) {
865
+ `,
866
+ 2: `
867
+ ## AGGRESSIVE COMPRESSION REQUIRED
868
+
869
+ Your previous reflection was still too large after compression guidance.
870
+
871
+ Please re-process with much more aggressive compression:
872
+ - Towards the beginning, heavily condense observations into high-level summaries
873
+ - Closer to the end, retain fine details (recent context matters more)
874
+ - Memory is getting very long - use a significantly more condensed style throughout
875
+ - Combine related items aggressively but do not lose important specific details of names, places, events, and people
876
+ - For example if there is a long nested observation list about repeated tool calls, you can combine those into a single line and observe that the tool was called multiple times for x reason, and finally y outcome happened.
877
+ - Remove redundant information and merge overlapping observations
878
+
879
+ Your current detail level was a 10/10, lets aim for a 6/10 detail level.
880
+ `
881
+ };
882
+ function buildReflectorPrompt(observations, manualPrompt, compressionLevel, skipContinuationHints) {
883
+ const level = typeof compressionLevel === "number" ? compressionLevel : compressionLevel ? 1 : 0;
858
884
  let prompt = `## OBSERVATIONS TO REFLECT ON
859
885
 
860
886
  ${observations}
@@ -869,10 +895,16 @@ Please analyze these observations and produce a refined, condensed version that
869
895
 
870
896
  ${manualPrompt}`;
871
897
  }
872
- if (compressionRetry) {
898
+ const guidance = COMPRESSION_GUIDANCE[level];
899
+ if (guidance) {
900
+ prompt += `
901
+
902
+ ${guidance}`;
903
+ }
904
+ if (skipContinuationHints) {
873
905
  prompt += `
874
906
 
875
- ${COMPRESSION_RETRY_PROMPT}`;
907
+ IMPORTANT: Do NOT include <current-task> or <suggested-response> sections in your output. Only output <observations>.`;
876
908
  }
877
909
  return prompt;
878
910
  }
@@ -1016,6 +1048,43 @@ var TokenCounter = class _TokenCounter {
1016
1048
  };
1017
1049
 
1018
1050
  // src/processors/observational-memory/observational-memory.ts
1051
+ var OM_DEBUG_LOG = process.env.OM_DEBUG ? join(process.cwd(), "om-debug.log") : null;
1052
+ function omDebug(msg) {
1053
+ if (!OM_DEBUG_LOG) return;
1054
+ try {
1055
+ appendFileSync(OM_DEBUG_LOG, `[${(/* @__PURE__ */ new Date()).toLocaleString()}] ${msg}
1056
+ `);
1057
+ } catch {
1058
+ }
1059
+ }
1060
+ function omError(msg, err) {
1061
+ const errStr = err instanceof Error ? err.stack ?? err.message : err !== void 0 ? String(err) : "";
1062
+ const full = errStr ? `${msg}: ${errStr}` : msg;
1063
+ omDebug(`[OM:ERROR] ${full}`);
1064
+ }
1065
+ omDebug(`[OM:process-start] OM module loaded, pid=${process.pid}`);
1066
+ var activeOps = /* @__PURE__ */ new Set();
1067
+ function opKey(recordId, op) {
1068
+ return `${recordId}:${op}`;
1069
+ }
1070
+ function registerOp(recordId, op) {
1071
+ activeOps.add(opKey(recordId, op));
1072
+ }
1073
+ function unregisterOp(recordId, op) {
1074
+ activeOps.delete(opKey(recordId, op));
1075
+ }
1076
+ function isOpActiveInProcess(recordId, op) {
1077
+ return activeOps.has(opKey(recordId, op));
1078
+ }
1079
+ if (OM_DEBUG_LOG) {
1080
+ const _origConsoleError = console.error;
1081
+ console.error = (...args) => {
1082
+ omDebug(
1083
+ `[console.error] ${args.map((a) => a instanceof Error ? a.stack ?? a.message : typeof a === "object" && a !== null ? JSON.stringify(a) : String(a)).join(" ")}`
1084
+ );
1085
+ _origConsoleError.apply(console, args);
1086
+ };
1087
+ }
1019
1088
  function formatRelativeTime(date, currentDate) {
1020
1089
  const diffMs = currentDate.getTime() - date.getTime();
1021
1090
  const diffDays = Math.floor(diffMs / (1e3 * 60 * 60 * 24));
@@ -1184,7 +1253,12 @@ var OBSERVATIONAL_MEMORY_DEFAULTS = {
1184
1253
  }
1185
1254
  }
1186
1255
  },
1187
- maxTokensPerBatch: 1e4
1256
+ maxTokensPerBatch: 1e4,
1257
+ // Async buffering defaults (enabled by default)
1258
+ bufferTokens: 0.2,
1259
+ // Buffer every 20% of messageTokens
1260
+ bufferActivation: 0.8
1261
+ // Activate to retain 20% of threshold
1188
1262
  },
1189
1263
  reflection: {
1190
1264
  model: "google/gemini-2.5-flash",
@@ -1200,10 +1274,13 @@ var OBSERVATIONAL_MEMORY_DEFAULTS = {
1200
1274
  thinkingBudget: 1024
1201
1275
  }
1202
1276
  }
1203
- }
1277
+ },
1278
+ // Async reflection buffering (enabled by default)
1279
+ bufferActivation: 0.5
1280
+ // Start buffering at 50% of observationTokens
1204
1281
  }
1205
1282
  };
1206
- var ObservationalMemory = class {
1283
+ var ObservationalMemory = class _ObservationalMemory {
1207
1284
  id = "observational-memory";
1208
1285
  name = "Observational Memory";
1209
1286
  storage;
@@ -1240,6 +1317,210 @@ var ObservationalMemory = class {
1240
1317
  * accept eventual consistency (acceptable for v1).
1241
1318
  */
1242
1319
  locks = /* @__PURE__ */ new Map();
1320
+ /**
1321
+ * Track in-flight async buffering operations per resource/thread.
1322
+ * STATIC: Shared across all ObservationalMemory instances in this process.
1323
+ * This is critical because multiple OM instances are created per agent loop step,
1324
+ * and we need them to share knowledge of in-flight operations.
1325
+ * Key format: "obs:{lockKey}" or "refl:{lockKey}"
1326
+ * Value: Promise that resolves when buffering completes
1327
+ */
1328
+ static asyncBufferingOps = /* @__PURE__ */ new Map();
1329
+ /**
1330
+ * Track the last token boundary at which we started buffering.
1331
+ * STATIC: Shared across all instances so boundary tracking persists across OM recreations.
1332
+ * Key format: "obs:{lockKey}" or "refl:{lockKey}"
1333
+ */
1334
+ static lastBufferedBoundary = /* @__PURE__ */ new Map();
1335
+ /**
1336
+ * Track the timestamp cursor for buffered messages.
1337
+ * STATIC: Shared across all instances so each buffer only observes messages
1338
+ * newer than the previous buffer's boundary.
1339
+ * Key format: "obs:{lockKey}"
1340
+ */
1341
+ static lastBufferedAtTime = /* @__PURE__ */ new Map();
1342
+ /**
1343
+ * Tracks cycleId for in-flight buffered reflections.
1344
+ * STATIC: Shared across instances so we can match cycleId at activation time.
1345
+ * Key format: "refl:{lockKey}"
1346
+ */
1347
+ static reflectionBufferCycleIds = /* @__PURE__ */ new Map();
1348
+ /**
1349
+ * Track message IDs that have been sealed during async buffering.
1350
+ * STATIC: Shared across all instances so saveMessagesWithSealedIdTracking
1351
+ * generates new IDs when re-saving messages that were sealed in a previous step.
1352
+ * Key format: threadId
1353
+ * Value: Set of sealed message IDs
1354
+ */
1355
+ static sealedMessageIds = /* @__PURE__ */ new Map();
1356
+ /**
1357
+ * Check if async buffering is enabled for observations.
1358
+ */
1359
+ isAsyncObservationEnabled() {
1360
+ const enabled = this.observationConfig.bufferTokens !== void 0 && this.observationConfig.bufferTokens > 0;
1361
+ return enabled;
1362
+ }
1363
+ /**
1364
+ * Check if async buffering is enabled for reflections.
1365
+ * Reflection buffering is enabled when bufferActivation is set (triggers at threshold * bufferActivation).
1366
+ */
1367
+ isAsyncReflectionEnabled() {
1368
+ return this.reflectionConfig.bufferActivation !== void 0 && this.reflectionConfig.bufferActivation > 0;
1369
+ }
1370
+ /**
1371
+ * Get the buffer interval boundary key for observations.
1372
+ */
1373
+ getObservationBufferKey(lockKey) {
1374
+ return `obs:${lockKey}`;
1375
+ }
1376
+ /**
1377
+ * Get the buffer interval boundary key for reflections.
1378
+ */
1379
+ getReflectionBufferKey(lockKey) {
1380
+ return `refl:${lockKey}`;
1381
+ }
1382
+ /**
1383
+ * Clean up static maps for a thread/resource to prevent memory leaks.
1384
+ * Called after activation (to remove activated message IDs from sealedMessageIds)
1385
+ * and from clear() (to fully remove all static state for a thread).
1386
+ */
1387
+ cleanupStaticMaps(threadId, resourceId, activatedMessageIds) {
1388
+ const lockKey = this.getLockKey(threadId, resourceId);
1389
+ const obsBufKey = this.getObservationBufferKey(lockKey);
1390
+ const reflBufKey = this.getReflectionBufferKey(lockKey);
1391
+ if (activatedMessageIds) {
1392
+ const sealedSet = _ObservationalMemory.sealedMessageIds.get(threadId);
1393
+ if (sealedSet) {
1394
+ for (const id of activatedMessageIds) {
1395
+ sealedSet.delete(id);
1396
+ }
1397
+ if (sealedSet.size === 0) {
1398
+ _ObservationalMemory.sealedMessageIds.delete(threadId);
1399
+ }
1400
+ }
1401
+ } else {
1402
+ _ObservationalMemory.sealedMessageIds.delete(threadId);
1403
+ _ObservationalMemory.lastBufferedAtTime.delete(obsBufKey);
1404
+ _ObservationalMemory.lastBufferedBoundary.delete(obsBufKey);
1405
+ _ObservationalMemory.lastBufferedBoundary.delete(reflBufKey);
1406
+ _ObservationalMemory.asyncBufferingOps.delete(obsBufKey);
1407
+ _ObservationalMemory.asyncBufferingOps.delete(reflBufKey);
1408
+ _ObservationalMemory.reflectionBufferCycleIds.delete(obsBufKey);
1409
+ }
1410
+ }
1411
+ /**
1412
+ * Safely get bufferedObservationChunks as an array.
1413
+ * Handles cases where it might be a JSON string or undefined.
1414
+ */
1415
+ getBufferedChunks(record) {
1416
+ if (!record?.bufferedObservationChunks) return [];
1417
+ if (Array.isArray(record.bufferedObservationChunks)) return record.bufferedObservationChunks;
1418
+ if (typeof record.bufferedObservationChunks === "string") {
1419
+ try {
1420
+ const parsed = JSON.parse(record.bufferedObservationChunks);
1421
+ return Array.isArray(parsed) ? parsed : [];
1422
+ } catch {
1423
+ return [];
1424
+ }
1425
+ }
1426
+ return [];
1427
+ }
1428
+ /**
1429
+ * Calculate the projected message tokens that would be removed if activation happened now.
1430
+ * This replicates the chunk boundary logic in swapBufferedToActive without actually activating.
1431
+ */
1432
+ calculateProjectedMessageRemoval(chunks, activationRatio, messageTokensThreshold, currentPendingTokens) {
1433
+ if (chunks.length === 0) return 0;
1434
+ const retentionFloor = messageTokensThreshold * (1 - activationRatio);
1435
+ const targetMessageTokens = Math.max(0, currentPendingTokens - retentionFloor);
1436
+ let cumulativeMessageTokens = 0;
1437
+ let bestBoundary = 0;
1438
+ let bestBoundaryMessageTokens = 0;
1439
+ for (let i = 0; i < chunks.length; i++) {
1440
+ cumulativeMessageTokens += chunks[i].messageTokens ?? 0;
1441
+ const boundary = i + 1;
1442
+ const isUnder = cumulativeMessageTokens <= targetMessageTokens;
1443
+ const bestIsUnder = bestBoundaryMessageTokens <= targetMessageTokens;
1444
+ if (bestBoundary === 0) {
1445
+ bestBoundary = boundary;
1446
+ bestBoundaryMessageTokens = cumulativeMessageTokens;
1447
+ } else if (isUnder && !bestIsUnder) {
1448
+ bestBoundary = boundary;
1449
+ bestBoundaryMessageTokens = cumulativeMessageTokens;
1450
+ } else if (isUnder && bestIsUnder) {
1451
+ if (cumulativeMessageTokens > bestBoundaryMessageTokens) {
1452
+ bestBoundary = boundary;
1453
+ bestBoundaryMessageTokens = cumulativeMessageTokens;
1454
+ }
1455
+ } else if (!isUnder && !bestIsUnder) {
1456
+ if (cumulativeMessageTokens < bestBoundaryMessageTokens) {
1457
+ bestBoundary = boundary;
1458
+ bestBoundaryMessageTokens = cumulativeMessageTokens;
1459
+ }
1460
+ }
1461
+ }
1462
+ if (bestBoundary === 0) {
1463
+ return chunks[0]?.messageTokens ?? 0;
1464
+ }
1465
+ return bestBoundaryMessageTokens;
1466
+ }
1467
+ /**
1468
+ * Check if we've crossed a new bufferTokens interval boundary.
1469
+ * Returns true if async buffering should be triggered.
1470
+ */
1471
+ shouldTriggerAsyncObservation(currentTokens, lockKey, record) {
1472
+ if (!this.isAsyncObservationEnabled()) return false;
1473
+ if (record.isBufferingObservation) {
1474
+ if (isOpActiveInProcess(record.id, "bufferingObservation")) return false;
1475
+ omDebug(`[OM:shouldTriggerAsyncObs] isBufferingObservation=true but stale, clearing`);
1476
+ this.storage.setBufferingObservationFlag(record.id, false).catch(() => {
1477
+ });
1478
+ }
1479
+ const bufferKey = this.getObservationBufferKey(lockKey);
1480
+ if (this.isAsyncBufferingInProgress(bufferKey)) return false;
1481
+ const bufferTokens = this.observationConfig.bufferTokens;
1482
+ const dbBoundary = record.lastBufferedAtTokens ?? 0;
1483
+ const memBoundary = _ObservationalMemory.lastBufferedBoundary.get(bufferKey) ?? 0;
1484
+ const lastBoundary = Math.max(dbBoundary, memBoundary);
1485
+ const currentInterval = Math.floor(currentTokens / bufferTokens);
1486
+ const lastInterval = Math.floor(lastBoundary / bufferTokens);
1487
+ const shouldTrigger = currentInterval > lastInterval;
1488
+ omDebug(
1489
+ `[OM:shouldTriggerAsyncObs] tokens=${currentTokens}, bufferTokens=${bufferTokens}, currentInterval=${currentInterval}, lastInterval=${lastInterval}, lastBoundary=${lastBoundary} (db=${dbBoundary}, mem=${memBoundary}), shouldTrigger=${shouldTrigger}`
1490
+ );
1491
+ return shouldTrigger;
1492
+ }
1493
+ /**
1494
+ * Check if async reflection buffering should be triggered.
1495
+ * Triggers once when observation tokens reach `threshold * bufferActivation`.
1496
+ * Only allows one buffered reflection at a time.
1497
+ */
1498
+ shouldTriggerAsyncReflection(currentObservationTokens, lockKey, record) {
1499
+ if (!this.isAsyncReflectionEnabled()) return false;
1500
+ if (record.isBufferingReflection) {
1501
+ if (isOpActiveInProcess(record.id, "bufferingReflection")) return false;
1502
+ omDebug(`[OM:shouldTriggerAsyncRefl] isBufferingReflection=true but stale, clearing`);
1503
+ this.storage.setBufferingReflectionFlag(record.id, false).catch(() => {
1504
+ });
1505
+ }
1506
+ const bufferKey = this.getReflectionBufferKey(lockKey);
1507
+ if (this.isAsyncBufferingInProgress(bufferKey)) return false;
1508
+ if (_ObservationalMemory.lastBufferedBoundary.has(bufferKey)) return false;
1509
+ if (record.bufferedReflection) return false;
1510
+ const reflectThreshold = this.getMaxThreshold(this.reflectionConfig.observationTokens);
1511
+ const activationPoint = reflectThreshold * this.reflectionConfig.bufferActivation;
1512
+ const shouldTrigger = currentObservationTokens >= activationPoint;
1513
+ omDebug(
1514
+ `[OM:shouldTriggerAsyncRefl] obsTokens=${currentObservationTokens}, reflThreshold=${reflectThreshold}, activationPoint=${activationPoint}, bufferActivation=${this.reflectionConfig.bufferActivation}, shouldTrigger=${shouldTrigger}, isBufferingRefl=${record.isBufferingReflection}, hasBufferedReflection=${!!record.bufferedReflection}`
1515
+ );
1516
+ return shouldTrigger;
1517
+ }
1518
+ /**
1519
+ * Check if an async buffering operation is already in progress.
1520
+ */
1521
+ isAsyncBufferingInProgress(bufferKey) {
1522
+ return _ObservationalMemory.asyncBufferingOps.has(bufferKey);
1523
+ }
1243
1524
  /**
1244
1525
  * Acquire a lock for the given key, execute the callback, then release.
1245
1526
  * If a lock is already held, waits for it to be released before acquiring.
@@ -1286,12 +1567,46 @@ var ObservationalMemory = class {
1286
1567
  this.shouldObscureThreadIds = config.obscureThreadIds || false;
1287
1568
  this.storage = config.storage;
1288
1569
  this.scope = config.scope ?? "thread";
1289
- const observationModel = config.model ?? config.observation?.model ?? OBSERVATIONAL_MEMORY_DEFAULTS.observation.model;
1290
- const reflectionModel = config.model ?? config.reflection?.model ?? OBSERVATIONAL_MEMORY_DEFAULTS.reflection.model;
1570
+ const resolveModel = (m) => m === "default" ? OBSERVATIONAL_MEMORY_DEFAULTS.observation.model : m;
1571
+ const observationModel = resolveModel(config.model) ?? resolveModel(config.observation?.model) ?? resolveModel(config.reflection?.model);
1572
+ const reflectionModel = resolveModel(config.model) ?? resolveModel(config.reflection?.model) ?? resolveModel(config.observation?.model);
1573
+ if (!observationModel || !reflectionModel) {
1574
+ throw new Error(
1575
+ `Observational Memory requires a model to be set. Use \`observationalMemory: true\` for the default (google/gemini-2.5-flash), or set a model explicitly:
1576
+
1577
+ observationalMemory: {
1578
+ model: "$provider/$model",
1579
+ }
1580
+
1581
+ See https://mastra.ai/docs/memory/observational-memory#models for model recommendations and alternatives.`
1582
+ );
1583
+ }
1291
1584
  const messageTokens = config.observation?.messageTokens ?? OBSERVATIONAL_MEMORY_DEFAULTS.observation.messageTokens;
1292
1585
  const observationTokens = config.reflection?.observationTokens ?? OBSERVATIONAL_MEMORY_DEFAULTS.reflection.observationTokens;
1293
1586
  const isSharedBudget = config.shareTokenBudget ?? false;
1294
1587
  const totalBudget = messageTokens + observationTokens;
1588
+ const userExplicitlyConfiguredAsync = config.observation?.bufferTokens !== void 0 || config.observation?.bufferActivation !== void 0 || config.reflection?.bufferActivation !== void 0;
1589
+ const asyncBufferingDisabled = config.observation?.bufferTokens === false || config.scope === "resource" && !userExplicitlyConfiguredAsync;
1590
+ if (isSharedBudget && !asyncBufferingDisabled) {
1591
+ const common = `shareTokenBudget requires async buffering to be disabled (this is a temporary limitation). Add observation: { bufferTokens: false } to your config:
1592
+
1593
+ observationalMemory: {
1594
+ shareTokenBudget: true,
1595
+ observation: { bufferTokens: false },
1596
+ }
1597
+ `;
1598
+ if (userExplicitlyConfiguredAsync) {
1599
+ throw new Error(
1600
+ common + `
1601
+ Remove any other async buffering settings (bufferTokens, bufferActivation, blockAfter).`
1602
+ );
1603
+ } else {
1604
+ throw new Error(
1605
+ common + `
1606
+ Async buffering is enabled by default \u2014 this opt-out is only needed when using shareTokenBudget.`
1607
+ );
1608
+ }
1609
+ }
1295
1610
  this.observationConfig = {
1296
1611
  model: observationModel,
1297
1612
  // When shared budget, store as range: min = base threshold, max = total budget
@@ -1303,7 +1618,16 @@ var ObservationalMemory = class {
1303
1618
  maxOutputTokens: config.observation?.modelSettings?.maxOutputTokens ?? OBSERVATIONAL_MEMORY_DEFAULTS.observation.modelSettings.maxOutputTokens
1304
1619
  },
1305
1620
  providerOptions: config.observation?.providerOptions ?? OBSERVATIONAL_MEMORY_DEFAULTS.observation.providerOptions,
1306
- maxTokensPerBatch: config.observation?.maxTokensPerBatch ?? OBSERVATIONAL_MEMORY_DEFAULTS.observation.maxTokensPerBatch
1621
+ maxTokensPerBatch: config.observation?.maxTokensPerBatch ?? OBSERVATIONAL_MEMORY_DEFAULTS.observation.maxTokensPerBatch,
1622
+ bufferTokens: asyncBufferingDisabled ? void 0 : this.resolveBufferTokens(
1623
+ config.observation?.bufferTokens ?? OBSERVATIONAL_MEMORY_DEFAULTS.observation.bufferTokens,
1624
+ config.observation?.messageTokens ?? OBSERVATIONAL_MEMORY_DEFAULTS.observation.messageTokens
1625
+ ),
1626
+ bufferActivation: asyncBufferingDisabled ? void 0 : config.observation?.bufferActivation ?? OBSERVATIONAL_MEMORY_DEFAULTS.observation.bufferActivation,
1627
+ blockAfter: asyncBufferingDisabled ? void 0 : this.resolveBlockAfter(
1628
+ config.observation?.blockAfter ?? (config.observation?.bufferTokens ?? OBSERVATIONAL_MEMORY_DEFAULTS.observation.bufferTokens ? 1.2 : void 0),
1629
+ config.observation?.messageTokens ?? OBSERVATIONAL_MEMORY_DEFAULTS.observation.messageTokens
1630
+ )
1307
1631
  };
1308
1632
  this.reflectionConfig = {
1309
1633
  model: reflectionModel,
@@ -1313,11 +1637,20 @@ var ObservationalMemory = class {
1313
1637
  temperature: config.reflection?.modelSettings?.temperature ?? OBSERVATIONAL_MEMORY_DEFAULTS.reflection.modelSettings.temperature,
1314
1638
  maxOutputTokens: config.reflection?.modelSettings?.maxOutputTokens ?? OBSERVATIONAL_MEMORY_DEFAULTS.reflection.modelSettings.maxOutputTokens
1315
1639
  },
1316
- providerOptions: config.reflection?.providerOptions ?? OBSERVATIONAL_MEMORY_DEFAULTS.reflection.providerOptions
1640
+ providerOptions: config.reflection?.providerOptions ?? OBSERVATIONAL_MEMORY_DEFAULTS.reflection.providerOptions,
1641
+ bufferActivation: asyncBufferingDisabled ? void 0 : config?.reflection?.bufferActivation ?? OBSERVATIONAL_MEMORY_DEFAULTS.reflection.bufferActivation,
1642
+ blockAfter: asyncBufferingDisabled ? void 0 : this.resolveBlockAfter(
1643
+ config.reflection?.blockAfter ?? (config.reflection?.bufferActivation ?? OBSERVATIONAL_MEMORY_DEFAULTS.reflection.bufferActivation ? 1.2 : void 0),
1644
+ config.reflection?.observationTokens ?? OBSERVATIONAL_MEMORY_DEFAULTS.reflection.observationTokens
1645
+ )
1317
1646
  };
1318
1647
  this.tokenCounter = new TokenCounter();
1319
1648
  this.onDebugEvent = config.onDebugEvent;
1320
1649
  this.messageHistory = new MessageHistory({ storage: this.storage });
1650
+ this.validateBufferConfig();
1651
+ omDebug(
1652
+ `[OM:init] new ObservationalMemory instance created \u2014 scope=${this.scope}, messageTokens=${JSON.stringify(this.observationConfig.messageTokens)}, obsAsyncEnabled=${this.isAsyncObservationEnabled()}, bufferTokens=${this.observationConfig.bufferTokens}, bufferActivation=${this.observationConfig.bufferActivation}, blockAfter=${this.observationConfig.blockAfter}, reflectionTokens=${this.reflectionConfig.observationTokens}, refAsyncEnabled=${this.isAsyncReflectionEnabled()}, refAsyncActivation=${this.reflectionConfig.bufferActivation}, refBlockAfter=${this.reflectionConfig.blockAfter}`
1653
+ );
1321
1654
  }
1322
1655
  /**
1323
1656
  * Get the current configuration for this OM instance.
@@ -1341,7 +1674,7 @@ var ObservationalMemory = class {
1341
1674
  async getResolvedConfig(requestContext) {
1342
1675
  const getModelToResolve = (model) => {
1343
1676
  if (Array.isArray(model)) {
1344
- return model[0]?.model ?? OBSERVATIONAL_MEMORY_DEFAULTS.observation.model;
1677
+ return model[0]?.model ?? "unknown";
1345
1678
  }
1346
1679
  return model;
1347
1680
  };
@@ -1354,7 +1687,7 @@ var ObservationalMemory = class {
1354
1687
  const resolved = await resolveModelConfig(modelToResolve, requestContext);
1355
1688
  return formatModelName(resolved);
1356
1689
  } catch (error) {
1357
- console.error("[OM] Failed to resolve model config:", error);
1690
+ omError("[OM] Failed to resolve model config", error);
1358
1691
  return "(unknown)";
1359
1692
  }
1360
1693
  };
@@ -1382,24 +1715,96 @@ var ObservationalMemory = class {
1382
1715
  this.onDebugEvent(event);
1383
1716
  }
1384
1717
  }
1385
- // ASYNC BUFFERING DISABLED - See note at top of file
1386
- // /**
1387
- // * Validate that bufferEvery is less than the threshold
1388
- // */
1389
- // private validateBufferConfig(): void {
1390
- // const observationThreshold = this.getMaxThreshold(this.observationConfig.messageTokens);
1391
- // if (this.observationConfig.bufferEvery && this.observationConfig.bufferEvery >= observationThreshold) {
1392
- // throw new Error(
1393
- // `observation.bufferEvery (${this.observationConfig.bufferEvery}) must be less than messageTokens (${observationThreshold})`,
1394
- // );
1395
- // }
1396
- // const reflectionThreshold = this.getMaxThreshold(this.reflectionConfig.observationTokens);
1397
- // if (this.reflectionConfig.bufferEvery && this.reflectionConfig.bufferEvery >= reflectionThreshold) {
1398
- // throw new Error(
1399
- // `reflection.bufferEvery (${this.reflectionConfig.bufferEvery}) must be less than observationTokens (${reflectionThreshold})`,
1400
- // );
1401
- // }
1402
- // }
1718
+ /**
1719
+ * Validate buffer configuration on first use.
1720
+ * Ensures bufferTokens is less than the threshold and bufferActivation is valid.
1721
+ */
1722
+ validateBufferConfig() {
1723
+ const hasAsyncBuffering = this.observationConfig.bufferTokens !== void 0 || this.observationConfig.bufferActivation !== void 0 || this.reflectionConfig.bufferActivation !== void 0;
1724
+ if (hasAsyncBuffering && this.scope === "resource") {
1725
+ throw new Error(
1726
+ `Async buffering is not yet supported with scope: 'resource'. Use scope: 'thread', or set observation: { bufferTokens: false } to disable async buffering.`
1727
+ );
1728
+ }
1729
+ const observationThreshold = this.getMaxThreshold(this.observationConfig.messageTokens);
1730
+ if (this.observationConfig.bufferTokens !== void 0) {
1731
+ if (this.observationConfig.bufferTokens <= 0) {
1732
+ throw new Error(`observation.bufferTokens must be > 0, got ${this.observationConfig.bufferTokens}`);
1733
+ }
1734
+ if (this.observationConfig.bufferTokens >= observationThreshold) {
1735
+ throw new Error(
1736
+ `observation.bufferTokens (${this.observationConfig.bufferTokens}) must be less than messageTokens (${observationThreshold})`
1737
+ );
1738
+ }
1739
+ }
1740
+ if (this.observationConfig.bufferActivation !== void 0) {
1741
+ if (this.observationConfig.bufferActivation <= 0 || this.observationConfig.bufferActivation > 1) {
1742
+ throw new Error(
1743
+ `observation.bufferActivation must be in range (0, 1], got ${this.observationConfig.bufferActivation}`
1744
+ );
1745
+ }
1746
+ }
1747
+ if (this.observationConfig.blockAfter !== void 0) {
1748
+ if (this.observationConfig.blockAfter < observationThreshold) {
1749
+ throw new Error(
1750
+ `observation.blockAfter (${this.observationConfig.blockAfter}) must be >= messageTokens (${observationThreshold})`
1751
+ );
1752
+ }
1753
+ if (!this.observationConfig.bufferTokens) {
1754
+ throw new Error(
1755
+ `observation.blockAfter requires observation.bufferTokens to be set (blockAfter only applies when async buffering is enabled)`
1756
+ );
1757
+ }
1758
+ }
1759
+ if (this.reflectionConfig.bufferActivation !== void 0) {
1760
+ if (this.reflectionConfig.bufferActivation <= 0 || this.reflectionConfig.bufferActivation > 1) {
1761
+ throw new Error(
1762
+ `reflection.bufferActivation must be in range (0, 1], got ${this.reflectionConfig.bufferActivation}`
1763
+ );
1764
+ }
1765
+ }
1766
+ if (this.reflectionConfig.blockAfter !== void 0) {
1767
+ const reflectionThreshold = this.getMaxThreshold(this.reflectionConfig.observationTokens);
1768
+ if (this.reflectionConfig.blockAfter < reflectionThreshold) {
1769
+ throw new Error(
1770
+ `reflection.blockAfter (${this.reflectionConfig.blockAfter}) must be >= reflection.observationTokens (${reflectionThreshold})`
1771
+ );
1772
+ }
1773
+ if (!this.reflectionConfig.bufferActivation) {
1774
+ throw new Error(
1775
+ `reflection.blockAfter requires reflection.bufferActivation to be set (blockAfter only applies when async reflection is enabled)`
1776
+ );
1777
+ }
1778
+ }
1779
+ }
1780
+ /**
1781
+ * Resolve bufferTokens: if it's a fraction (0 < value < 1), multiply by messageTokens threshold.
1782
+ * Otherwise return the absolute token count.
1783
+ */
1784
+ resolveBufferTokens(bufferTokens, messageTokens) {
1785
+ if (bufferTokens === false) return void 0;
1786
+ if (bufferTokens === void 0) return void 0;
1787
+ if (bufferTokens > 0 && bufferTokens < 1) {
1788
+ const threshold = typeof messageTokens === "number" ? messageTokens : messageTokens.max;
1789
+ return Math.round(threshold * bufferTokens);
1790
+ }
1791
+ return bufferTokens;
1792
+ }
1793
+ /**
1794
+ * Resolve blockAfter config value.
1795
+ * Values between 1 and 2 (exclusive) are treated as multipliers of the threshold.
1796
+ * e.g. blockAfter: 1.5 with messageTokens: 20_000 → 30_000
1797
+ * Values >= 2 are treated as absolute token counts.
1798
+ * Defaults to 1.2 (120% of threshold) when async buffering is enabled but blockAfter is omitted.
1799
+ */
1800
+ resolveBlockAfter(blockAfter, messageTokens) {
1801
+ if (blockAfter === void 0) return void 0;
1802
+ if (blockAfter >= 1 && blockAfter < 2) {
1803
+ const threshold = typeof messageTokens === "number" ? messageTokens : messageTokens.max;
1804
+ return Math.round(threshold * blockAfter);
1805
+ }
1806
+ return blockAfter;
1807
+ }
1403
1808
  /**
1404
1809
  * Get the maximum value from a threshold (simple number or range)
1405
1810
  */
@@ -1589,6 +1994,112 @@ var ObservationalMemory = class {
1589
1994
  }
1590
1995
  };
1591
1996
  }
1997
+ /**
1998
+ * Create a start marker for when async buffering begins.
1999
+ */
2000
+ createBufferingStartMarker(params) {
2001
+ return {
2002
+ type: "data-om-buffering-start",
2003
+ data: {
2004
+ cycleId: params.cycleId,
2005
+ operationType: params.operationType,
2006
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
2007
+ tokensToBuffer: params.tokensToBuffer,
2008
+ recordId: params.recordId,
2009
+ threadId: params.threadId,
2010
+ threadIds: params.threadIds,
2011
+ config: this.getObservationMarkerConfig()
2012
+ }
2013
+ };
2014
+ }
2015
+ /**
2016
+ * Create an end marker for when async buffering completes successfully.
2017
+ */
2018
+ createBufferingEndMarker(params) {
2019
+ const completedAt = (/* @__PURE__ */ new Date()).toISOString();
2020
+ const durationMs = new Date(completedAt).getTime() - new Date(params.startedAt).getTime();
2021
+ return {
2022
+ type: "data-om-buffering-end",
2023
+ data: {
2024
+ cycleId: params.cycleId,
2025
+ operationType: params.operationType,
2026
+ completedAt,
2027
+ durationMs,
2028
+ tokensBuffered: params.tokensBuffered,
2029
+ bufferedTokens: params.bufferedTokens,
2030
+ recordId: params.recordId,
2031
+ threadId: params.threadId,
2032
+ observations: params.observations
2033
+ }
2034
+ };
2035
+ }
2036
+ /**
2037
+ * Create a failed marker for when async buffering fails.
2038
+ */
2039
+ createBufferingFailedMarker(params) {
2040
+ const failedAt = (/* @__PURE__ */ new Date()).toISOString();
2041
+ const durationMs = new Date(failedAt).getTime() - new Date(params.startedAt).getTime();
2042
+ return {
2043
+ type: "data-om-buffering-failed",
2044
+ data: {
2045
+ cycleId: params.cycleId,
2046
+ operationType: params.operationType,
2047
+ failedAt,
2048
+ durationMs,
2049
+ tokensAttempted: params.tokensAttempted,
2050
+ error: params.error,
2051
+ recordId: params.recordId,
2052
+ threadId: params.threadId
2053
+ }
2054
+ };
2055
+ }
2056
+ /**
2057
+ * Create an activation marker for when buffered observations are activated.
2058
+ */
2059
+ createActivationMarker(params) {
2060
+ return {
2061
+ type: "data-om-activation",
2062
+ data: {
2063
+ cycleId: params.cycleId,
2064
+ operationType: params.operationType,
2065
+ activatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2066
+ chunksActivated: params.chunksActivated,
2067
+ tokensActivated: params.tokensActivated,
2068
+ observationTokens: params.observationTokens,
2069
+ messagesActivated: params.messagesActivated,
2070
+ recordId: params.recordId,
2071
+ threadId: params.threadId,
2072
+ generationCount: params.generationCount,
2073
+ config: this.getObservationMarkerConfig(),
2074
+ observations: params.observations
2075
+ }
2076
+ };
2077
+ }
2078
+ /**
2079
+ * Persist a data-om-* marker part on the last assistant message in messageList
2080
+ * AND save the updated message to the DB so it survives page reload.
2081
+ * (data-* parts are filtered out before sending to the LLM, so they don't affect model calls.)
2082
+ */
2083
+ async persistMarkerToMessage(marker, messageList, threadId, resourceId) {
2084
+ if (!messageList) return;
2085
+ const allMsgs = messageList.get.all.db();
2086
+ for (let i = allMsgs.length - 1; i >= 0; i--) {
2087
+ const msg = allMsgs[i];
2088
+ if (msg?.role === "assistant" && msg.content?.parts && Array.isArray(msg.content.parts)) {
2089
+ msg.content.parts.push(marker);
2090
+ try {
2091
+ await this.messageHistory.persistMessages({
2092
+ messages: [msg],
2093
+ threadId,
2094
+ resourceId
2095
+ });
2096
+ } catch (e) {
2097
+ omDebug(`[OM:persistMarker] failed to save marker to DB: ${e}`);
2098
+ }
2099
+ return;
2100
+ }
2101
+ }
2102
+ }
1592
2103
  /**
1593
2104
  * Find the last completed observation boundary in a message's parts.
1594
2105
  * A completed observation is a start marker followed by an end marker.
@@ -1626,6 +2137,44 @@ var ObservationalMemory = class {
1626
2137
  }
1627
2138
  return lastStartIndex !== -1 && lastStartIndex > lastEndOrFailedIndex;
1628
2139
  }
2140
+ /**
2141
+ * Seal messages to prevent new parts from being merged into them.
2142
+ * This is used when starting buffering to capture the current content state.
2143
+ *
2144
+ * Sealing works by:
2145
+ * 1. Setting `message.content.metadata.mastra.sealed = true` (message-level flag)
2146
+ * 2. Adding `metadata.mastra.sealedAt` to the last part (boundary marker)
2147
+ *
2148
+ * When MessageList.add() receives a message with the same ID as a sealed message,
2149
+ * it creates a new message with only the parts beyond the seal boundary.
2150
+ *
2151
+ * The messages are mutated in place - since they're references to the same objects
2152
+ * in the MessageList, the seal will be recognized immediately.
2153
+ *
2154
+ * @param messages - Messages to seal (mutated in place)
2155
+ */
2156
+ sealMessagesForBuffering(messages) {
2157
+ const sealedAt = Date.now();
2158
+ for (const msg of messages) {
2159
+ if (!msg.content?.parts?.length) continue;
2160
+ if (!msg.content.metadata) {
2161
+ msg.content.metadata = {};
2162
+ }
2163
+ const metadata = msg.content.metadata;
2164
+ if (!metadata.mastra) {
2165
+ metadata.mastra = {};
2166
+ }
2167
+ metadata.mastra.sealed = true;
2168
+ const lastPart = msg.content.parts[msg.content.parts.length - 1];
2169
+ if (!lastPart.metadata) {
2170
+ lastPart.metadata = {};
2171
+ }
2172
+ if (!lastPart.metadata.mastra) {
2173
+ lastPart.metadata.mastra = {};
2174
+ }
2175
+ lastPart.metadata.mastra.sealedAt = sealedAt;
2176
+ }
2177
+ }
1629
2178
  /**
1630
2179
  * Insert an observation marker into a message.
1631
2180
  * The marker is appended directly to the message's parts array (mutating in place).
@@ -1692,10 +2241,22 @@ var ObservationalMemory = class {
1692
2241
  * This handles the case where a single message accumulates many parts
1693
2242
  * (like tool calls) during an agentic loop - we only observe the new parts.
1694
2243
  */
1695
- getUnobservedMessages(allMessages, record) {
2244
+ getUnobservedMessages(allMessages, record, opts) {
1696
2245
  const lastObservedAt = record.lastObservedAt;
1697
- const observedMessageIds = Array.isArray(record.observedMessageIds) ? new Set(record.observedMessageIds) : void 0;
1698
- if (!lastObservedAt) {
2246
+ const observedMessageIds = new Set(
2247
+ Array.isArray(record.observedMessageIds) ? record.observedMessageIds : []
2248
+ );
2249
+ if (opts?.excludeBuffered) {
2250
+ const bufferedChunks = this.getBufferedChunks(record);
2251
+ for (const chunk of bufferedChunks) {
2252
+ if (Array.isArray(chunk.messageIds)) {
2253
+ for (const id of chunk.messageIds) {
2254
+ observedMessageIds.add(id);
2255
+ }
2256
+ }
2257
+ }
2258
+ }
2259
+ if (!lastObservedAt && observedMessageIds.size === 0) {
1699
2260
  return allMessages;
1700
2261
  }
1701
2262
  const result = [];
@@ -1713,7 +2274,7 @@ var ObservationalMemory = class {
1713
2274
  result.push(virtualMsg);
1714
2275
  }
1715
2276
  } else {
1716
- if (!msg.createdAt) {
2277
+ if (!msg.createdAt || !lastObservedAt) {
1717
2278
  result.push(msg);
1718
2279
  } else {
1719
2280
  const msgDate = new Date(msg.createdAt);
@@ -1744,16 +2305,16 @@ var ObservationalMemory = class {
1744
2305
  /**
1745
2306
  * Call the Observer agent to extract observations.
1746
2307
  */
1747
- async callObserver(existingObservations, messagesToObserve, abortSignal) {
2308
+ async callObserver(existingObservations, messagesToObserve, abortSignal, options) {
1748
2309
  const agent = this.getObserverAgent();
1749
- const prompt = buildObserverPrompt(existingObservations, messagesToObserve);
2310
+ const prompt = buildObserverPrompt(existingObservations, messagesToObserve, options);
1750
2311
  const result = await this.withAbortCheck(
1751
2312
  () => agent.generate(prompt, {
1752
2313
  modelSettings: {
1753
2314
  ...this.observationConfig.modelSettings
1754
2315
  },
1755
2316
  providerOptions: this.observationConfig.providerOptions,
1756
- abortSignal
2317
+ ...abortSignal ? { abortSignal } : {}
1757
2318
  }),
1758
2319
  abortSignal
1759
2320
  );
@@ -1797,7 +2358,7 @@ var ObservationalMemory = class {
1797
2358
  ...this.observationConfig.modelSettings
1798
2359
  },
1799
2360
  providerOptions: this.observationConfig.providerOptions,
1800
- abortSignal
2361
+ ...abortSignal ? { abortSignal } : {}
1801
2362
  }),
1802
2363
  abortSignal
1803
2364
  );
@@ -1829,21 +2390,48 @@ var ObservationalMemory = class {
1829
2390
  * Call the Reflector agent to condense observations.
1830
2391
  * Includes compression validation and retry logic.
1831
2392
  */
1832
- async callReflector(observations, manualPrompt, streamContext, observationTokensThreshold, abortSignal) {
2393
+ async callReflector(observations, manualPrompt, streamContext, observationTokensThreshold, abortSignal, skipContinuationHints, compressionStartLevel) {
1833
2394
  const agent = this.getReflectorAgent();
1834
2395
  const originalTokens = this.tokenCounter.countObservations(observations);
1835
2396
  const targetThreshold = observationTokensThreshold ?? this.getMaxThreshold(this.reflectionConfig.observationTokens);
1836
2397
  let totalUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
1837
- let prompt = buildReflectorPrompt(observations, manualPrompt, false);
1838
- let result = await this.withAbortCheck(
1839
- () => agent.generate(prompt, {
1840
- modelSettings: {
1841
- ...this.reflectionConfig.modelSettings
1842
- },
1843
- providerOptions: this.reflectionConfig.providerOptions,
1844
- abortSignal
1845
- }),
1846
- abortSignal
2398
+ const firstLevel = compressionStartLevel ?? 0;
2399
+ const retryLevel = Math.min(firstLevel + 1, 2);
2400
+ let prompt = buildReflectorPrompt(observations, manualPrompt, firstLevel, skipContinuationHints);
2401
+ omDebug(
2402
+ `[OM:callReflector] starting first attempt: originalTokens=${originalTokens}, targetThreshold=${targetThreshold}, promptLen=${prompt.length}, skipContinuationHints=${skipContinuationHints}`
2403
+ );
2404
+ let chunkCount = 0;
2405
+ const generatePromise = agent.generate(prompt, {
2406
+ modelSettings: {
2407
+ ...this.reflectionConfig.modelSettings
2408
+ },
2409
+ providerOptions: this.reflectionConfig.providerOptions,
2410
+ ...abortSignal ? { abortSignal } : {},
2411
+ onChunk(chunk) {
2412
+ chunkCount++;
2413
+ if (chunkCount === 1 || chunkCount % 50 === 0) {
2414
+ const preview = chunk.type === "text-delta" ? ` text="${chunk.textDelta?.slice(0, 80)}..."` : chunk.type === "tool-call" ? ` tool=${chunk.toolName}` : "";
2415
+ omDebug(`[OM:callReflector] chunk#${chunkCount}: type=${chunk.type}${preview}`);
2416
+ }
2417
+ },
2418
+ onFinish(event) {
2419
+ omDebug(
2420
+ `[OM:callReflector] onFinish: chunks=${chunkCount}, finishReason=${event.finishReason}, inputTokens=${event.usage?.inputTokens}, outputTokens=${event.usage?.outputTokens}, textLen=${event.text?.length}`
2421
+ );
2422
+ },
2423
+ onAbort(event) {
2424
+ omDebug(`[OM:callReflector] onAbort: chunks=${chunkCount}, reason=${event?.reason ?? "unknown"}`);
2425
+ },
2426
+ onError({ error }) {
2427
+ omError(`[OM:callReflector] onError after ${chunkCount} chunks`, error);
2428
+ }
2429
+ });
2430
+ let result = await this.withAbortCheck(async () => {
2431
+ return await generatePromise;
2432
+ }, abortSignal);
2433
+ omDebug(
2434
+ `[OM:callReflector] first attempt returned: textLen=${result.text?.length}, textPreview="${result.text?.slice(0, 120)}...", inputTokens=${result.usage?.inputTokens ?? result.totalUsage?.inputTokens}, outputTokens=${result.usage?.outputTokens ?? result.totalUsage?.outputTokens}, keys=${Object.keys(result).join(",")}`
1847
2435
  );
1848
2436
  const firstUsage = result.totalUsage ?? result.usage;
1849
2437
  if (firstUsage) {
@@ -1853,6 +2441,9 @@ var ObservationalMemory = class {
1853
2441
  }
1854
2442
  let parsed = parseReflectorOutput(result.text);
1855
2443
  let reflectedTokens = this.tokenCounter.countObservations(parsed.observations);
2444
+ omDebug(
2445
+ `[OM:callReflector] first attempt parsed: reflectedTokens=${reflectedTokens}, targetThreshold=${targetThreshold}, compressionValid=${validateCompression(reflectedTokens, targetThreshold)}, parsedObsLen=${parsed.observations?.length}`
2446
+ );
1856
2447
  if (!validateCompression(reflectedTokens, targetThreshold)) {
1857
2448
  if (streamContext?.writer) {
1858
2449
  const failedMarker = this.createObservationFailedMarker({
@@ -1880,17 +2471,21 @@ var ObservationalMemory = class {
1880
2471
  await streamContext.writer.custom(startMarker).catch(() => {
1881
2472
  });
1882
2473
  }
1883
- prompt = buildReflectorPrompt(observations, manualPrompt, true);
2474
+ prompt = buildReflectorPrompt(observations, manualPrompt, retryLevel, skipContinuationHints);
2475
+ omDebug(`[OM:callReflector] starting retry: promptLen=${prompt.length}`);
1884
2476
  result = await this.withAbortCheck(
1885
2477
  () => agent.generate(prompt, {
1886
2478
  modelSettings: {
1887
2479
  ...this.reflectionConfig.modelSettings
1888
2480
  },
1889
2481
  providerOptions: this.reflectionConfig.providerOptions,
1890
- abortSignal
2482
+ ...abortSignal ? { abortSignal } : {}
1891
2483
  }),
1892
2484
  abortSignal
1893
2485
  );
2486
+ omDebug(
2487
+ `[OM:callReflector] retry returned: textLen=${result.text?.length}, inputTokens=${result.usage?.inputTokens ?? result.totalUsage?.inputTokens}, outputTokens=${result.usage?.outputTokens ?? result.totalUsage?.outputTokens}`
2488
+ );
1894
2489
  const retryUsage = result.totalUsage ?? result.usage;
1895
2490
  if (retryUsage) {
1896
2491
  totalUsage.inputTokens += retryUsage.inputTokens ?? 0;
@@ -1899,6 +2494,9 @@ var ObservationalMemory = class {
1899
2494
  }
1900
2495
  parsed = parseReflectorOutput(result.text);
1901
2496
  reflectedTokens = this.tokenCounter.countObservations(parsed.observations);
2497
+ omDebug(
2498
+ `[OM:callReflector] retry parsed: reflectedTokens=${reflectedTokens}, compressionValid=${validateCompression(reflectedTokens, targetThreshold)}`
2499
+ );
1902
2500
  }
1903
2501
  return {
1904
2502
  observations: parsed.observations,
@@ -1981,34 +2579,34 @@ ${suggestedResponse}
1981
2579
  }
1982
2580
  return null;
1983
2581
  }
2582
+ // ══════════════════════════════════════════════════════════════════════════
2583
+ // PROCESS INPUT STEP HELPERS
2584
+ // These helpers extract logical units from processInputStep for clarity.
2585
+ // ══════════════════════════════════════════════════════════════════════════
1984
2586
  /**
1985
- * Process input at each step - check threshold, observe if needed, save, inject observations.
1986
- * This is the ONLY processor method - all OM logic happens here.
1987
- *
1988
- * Flow:
1989
- * 1. Load historical messages (step 0 only)
1990
- * 2. Check if observation threshold is reached
1991
- * 3. If threshold reached: observe, save messages with markers
1992
- * 4. Inject observations into context
1993
- * 5. Filter out already-observed messages
2587
+ * Load historical unobserved messages into the message list (step 0 only).
2588
+ * In resource scope, loads only current thread's messages.
2589
+ * In thread scope, loads all unobserved messages for the thread.
1994
2590
  */
1995
- async processInputStep(args) {
1996
- const { messageList, requestContext, stepNumber, state: _state, writer, abortSignal, abort } = args;
1997
- const state = _state ?? {};
1998
- const context = this.getThreadContext(requestContext, messageList);
1999
- if (!context) {
2000
- return messageList;
2591
+ async loadHistoricalMessagesIfNeeded(messageList, state, threadId, resourceId, lastObservedAt) {
2592
+ if (state.initialSetupDone) {
2593
+ return;
2001
2594
  }
2002
- const { threadId, resourceId } = context;
2003
- const memoryContext = parseMemoryRequestContext(requestContext);
2004
- const readOnly = memoryContext?.memoryConfig?.readOnly;
2005
- let record = await this.getOrCreateRecord(threadId, resourceId);
2006
- if (!state.initialSetupDone) {
2007
- state.initialSetupDone = true;
2008
- const lastObservedAt = record.lastObservedAt;
2009
- if (this.scope === "resource" && resourceId) {
2010
- const currentThreadMessages = await this.loadUnobservedMessages(threadId, void 0, lastObservedAt);
2011
- for (const msg of currentThreadMessages) {
2595
+ state.initialSetupDone = true;
2596
+ if (this.scope === "resource" && resourceId) {
2597
+ const currentThreadMessages = await this.loadUnobservedMessages(threadId, void 0, lastObservedAt);
2598
+ for (const msg of currentThreadMessages) {
2599
+ if (msg.role !== "system") {
2600
+ if (!this.hasUnobservedParts(msg) && this.findLastCompletedObservationBoundary(msg) !== -1) {
2601
+ continue;
2602
+ }
2603
+ messageList.add(msg, "memory");
2604
+ }
2605
+ }
2606
+ } else {
2607
+ const historicalMessages = await this.loadUnobservedMessages(threadId, resourceId, lastObservedAt);
2608
+ if (historicalMessages.length > 0) {
2609
+ for (const msg of historicalMessages) {
2012
2610
  if (msg.role !== "system") {
2013
2611
  if (!this.hasUnobservedParts(msg) && this.findLastCompletedObservationBoundary(msg) !== -1) {
2014
2612
  continue;
@@ -2016,261 +2614,637 @@ ${suggestedResponse}
2016
2614
  messageList.add(msg, "memory");
2017
2615
  }
2018
2616
  }
2019
- } else {
2020
- const historicalMessages = await this.loadUnobservedMessages(threadId, resourceId, lastObservedAt);
2021
- if (historicalMessages.length > 0) {
2022
- for (const msg of historicalMessages) {
2023
- if (msg.role !== "system") {
2024
- if (!this.hasUnobservedParts(msg) && this.findLastCompletedObservationBoundary(msg) !== -1) {
2025
- continue;
2026
- }
2027
- messageList.add(msg, "memory");
2028
- }
2029
- }
2030
- }
2031
2617
  }
2032
2618
  }
2033
- let unobservedContextBlocks;
2034
- if (this.scope === "resource" && resourceId) {
2035
- unobservedContextBlocks = await this.loadOtherThreadsContext(resourceId, threadId);
2036
- }
2037
- if (!readOnly) {
2038
- const allMessages = messageList.get.all.db();
2039
- const unobservedMessages = this.getUnobservedMessages(allMessages, record);
2040
- const currentSessionTokens = this.tokenCounter.countMessages(unobservedMessages);
2041
- const otherThreadTokens = unobservedContextBlocks ? this.tokenCounter.countString(unobservedContextBlocks) : 0;
2042
- const currentObservationTokens = record.observationTokenCount ?? 0;
2043
- const pendingTokens = record.pendingMessageTokens ?? 0;
2044
- const totalPendingTokens = pendingTokens + currentSessionTokens + otherThreadTokens;
2045
- const threshold = this.calculateDynamicThreshold(this.observationConfig.messageTokens, currentObservationTokens);
2046
- const baseReflectionThreshold = this.getMaxThreshold(this.reflectionConfig.observationTokens);
2047
- const isSharedBudget = typeof this.observationConfig.messageTokens !== "number";
2048
- const totalBudget = isSharedBudget ? this.observationConfig.messageTokens.max : 0;
2049
- const effectiveObservationTokensThreshold = isSharedBudget ? Math.max(totalBudget - threshold, 1e3) : baseReflectionThreshold;
2050
- const observationTokensPercent = Math.round(
2051
- currentObservationTokens / effectiveObservationTokensThreshold * 100
2619
+ }
2620
+ /**
2621
+ * Calculate all threshold-related values for observation decision making.
2622
+ */
2623
+ calculateObservationThresholds(allMessages, _unobservedMessages, _pendingTokens, otherThreadTokens, currentObservationTokens, _record) {
2624
+ const contextWindowTokens = this.tokenCounter.countMessages(allMessages);
2625
+ const totalPendingTokens = Math.max(0, contextWindowTokens + otherThreadTokens);
2626
+ const threshold = this.calculateDynamicThreshold(this.observationConfig.messageTokens, currentObservationTokens);
2627
+ const baseReflectionThreshold = this.getMaxThreshold(this.reflectionConfig.observationTokens);
2628
+ const isSharedBudget = typeof this.observationConfig.messageTokens !== "number";
2629
+ const totalBudget = isSharedBudget ? this.observationConfig.messageTokens.max : 0;
2630
+ const effectiveObservationTokensThreshold = isSharedBudget ? Math.max(totalBudget - threshold, 1e3) : baseReflectionThreshold;
2631
+ return {
2632
+ totalPendingTokens,
2633
+ threshold,
2634
+ effectiveObservationTokensThreshold,
2635
+ isSharedBudget
2636
+ };
2637
+ }
2638
+ /**
2639
+ * Emit debug event and stream progress part for UI feedback.
2640
+ */
2641
+ async emitStepProgress(writer, threadId, resourceId, stepNumber, record, thresholds, currentObservationTokens) {
2642
+ const { totalPendingTokens, threshold, effectiveObservationTokensThreshold } = thresholds;
2643
+ this.emitDebugEvent({
2644
+ type: "step_progress",
2645
+ timestamp: /* @__PURE__ */ new Date(),
2646
+ threadId,
2647
+ resourceId: resourceId ?? "",
2648
+ stepNumber,
2649
+ finishReason: "unknown",
2650
+ pendingTokens: totalPendingTokens,
2651
+ threshold,
2652
+ thresholdPercent: Math.round(totalPendingTokens / threshold * 100),
2653
+ willSave: totalPendingTokens >= threshold,
2654
+ willObserve: totalPendingTokens >= threshold
2655
+ });
2656
+ if (writer) {
2657
+ const bufferedChunks = this.getBufferedChunks(record);
2658
+ const bufferedObservationTokens = bufferedChunks.reduce((sum, chunk) => sum + (chunk.tokenCount ?? 0), 0);
2659
+ const rawBufferedMessageTokens = bufferedChunks.reduce((sum, chunk) => sum + (chunk.messageTokens ?? 0), 0);
2660
+ const bufferedMessageTokens = Math.min(rawBufferedMessageTokens, totalPendingTokens);
2661
+ const projectedMessageRemoval = this.calculateProjectedMessageRemoval(
2662
+ bufferedChunks,
2663
+ this.observationConfig.bufferActivation ?? 1,
2664
+ this.getMaxThreshold(this.observationConfig.messageTokens),
2665
+ totalPendingTokens
2052
2666
  );
2053
- this.emitDebugEvent({
2054
- type: "step_progress",
2055
- timestamp: /* @__PURE__ */ new Date(),
2056
- threadId,
2057
- resourceId: resourceId ?? "",
2058
- stepNumber,
2059
- finishReason: "unknown",
2060
- pendingTokens: totalPendingTokens,
2061
- threshold,
2062
- thresholdPercent: Math.round(totalPendingTokens / threshold * 100),
2063
- willSave: totalPendingTokens >= threshold,
2064
- willObserve: totalPendingTokens >= threshold
2065
- });
2066
- if (writer) {
2067
- const progressPart = {
2068
- type: "data-om-progress",
2069
- data: {
2070
- pendingTokens: totalPendingTokens,
2071
- messageTokens: threshold,
2072
- messageTokensPercent: Math.round(totalPendingTokens / threshold * 100),
2073
- observationTokens: currentObservationTokens,
2074
- observationTokensThreshold: effectiveObservationTokensThreshold,
2075
- observationTokensPercent,
2076
- willObserve: totalPendingTokens >= threshold,
2077
- recordId: record.id,
2078
- threadId,
2079
- stepNumber
2080
- }
2081
- };
2082
- await writer.custom(progressPart).catch(() => {
2083
- });
2667
+ let obsBufferStatus = "idle";
2668
+ if (record.isBufferingObservation) {
2669
+ obsBufferStatus = "running";
2670
+ } else if (bufferedChunks.length > 0) {
2671
+ obsBufferStatus = "complete";
2084
2672
  }
2085
- const sealedIds = state.sealedIds ?? /* @__PURE__ */ new Set();
2086
- if (stepNumber > 0 && totalPendingTokens >= threshold) {
2087
- const lockKey = this.getLockKey(threadId, resourceId);
2088
- let observationSucceeded = false;
2089
- await this.withLock(lockKey, async () => {
2090
- const freshRecord = await this.getOrCreateRecord(threadId, resourceId);
2091
- const freshAllMessages = messageList.get.all.db();
2092
- const freshUnobservedMessages = this.getUnobservedMessages(freshAllMessages, freshRecord);
2093
- const freshCurrentTokens = this.tokenCounter.countMessages(freshUnobservedMessages);
2094
- const freshPending = freshRecord.pendingMessageTokens ?? 0;
2095
- let freshOtherThreadTokens = 0;
2096
- if (this.scope === "resource" && resourceId) {
2097
- const freshOtherContext = await this.loadOtherThreadsContext(resourceId, threadId);
2098
- freshOtherThreadTokens = freshOtherContext ? this.tokenCounter.countString(freshOtherContext) : 0;
2099
- }
2100
- const freshTotal = freshPending + freshCurrentTokens + freshOtherThreadTokens;
2101
- if (freshTotal < threshold) {
2102
- return;
2103
- }
2104
- const preObservationTime = freshRecord.lastObservedAt?.getTime() ?? 0;
2105
- if (freshUnobservedMessages.length > 0) {
2106
- try {
2107
- if (this.scope === "resource" && resourceId) {
2108
- await this.doResourceScopedObservation(
2109
- freshRecord,
2110
- threadId,
2111
- resourceId,
2112
- freshUnobservedMessages,
2113
- writer,
2114
- abortSignal
2115
- );
2116
- } else {
2117
- await this.doSynchronousObservation(
2118
- freshRecord,
2119
- threadId,
2120
- freshUnobservedMessages,
2121
- writer,
2122
- abortSignal
2123
- );
2673
+ let refBufferStatus = "idle";
2674
+ if (record.isBufferingReflection) {
2675
+ refBufferStatus = "running";
2676
+ } else if (record.bufferedReflection && record.bufferedReflection.length > 0) {
2677
+ refBufferStatus = "complete";
2678
+ }
2679
+ const statusPart = {
2680
+ type: "data-om-status",
2681
+ data: {
2682
+ windows: {
2683
+ active: {
2684
+ messages: {
2685
+ tokens: totalPendingTokens,
2686
+ threshold
2687
+ },
2688
+ observations: {
2689
+ tokens: currentObservationTokens,
2690
+ threshold: effectiveObservationTokensThreshold
2124
2691
  }
2125
- const updatedRecord = await this.getOrCreateRecord(threadId, resourceId);
2126
- const updatedTime = updatedRecord.lastObservedAt?.getTime() ?? 0;
2127
- observationSucceeded = updatedTime > preObservationTime;
2128
- } catch (error) {
2129
- if (abortSignal?.aborted) {
2130
- abort("Agent execution was aborted");
2131
- } else {
2132
- abort(
2133
- `Encountered error during memory observation ${error instanceof Error ? error.message : JSON.stringify(error, null, 2)}`
2134
- );
2692
+ },
2693
+ buffered: {
2694
+ observations: {
2695
+ chunks: bufferedChunks.length,
2696
+ messageTokens: bufferedMessageTokens,
2697
+ projectedMessageRemoval,
2698
+ observationTokens: bufferedObservationTokens,
2699
+ status: obsBufferStatus
2700
+ },
2701
+ reflection: {
2702
+ inputObservationTokens: record.bufferedReflectionInputTokens ?? 0,
2703
+ observationTokens: record.bufferedReflectionTokens ?? 0,
2704
+ status: refBufferStatus
2135
2705
  }
2136
- observationSucceeded = false;
2137
2706
  }
2707
+ },
2708
+ recordId: record.id,
2709
+ threadId,
2710
+ stepNumber,
2711
+ generationCount: record.generationCount
2712
+ }
2713
+ };
2714
+ omDebug(
2715
+ `[OM:status] step=${stepNumber} msgs=${totalPendingTokens}/${threshold} obs=${currentObservationTokens}/${effectiveObservationTokensThreshold} bufObs={chunks=${bufferedChunks.length},msgTok=${bufferedMessageTokens},obsTok=${bufferedObservationTokens},status=${obsBufferStatus}} bufRef={inTok=${record.bufferedReflectionInputTokens ?? 0},outTok=${record.bufferedReflectionTokens ?? 0},status=${refBufferStatus}} gen=${record.generationCount}`
2716
+ );
2717
+ await writer.custom(statusPart).catch(() => {
2718
+ });
2719
+ }
2720
+ }
2721
+ /**
2722
+ * Handle observation when threshold is reached.
2723
+ * Tries async activation first if enabled, then falls back to sync observation.
2724
+ * Returns whether observation succeeded.
2725
+ */
2726
+ async handleThresholdReached(messageList, record, threadId, resourceId, threshold, lockKey, writer, abortSignal, abort) {
2727
+ let observationSucceeded = false;
2728
+ let updatedRecord = record;
2729
+ let activatedMessageIds;
2730
+ await this.withLock(lockKey, async () => {
2731
+ let freshRecord = await this.getOrCreateRecord(threadId, resourceId);
2732
+ const freshAllMessages = messageList.get.all.db();
2733
+ let freshUnobservedMessages = this.getUnobservedMessages(freshAllMessages, freshRecord);
2734
+ const freshContextTokens = this.tokenCounter.countMessages(freshAllMessages);
2735
+ let freshOtherThreadTokens = 0;
2736
+ if (this.scope === "resource" && resourceId) {
2737
+ const freshOtherContext = await this.loadOtherThreadsContext(resourceId, threadId);
2738
+ freshOtherThreadTokens = freshOtherContext ? this.tokenCounter.countString(freshOtherContext) : 0;
2739
+ }
2740
+ const freshTotal = freshContextTokens + freshOtherThreadTokens;
2741
+ omDebug(
2742
+ `[OM:threshold] handleThresholdReached (inside lock): freshTotal=${freshTotal}, threshold=${threshold}, freshUnobserved=${freshUnobservedMessages.length}, freshOtherThreadTokens=${freshOtherThreadTokens}, freshCurrentTokens=${freshContextTokens}`
2743
+ );
2744
+ if (freshTotal < threshold) {
2745
+ omDebug(`[OM:threshold] freshTotal < threshold, bailing out`);
2746
+ return;
2747
+ }
2748
+ const preObservationTime = freshRecord.lastObservedAt?.getTime() ?? 0;
2749
+ let activationResult = { success: false };
2750
+ if (this.isAsyncObservationEnabled()) {
2751
+ const bufferKey = this.getObservationBufferKey(lockKey);
2752
+ const asyncOp = _ObservationalMemory.asyncBufferingOps.get(bufferKey);
2753
+ if (asyncOp) {
2754
+ try {
2755
+ await Promise.race([
2756
+ asyncOp,
2757
+ new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), 3e4))
2758
+ ]);
2759
+ } catch {
2138
2760
  }
2139
- });
2140
- if (observationSucceeded) {
2141
- const allMsgs = messageList.get.all.db();
2142
- let markerIdx = -1;
2143
- let markerMsg = null;
2144
- for (let i = allMsgs.length - 1; i >= 0; i--) {
2145
- const msg = allMsgs[i];
2146
- if (!msg) continue;
2147
- if (this.findLastCompletedObservationBoundary(msg) !== -1) {
2148
- markerIdx = i;
2149
- markerMsg = msg;
2150
- break;
2151
- }
2761
+ }
2762
+ const recordAfterWait = await this.getOrCreateRecord(threadId, resourceId);
2763
+ const chunksAfterWait = this.getBufferedChunks(recordAfterWait);
2764
+ omDebug(
2765
+ `[OM:threshold] tryActivation: chunksAvailable=${chunksAfterWait.length}, isBufferingObs=${recordAfterWait.isBufferingObservation}`
2766
+ );
2767
+ activationResult = await this.tryActivateBufferedObservations(
2768
+ recordAfterWait,
2769
+ lockKey,
2770
+ freshTotal,
2771
+ writer,
2772
+ messageList
2773
+ );
2774
+ omDebug(`[OM:threshold] activationResult: success=${activationResult.success}`);
2775
+ if (activationResult.success) {
2776
+ observationSucceeded = true;
2777
+ updatedRecord = activationResult.updatedRecord ?? recordAfterWait;
2778
+ activatedMessageIds = activationResult.activatedMessageIds;
2779
+ omDebug(
2780
+ `[OM:threshold] activation succeeded, obsTokens=${updatedRecord.observationTokenCount}, activeObsLen=${updatedRecord.activeObservations?.length}`
2781
+ );
2782
+ await this.maybeAsyncReflect(updatedRecord, updatedRecord.observationTokenCount ?? 0, writer, messageList);
2783
+ return;
2784
+ }
2785
+ if (this.observationConfig.blockAfter && freshTotal >= this.observationConfig.blockAfter) {
2786
+ omDebug(
2787
+ `[OM:threshold] blockAfter exceeded (${freshTotal} >= ${this.observationConfig.blockAfter}), falling through to sync observation`
2788
+ );
2789
+ freshRecord = await this.getOrCreateRecord(threadId, resourceId);
2790
+ const refreshedAll = messageList.get.all.db();
2791
+ freshUnobservedMessages = this.getUnobservedMessages(refreshedAll, freshRecord);
2792
+ } else {
2793
+ omDebug(`[OM:threshold] activation failed, no blockAfter or below it \u2014 letting async buffering catch up`);
2794
+ return;
2795
+ }
2796
+ }
2797
+ if (freshUnobservedMessages.length > 0) {
2798
+ try {
2799
+ if (this.scope === "resource" && resourceId) {
2800
+ await this.doResourceScopedObservation(
2801
+ freshRecord,
2802
+ threadId,
2803
+ resourceId,
2804
+ freshUnobservedMessages,
2805
+ writer,
2806
+ abortSignal
2807
+ );
2808
+ } else {
2809
+ await this.doSynchronousObservation(freshRecord, threadId, freshUnobservedMessages, writer, abortSignal);
2152
2810
  }
2153
- if (markerMsg && markerIdx !== -1) {
2154
- const idsToRemove = [];
2155
- const messagesToSave = [];
2156
- for (let i = 0; i < markerIdx; i++) {
2157
- const msg = allMsgs[i];
2158
- if (msg?.id && msg.id !== "om-continuation") {
2159
- idsToRemove.push(msg.id);
2160
- messagesToSave.push(msg);
2161
- }
2162
- }
2163
- messagesToSave.push(markerMsg);
2164
- const unobservedParts = this.getUnobservedParts(markerMsg);
2165
- if (unobservedParts.length === 0) {
2166
- if (markerMsg.id) {
2167
- idsToRemove.push(markerMsg.id);
2168
- }
2169
- } else if (unobservedParts.length < (markerMsg.content?.parts?.length ?? 0)) {
2170
- markerMsg.content.parts = unobservedParts;
2171
- }
2172
- if (messagesToSave.length > 0) {
2173
- await this.saveMessagesWithSealedIdTracking(messagesToSave, sealedIds, threadId, resourceId, state);
2174
- }
2175
- if (idsToRemove.length > 0) {
2176
- messageList.removeByIds(idsToRemove);
2177
- }
2811
+ updatedRecord = await this.getOrCreateRecord(threadId, resourceId);
2812
+ const updatedTime = updatedRecord.lastObservedAt?.getTime() ?? 0;
2813
+ observationSucceeded = updatedTime > preObservationTime;
2814
+ } catch (error) {
2815
+ if (abortSignal?.aborted) {
2816
+ abort("Agent execution was aborted");
2178
2817
  } else {
2179
- const newInput = messageList.clear.input.db();
2180
- const newOutput = messageList.clear.response.db();
2181
- const messagesToSave = [...newInput, ...newOutput];
2182
- if (messagesToSave.length > 0) {
2183
- await this.saveMessagesWithSealedIdTracking(messagesToSave, sealedIds, threadId, resourceId, state);
2184
- }
2818
+ abort(
2819
+ `Encountered error during memory observation ${error instanceof Error ? error.message : JSON.stringify(error, null, 2)}`
2820
+ );
2185
2821
  }
2186
- messageList.clear.input.db();
2187
- messageList.clear.response.db();
2188
2822
  }
2189
- record = await this.getOrCreateRecord(threadId, resourceId);
2190
- } else if (stepNumber > 0) {
2191
- const newInput = messageList.clear.input.db();
2192
- const newOutput = messageList.clear.response.db();
2193
- const messagesToSave = [...newInput, ...newOutput];
2194
- if (messagesToSave.length > 0) {
2195
- await this.saveMessagesWithSealedIdTracking(messagesToSave, sealedIds, threadId, resourceId, state);
2196
- for (const msg of messagesToSave) {
2197
- messageList.add(msg, "memory");
2198
- }
2823
+ }
2824
+ });
2825
+ return { observationSucceeded, updatedRecord, activatedMessageIds };
2826
+ }
2827
+ /**
2828
+ * Remove observed messages from message list after successful observation.
2829
+ * Accepts optional observedMessageIds for activation-based cleanup (when no markers are present).
2830
+ */
2831
+ async cleanupAfterObservation(messageList, sealedIds, threadId, resourceId, state, observedMessageIds) {
2832
+ const allMsgs = messageList.get.all.db();
2833
+ let markerIdx = -1;
2834
+ let markerMsg = null;
2835
+ for (let i = allMsgs.length - 1; i >= 0; i--) {
2836
+ const msg = allMsgs[i];
2837
+ if (!msg) continue;
2838
+ if (this.findLastCompletedObservationBoundary(msg) !== -1) {
2839
+ markerIdx = i;
2840
+ markerMsg = msg;
2841
+ break;
2842
+ }
2843
+ }
2844
+ omDebug(
2845
+ `[OM:cleanupBranch] allMsgs=${allMsgs.length}, markerFound=${markerIdx !== -1}, markerIdx=${markerIdx}, observedMessageIds=${observedMessageIds?.length ?? "undefined"}, allIds=${allMsgs.map((m) => m.id?.slice(0, 8)).join(",")}`
2846
+ );
2847
+ if (markerMsg && markerIdx !== -1) {
2848
+ const idsToRemove = [];
2849
+ const messagesToSave = [];
2850
+ for (let i = 0; i < markerIdx; i++) {
2851
+ const msg = allMsgs[i];
2852
+ if (msg?.id && msg.id !== "om-continuation") {
2853
+ idsToRemove.push(msg.id);
2854
+ messagesToSave.push(msg);
2855
+ }
2856
+ }
2857
+ messagesToSave.push(markerMsg);
2858
+ const unobservedParts = this.getUnobservedParts(markerMsg);
2859
+ if (unobservedParts.length === 0) {
2860
+ if (markerMsg.id) {
2861
+ idsToRemove.push(markerMsg.id);
2199
2862
  }
2863
+ } else if (unobservedParts.length < (markerMsg.content?.parts?.length ?? 0)) {
2864
+ markerMsg.content.parts = unobservedParts;
2865
+ }
2866
+ if (idsToRemove.length > 0) {
2867
+ messageList.removeByIds(idsToRemove);
2868
+ }
2869
+ if (messagesToSave.length > 0) {
2870
+ await this.saveMessagesWithSealedIdTracking(messagesToSave, sealedIds, threadId, resourceId, state);
2871
+ }
2872
+ } else if (observedMessageIds && observedMessageIds.length > 0) {
2873
+ const observedSet = new Set(observedMessageIds);
2874
+ const idsToRemove = [];
2875
+ for (const msg of allMsgs) {
2876
+ if (msg?.id && msg.id !== "om-continuation" && observedSet.has(msg.id)) {
2877
+ idsToRemove.push(msg.id);
2878
+ }
2879
+ }
2880
+ omDebug(
2881
+ `[OM:cleanupActivation] observedSet=${[...observedSet].map((id) => id.slice(0, 8)).join(",")}, matched=${idsToRemove.length}, idsToRemove=${idsToRemove.map((id) => id.slice(0, 8)).join(",")}`
2882
+ );
2883
+ if (idsToRemove.length > 0) {
2884
+ messageList.removeByIds(idsToRemove);
2885
+ omDebug(
2886
+ `[OM:cleanupActivation] removed ${idsToRemove.length} messages, remaining=${messageList.get.all.db().length}`
2887
+ );
2888
+ }
2889
+ } else {
2890
+ const newInput = messageList.clear.input.db();
2891
+ const newOutput = messageList.clear.response.db();
2892
+ const messagesToSave = [...newInput, ...newOutput];
2893
+ if (messagesToSave.length > 0) {
2894
+ await this.saveMessagesWithSealedIdTracking(messagesToSave, sealedIds, threadId, resourceId, state);
2895
+ }
2896
+ }
2897
+ messageList.clear.input.db();
2898
+ messageList.clear.response.db();
2899
+ }
2900
+ /**
2901
+ * Handle per-step save when threshold is not reached.
2902
+ * Persists messages incrementally to prevent data loss on interruption.
2903
+ */
2904
+ async handlePerStepSave(messageList, sealedIds, threadId, resourceId, state) {
2905
+ const newInput = messageList.clear.input.db();
2906
+ const newOutput = messageList.clear.response.db();
2907
+ const messagesToSave = [...newInput, ...newOutput];
2908
+ omDebug(
2909
+ `[OM:handlePerStepSave] cleared input=${newInput.length}, response=${newOutput.length}, toSave=${messagesToSave.length}, ids=${messagesToSave.map((m) => m.id?.slice(0, 8)).join(",")}`
2910
+ );
2911
+ if (messagesToSave.length > 0) {
2912
+ await this.saveMessagesWithSealedIdTracking(messagesToSave, sealedIds, threadId, resourceId, state);
2913
+ for (const msg of messagesToSave) {
2914
+ messageList.add(msg, "memory");
2200
2915
  }
2201
2916
  }
2917
+ }
2918
+ /**
2919
+ * Inject observations as system message and add continuation reminder.
2920
+ */
2921
+ async injectObservationsIntoContext(messageList, record, threadId, resourceId, unobservedContextBlocks, requestContext) {
2202
2922
  const thread = await this.storage.getThreadById({ threadId });
2203
2923
  const threadOMMetadata = getThreadOMMetadata(thread?.metadata);
2204
2924
  const currentTask = threadOMMetadata?.currentTask;
2205
2925
  const suggestedResponse = threadOMMetadata?.suggestedResponse;
2206
- const currentDate = requestContext?.get("currentDate") ?? /* @__PURE__ */ new Date();
2207
- if (record.activeObservations) {
2208
- const observationSystemMessage = this.formatObservationsForContext(
2209
- record.activeObservations,
2210
- currentTask,
2211
- suggestedResponse,
2212
- unobservedContextBlocks,
2213
- currentDate
2214
- );
2215
- messageList.clearSystemMessages("observational-memory");
2216
- messageList.addSystem(observationSystemMessage, "observational-memory");
2217
- const continuationMessage = {
2218
- id: `om-continuation`,
2219
- role: "user",
2220
- createdAt: /* @__PURE__ */ new Date(0),
2221
- content: {
2222
- format: 2,
2223
- parts: [
2224
- {
2225
- type: "text",
2226
- text: `<system-reminder>This message is not from the user, the conversation history grew too long and wouldn't fit in context! Thankfully the entire conversation is stored in your memory observations. Please continue from where the observations left off. Do not refer to your "memory observations" directly, the user doesn't know about them, they are your memories! Just respond naturally as if you're remembering the conversation (you are!). Do not say "Hi there!" or "based on our previous conversation" as if the conversation is just starting, this is not a new conversation. This is an ongoing conversation, keep continuity by responding based on your memory. For example do not say "I understand. I've reviewed my memory observations", or "I remember [...]". Answer naturally following the suggestion from your memory. Note that your memory may contain a suggested first response, which you should follow.
2926
+ const rawCurrentDate = requestContext?.get("currentDate");
2927
+ const currentDate = rawCurrentDate instanceof Date ? rawCurrentDate : typeof rawCurrentDate === "string" ? new Date(rawCurrentDate) : /* @__PURE__ */ new Date();
2928
+ if (!record.activeObservations) {
2929
+ return;
2930
+ }
2931
+ const observationSystemMessage = this.formatObservationsForContext(
2932
+ record.activeObservations,
2933
+ currentTask,
2934
+ suggestedResponse,
2935
+ unobservedContextBlocks,
2936
+ currentDate
2937
+ );
2938
+ messageList.clearSystemMessages("observational-memory");
2939
+ messageList.addSystem(observationSystemMessage, "observational-memory");
2940
+ const continuationMessage = {
2941
+ id: `om-continuation`,
2942
+ role: "user",
2943
+ createdAt: /* @__PURE__ */ new Date(0),
2944
+ content: {
2945
+ format: 2,
2946
+ parts: [
2947
+ {
2948
+ type: "text",
2949
+ text: `<system-reminder>This message is not from the user, the conversation history grew too long and wouldn't fit in context! Thankfully the entire conversation is stored in your memory observations. Please continue from where the observations left off. Do not refer to your "memory observations" directly, the user doesn't know about them, they are your memories! Just respond naturally as if you're remembering the conversation (you are!). Do not say "Hi there!" or "based on our previous conversation" as if the conversation is just starting, this is not a new conversation. This is an ongoing conversation, keep continuity by responding based on your memory. For example do not say "I understand. I've reviewed my memory observations", or "I remember [...]". Answer naturally following the suggestion from your memory. Note that your memory may contain a suggested first response, which you should follow.
2227
2950
 
2228
2951
  IMPORTANT: this system reminder is NOT from the user. The system placed it here as part of your memory system. This message is part of you remembering your conversation with the user.
2229
2952
 
2230
2953
  NOTE: Any messages following this system reminder are newer than your memories.
2231
2954
  </system-reminder>`
2232
- }
2233
- ]
2234
- },
2235
- threadId,
2236
- resourceId
2237
- };
2238
- messageList.add(continuationMessage, "memory");
2955
+ }
2956
+ ]
2957
+ },
2958
+ threadId,
2959
+ resourceId
2960
+ };
2961
+ messageList.add(continuationMessage, "memory");
2962
+ }
2963
+ /**
2964
+ * Filter out already-observed messages from message list (step 0 only).
2965
+ * Historical messages loaded from DB may contain observation markers from previous sessions.
2966
+ */
2967
+ filterAlreadyObservedMessages(messageList, record) {
2968
+ const allMessages = messageList.get.all.db();
2969
+ let markerMessageIndex = -1;
2970
+ let markerMessage = null;
2971
+ for (let i = allMessages.length - 1; i >= 0; i--) {
2972
+ const msg = allMessages[i];
2973
+ if (!msg) continue;
2974
+ if (this.findLastCompletedObservationBoundary(msg) !== -1) {
2975
+ markerMessageIndex = i;
2976
+ markerMessage = msg;
2977
+ break;
2978
+ }
2239
2979
  }
2240
- if (stepNumber === 0) {
2241
- const allMessages = messageList.get.all.db();
2242
- let markerMessageIndex = -1;
2243
- let markerMessage = null;
2244
- for (let i = allMessages.length - 1; i >= 0; i--) {
2980
+ if (markerMessage && markerMessageIndex !== -1) {
2981
+ const messagesToRemove = [];
2982
+ for (let i = 0; i < markerMessageIndex; i++) {
2245
2983
  const msg = allMessages[i];
2246
- if (!msg) continue;
2247
- if (this.findLastCompletedObservationBoundary(msg) !== -1) {
2248
- markerMessageIndex = i;
2249
- markerMessage = msg;
2250
- break;
2984
+ if (msg?.id && msg.id !== "om-continuation") {
2985
+ messagesToRemove.push(msg.id);
2251
2986
  }
2252
2987
  }
2253
- if (markerMessage && markerMessageIndex !== -1) {
2254
- const messagesToRemove = [];
2255
- for (let i = 0; i < markerMessageIndex; i++) {
2256
- const msg = allMessages[i];
2257
- if (msg?.id && msg.id !== "om-continuation") {
2988
+ if (messagesToRemove.length > 0) {
2989
+ messageList.removeByIds(messagesToRemove);
2990
+ }
2991
+ const unobservedParts = this.getUnobservedParts(markerMessage);
2992
+ if (unobservedParts.length === 0) {
2993
+ if (markerMessage.id) {
2994
+ messageList.removeByIds([markerMessage.id]);
2995
+ }
2996
+ } else if (unobservedParts.length < (markerMessage.content?.parts?.length ?? 0)) {
2997
+ markerMessage.content.parts = unobservedParts;
2998
+ }
2999
+ } else if (record) {
3000
+ const observedIds = new Set(Array.isArray(record.observedMessageIds) ? record.observedMessageIds : []);
3001
+ const lastObservedAt = record.lastObservedAt;
3002
+ const messagesToRemove = [];
3003
+ for (const msg of allMessages) {
3004
+ if (!msg?.id || msg.id === "om-continuation") continue;
3005
+ if (observedIds.has(msg.id)) {
3006
+ messagesToRemove.push(msg.id);
3007
+ continue;
3008
+ }
3009
+ if (lastObservedAt && msg.createdAt) {
3010
+ const msgDate = new Date(msg.createdAt);
3011
+ if (msgDate <= lastObservedAt) {
2258
3012
  messagesToRemove.push(msg.id);
2259
3013
  }
2260
3014
  }
2261
- if (messagesToRemove.length > 0) {
2262
- messageList.removeByIds(messagesToRemove);
3015
+ }
3016
+ if (messagesToRemove.length > 0) {
3017
+ messageList.removeByIds(messagesToRemove);
3018
+ }
3019
+ }
3020
+ }
3021
+ /**
3022
+ * Process input at each step - check threshold, observe if needed, save, inject observations.
3023
+ * This is the ONLY processor method - all OM logic happens here.
3024
+ *
3025
+ * Flow:
3026
+ * 1. Load historical messages (step 0 only)
3027
+ * 2. Check if observation threshold is reached
3028
+ * 3. If threshold reached: observe, save messages with markers
3029
+ * 4. Inject observations into context
3030
+ * 5. Filter out already-observed messages
3031
+ */
3032
+ async processInputStep(args) {
3033
+ const { messageList, requestContext, stepNumber, state: _state, writer, abortSignal, abort } = args;
3034
+ const state = _state ?? {};
3035
+ const context = this.getThreadContext(requestContext, messageList);
3036
+ if (!context) {
3037
+ return messageList;
3038
+ }
3039
+ const { threadId, resourceId } = context;
3040
+ const memoryContext = parseMemoryRequestContext(requestContext);
3041
+ const readOnly = memoryContext?.memoryConfig?.readOnly;
3042
+ let record = await this.getOrCreateRecord(threadId, resourceId);
3043
+ omDebug(
3044
+ `[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}`
3045
+ );
3046
+ await this.loadHistoricalMessagesIfNeeded(messageList, state, threadId, resourceId, record.lastObservedAt);
3047
+ let unobservedContextBlocks;
3048
+ if (this.scope === "resource" && resourceId) {
3049
+ unobservedContextBlocks = await this.loadOtherThreadsContext(resourceId, threadId);
3050
+ }
3051
+ if (stepNumber === 0 && !readOnly && this.isAsyncObservationEnabled()) {
3052
+ const lockKey = this.getLockKey(threadId, resourceId);
3053
+ const bufferedChunks = this.getBufferedChunks(record);
3054
+ omDebug(
3055
+ `[OM:step0-activation] asyncObsEnabled=true, bufferedChunks=${bufferedChunks.length}, isBufferingObs=${record.isBufferingObservation}`
3056
+ );
3057
+ {
3058
+ const bufKey = this.getObservationBufferKey(lockKey);
3059
+ const dbBoundary = record.lastBufferedAtTokens ?? 0;
3060
+ const currentContextTokens = this.tokenCounter.countMessages(messageList.get.all.db());
3061
+ if (dbBoundary > currentContextTokens) {
3062
+ omDebug(
3063
+ `[OM:step0-boundary-reset] dbBoundary=${dbBoundary} > currentContext=${currentContextTokens}, resetting to current`
3064
+ );
3065
+ _ObservationalMemory.lastBufferedBoundary.set(bufKey, currentContextTokens);
3066
+ this.storage.setBufferingObservationFlag(record.id, false, currentContextTokens).catch(() => {
3067
+ });
3068
+ }
3069
+ }
3070
+ if (bufferedChunks.length > 0) {
3071
+ const allMsgsForCheck = messageList.get.all.db();
3072
+ const otherThreadTokensForCheck = unobservedContextBlocks ? this.tokenCounter.countString(unobservedContextBlocks) : 0;
3073
+ const currentObsTokensForCheck = record.observationTokenCount ?? 0;
3074
+ const { totalPendingTokens: step0PendingTokens, threshold: step0Threshold } = this.calculateObservationThresholds(
3075
+ allMsgsForCheck,
3076
+ [],
3077
+ // unobserved not needed for threshold calculation
3078
+ 0,
3079
+ // pendingTokens not needed — allMessages covers context
3080
+ otherThreadTokensForCheck,
3081
+ currentObsTokensForCheck,
3082
+ record
3083
+ );
3084
+ omDebug(
3085
+ `[OM:step0-activation] pendingTokens=${step0PendingTokens}, threshold=${step0Threshold}, blockAfter=${this.observationConfig.blockAfter}, shouldActivate=${step0PendingTokens >= step0Threshold}, allMsgs=${allMsgsForCheck.length}`
3086
+ );
3087
+ if (step0PendingTokens >= step0Threshold) {
3088
+ const activationResult = await this.tryActivateBufferedObservations(
3089
+ record,
3090
+ lockKey,
3091
+ step0PendingTokens,
3092
+ writer,
3093
+ messageList
3094
+ );
3095
+ if (activationResult.success && activationResult.updatedRecord) {
3096
+ record = activationResult.updatedRecord;
3097
+ const activatedIds = activationResult.activatedMessageIds ?? [];
3098
+ if (activatedIds.length > 0) {
3099
+ const activatedSet = new Set(activatedIds);
3100
+ const allMsgs = messageList.get.all.db();
3101
+ const idsToRemove = allMsgs.filter((msg) => msg?.id && msg.id !== "om-continuation" && activatedSet.has(msg.id)).map((msg) => msg.id);
3102
+ if (idsToRemove.length > 0) {
3103
+ messageList.removeByIds(idsToRemove);
3104
+ }
3105
+ }
3106
+ this.cleanupStaticMaps(threadId, resourceId, activatedIds);
3107
+ const bufKey = this.getObservationBufferKey(lockKey);
3108
+ _ObservationalMemory.lastBufferedBoundary.set(bufKey, 0);
3109
+ this.storage.setBufferingObservationFlag(record.id, false, 0).catch(() => {
3110
+ });
3111
+ await this.maybeReflect(
3112
+ record,
3113
+ record.observationTokenCount ?? 0,
3114
+ threadId,
3115
+ writer,
3116
+ void 0,
3117
+ messageList
3118
+ );
3119
+ record = await this.getOrCreateRecord(threadId, resourceId);
3120
+ }
3121
+ }
3122
+ }
3123
+ }
3124
+ if (stepNumber === 0 && !readOnly) {
3125
+ const obsTokens = record.observationTokenCount ?? 0;
3126
+ if (this.shouldReflect(obsTokens)) {
3127
+ omDebug(`[OM:step0-reflect] obsTokens=${obsTokens} over reflectThreshold, triggering reflection`);
3128
+ await this.maybeReflect(record, obsTokens, threadId, writer, void 0, messageList);
3129
+ record = await this.getOrCreateRecord(threadId, resourceId);
3130
+ } else if (this.isAsyncReflectionEnabled()) {
3131
+ const lockKey = this.getLockKey(threadId, resourceId);
3132
+ if (this.shouldTriggerAsyncReflection(obsTokens, lockKey, record)) {
3133
+ omDebug(`[OM:step0-reflect] obsTokens=${obsTokens} above activation point, triggering async reflection`);
3134
+ await this.maybeAsyncReflect(record, obsTokens, writer, messageList);
3135
+ record = await this.getOrCreateRecord(threadId, resourceId);
3136
+ }
3137
+ }
3138
+ }
3139
+ if (!readOnly) {
3140
+ const allMessages = messageList.get.all.db();
3141
+ const unobservedMessages = this.getUnobservedMessages(allMessages, record);
3142
+ const otherThreadTokens = unobservedContextBlocks ? this.tokenCounter.countString(unobservedContextBlocks) : 0;
3143
+ const currentObservationTokens = record.observationTokenCount ?? 0;
3144
+ const thresholds = this.calculateObservationThresholds(
3145
+ allMessages,
3146
+ unobservedMessages,
3147
+ 0,
3148
+ // pendingTokens not needed — allMessages covers context
3149
+ otherThreadTokens,
3150
+ currentObservationTokens,
3151
+ record
3152
+ );
3153
+ const { totalPendingTokens, threshold } = thresholds;
3154
+ const stateSealedIds = state.sealedIds ?? /* @__PURE__ */ new Set();
3155
+ const staticSealedIds = _ObservationalMemory.sealedMessageIds.get(threadId) ?? /* @__PURE__ */ new Set();
3156
+ const sealedIds = /* @__PURE__ */ new Set([...stateSealedIds, ...staticSealedIds]);
3157
+ state.sealedIds = sealedIds;
3158
+ const lockKey = this.getLockKey(threadId, resourceId);
3159
+ if (this.isAsyncObservationEnabled() && totalPendingTokens < threshold) {
3160
+ const shouldTrigger = this.shouldTriggerAsyncObservation(totalPendingTokens, lockKey, record);
3161
+ omDebug(
3162
+ `[OM:async-obs] belowThreshold: pending=${totalPendingTokens}, threshold=${threshold}, shouldTrigger=${shouldTrigger}, isBufferingObs=${record.isBufferingObservation}, lastBufferedAt=${record.lastBufferedAtTokens}`
3163
+ );
3164
+ if (shouldTrigger) {
3165
+ this.startAsyncBufferedObservation(record, threadId, unobservedMessages, lockKey, writer, totalPendingTokens);
3166
+ }
3167
+ } else if (this.isAsyncObservationEnabled()) {
3168
+ const shouldTrigger = this.shouldTriggerAsyncObservation(totalPendingTokens, lockKey, record);
3169
+ omDebug(
3170
+ `[OM:async-obs] atOrAboveThreshold: pending=${totalPendingTokens}, threshold=${threshold}, step=${stepNumber}, shouldTrigger=${shouldTrigger}`
3171
+ );
3172
+ if (shouldTrigger) {
3173
+ this.startAsyncBufferedObservation(record, threadId, unobservedMessages, lockKey, writer, totalPendingTokens);
2263
3174
  }
2264
- const unobservedParts = this.getUnobservedParts(markerMessage);
2265
- if (unobservedParts.length === 0) {
2266
- if (markerMessage.id) {
2267
- messageList.removeByIds([markerMessage.id]);
3175
+ }
3176
+ if (stepNumber > 0) {
3177
+ await this.handlePerStepSave(messageList, sealedIds, threadId, resourceId, state);
3178
+ }
3179
+ if (stepNumber > 0 && totalPendingTokens >= threshold) {
3180
+ const { observationSucceeded, updatedRecord, activatedMessageIds } = await this.handleThresholdReached(
3181
+ messageList,
3182
+ record,
3183
+ threadId,
3184
+ resourceId,
3185
+ threshold,
3186
+ lockKey,
3187
+ writer,
3188
+ abortSignal,
3189
+ abort
3190
+ );
3191
+ if (observationSucceeded) {
3192
+ const observedIds = activatedMessageIds?.length ? activatedMessageIds : Array.isArray(updatedRecord.observedMessageIds) ? updatedRecord.observedMessageIds : void 0;
3193
+ omDebug(
3194
+ `[OM:cleanup] observedIds=${observedIds?.length ?? "undefined"}, ids=${observedIds?.join(",") ?? "none"}, updatedRecord.observedMessageIds=${JSON.stringify(updatedRecord.observedMessageIds)}`
3195
+ );
3196
+ await this.cleanupAfterObservation(messageList, sealedIds, threadId, resourceId, state, observedIds);
3197
+ if (activatedMessageIds?.length) {
3198
+ this.cleanupStaticMaps(threadId, resourceId, activatedMessageIds);
3199
+ }
3200
+ if (this.isAsyncObservationEnabled()) {
3201
+ const bufKey = this.getObservationBufferKey(lockKey);
3202
+ _ObservationalMemory.lastBufferedBoundary.set(bufKey, 0);
3203
+ this.storage.setBufferingObservationFlag(updatedRecord.id, false, 0).catch(() => {
3204
+ });
3205
+ omDebug(`[OM:threshold] post-activation boundary reset to 0`);
2268
3206
  }
2269
- } else if (unobservedParts.length < (markerMessage.content?.parts?.length ?? 0)) {
2270
- markerMessage.content.parts = unobservedParts;
2271
3207
  }
3208
+ record = updatedRecord;
2272
3209
  }
2273
3210
  }
3211
+ await this.injectObservationsIntoContext(
3212
+ messageList,
3213
+ record,
3214
+ threadId,
3215
+ resourceId,
3216
+ unobservedContextBlocks,
3217
+ requestContext
3218
+ );
3219
+ if (stepNumber === 0) {
3220
+ this.filterAlreadyObservedMessages(messageList, record);
3221
+ }
3222
+ {
3223
+ const freshRecord = await this.getOrCreateRecord(threadId, resourceId);
3224
+ const contextMessages = messageList.get.all.db();
3225
+ const freshUnobservedTokens = this.tokenCounter.countMessages(contextMessages);
3226
+ const otherThreadTokens = unobservedContextBlocks ? this.tokenCounter.countString(unobservedContextBlocks) : 0;
3227
+ const currentObservationTokens = freshRecord.observationTokenCount ?? 0;
3228
+ const threshold = this.calculateDynamicThreshold(this.observationConfig.messageTokens, currentObservationTokens);
3229
+ const baseReflectionThreshold = this.getMaxThreshold(this.reflectionConfig.observationTokens);
3230
+ const isSharedBudget = typeof this.observationConfig.messageTokens !== "number";
3231
+ const totalBudget = isSharedBudget ? this.observationConfig.messageTokens.max : 0;
3232
+ const effectiveObservationTokensThreshold = isSharedBudget ? Math.max(totalBudget - threshold, 1e3) : baseReflectionThreshold;
3233
+ const totalPendingTokens = freshUnobservedTokens + otherThreadTokens;
3234
+ await this.emitStepProgress(
3235
+ writer,
3236
+ threadId,
3237
+ resourceId,
3238
+ stepNumber,
3239
+ freshRecord,
3240
+ {
3241
+ totalPendingTokens,
3242
+ threshold,
3243
+ effectiveObservationTokensThreshold
3244
+ },
3245
+ currentObservationTokens
3246
+ );
3247
+ }
2274
3248
  return messageList;
2275
3249
  }
2276
3250
  /**
@@ -2296,11 +3270,21 @@ NOTE: Any messages following this system reminder are newer than your memories.
2296
3270
  const newInput = messageList.get.input.db();
2297
3271
  const newOutput = messageList.get.response.db();
2298
3272
  const messagesToSave = [...newInput, ...newOutput];
3273
+ omDebug(
3274
+ `[OM:processOutputResult] threadId=${threadId}, inputMsgs=${newInput.length}, responseMsgs=${newOutput.length}, totalToSave=${messagesToSave.length}, allMsgsInList=${messageList.get.all.db().length}`
3275
+ );
2299
3276
  if (messagesToSave.length === 0) {
3277
+ omDebug(`[OM:processOutputResult] nothing to save \u2014 all messages were already saved during per-step saves`);
2300
3278
  return messageList;
2301
3279
  }
2302
3280
  const sealedIds = state.sealedIds ?? /* @__PURE__ */ new Set();
3281
+ omDebug(
3282
+ `[OM:processOutputResult] saving ${messagesToSave.length} messages, sealedIds=${sealedIds.size}, ids=${messagesToSave.map((m) => m.id?.slice(0, 8)).join(",")}`
3283
+ );
2303
3284
  await this.saveMessagesWithSealedIdTracking(messagesToSave, sealedIds, threadId, resourceId, state);
3285
+ omDebug(
3286
+ `[OM:processOutputResult] saved successfully, finalIds=${messagesToSave.map((m) => m.id?.slice(0, 8)).join(",")}`
3287
+ );
2304
3288
  return messageList;
2305
3289
  }
2306
3290
  /**
@@ -2530,6 +3514,7 @@ ${newThreadSection}`;
2530
3514
  }))
2531
3515
  });
2532
3516
  await this.storage.setObservingFlag(record.id, true);
3517
+ registerOp(record.id, "observing");
2533
3518
  const cycleId = crypto.randomUUID();
2534
3519
  const tokensToObserve = this.tokenCounter.countMessages(unobservedMessages);
2535
3520
  const lastMessage = unobservedMessages[unobservedMessages.length - 1];
@@ -2555,9 +3540,20 @@ ${newThreadSection}`;
2555
3540
  return;
2556
3541
  }
2557
3542
  }
3543
+ let messagesToObserve = unobservedMessages;
3544
+ const bufferActivation = this.observationConfig.bufferActivation;
3545
+ if (bufferActivation && bufferActivation < 1 && unobservedMessages.length >= 1) {
3546
+ const newestMsg = unobservedMessages[unobservedMessages.length - 1];
3547
+ if (newestMsg?.content?.parts?.length) {
3548
+ this.sealMessagesForBuffering([newestMsg]);
3549
+ omDebug(
3550
+ `[OM:sync-obs] sealed newest message (${newestMsg.role}, ${newestMsg.content.parts.length} parts) for ratio-aware observation`
3551
+ );
3552
+ }
3553
+ }
2558
3554
  const result = await this.callObserver(
2559
3555
  freshRecord?.activeObservations ?? record.activeObservations,
2560
- unobservedMessages,
3556
+ messagesToObserve,
2561
3557
  abortSignal
2562
3558
  );
2563
3559
  const existingObservations = freshRecord?.activeObservations ?? record.activeObservations ?? "";
@@ -2572,8 +3568,8 @@ ${result.observations}` : result.observations;
2572
3568
  }
2573
3569
  let totalTokenCount = this.tokenCounter.countObservations(newObservations);
2574
3570
  const cycleObservationTokens = this.tokenCounter.countObservations(result.observations);
2575
- const lastObservedAt = this.getMaxMessageTimestamp(unobservedMessages);
2576
- const newMessageIds = unobservedMessages.map((m) => m.id);
3571
+ const lastObservedAt = this.getMaxMessageTimestamp(messagesToObserve);
3572
+ const newMessageIds = messagesToObserve.map((m) => m.id);
2577
3573
  const existingIds = freshRecord?.observedMessageIds ?? record.observedMessageIds ?? [];
2578
3574
  const allObservedIds = [.../* @__PURE__ */ new Set([...Array.isArray(existingIds) ? existingIds : [], ...newMessageIds])];
2579
3575
  await this.storage.updateActiveObservations({
@@ -2597,12 +3593,13 @@ ${result.observations}` : result.observations;
2597
3593
  });
2598
3594
  }
2599
3595
  }
3596
+ const actualTokensObserved = this.tokenCounter.countMessages(messagesToObserve);
2600
3597
  if (lastMessage?.id) {
2601
3598
  const endMarker = this.createObservationEndMarker({
2602
3599
  cycleId,
2603
3600
  operationType: "observation",
2604
3601
  startedAt,
2605
- tokensObserved: tokensToObserve,
3602
+ tokensObserved: actualTokensObserved,
2606
3603
  observationTokens: cycleObservationTokens,
2607
3604
  observations: result.observations,
2608
3605
  currentTask: result.currentTask,
@@ -2623,7 +3620,7 @@ ${result.observations}` : result.observations;
2623
3620
  observations: newObservations,
2624
3621
  rawObserverOutput: result.observations,
2625
3622
  previousObservations: record.activeObservations,
2626
- messages: unobservedMessages.map((m) => ({
3623
+ messages: messagesToObserve.map((m) => ({
2627
3624
  role: m.role,
2628
3625
  content: typeof m.content === "string" ? m.content : JSON.stringify(m.content)
2629
3626
  })),
@@ -2655,11 +3652,514 @@ ${result.observations}` : result.observations;
2655
3652
  if (abortSignal?.aborted) {
2656
3653
  throw error;
2657
3654
  }
2658
- console.error(`[OM] Observation failed:`, error instanceof Error ? error.message : String(error));
3655
+ omError("[OM] Observation failed", error);
2659
3656
  } finally {
2660
3657
  await this.storage.setObservingFlag(record.id, false);
3658
+ unregisterOp(record.id, "observing");
2661
3659
  }
2662
3660
  }
3661
+ /**
3662
+ * Start an async background observation that stores results to bufferedObservations.
3663
+ * This is a fire-and-forget operation that runs in the background.
3664
+ * The results will be swapped to active when the main threshold is reached.
3665
+ *
3666
+ * If another buffering operation is already in progress for this scope, this will
3667
+ * wait for it to complete before starting a new one (mutex behavior).
3668
+ *
3669
+ * @param record - Current OM record
3670
+ * @param threadId - Thread ID
3671
+ * @param unobservedMessages - All unobserved messages (will be filtered for already-buffered)
3672
+ * @param lockKey - Lock key for this scope
3673
+ * @param writer - Optional stream writer for emitting buffering markers
3674
+ */
3675
+ startAsyncBufferedObservation(record, threadId, unobservedMessages, lockKey, writer, contextWindowTokens) {
3676
+ const bufferKey = this.getObservationBufferKey(lockKey);
3677
+ const currentTokens = contextWindowTokens ?? this.tokenCounter.countMessages(unobservedMessages) + (record.pendingMessageTokens ?? 0);
3678
+ _ObservationalMemory.lastBufferedBoundary.set(bufferKey, currentTokens);
3679
+ registerOp(record.id, "bufferingObservation");
3680
+ this.storage.setBufferingObservationFlag(record.id, true, currentTokens).catch((err) => {
3681
+ omError("[OM] Failed to set buffering observation flag", err);
3682
+ });
3683
+ const asyncOp = this.runAsyncBufferedObservation(record, threadId, unobservedMessages, bufferKey, writer).finally(
3684
+ () => {
3685
+ _ObservationalMemory.asyncBufferingOps.delete(bufferKey);
3686
+ unregisterOp(record.id, "bufferingObservation");
3687
+ this.storage.setBufferingObservationFlag(record.id, false).catch((err) => {
3688
+ omError("[OM] Failed to clear buffering observation flag", err);
3689
+ });
3690
+ }
3691
+ );
3692
+ _ObservationalMemory.asyncBufferingOps.set(bufferKey, asyncOp);
3693
+ }
3694
+ /**
3695
+ * Internal method that waits for existing buffering operation and then runs new buffering.
3696
+ * This implements the mutex-wait behavior.
3697
+ */
3698
+ async runAsyncBufferedObservation(record, threadId, unobservedMessages, bufferKey, writer) {
3699
+ const existingOp = _ObservationalMemory.asyncBufferingOps.get(bufferKey);
3700
+ if (existingOp) {
3701
+ try {
3702
+ await existingOp;
3703
+ } catch {
3704
+ }
3705
+ }
3706
+ const freshRecord = await this.storage.getObservationalMemory(record.threadId, record.resourceId);
3707
+ if (!freshRecord) {
3708
+ return;
3709
+ }
3710
+ let bufferCursor = _ObservationalMemory.lastBufferedAtTime.get(bufferKey) ?? freshRecord.lastBufferedAtTime ?? null;
3711
+ if (freshRecord.lastObservedAt) {
3712
+ const lastObserved = new Date(freshRecord.lastObservedAt);
3713
+ if (!bufferCursor || lastObserved > bufferCursor) {
3714
+ bufferCursor = lastObserved;
3715
+ }
3716
+ }
3717
+ let candidateMessages = this.getUnobservedMessages(unobservedMessages, freshRecord, {
3718
+ excludeBuffered: true
3719
+ });
3720
+ const preFilterCount = candidateMessages.length;
3721
+ if (bufferCursor) {
3722
+ candidateMessages = candidateMessages.filter((msg) => {
3723
+ if (!msg.createdAt) return true;
3724
+ return new Date(msg.createdAt) > bufferCursor;
3725
+ });
3726
+ }
3727
+ omDebug(
3728
+ `[OM:bufferCursor] cursor=${bufferCursor?.toISOString() ?? "null"}, unobserved=${unobservedMessages.length}, afterExcludeBuffered=${preFilterCount}, afterCursorFilter=${candidateMessages.length}`
3729
+ );
3730
+ const bufferTokens = this.observationConfig.bufferTokens ?? 5e3;
3731
+ const minNewTokens = bufferTokens / 2;
3732
+ const newTokens = this.tokenCounter.countMessages(candidateMessages);
3733
+ if (newTokens < minNewTokens) {
3734
+ return;
3735
+ }
3736
+ const messagesToBuffer = candidateMessages;
3737
+ this.sealMessagesForBuffering(messagesToBuffer);
3738
+ await this.messageHistory.persistMessages({
3739
+ messages: messagesToBuffer,
3740
+ threadId,
3741
+ resourceId: freshRecord.resourceId ?? void 0
3742
+ });
3743
+ let staticSealedIds = _ObservationalMemory.sealedMessageIds.get(threadId);
3744
+ if (!staticSealedIds) {
3745
+ staticSealedIds = /* @__PURE__ */ new Set();
3746
+ _ObservationalMemory.sealedMessageIds.set(threadId, staticSealedIds);
3747
+ }
3748
+ for (const msg of messagesToBuffer) {
3749
+ staticSealedIds.add(msg.id);
3750
+ }
3751
+ const cycleId = `buffer-obs-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
3752
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
3753
+ const tokensToBuffer = this.tokenCounter.countMessages(messagesToBuffer);
3754
+ if (writer) {
3755
+ const startMarker = this.createBufferingStartMarker({
3756
+ cycleId,
3757
+ operationType: "observation",
3758
+ tokensToBuffer,
3759
+ recordId: freshRecord.id,
3760
+ threadId,
3761
+ threadIds: [threadId]
3762
+ });
3763
+ void writer.custom(startMarker).catch(() => {
3764
+ });
3765
+ }
3766
+ try {
3767
+ omDebug(
3768
+ `[OM:bufferInput] cycleId=${cycleId}, msgCount=${messagesToBuffer.length}, msgTokens=${this.tokenCounter.countMessages(messagesToBuffer)}, ids=${messagesToBuffer.map((m) => `${m.id?.slice(0, 8)}@${m.createdAt ? new Date(m.createdAt).toISOString() : "none"}`).join(",")}`
3769
+ );
3770
+ await this.doAsyncBufferedObservation(freshRecord, threadId, messagesToBuffer, cycleId, startedAt, writer);
3771
+ const maxTs = this.getMaxMessageTimestamp(messagesToBuffer);
3772
+ const cursor = new Date(maxTs.getTime() + 1);
3773
+ _ObservationalMemory.lastBufferedAtTime.set(bufferKey, cursor);
3774
+ } catch (error) {
3775
+ if (writer) {
3776
+ const failedMarker = this.createBufferingFailedMarker({
3777
+ cycleId,
3778
+ operationType: "observation",
3779
+ startedAt,
3780
+ tokensAttempted: tokensToBuffer,
3781
+ error: error instanceof Error ? error.message : String(error),
3782
+ recordId: freshRecord.id,
3783
+ threadId
3784
+ });
3785
+ void writer.custom(failedMarker).catch(() => {
3786
+ });
3787
+ }
3788
+ omError("[OM] Async buffered observation failed", error);
3789
+ }
3790
+ }
3791
+ /**
3792
+ * Perform async buffered observation - observes messages and stores to bufferedObservations.
3793
+ * Does NOT update activeObservations or trigger reflection.
3794
+ *
3795
+ * The observer sees: active observations + existing buffered observations + message history
3796
+ * (excluding already-buffered messages).
3797
+ */
3798
+ async doAsyncBufferedObservation(record, threadId, messagesToBuffer, cycleId, startedAt, writer) {
3799
+ const bufferedChunks = this.getBufferedChunks(record);
3800
+ const bufferedChunksText = bufferedChunks.map((c) => c.observations).join("\n\n");
3801
+ const combinedObservations = this.combineObservationsForBuffering(record.activeObservations, bufferedChunksText);
3802
+ const result = await this.callObserver(
3803
+ combinedObservations,
3804
+ messagesToBuffer,
3805
+ void 0,
3806
+ // No abort signal for background ops
3807
+ { skipContinuationHints: true }
3808
+ );
3809
+ let newObservations;
3810
+ if (this.scope === "resource") {
3811
+ newObservations = await this.wrapWithThreadTag(threadId, result.observations);
3812
+ } else {
3813
+ newObservations = result.observations;
3814
+ }
3815
+ const newTokenCount = this.tokenCounter.countObservations(newObservations);
3816
+ const newMessageIds = messagesToBuffer.map((m) => m.id);
3817
+ const messageTokens = this.tokenCounter.countMessages(messagesToBuffer);
3818
+ const maxMessageTimestamp = this.getMaxMessageTimestamp(messagesToBuffer);
3819
+ const lastObservedAt = new Date(maxMessageTimestamp.getTime() + 1);
3820
+ await this.storage.updateBufferedObservations({
3821
+ id: record.id,
3822
+ chunk: {
3823
+ cycleId,
3824
+ observations: newObservations,
3825
+ tokenCount: newTokenCount,
3826
+ messageIds: newMessageIds,
3827
+ messageTokens,
3828
+ lastObservedAt
3829
+ },
3830
+ lastBufferedAtTime: lastObservedAt
3831
+ });
3832
+ if (writer) {
3833
+ const tokensBuffered = this.tokenCounter.countMessages(messagesToBuffer);
3834
+ const updatedRecord = await this.storage.getObservationalMemory(record.threadId, record.resourceId);
3835
+ const updatedChunks = this.getBufferedChunks(updatedRecord);
3836
+ const totalBufferedTokens = updatedChunks.reduce((sum, c) => sum + (c.tokenCount ?? 0), 0) || newTokenCount;
3837
+ const endMarker = this.createBufferingEndMarker({
3838
+ cycleId,
3839
+ operationType: "observation",
3840
+ startedAt,
3841
+ tokensBuffered,
3842
+ bufferedTokens: totalBufferedTokens,
3843
+ recordId: record.id,
3844
+ threadId,
3845
+ observations: newObservations
3846
+ });
3847
+ void writer.custom(endMarker).catch(() => {
3848
+ });
3849
+ }
3850
+ }
3851
+ /**
3852
+ * Combine active and buffered observations for the buffering observer context.
3853
+ * The buffering observer needs to see both so it doesn't duplicate content.
3854
+ */
3855
+ combineObservationsForBuffering(activeObservations, bufferedObservations) {
3856
+ if (!activeObservations && !bufferedObservations) {
3857
+ return void 0;
3858
+ }
3859
+ if (!activeObservations) {
3860
+ return bufferedObservations;
3861
+ }
3862
+ if (!bufferedObservations) {
3863
+ return activeObservations;
3864
+ }
3865
+ return `${activeObservations}
3866
+
3867
+ --- BUFFERED (pending activation) ---
3868
+
3869
+ ${bufferedObservations}`;
3870
+ }
3871
+ /**
3872
+ * Try to activate buffered observations when threshold is reached.
3873
+ * Returns true if activation succeeded, false if no buffered content or activation failed.
3874
+ *
3875
+ * @param record - Current OM record
3876
+ * @param lockKey - Lock key for this scope
3877
+ * @param writer - Optional writer for emitting UI markers
3878
+ */
3879
+ async tryActivateBufferedObservations(record, lockKey, currentPendingTokens, writer, messageList) {
3880
+ const chunks = this.getBufferedChunks(record);
3881
+ omDebug(`[OM:tryActivate] chunks=${chunks.length}, recordId=${record.id}`);
3882
+ if (!chunks.length) {
3883
+ omDebug(`[OM:tryActivate] no chunks, returning false`);
3884
+ return { success: false };
3885
+ }
3886
+ const bufferKey = this.getObservationBufferKey(lockKey);
3887
+ const asyncOp = _ObservationalMemory.asyncBufferingOps.get(bufferKey);
3888
+ if (asyncOp) {
3889
+ try {
3890
+ await Promise.race([
3891
+ asyncOp,
3892
+ new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), 6e4))
3893
+ ]);
3894
+ } catch {
3895
+ }
3896
+ }
3897
+ const freshRecord = await this.storage.getObservationalMemory(record.threadId, record.resourceId);
3898
+ if (!freshRecord) {
3899
+ return { success: false };
3900
+ }
3901
+ const freshChunks = this.getBufferedChunks(freshRecord);
3902
+ if (!freshChunks.length) {
3903
+ return { success: false };
3904
+ }
3905
+ const activationRatio = this.observationConfig.bufferActivation ?? 0.7;
3906
+ omDebug(
3907
+ `[OM:tryActivate] swapping: freshChunks=${freshChunks.length}, activationRatio=${activationRatio}, totalChunkTokens=${freshChunks.reduce((s, c) => s + (c.tokenCount ?? 0), 0)}`
3908
+ );
3909
+ const messageTokensThreshold = this.getMaxThreshold(this.observationConfig.messageTokens);
3910
+ const activationResult = await this.storage.swapBufferedToActive({
3911
+ id: freshRecord.id,
3912
+ activationRatio,
3913
+ messageTokensThreshold,
3914
+ currentPendingTokens
3915
+ });
3916
+ omDebug(
3917
+ `[OM:tryActivate] swapResult: chunksActivated=${activationResult.chunksActivated}, tokensActivated=${activationResult.messageTokensActivated}, obsTokensActivated=${activationResult.observationTokensActivated}, activatedCycleIds=${activationResult.activatedCycleIds.join(",")}`
3918
+ );
3919
+ await this.storage.setBufferingObservationFlag(freshRecord.id, false);
3920
+ unregisterOp(freshRecord.id, "bufferingObservation");
3921
+ const updatedRecord = await this.storage.getObservationalMemory(record.threadId, record.resourceId);
3922
+ if (writer && updatedRecord && activationResult.activatedCycleIds.length > 0) {
3923
+ const perChunkMap = new Map(activationResult.perChunk?.map((c) => [c.cycleId, c]));
3924
+ for (const cycleId of activationResult.activatedCycleIds) {
3925
+ const chunkData = perChunkMap.get(cycleId);
3926
+ const activationMarker = this.createActivationMarker({
3927
+ cycleId,
3928
+ // Use the original buffering cycleId so UI can link them
3929
+ operationType: "observation",
3930
+ chunksActivated: 1,
3931
+ tokensActivated: chunkData?.messageTokens ?? activationResult.messageTokensActivated,
3932
+ observationTokens: chunkData?.observationTokens ?? activationResult.observationTokensActivated,
3933
+ messagesActivated: chunkData?.messageCount ?? activationResult.messagesActivated,
3934
+ recordId: updatedRecord.id,
3935
+ threadId: updatedRecord.threadId ?? record.threadId ?? "",
3936
+ generationCount: updatedRecord.generationCount ?? 0,
3937
+ observations: chunkData?.observations ?? activationResult.observations
3938
+ });
3939
+ void writer.custom(activationMarker).catch(() => {
3940
+ });
3941
+ await this.persistMarkerToMessage(
3942
+ activationMarker,
3943
+ messageList,
3944
+ record.threadId ?? "",
3945
+ record.resourceId ?? void 0
3946
+ );
3947
+ }
3948
+ }
3949
+ return {
3950
+ success: true,
3951
+ updatedRecord: updatedRecord ?? void 0,
3952
+ messageTokensActivated: activationResult.messageTokensActivated,
3953
+ activatedMessageIds: activationResult.activatedMessageIds
3954
+ };
3955
+ }
3956
+ /**
3957
+ * Start an async background reflection that stores results to bufferedReflection.
3958
+ * This is a fire-and-forget operation that runs in the background.
3959
+ * The results will be swapped to active when the main reflection threshold is reached.
3960
+ *
3961
+ * @param record - Current OM record
3962
+ * @param observationTokens - Current observation token count
3963
+ * @param lockKey - Lock key for this scope
3964
+ */
3965
+ startAsyncBufferedReflection(record, observationTokens, lockKey, writer) {
3966
+ const bufferKey = this.getReflectionBufferKey(lockKey);
3967
+ if (this.isAsyncBufferingInProgress(bufferKey)) {
3968
+ return;
3969
+ }
3970
+ _ObservationalMemory.lastBufferedBoundary.set(bufferKey, observationTokens);
3971
+ registerOp(record.id, "bufferingReflection");
3972
+ this.storage.setBufferingReflectionFlag(record.id, true).catch((err) => {
3973
+ omError("[OM] Failed to set buffering reflection flag", err);
3974
+ });
3975
+ const asyncOp = this.doAsyncBufferedReflection(record, bufferKey, writer).catch((error) => {
3976
+ if (writer) {
3977
+ const failedMarker = this.createBufferingFailedMarker({
3978
+ cycleId: `reflect-buf-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,
3979
+ operationType: "reflection",
3980
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
3981
+ tokensAttempted: observationTokens,
3982
+ error: error instanceof Error ? error.message : String(error),
3983
+ recordId: record.id,
3984
+ threadId: record.threadId ?? ""
3985
+ });
3986
+ void writer.custom(failedMarker).catch(() => {
3987
+ });
3988
+ }
3989
+ omError("[OM] Async buffered reflection failed", error);
3990
+ }).finally(() => {
3991
+ _ObservationalMemory.asyncBufferingOps.delete(bufferKey);
3992
+ unregisterOp(record.id, "bufferingReflection");
3993
+ this.storage.setBufferingReflectionFlag(record.id, false).catch((err) => {
3994
+ omError("[OM] Failed to clear buffering reflection flag", err);
3995
+ });
3996
+ });
3997
+ _ObservationalMemory.asyncBufferingOps.set(bufferKey, asyncOp);
3998
+ }
3999
+ /**
4000
+ * Perform async buffered reflection - reflects observations and stores to bufferedReflection.
4001
+ * Does NOT create a new generation or update activeObservations.
4002
+ */
4003
+ async doAsyncBufferedReflection(record, _bufferKey, writer) {
4004
+ const freshRecord = await this.storage.getObservationalMemory(record.threadId, record.resourceId);
4005
+ const currentRecord = freshRecord ?? record;
4006
+ const observationTokens = currentRecord.observationTokenCount ?? 0;
4007
+ const reflectThreshold = this.getMaxThreshold(this.reflectionConfig.observationTokens);
4008
+ const bufferActivation = this.reflectionConfig.bufferActivation ?? 0.5;
4009
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
4010
+ const cycleId = `reflect-buf-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
4011
+ _ObservationalMemory.reflectionBufferCycleIds.set(_bufferKey, cycleId);
4012
+ const fullObservations = currentRecord.activeObservations ?? "";
4013
+ const allLines = fullObservations.split("\n");
4014
+ const totalLines = allLines.length;
4015
+ const avgTokensPerLine = totalLines > 0 ? observationTokens / totalLines : 0;
4016
+ const activationPointTokens = reflectThreshold * bufferActivation;
4017
+ const linesToReflect = avgTokensPerLine > 0 ? Math.min(Math.floor(activationPointTokens / avgTokensPerLine), totalLines) : totalLines;
4018
+ const activeObservations = allLines.slice(0, linesToReflect).join("\n");
4019
+ const reflectedObservationLineCount = linesToReflect;
4020
+ const sliceTokenEstimate = Math.round(avgTokensPerLine * linesToReflect);
4021
+ const compressionTarget = Math.min(sliceTokenEstimate * bufferActivation, reflectThreshold);
4022
+ omDebug(
4023
+ `[OM:reflect] doAsyncBufferedReflection: slicing observations for reflection \u2014 totalLines=${totalLines}, avgTokPerLine=${avgTokensPerLine.toFixed(1)}, activationPointTokens=${activationPointTokens}, linesToReflect=${linesToReflect}/${totalLines}, sliceTokenEstimate=${sliceTokenEstimate}, compressionTarget=${compressionTarget}`
4024
+ );
4025
+ omDebug(
4026
+ `[OM:reflect] doAsyncBufferedReflection: starting reflector call, recordId=${currentRecord.id}, observationTokens=${sliceTokenEstimate}, compressionTarget=${compressionTarget} (inputTokens), activeObsLength=${activeObservations.length}, reflectedLineCount=${reflectedObservationLineCount}`
4027
+ );
4028
+ if (writer) {
4029
+ const startMarker = this.createBufferingStartMarker({
4030
+ cycleId,
4031
+ operationType: "reflection",
4032
+ tokensToBuffer: sliceTokenEstimate,
4033
+ recordId: record.id,
4034
+ threadId: record.threadId ?? "",
4035
+ threadIds: record.threadId ? [record.threadId] : []
4036
+ });
4037
+ void writer.custom(startMarker).catch(() => {
4038
+ });
4039
+ }
4040
+ const reflectResult = await this.callReflector(
4041
+ activeObservations,
4042
+ void 0,
4043
+ // No manual prompt
4044
+ void 0,
4045
+ // No stream context for background ops
4046
+ compressionTarget,
4047
+ void 0,
4048
+ // No abort signal for background ops
4049
+ true,
4050
+ // Skip continuation hints for async buffering
4051
+ 1
4052
+ // Start at compression level 1 for buffered reflection
4053
+ );
4054
+ const reflectionTokenCount = this.tokenCounter.countObservations(reflectResult.observations);
4055
+ omDebug(
4056
+ `[OM:reflect] doAsyncBufferedReflection: reflector returned ${reflectionTokenCount} tokens (${reflectResult.observations?.length} chars), saving to recordId=${currentRecord.id}`
4057
+ );
4058
+ await this.storage.updateBufferedReflection({
4059
+ id: currentRecord.id,
4060
+ reflection: reflectResult.observations,
4061
+ tokenCount: reflectionTokenCount,
4062
+ inputTokenCount: sliceTokenEstimate,
4063
+ reflectedObservationLineCount
4064
+ });
4065
+ omDebug(
4066
+ `[OM:reflect] doAsyncBufferedReflection: bufferedReflection saved with lineCount=${reflectedObservationLineCount}`
4067
+ );
4068
+ if (writer) {
4069
+ const endMarker = this.createBufferingEndMarker({
4070
+ cycleId,
4071
+ operationType: "reflection",
4072
+ startedAt,
4073
+ tokensBuffered: observationTokens,
4074
+ bufferedTokens: reflectionTokenCount,
4075
+ recordId: currentRecord.id,
4076
+ threadId: currentRecord.threadId ?? "",
4077
+ observations: reflectResult.observations
4078
+ });
4079
+ void writer.custom(endMarker).catch(() => {
4080
+ });
4081
+ }
4082
+ }
4083
+ /**
4084
+ * Try to activate buffered reflection when threshold is reached.
4085
+ * Returns true if activation succeeded, false if no buffered content or activation failed.
4086
+ *
4087
+ * @param record - Current OM record
4088
+ * @param lockKey - Lock key for this scope
4089
+ */
4090
+ async tryActivateBufferedReflection(record, lockKey, writer, messageList) {
4091
+ const bufferKey = this.getReflectionBufferKey(lockKey);
4092
+ const asyncOp = _ObservationalMemory.asyncBufferingOps.get(bufferKey);
4093
+ if (asyncOp) {
4094
+ omDebug(`[OM:reflect] tryActivateBufferedReflection: waiting for in-progress op...`);
4095
+ try {
4096
+ await Promise.race([
4097
+ asyncOp,
4098
+ new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), 6e4))
4099
+ ]);
4100
+ } catch {
4101
+ }
4102
+ }
4103
+ const freshRecord = await this.storage.getObservationalMemory(record.threadId, record.resourceId);
4104
+ omDebug(
4105
+ `[OM:reflect] tryActivateBufferedReflection: recordId=${record.id}, hasBufferedReflection=${!!freshRecord?.bufferedReflection}, bufferedReflectionLen=${freshRecord?.bufferedReflection?.length ?? 0}`
4106
+ );
4107
+ omDebug(
4108
+ `[OM:reflect] tryActivateBufferedReflection: freshRecord.id=${freshRecord?.id}, freshBufferedReflection=${freshRecord?.bufferedReflection ? "present (" + freshRecord.bufferedReflection.length + " chars)" : "empty"}, freshObsTokens=${freshRecord?.observationTokenCount}`
4109
+ );
4110
+ if (!freshRecord?.bufferedReflection) {
4111
+ omDebug(`[OM:reflect] tryActivateBufferedReflection: no buffered reflection after re-fetch, returning false`);
4112
+ return false;
4113
+ }
4114
+ const beforeTokens = freshRecord.observationTokenCount ?? 0;
4115
+ const reflectedLineCount = freshRecord.reflectedObservationLineCount ?? 0;
4116
+ const currentObservations = freshRecord.activeObservations ?? "";
4117
+ const allLines = currentObservations.split("\n");
4118
+ const unreflectedLines = allLines.slice(reflectedLineCount);
4119
+ const unreflectedContent = unreflectedLines.join("\n").trim();
4120
+ const combinedObservations = unreflectedContent ? `${freshRecord.bufferedReflection}
4121
+
4122
+ ${unreflectedContent}` : freshRecord.bufferedReflection;
4123
+ const combinedTokenCount = this.tokenCounter.countObservations(combinedObservations);
4124
+ omDebug(
4125
+ `[OM:reflect] tryActivateBufferedReflection: activating, beforeTokens=${beforeTokens}, combinedTokenCount=${combinedTokenCount}, reflectedLineCount=${reflectedLineCount}, unreflectedLines=${unreflectedLines.length}`
4126
+ );
4127
+ await this.storage.swapBufferedReflectionToActive({
4128
+ currentRecord: freshRecord,
4129
+ tokenCount: combinedTokenCount
4130
+ });
4131
+ _ObservationalMemory.lastBufferedBoundary.delete(bufferKey);
4132
+ const afterRecord = await this.storage.getObservationalMemory(record.threadId, record.resourceId);
4133
+ const afterTokens = afterRecord?.observationTokenCount ?? 0;
4134
+ omDebug(
4135
+ `[OM:reflect] tryActivateBufferedReflection: activation complete! beforeTokens=${beforeTokens}, afterTokens=${afterTokens}, newRecordId=${afterRecord?.id}, newGenCount=${afterRecord?.generationCount}`
4136
+ );
4137
+ if (writer) {
4138
+ const originalCycleId = _ObservationalMemory.reflectionBufferCycleIds.get(bufferKey);
4139
+ const activationMarker = this.createActivationMarker({
4140
+ cycleId: originalCycleId ?? `reflect-act-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,
4141
+ operationType: "reflection",
4142
+ chunksActivated: 1,
4143
+ tokensActivated: beforeTokens,
4144
+ observationTokens: afterTokens,
4145
+ messagesActivated: 0,
4146
+ recordId: freshRecord.id,
4147
+ threadId: freshRecord.threadId ?? "",
4148
+ generationCount: afterRecord?.generationCount ?? freshRecord.generationCount ?? 0,
4149
+ observations: afterRecord?.activeObservations
4150
+ });
4151
+ void writer.custom(activationMarker).catch(() => {
4152
+ });
4153
+ await this.persistMarkerToMessage(
4154
+ activationMarker,
4155
+ messageList,
4156
+ freshRecord.threadId ?? "",
4157
+ freshRecord.resourceId ?? void 0
4158
+ );
4159
+ }
4160
+ _ObservationalMemory.reflectionBufferCycleIds.delete(bufferKey);
4161
+ return true;
4162
+ }
2663
4163
  /**
2664
4164
  * Resource-scoped observation: observe ALL threads with unobserved messages.
2665
4165
  * Threads are observed in oldest-first order to ensure no thread's messages
@@ -2747,6 +4247,7 @@ ${result.observations}` : result.observations;
2747
4247
  new Map(threadsToObserve.map((tid) => [tid, messagesByThread.get(tid) ?? []]))
2748
4248
  );
2749
4249
  await this.storage.setObservingFlag(record.id, true);
4250
+ registerOp(record.id, "observing");
2750
4251
  const cycleId = crypto.randomUUID();
2751
4252
  const threadsWithMessages = /* @__PURE__ */ new Map();
2752
4253
  const threadTokensToObserve = /* @__PURE__ */ new Map();
@@ -2962,24 +4463,91 @@ ${result.observations}` : result.observations;
2962
4463
  if (abortSignal?.aborted) {
2963
4464
  throw error;
2964
4465
  }
2965
- console.error(`[OM] Resource-scoped observation failed:`, error instanceof Error ? error.message : String(error));
4466
+ omError("[OM] Resource-scoped observation failed", error);
2966
4467
  } finally {
2967
4468
  await this.storage.setObservingFlag(record.id, false);
4469
+ unregisterOp(record.id, "observing");
4470
+ }
4471
+ }
4472
+ /**
4473
+ * Check if async reflection should be triggered or activated.
4474
+ * Only handles the async path — will never do synchronous (blocking) reflection.
4475
+ * Safe to call after buffered observation activation.
4476
+ */
4477
+ async maybeAsyncReflect(record, observationTokens, writer, messageList) {
4478
+ if (!this.isAsyncReflectionEnabled()) return;
4479
+ const lockKey = this.getLockKey(record.threadId, record.resourceId);
4480
+ const reflectThreshold = this.getMaxThreshold(this.reflectionConfig.observationTokens);
4481
+ omDebug(
4482
+ `[OM:reflect] maybeAsyncReflect: observationTokens=${observationTokens}, reflectThreshold=${reflectThreshold}, isReflecting=${record.isReflecting}, bufferedReflection=${record.bufferedReflection ? "present (" + record.bufferedReflection.length + " chars)" : "empty"}, recordId=${record.id}, genCount=${record.generationCount}`
4483
+ );
4484
+ if (observationTokens < reflectThreshold) {
4485
+ const shouldTrigger = this.shouldTriggerAsyncReflection(observationTokens, lockKey, record);
4486
+ omDebug(`[OM:reflect] below threshold: shouldTrigger=${shouldTrigger}`);
4487
+ if (shouldTrigger) {
4488
+ this.startAsyncBufferedReflection(record, observationTokens, lockKey, writer);
4489
+ }
4490
+ return;
4491
+ }
4492
+ if (record.isReflecting) {
4493
+ if (isOpActiveInProcess(record.id, "reflecting")) {
4494
+ omDebug(`[OM:reflect] skipping - actively reflecting in this process`);
4495
+ return;
4496
+ }
4497
+ omDebug(`[OM:reflect] isReflecting=true but stale (not active in this process), clearing`);
4498
+ await this.storage.setReflectingFlag(record.id, false);
2968
4499
  }
4500
+ omDebug(`[OM:reflect] at/above threshold, trying activation...`);
4501
+ const activationSuccess = await this.tryActivateBufferedReflection(record, lockKey, writer, messageList);
4502
+ omDebug(`[OM:reflect] activationSuccess=${activationSuccess}`);
4503
+ if (activationSuccess) return;
4504
+ omDebug(`[OM:reflect] no buffered reflection, starting background reflection...`);
4505
+ this.startAsyncBufferedReflection(record, observationTokens, lockKey, writer);
2969
4506
  }
2970
4507
  /**
2971
4508
  * Check if reflection needed and trigger if so.
2972
- * SIMPLIFIED: Always uses synchronous reflection (async buffering disabled).
4509
+ * Supports both synchronous reflection and async buffered reflection.
4510
+ * When async buffering is enabled via `bufferTokens`, reflection is triggered
4511
+ * in the background at intervals, and activated when the threshold is reached.
2973
4512
  */
2974
- async maybeReflect(record, observationTokens, _threadId, writer, abortSignal) {
4513
+ async maybeReflect(record, observationTokens, _threadId, writer, abortSignal, messageList) {
4514
+ const lockKey = this.getLockKey(record.threadId, record.resourceId);
4515
+ const reflectThreshold = this.getMaxThreshold(this.reflectionConfig.observationTokens);
4516
+ if (this.isAsyncReflectionEnabled() && observationTokens < reflectThreshold) {
4517
+ if (this.shouldTriggerAsyncReflection(observationTokens, lockKey, record)) {
4518
+ this.startAsyncBufferedReflection(record, observationTokens, lockKey, writer);
4519
+ }
4520
+ }
2975
4521
  if (!this.shouldReflect(observationTokens)) {
2976
4522
  return;
2977
4523
  }
2978
4524
  if (record.isReflecting) {
2979
- return;
4525
+ if (isOpActiveInProcess(record.id, "reflecting")) {
4526
+ omDebug(`[OM:reflect] isReflecting=true and active in this process, skipping`);
4527
+ return;
4528
+ }
4529
+ omDebug(`[OM:reflect] isReflecting=true but NOT active in this process \u2014 stale flag from dead process, clearing`);
4530
+ await this.storage.setReflectingFlag(record.id, false);
4531
+ }
4532
+ if (this.isAsyncReflectionEnabled()) {
4533
+ const activationSuccess = await this.tryActivateBufferedReflection(record, lockKey, writer, messageList);
4534
+ if (activationSuccess) {
4535
+ return;
4536
+ }
4537
+ if (this.reflectionConfig.blockAfter && observationTokens >= this.reflectionConfig.blockAfter) {
4538
+ omDebug(
4539
+ `[OM:reflect] blockAfter exceeded (${observationTokens} >= ${this.reflectionConfig.blockAfter}), falling through to sync reflection`
4540
+ );
4541
+ } else {
4542
+ omDebug(
4543
+ `[OM:reflect] async activation failed, no blockAfter or below it (obsTokens=${observationTokens}, blockAfter=${this.reflectionConfig.blockAfter}) \u2014 starting background reflection`
4544
+ );
4545
+ this.startAsyncBufferedReflection(record, observationTokens, lockKey, writer);
4546
+ return;
4547
+ }
2980
4548
  }
2981
- const reflectThreshold = this.getMaxThreshold(this.reflectionConfig.observationTokens);
2982
4549
  await this.storage.setReflectingFlag(record.id, true);
4550
+ registerOp(record.id, "reflecting");
2983
4551
  const cycleId = crypto.randomUUID();
2984
4552
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
2985
4553
  const threadId = _threadId ?? "unknown";
@@ -3065,9 +4633,10 @@ ${result.observations}` : result.observations;
3065
4633
  if (abortSignal?.aborted) {
3066
4634
  throw error;
3067
4635
  }
3068
- console.error(`[OM] Reflection failed:`, error instanceof Error ? error.message : String(error));
4636
+ omError("[OM] Reflection failed", error);
3069
4637
  } finally {
3070
4638
  await this.storage.setReflectingFlag(record.id, false);
4639
+ unregisterOp(record.id, "reflecting");
3071
4640
  }
3072
4641
  }
3073
4642
  /**
@@ -3115,6 +4684,7 @@ ${result.observations}` : result.observations;
3115
4684
  return;
3116
4685
  }
3117
4686
  await this.storage.setReflectingFlag(record.id, true);
4687
+ registerOp(record.id, "reflecting");
3118
4688
  try {
3119
4689
  const reflectThreshold = this.getMaxThreshold(this.reflectionConfig.observationTokens);
3120
4690
  const reflectResult = await this.callReflector(record.activeObservations, prompt, void 0, reflectThreshold);
@@ -3126,6 +4696,7 @@ ${result.observations}` : result.observations;
3126
4696
  });
3127
4697
  } finally {
3128
4698
  await this.storage.setReflectingFlag(record.id, false);
4699
+ unregisterOp(record.id, "reflecting");
3129
4700
  }
3130
4701
  }
3131
4702
  /**
@@ -3156,6 +4727,7 @@ ${result.observations}` : result.observations;
3156
4727
  async clear(threadId, resourceId) {
3157
4728
  const ids = this.getStorageIds(threadId, resourceId);
3158
4729
  await this.storage.clearObservationalMemory(ids.threadId, ids.resourceId);
4730
+ this.cleanupStaticMaps(ids.threadId ?? ids.resourceId, ids.resourceId);
3159
4731
  }
3160
4732
  /**
3161
4733
  * Get the underlying storage adapter
@@ -3184,5 +4756,5 @@ ${result.observations}` : result.observations;
3184
4756
  };
3185
4757
 
3186
4758
  export { OBSERVATIONAL_MEMORY_DEFAULTS, OBSERVER_SYSTEM_PROMPT, ObservationalMemory, TokenCounter, buildObserverPrompt, buildObserverSystemPrompt, extractCurrentTask, formatMessagesForObserver, hasCurrentTaskSection, optimizeObservationsForContext, parseObserverOutput };
3187
- //# sourceMappingURL=chunk-6TXUWFIU.js.map
3188
- //# sourceMappingURL=chunk-6TXUWFIU.js.map
4759
+ //# sourceMappingURL=chunk-TYVPTNCP.js.map
4760
+ //# sourceMappingURL=chunk-TYVPTNCP.js.map