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