@mastra/memory 1.37.0 → 1.37.1-alpha.1

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 (55) hide show
  1. package/dist/docs/SKILL.md +1 -1
  2. package/dist/docs/assets/SOURCE_MAP.json +1 -1
  3. package/dist/docs/references/docs-memory-multi-user-threads.md +1 -1
  4. package/dist/docs/references/docs-memory-observational-memory.md +17 -15
  5. package/dist/docs/references/reference-memory-listThreads.md +1 -1
  6. package/dist/docs/references/reference-memory-observational-memory.md +3 -3
  7. package/dist/docs/references/reference-processors-token-limiter-processor.md +2 -2
  8. package/dist/docs/references/reference-vectors-libsql.md +1 -1
  9. package/dist/docs/references/reference-vectors-mongodb.md +2 -1
  10. package/dist/docs/references/reference-vectors-pg.md +16 -11
  11. package/dist/docs/references/reference-vectors-upstash.md +10 -4
  12. package/dist/index.cjs +1 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +1 -1
  15. package/dist/processors/index.cjs +1 -1
  16. package/dist/processors/index.js +1 -1
  17. package/dist/processors/observational-memory/buffering-coordinator.d.ts +25 -0
  18. package/dist/processors/observational-memory/buffering-coordinator.d.ts.map +1 -1
  19. package/dist/processors/observational-memory/commit-queue.d.ts +46 -0
  20. package/dist/processors/observational-memory/commit-queue.d.ts.map +1 -0
  21. package/dist/processors/observational-memory/extraction-runner.d.ts.map +1 -1
  22. package/dist/processors/observational-memory/observation-strategies/async-buffer.d.ts +7 -4
  23. package/dist/processors/observational-memory/observation-strategies/async-buffer.d.ts.map +1 -1
  24. package/dist/processors/observational-memory/observation-strategies/base.d.ts +93 -6
  25. package/dist/processors/observational-memory/observation-strategies/base.d.ts.map +1 -1
  26. package/dist/processors/observational-memory/observation-strategies/resource-scoped.d.ts +20 -3
  27. package/dist/processors/observational-memory/observation-strategies/resource-scoped.d.ts.map +1 -1
  28. package/dist/processors/observational-memory/observation-strategies/sync.d.ts +10 -3
  29. package/dist/processors/observational-memory/observation-strategies/sync.d.ts.map +1 -1
  30. package/dist/processors/observational-memory/observation-strategies/types.d.ts +30 -0
  31. package/dist/processors/observational-memory/observation-strategies/types.d.ts.map +1 -1
  32. package/dist/processors/observational-memory/observation-turn/step.d.ts +1 -1
  33. package/dist/processors/observational-memory/observation-turn/step.d.ts.map +1 -1
  34. package/dist/processors/observational-memory/observational-memory.d.ts +30 -6
  35. package/dist/processors/observational-memory/observational-memory.d.ts.map +1 -1
  36. package/dist/processors/observational-memory/observer-agent.d.ts +8 -0
  37. package/dist/processors/observational-memory/observer-agent.d.ts.map +1 -1
  38. package/dist/processors/observational-memory/observer-runner.d.ts +1 -1
  39. package/dist/processors/observational-memory/observer-runner.d.ts.map +1 -1
  40. package/dist/processors/observational-memory/processor.d.ts.map +1 -1
  41. package/dist/processors/observational-memory/record-lineage.d.ts +21 -0
  42. package/dist/processors/observational-memory/record-lineage.d.ts.map +1 -0
  43. package/dist/processors/observational-memory/reflector-agent.d.ts.map +1 -1
  44. package/dist/processors/observational-memory/reflector-runner.d.ts +9 -0
  45. package/dist/processors/observational-memory/reflector-runner.d.ts.map +1 -1
  46. package/dist/processors/observational-memory/types.d.ts +10 -8
  47. package/dist/processors/observational-memory/types.d.ts.map +1 -1
  48. package/dist/processors/observational-memory/working-memory-extractor.d.ts.map +1 -1
  49. package/dist/{src-D-jA6eRb.cjs → src-BcRCGtUY.cjs} +1062 -302
  50. package/dist/src-BcRCGtUY.cjs.map +1 -0
  51. package/dist/{src-B9qhQmNI.js → src-DjDnBIS0.js} +1063 -303
  52. package/dist/src-DjDnBIS0.js.map +1 -0
  53. package/package.json +3 -3
  54. package/dist/src-B9qhQmNI.js.map +0 -1
  55. package/dist/src-D-jA6eRb.cjs.map +0 -1
@@ -19,7 +19,7 @@ import { join } from "path";
19
19
  import { createTool } from "@mastra/core/tools";
20
20
  import { MASTRA_THREAD_ID_KEY, RequestContext } from "@mastra/core/request-context";
21
21
  import { modelSupportsAttachments, modelSupportsTemperature, resolveModelConfig } from "@mastra/core/llm";
22
- import { AsyncLocalStorage } from "async_hooks";
22
+ import { AsyncLocalStorage, AsyncResource } from "async_hooks";
23
23
  import { createHash } from "crypto";
24
24
  import probeImageSizeSync from "probe-image-size/sync.js";
25
25
  import { isStandardSchemaWithJSON as isStandardSchemaWithJSON$1, toStandardSchema as toStandardSchema$1 } from "@mastra/core/schema";
@@ -19230,8 +19230,12 @@ function formatOmError(error) {
19230
19230
  function isAbortError(error, abortSignal) {
19231
19231
  return abortSignal?.aborted === true || hasAbortInChain(error);
19232
19232
  }
19233
+ function hasEmptyObjectValue(object, extractor) {
19234
+ const value = object[extractor.slug];
19235
+ return typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length === 0;
19236
+ }
19233
19237
  function shouldRetryEmptyStructuredObject(object, extractors) {
19234
- return Object.keys(object).length === 0 && extractors.some((extractor) => extractor.retryStructuredExtractionOnEmptyObject);
19238
+ return extractors.some((extractor) => extractor.retryStructuredExtractionOnEmptyObject && (Object.keys(object).length === 0 || hasEmptyObjectValue(object, extractor)));
19235
19239
  }
19236
19240
  async function extractStructuredValues(opts) {
19237
19241
  const structuredExtractors = (opts.extractors ?? []).filter((extractor) => extractor.mode === "structured");
@@ -19305,17 +19309,28 @@ ${extractorInstructions}${priorLines.length > 0 ? `\n\n## Prior Extracted Values
19305
19309
  }
19306
19310
  }
19307
19311
  if (retryEmptyObject) try {
19308
- object = await streamWithStructuredOutput(coreFeatures.has("json-prompt-injection:inline") ? "inline" : true);
19312
+ const fallbackJsonPromptInjection = coreFeatures.has("json-prompt-injection:inline") ? "inline" : true;
19313
+ object = {
19314
+ ...object,
19315
+ ...await streamWithStructuredOutput(fallbackJsonPromptInjection)
19316
+ };
19309
19317
  } catch (fallbackError) {
19310
19318
  if (isAbortError(fallbackError, opts.abortSignal)) throw fallbackError;
19311
19319
  const message = fallbackError instanceof Error ? fallbackError.message : String(fallbackError);
19312
- return {
19320
+ if (Object.keys(object).length === 0) return {
19313
19321
  values,
19314
19322
  failures: structuredExtractors.map((extractor) => ({
19315
19323
  slug: extractor.slug,
19316
19324
  error: message
19317
19325
  }))
19318
19326
  };
19327
+ for (const extractor of structuredExtractors) if (extractor.retryStructuredExtractionOnEmptyObject && hasEmptyObjectValue(object, extractor)) {
19328
+ failures.push({
19329
+ slug: extractor.slug,
19330
+ error: message
19331
+ });
19332
+ delete object[extractor.slug];
19333
+ }
19319
19334
  }
19320
19335
  for (const extractor of structuredExtractors) {
19321
19336
  const value = object[extractor.slug];
@@ -20557,7 +20572,7 @@ Group related observations (like tool sequences) by indenting:
20557
20572
  * -> applied fix, tests now pass
20558
20573
  * ✅ Tests passing, auth issue resolved
20559
20574
 
20560
- Group observations by date, then list each with 24-hour time.
20575
+ Group observations by date. Within a date, list observations in the order the events happened (not by importance), each with 24-hour time.
20561
20576
 
20562
20577
  <observations>
20563
20578
  Date: Dec 4, 2025
@@ -20930,8 +20945,8 @@ function extractToolResultAttachments(result, counter, attachmentFilter) {
20930
20945
  attachments
20931
20946
  };
20932
20947
  }
20933
- function formatObserverPartLine(title, body, time, previousTime) {
20934
- const timeLabel = time && time !== previousTime ? `(${time})` : "";
20948
+ function formatObserverPartLine(title, body, time, previousTime, alwaysShowTime) {
20949
+ const timeLabel = time && (alwaysShowTime || time !== previousTime) ? `(${time})` : "";
20935
20950
  if (!title) return timeLabel ? `${timeLabel}: ${body}` : body;
20936
20951
  return `${title}${timeLabel ? ` ${timeLabel}` : ""}: ${body}`;
20937
20952
  }
@@ -20956,7 +20971,7 @@ function formatObserverLines(lines, context = {}) {
20956
20971
  previousDate = line.date;
20957
20972
  previousTime = void 0;
20958
20973
  }
20959
- output.push(formatObserverPartLine(line.title, line.body, line.time, previousTime));
20974
+ output.push(formatObserverPartLine(line.title, line.body, line.time, previousTime, line.alwaysShowTime));
20960
20975
  previousTime = line.time || previousTime;
20961
20976
  }
20962
20977
  return {
@@ -21057,14 +21072,15 @@ function formatObserverMessage(msg, counter, toolFormatting, options) {
21057
21072
  const unavailableUrls = getUnavailableAttachmentUrls(msg);
21058
21073
  let lines = [];
21059
21074
  const temporalGapText = isTemporalGapMarker(msg) ? getTemporalGapMarkerText(msg) : void 0;
21060
- const pushLine = (title, body, createdAt) => {
21075
+ const pushLine = (title, body, createdAt, opts) => {
21061
21076
  if (!body) return;
21062
21077
  const normalizedCreatedAt = normalizeObserverCreatedAt(createdAt) ?? messageCreatedAt;
21063
21078
  lines.push({
21064
21079
  date: formatObserverDate(normalizedCreatedAt, timeZone),
21065
21080
  time: formatObserverTime(normalizedCreatedAt, timeZone),
21066
21081
  title,
21067
- body
21082
+ body,
21083
+ alwaysShowTime: opts?.tool === true
21068
21084
  });
21069
21085
  };
21070
21086
  if (temporalGapText) pushLine("", temporalGapText, messageCreatedAt);
@@ -21081,7 +21097,7 @@ function formatObserverMessage(msg, counter, toolFormatting, options) {
21081
21097
  const exchange = toolFormatting.exchanges.get(inv.toolCallId);
21082
21098
  if (exchange && occurrence === exchange.firstOccurrence && exchange.signature && !toolFormatting.emittedCalls.has(inv.toolCallId)) {
21083
21099
  toolFormatting.emittedCalls.add(inv.toolCallId);
21084
- pushLine(`Tool Call ${exchange.signature.toolName}`, formatObserverToolArguments(exchange.signature.args, maxLen), partCreatedAt);
21100
+ pushLine(`Tool Call ${exchange.signature.toolName}`, formatObserverToolArguments(exchange.signature.args, maxLen), partCreatedAt, { tool: true });
21085
21101
  }
21086
21102
  if (inv.state === "result") {
21087
21103
  const { value: resultForObserver } = resolveToolResultValue(part, inv.result);
@@ -21091,17 +21107,17 @@ function formatObserverMessage(msg, counter, toolFormatting, options) {
21091
21107
  toolFormatting.emittedOutcomes.add(inv.toolCallId);
21092
21108
  const hasResult = resultWithoutAttachments !== void 0 && (typeof resultWithoutAttachments !== "string" || resultWithoutAttachments.trim().length > 0);
21093
21109
  const body = inv.isError ? hasResult ? formatToolResultForObserver(resultWithoutAttachments, { maxTokens: maxToolResultTokens }) : inv.errorText?.trim() ? inv.errorText : "Tool execution failed" : hasResult ? formatToolResultForObserver(resultWithoutAttachments, { maxTokens: maxToolResultTokens }) : "[empty result]";
21094
- pushLine(`${inv.isError ? "Tool Error" : "Tool Result"} ${inv.toolName}`, maybeTruncate(body, maxLen), partCreatedAt);
21110
+ pushLine(`${inv.isError ? "Tool Error" : "Tool Result"} ${inv.toolName}`, maybeTruncate(body, maxLen), partCreatedAt, { tool: true });
21095
21111
  }
21096
21112
  return;
21097
21113
  }
21098
21114
  if (exchange?.terminal?.occurrence === occurrence && !toolFormatting.emittedOutcomes.has(inv.toolCallId)) {
21099
21115
  toolFormatting.emittedOutcomes.add(inv.toolCallId);
21100
- if (inv.state === "output-error") pushLine(`Tool Error ${inv.toolName}`, maybeTruncate(inv.errorText?.trim() ? inv.errorText : "Tool execution failed", maxLen), partCreatedAt);
21101
- else if (inv.state === "output-denied") pushLine(`Tool Denied ${inv.toolName}`, maybeTruncate(inv.approval?.reason?.trim() ? inv.approval.reason : "Tool call was not approved by the user", maxLen), partCreatedAt);
21116
+ if (inv.state === "output-error") pushLine(`Tool Error ${inv.toolName}`, maybeTruncate(inv.errorText?.trim() ? inv.errorText : "Tool execution failed", maxLen), partCreatedAt, { tool: true });
21117
+ else if (inv.state === "output-denied") pushLine(`Tool Denied ${inv.toolName}`, maybeTruncate(inv.approval?.reason?.trim() ? inv.approval.reason : "Tool call was not approved by the user", maxLen), partCreatedAt, { tool: true });
21102
21118
  return;
21103
21119
  }
21104
- if (!exchange) pushLine(`Tool Call ${inv.toolName}`, formatObserverToolArguments(inv.args, maxLen), partCreatedAt);
21120
+ if (!exchange) pushLine(`Tool Call ${inv.toolName}`, formatObserverToolArguments(inv.args, maxLen), partCreatedAt, { tool: true });
21105
21121
  return;
21106
21122
  }
21107
21123
  const partType = part.type;
@@ -21256,13 +21272,17 @@ function surroundObserverHistory(history, contextPrompt, taskInstructions) {
21256
21272
  function buildMultiThreadObserverTaskInstructions(includeThreadTitle) {
21257
21273
  return `## Your Task\n\nExtract new observations from each thread. Output your observations grouped by thread using <thread id="..."> tags inside your <observations> block. Each thread block should contain that thread's observations, current-task, suggested-response${includeThreadTitle ? ", and thread-title" : ""}, in the format specified in your instructions.`;
21258
21274
  }
21275
+ function buildTruncationNotice(wasTruncated) {
21276
+ if (!wasTruncated) return "";
21277
+ return "Previous observations were truncated for context budget reasons.\nThe main agent still has full memory context outside this observer window.\n\n";
21278
+ }
21259
21279
  function buildMultiThreadObserverContextPrompt(existingObservations, threadOrder, priorMetadataByThread, wasTruncated, includeThreadTitle, extractors = []) {
21260
21280
  let prompt = "";
21261
21281
  if (existingObservations) {
21262
21282
  prompt += `## Previous Observations\n\n${existingObservations}\n\n---\n\n`;
21263
21283
  prompt += "Do not repeat these existing observations. Your new observations will be appended to the existing observations.\n\n";
21264
21284
  }
21265
- const hasTruncatedObservations = wasTruncated ?? false;
21285
+ prompt += buildTruncationNotice(wasTruncated);
21266
21286
  const threadMetadataLines = threadOrder?.map((threadId) => {
21267
21287
  const metadata = priorMetadataByThread?.get(threadId);
21268
21288
  const extractorPriorLines = buildExtractorPriorLines(extractors, metadata?.extracted);
@@ -21276,10 +21296,6 @@ function buildMultiThreadObserverContextPrompt(existingObservations, threadOrder
21276
21296
  }).filter(Boolean).join("\n");
21277
21297
  if (threadMetadataLines) {
21278
21298
  prompt += `## Prior Thread Metadata\n\n${threadMetadataLines}\n\n`;
21279
- if (hasTruncatedObservations) {
21280
- prompt += `Previous observations were truncated for context budget reasons.\n`;
21281
- prompt += `The main agent still has full memory context outside this observer window.\n`;
21282
- }
21283
21299
  prompt += `Use each thread's prior current-task, suggested-response${includeThreadTitle ? ", and thread-title" : ""} as continuity hints, then update them based on that thread's new messages.\n\n---\n\n`;
21284
21300
  }
21285
21301
  return prompt;
@@ -21322,7 +21338,7 @@ function parseMultiThreadObserverOutput(output, extractors = []) {
21322
21338
  threadTitle = threadTitleMatch[1].trim();
21323
21339
  observations = observations.replace(/<thread-title>[\s\S]*?<\/thread-title>/i, "");
21324
21340
  }
21325
- observations = sanitizeObservationLines(observations.trim());
21341
+ observations = sortObservationsByTime(sanitizeObservationLines(observations.trim()));
21326
21342
  threads.set(threadId, {
21327
21343
  observations,
21328
21344
  currentTask: currentTask || getStringExtractedValue$1(parsedExtractedValues.values, "current-task"),
@@ -21362,7 +21378,7 @@ function buildObserverContextPrompt(existingObservations, options) {
21362
21378
  prompt += `## Previous Observations\n\n${existingObservations}\n\n---\n\n`;
21363
21379
  prompt += "Do not repeat these existing observations. Your new observations will be appended to the existing observations.\n\n";
21364
21380
  }
21365
- const hasTruncatedObservations = options?.wasTruncated ?? false;
21381
+ prompt += buildTruncationNotice(options?.wasTruncated);
21366
21382
  const priorMetadataLines = [];
21367
21383
  if (options?.priorCurrentTask) priorMetadataLines.push(`- prior current-task: ${options.priorCurrentTask}`);
21368
21384
  if (options?.priorSuggestedResponse) priorMetadataLines.push(`- prior suggested-response: ${options.priorSuggestedResponse}`);
@@ -21370,10 +21386,6 @@ function buildObserverContextPrompt(existingObservations, options) {
21370
21386
  priorMetadataLines.push(...buildExtractorPriorLines(options?.extractors ?? [], options?.priorExtractedValues));
21371
21387
  if (priorMetadataLines.length > 0) {
21372
21388
  prompt += `## Prior Thread Metadata\n\n${priorMetadataLines.join("\n")}\n\n`;
21373
- if (hasTruncatedObservations) {
21374
- prompt += `Previous observations were truncated for context budget reasons.\n`;
21375
- prompt += `The main agent still has full memory context outside this observer window.\n`;
21376
- }
21377
21389
  const titleHint = options?.includeThreadTitle ? ", and thread-title" : "";
21378
21390
  prompt += `Use the prior current-task, suggested-response${titleHint} as continuity hints, then update them based on the new messages.\n\n---\n\n`;
21379
21391
  }
@@ -21404,7 +21416,7 @@ function parseObserverOutput(output, extractors = []) {
21404
21416
  const parsedExtractedValues = parseExtractedValues(output, inlineExtractors);
21405
21417
  const parsed = parseMemorySectionXml(stripExtractorSections(output, inlineExtractors));
21406
21418
  return {
21407
- observations: sanitizeObservationLines(parsed.observations || ""),
21419
+ observations: sortObservationsByTime(sanitizeObservationLines(parsed.observations || "")),
21408
21420
  currentTask: parsed.currentTask || getStringExtractedValue$1(parsedExtractedValues.values, "current-task"),
21409
21421
  suggestedContinuation: parsed.suggestedResponse || getStringExtractedValue$1(parsedExtractedValues.values, "suggested-response"),
21410
21422
  threadTitle: parsed.threadTitle || getStringExtractedValue$1(parsedExtractedValues.values, "thread-title"),
@@ -21498,6 +21510,68 @@ function sanitizeObservationLines(observations) {
21498
21510
  }
21499
21511
  return changed ? lines.join("\n") : observations;
21500
21512
  }
21513
+ const OBSERVATION_TIME = /^[*-]\s+(?:\S+\s+)?\((\d{1,2}):(\d{2})\s*([AaPp])?\.?(?:[Mm]\.?)?\)/;
21514
+ /** Minutes since midnight for each item, or undefined when the group's times can't be ordered safely. */
21515
+ function observationMinutes(items) {
21516
+ const times = items.map((item) => OBSERVATION_TIME.exec(item[0]));
21517
+ if (times.some((t) => !t)) return void 0;
21518
+ const matches = times;
21519
+ const marked = matches.filter((m) => m[3]).length;
21520
+ if (marked > 0 && marked < matches.length) return void 0;
21521
+ if (marked === matches.length) return matches.map((m) => Number(m[1]) % 12 * 60 + (/p/i.test(m[3]) ? 720 : 0) + Number(m[2]));
21522
+ const hours = matches.map((m) => m[1]);
21523
+ const twelveHour = hours.some((h) => /^[1-9]$/.test(h));
21524
+ const twentyFourHour = hours.some((h) => /^(0\d|1[3-9]|2\d)$/.test(h));
21525
+ if (twelveHour && (twentyFourHour || new Set(hours).size > 1)) return void 0;
21526
+ return matches.map((m) => Number(m[1]) * 60 + Number(m[2]));
21527
+ }
21528
+ /**
21529
+ * Put the top-level observations of each "Date:" group in time order, keeping indented
21530
+ * sub-items with their parent. The Observer writes a time on every observation but tends
21531
+ * to list a user's message ahead of the earlier events it follows, which can make a
21532
+ * superseded state read as the latest one. Groups are left as written when any
21533
+ * observation has no time or the times are ambiguous.
21534
+ */
21535
+ function sortObservationsByTime(observations) {
21536
+ if (!observations) return observations;
21537
+ const output = [];
21538
+ let group;
21539
+ const flush = () => {
21540
+ if (!group) return;
21541
+ const minutes = group.sortable && group.items.length > 1 ? observationMinutes(group.items) : void 0;
21542
+ const items = minutes ? group.items.map((item, i) => ({
21543
+ item,
21544
+ minute: minutes[i]
21545
+ })).sort((a, b) => a.minute - b.minute).map((x) => x.item) : group.items;
21546
+ output.push(...group.header, ...items.flat(), ...group.trailing);
21547
+ group = void 0;
21548
+ };
21549
+ for (const line of observations.split("\n")) if (/^\s*Date:\s/.test(line)) {
21550
+ flush();
21551
+ group = {
21552
+ header: [line],
21553
+ items: [],
21554
+ trailing: [],
21555
+ sortable: true
21556
+ };
21557
+ } else if (!group) output.push(line);
21558
+ else if (/^[*-]\s/.test(line)) {
21559
+ group.items.at(-1)?.push(...group.trailing);
21560
+ group.trailing = [];
21561
+ group.items.push([line]);
21562
+ } else if (group.items.length === 0) group.header.push(line);
21563
+ else if (line.trim() === "") group.trailing.push(line);
21564
+ else if (/^\s/.test(line)) {
21565
+ group.items.at(-1).push(...group.trailing, line);
21566
+ group.trailing = [];
21567
+ } else {
21568
+ group.sortable = false;
21569
+ group.items.at(-1).push(...group.trailing, line);
21570
+ group.trailing = [];
21571
+ }
21572
+ flush();
21573
+ return output.join("\n");
21574
+ }
21501
21575
  function analyzeDegenerateRepetition(text) {
21502
21576
  const lines = collapseBoundedLineRuns(text.split("\n"));
21503
21577
  const windowText = lines.filter((line) => line.trim().length >= MIN_DUPLICATE_LINE_CHARS && line.length <= MAX_OBSERVATION_LINE_CHARS).join("\n");
@@ -22040,7 +22114,7 @@ var ObserverRunner = class {
22040
22114
  /**
22041
22115
  * Call the Observer agent for multiple threads in a single batched request.
22042
22116
  */
22043
- async callMultiThread(existingObservations, allMessagesByThread, allThreadOrder, abortSignal, requestContext, priorMetadataByThread, observabilityContext, model, hookContext, timeZone, mainAgent, currentModel) {
22117
+ async callMultiThread(existingObservations, allMessagesByThread, allThreadOrder, abortSignal, requestContext, priorMetadataByThread, observabilityContext, model, hookContext, timeZone, mainAgent, currentModel, wasTruncated) {
22044
22118
  const contextFor = (threadId) => ({
22045
22119
  threadId,
22046
22120
  ...hookContext
@@ -22051,7 +22125,7 @@ var ObserverRunner = class {
22051
22125
  if (filtered.length > 0) messagesByThread.set(threadId, filtered);
22052
22126
  }
22053
22127
  const threadOrder = allThreadOrder.filter((threadId) => messagesByThread.has(threadId));
22054
- const output = await this.callMultiThreadObserver(existingObservations, messagesByThread, threadOrder, abortSignal, requestContext, priorMetadataByThread, observabilityContext, model, timeZone, mainAgent, currentModel);
22128
+ const output = await this.callMultiThreadObserver(existingObservations, messagesByThread, threadOrder, abortSignal, requestContext, priorMetadataByThread, observabilityContext, model, timeZone, mainAgent, currentModel, wasTruncated);
22055
22129
  for (const threadId of allThreadOrder) {
22056
22130
  const threadResult = output.results.get(threadId);
22057
22131
  if (!threadResult) {
@@ -22062,7 +22136,7 @@ var ObserverRunner = class {
22062
22136
  }
22063
22137
  return output;
22064
22138
  }
22065
- async callMultiThreadObserver(existingObservations, messagesByThread, threadOrder, abortSignal, requestContext, priorMetadataByThread, observabilityContext, model, timeZone, mainAgent, currentModel) {
22139
+ async callMultiThreadObserver(existingObservations, messagesByThread, threadOrder, abortSignal, requestContext, priorMetadataByThread, observabilityContext, model, timeZone, mainAgent, currentModel, wasTruncated) {
22066
22140
  if (threadOrder.length === 0) return { results: /* @__PURE__ */ new Map() };
22067
22141
  const inputTokens = Array.from(messagesByThread.values()).reduce((total, messages) => total + this.tokenCounter.countMessages(messages), 0);
22068
22142
  let resolvedModel;
@@ -22108,7 +22182,8 @@ var ObserverRunner = class {
22108
22182
  priorThreadTitle: priorMetadataByThread?.get(threadId)?.threadTitle,
22109
22183
  priorExtractedValues: priorMetadataByThread?.get(threadId)?.extracted,
22110
22184
  model: resolvedModel.model,
22111
- timeZone
22185
+ timeZone,
22186
+ wasTruncated
22112
22187
  });
22113
22188
  results.set(threadId, {
22114
22189
  observations: threadResult.observations,
@@ -22134,7 +22209,7 @@ var ObserverRunner = class {
22134
22209
  const agent = this.createAgent(resolvedModel.model, true, void 0, activeExtractors);
22135
22210
  const internalRequestContext = withOmInternalThreadId(requestContext, agent.id);
22136
22211
  const multiThreadAttachmentFilter = this.resolveAttachmentFilter(resolvedModel.model, requestContext);
22137
- const observerMessages = [buildMultiThreadObserverRequestMessage(existingObservations, messagesByThread, threadOrder, priorMetadataByThread, void 0, this.observationConfig.threadTitle, activeExtractors, {
22212
+ const observerMessages = [buildMultiThreadObserverRequestMessage(existingObservations, messagesByThread, threadOrder, priorMetadataByThread, wasTruncated, this.observationConfig.threadTitle, activeExtractors, {
22138
22213
  attachmentFilter: multiThreadAttachmentFilter,
22139
22214
  timeZone
22140
22215
  })];
@@ -22148,6 +22223,7 @@ var ObserverRunner = class {
22148
22223
  metadata: {
22149
22224
  omThreadCount: threadOrder.length,
22150
22225
  omPreviousObserverTokens: this.observationConfig.previousObserverTokens,
22226
+ omWasTruncated: wasTruncated ?? false,
22151
22227
  omThreadTitleEnabled: this.observationConfig.threadTitle,
22152
22228
  ...resolvedModel.selectedThreshold !== void 0 ? { omSelectedThreshold: resolvedModel.selectedThreshold } : {},
22153
22229
  ...resolvedModel.routingStrategy ? { omRoutingStrategy: resolvedModel.routingStrategy } : {},
@@ -24080,6 +24156,9 @@ async function validateAgainstConfiguredSchema(schema, value) {
24080
24156
  }).join("; ");
24081
24157
  throw new Error(`Working memory update does not match the configured schema, so it was not saved: ${details}`);
24082
24158
  }
24159
+ function isEmptyPlainObject(value) {
24160
+ return typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length === 0;
24161
+ }
24083
24162
  async function getWorkingMemoryDetails(context) {
24084
24163
  const memory = context.memory;
24085
24164
  const memoryConfig = parseMemoryRequestContext(context.requestContext)?.memoryConfig;
@@ -24127,8 +24206,9 @@ var WorkingMemoryExtractor = class extends Extractor {
24127
24206
  const configuredSchema = memory.getMergedThreadConfig(memoryConfig ?? {}).workingMemory?.schema;
24128
24207
  let document = current;
24129
24208
  if (configuredSchema) {
24130
- if (current === null) return;
24209
+ if (current === null || isEmptyPlainObject(current)) return;
24131
24210
  document = await validateAgainstConfiguredSchema(configuredSchema, current);
24211
+ if (document === null || isEmptyPlainObject(document)) return;
24132
24212
  }
24133
24213
  const workingMemory = typeof document === "string" ? document : JSON.stringify(document) ?? "";
24134
24214
  if (!workingMemory.trim()) return;
@@ -26416,6 +26496,82 @@ function resolveAutoModelId(mainModelId, options) {
26416
26496
  return pick ? `${prefix}${pick}` : mainModelId;
26417
26497
  }
26418
26498
  //#endregion
26499
+ //#region src/processors/observational-memory/commit-queue.ts
26500
+ /** How long an op may hold its key before later ops on that key stop waiting for it. */
26501
+ const OM_COMMIT_STALL_MS = 6e4;
26502
+ /** The per-thread / per-resource key shared by the commit queue and `BufferingCoordinator`. */
26503
+ function getOMLockKey(scope, threadId, resourceId) {
26504
+ if (scope === "resource" && resourceId) return `resource:${resourceId}`;
26505
+ return `thread:${threadId ?? "unknown"}`;
26506
+ }
26507
+ const REGISTRY = Symbol.for("@mastra/memory/om-commit-queue");
26508
+ const { queues, heldKeys } = globalThis[REGISTRY] ??= {
26509
+ queues: /* @__PURE__ */ new Map(),
26510
+ heldKeys: new AsyncLocalStorage()
26511
+ };
26512
+ function holdsKey(key) {
26513
+ const token = heldKeys.getStore()?.get(key);
26514
+ return token !== void 0 && queues.get(key)?.running === token;
26515
+ }
26516
+ function runNext(key, queue) {
26517
+ const next = queue.reflection.shift() ?? queue.normal.shift();
26518
+ if (!next) {
26519
+ queue.running = null;
26520
+ queues.delete(key);
26521
+ return;
26522
+ }
26523
+ next();
26524
+ }
26525
+ /**
26526
+ * Runs `op` once every earlier op on `key` has settled (reflection-priority ops first among those
26527
+ * still waiting) and resolves or rejects with its result. A rejection reaches only this caller.
26528
+ *
26529
+ * Enqueueing on a key from inside an op that holds the same key, directly or through nested ops
26530
+ * on other keys, would deadlock, so it rejects.
26531
+ */
26532
+ function runOMCommit(key, op, { priority = "normal" } = {}) {
26533
+ if (holdsKey(key)) return Promise.reject(/* @__PURE__ */ new Error(`Re-entrant observational memory commit on "${key}": an op cannot enqueue on the key it holds.`));
26534
+ return new Promise((resolve, reject) => {
26535
+ let queue = queues.get(key);
26536
+ if (!queue) {
26537
+ queue = {
26538
+ running: null,
26539
+ reflection: [],
26540
+ normal: []
26541
+ };
26542
+ queues.set(key, queue);
26543
+ }
26544
+ const owner = queue;
26545
+ const start = AsyncResource.bind(() => {
26546
+ const token = Symbol(key);
26547
+ owner.running = token;
26548
+ const held = new Map(heldKeys.getStore());
26549
+ held.set(key, token);
26550
+ let released = false;
26551
+ const release = () => {
26552
+ if (released) return;
26553
+ released = true;
26554
+ clearTimeout(stallTimer);
26555
+ runNext(key, owner);
26556
+ };
26557
+ const stallTimer = setTimeout(() => {
26558
+ console.warn(`[observational-memory] Commit on "${key}" has not settled after ${OM_COMMIT_STALL_MS}ms; releasing the key so later commits can run. The stalled storage call is still pending.`);
26559
+ release();
26560
+ }, OM_COMMIT_STALL_MS);
26561
+ stallTimer.unref?.();
26562
+ heldKeys.run(held, () => Promise.resolve().then(op)).then((value) => {
26563
+ release();
26564
+ resolve(value);
26565
+ }, (error) => {
26566
+ release();
26567
+ reject(error);
26568
+ });
26569
+ });
26570
+ (priority === "reflection" ? owner.reflection : owner.normal).push(start);
26571
+ if (owner.running === null) runNext(key, owner);
26572
+ });
26573
+ }
26574
+ //#endregion
26419
26575
  //#region src/processors/observational-memory/operation-registry.ts
26420
26576
  const activeOps = /* @__PURE__ */ new Map();
26421
26577
  function opKey(recordId, op) {
@@ -26455,11 +26611,37 @@ var BufferingCoordinator = class BufferingCoordinator {
26455
26611
  */
26456
26612
  static asyncBufferingOps = /* @__PURE__ */ new Map();
26457
26613
  /**
26614
+ * Observation buffer ops that may still append a buffered chunk.
26615
+ * Key format: "obs:{lockKey}"
26616
+ * Value: Promise that resolves once the op has persisted its chunk (or ended without one)
26617
+ *
26618
+ * Above the threshold→blockAfter band, activation waits (bounded) on this phase so the chunk
26619
+ * activates instead of being re-observed by the sync pass. It is not needed for safety: storage
26620
+ * keeps a chunk appended during activation. An op stays in asyncBufferingOps through its
26621
+ * post-persist work (indexing, thread title), which activation never waits for.
26622
+ */
26623
+ static pendingChunkWrites = /* @__PURE__ */ new Map();
26624
+ /**
26458
26625
  * Track the last token boundary at which we started buffering.
26459
26626
  * Key format: "obs:{lockKey}" or "refl:{lockKey}"
26460
26627
  */
26461
26628
  static lastBufferedBoundary = /* @__PURE__ */ new Map();
26462
26629
  /**
26630
+ * The buffer op that set the observation boundary, if one did. Any other write (an activation
26631
+ * reset, another op) replaces or clears it, so an op that outlived it never writes its boundary
26632
+ * back. Key format: "obs:{lockKey}"
26633
+ */
26634
+ static observationBoundaryOwners = /* @__PURE__ */ new Map();
26635
+ /** Set the observation boundary; `owner` is the buffer op setting it, if any. */
26636
+ static setObservationBoundary(bufferKey, tokens, owner) {
26637
+ BufferingCoordinator.lastBufferedBoundary.set(bufferKey, tokens);
26638
+ if (owner) BufferingCoordinator.observationBoundaryOwners.set(bufferKey, owner);
26639
+ else BufferingCoordinator.observationBoundaryOwners.delete(bufferKey);
26640
+ }
26641
+ static ownsObservationBoundary(bufferKey, owner) {
26642
+ return BufferingCoordinator.observationBoundaryOwners.get(bufferKey) === owner;
26643
+ }
26644
+ /**
26463
26645
  * Track the timestamp cursor for buffered messages.
26464
26646
  * Key format: "obs:{lockKey}"
26465
26647
  */
@@ -26475,8 +26657,7 @@ var BufferingCoordinator = class BufferingCoordinator {
26475
26657
  this.scope = opts.scope;
26476
26658
  }
26477
26659
  getLockKey(threadId, resourceId) {
26478
- if (this.scope === "resource" && resourceId) return `resource:${resourceId}`;
26479
- return `thread:${threadId ?? "unknown"}`;
26660
+ return getOMLockKey(this.scope, threadId, resourceId);
26480
26661
  }
26481
26662
  isAsyncObservationEnabled() {
26482
26663
  return this.observationConfig.bufferTokens !== void 0 && this.observationConfig.bufferTokens > 0;
@@ -26494,6 +26675,19 @@ var BufferingCoordinator = class BufferingCoordinator {
26494
26675
  return BufferingCoordinator.asyncBufferingOps.has(bufferKey);
26495
26676
  }
26496
26677
  /**
26678
+ * Mark an observation buffer op as able to append a chunk. Call the returned release
26679
+ * function once it no longer can; releasing more than once is a no-op.
26680
+ */
26681
+ static trackChunkWrite(bufferKey) {
26682
+ let resolve;
26683
+ const pending = new Promise((r) => resolve = r);
26684
+ BufferingCoordinator.pendingChunkWrites.set(bufferKey, pending);
26685
+ return () => {
26686
+ if (BufferingCoordinator.pendingChunkWrites.get(bufferKey) === pending) BufferingCoordinator.pendingChunkWrites.delete(bufferKey);
26687
+ resolve();
26688
+ };
26689
+ }
26690
+ /**
26497
26691
  * Clean up static maps for a thread/resource to prevent memory leaks.
26498
26692
  */
26499
26693
  cleanupStaticMaps(threadId, resourceId, activatedMessageIds) {
@@ -26502,10 +26696,12 @@ var BufferingCoordinator = class BufferingCoordinator {
26502
26696
  const reflBufKey = this.getReflectionBufferKey(lockKey);
26503
26697
  if (activatedMessageIds) {
26504
26698
  BufferingCoordinator.lastBufferedBoundary.delete(obsBufKey);
26699
+ BufferingCoordinator.observationBoundaryOwners.delete(obsBufKey);
26505
26700
  BufferingCoordinator.lastBufferedAtTime.delete(obsBufKey);
26506
26701
  } else {
26507
26702
  BufferingCoordinator.lastBufferedAtTime.delete(obsBufKey);
26508
26703
  BufferingCoordinator.lastBufferedBoundary.delete(obsBufKey);
26704
+ BufferingCoordinator.observationBoundaryOwners.delete(obsBufKey);
26509
26705
  BufferingCoordinator.lastBufferedBoundary.delete(reflBufKey);
26510
26706
  BufferingCoordinator.asyncBufferingOps.delete(obsBufKey);
26511
26707
  BufferingCoordinator.asyncBufferingOps.delete(reflBufKey);
@@ -26520,7 +26716,7 @@ var BufferingCoordinator = class BufferingCoordinator {
26520
26716
  if (record.isBufferingObservation) {
26521
26717
  if (isOpActiveInProcess(record.id, "bufferingObservation")) return false;
26522
26718
  omDebug(`[OM:shouldTriggerAsyncObs] isBufferingObservation=true but stale, clearing`);
26523
- storage?.setBufferingObservationFlag(record.id, false)?.catch(() => {});
26719
+ if (storage) runOMCommit(lockKey, () => storage.setBufferingObservationFlag(record.id, false)).catch(() => {});
26524
26720
  }
26525
26721
  const bufferKey = this.getObservationBufferKey(lockKey);
26526
26722
  if (this.isAsyncBufferingInProgress(bufferKey)) return false;
@@ -26540,7 +26736,7 @@ var BufferingCoordinator = class BufferingCoordinator {
26540
26736
  * Await any in-flight async buffering operations for a given thread/resource.
26541
26737
  */
26542
26738
  static async awaitBuffering(threadId, resourceId, scope, timeoutMs = 3e4) {
26543
- const lockKey = scope === "resource" && resourceId ? `resource:${resourceId}` : `thread:${threadId ?? "unknown"}`;
26739
+ const lockKey = getOMLockKey(scope, threadId, resourceId);
26544
26740
  const obsKey = `obs:${lockKey}`;
26545
26741
  const reflKey = `refl:${lockKey}`;
26546
26742
  const promises = [];
@@ -26838,6 +27034,36 @@ function createThreadUpdateMarker(params) {
26838
27034
  }
26839
27035
  };
26840
27036
  }
27037
+ //#endregion
27038
+ //#region src/processors/observational-memory/record-lineage.ts
27039
+ /**
27040
+ * The current head of `origin`'s lineage (the generations reflections have created from it),
27041
+ * or `null` when that lineage is gone.
27042
+ *
27043
+ * Use this instead of reading the head by thread/resource when committing work that started
27044
+ * from `origin`. Clearing a record (`om.clear`, `Memory.deleteThread`) deletes every generation
27045
+ * of the thread/resource, and the thread can then get a new, unrelated record. Work produced
27046
+ * from the cleared messages must never be committed to it.
27047
+ *
27048
+ * Generation ids are never reused and clearing deletes all of them, so the head belongs to
27049
+ * `origin`'s lineage iff `origin` still exists.
27050
+ */
27051
+ async function getLineageHead(storage, origin) {
27052
+ const head = await storage.getObservationalMemory(origin.threadId, origin.resourceId);
27053
+ if (!head || head.id === origin.id) return head;
27054
+ if (head.generationCount <= origin.generationCount) return null;
27055
+ return (await storage.getObservationalMemoryHistory(origin.threadId, origin.resourceId, head.generationCount - origin.generationCount + 3)).some((record) => record.id === origin.id) ? head : null;
27056
+ }
27057
+ /**
27058
+ * The generation `id` of `origin`'s lineage, or `null` when that lineage is gone or `id` is not
27059
+ * part of it. Unlike {@link getLineageHead}, this keeps naming `id` after later reflections have
27060
+ * superseded it, e.g. to report which generation a write landed on.
27061
+ */
27062
+ async function getLineageRecord(storage, origin, id) {
27063
+ const head = await getLineageHead(storage, origin);
27064
+ if (!head || head.id === id) return head;
27065
+ return (await storage.getObservationalMemoryHistory(origin.threadId, origin.resourceId, head.generationCount - origin.generationCount + 3)).find((record) => record.id === id) ?? null;
27066
+ }
26841
27067
  function isRecord$1(value) {
26842
27068
  return !!value && typeof value === "object" && !Array.isArray(value);
26843
27069
  }
@@ -26984,6 +27210,39 @@ function calculateProjectedMessageRemoval(chunks, bufferActivation, messageToken
26984
27210
  //#region src/processors/observational-memory/observation-strategies/base.ts
26985
27211
  /** Module-level xxhash singleton — loaded once, shared across all strategy instances. */
26986
27212
  const hasherPromise = xxhash();
27213
+ /** Recompose-and-retry rounds for an observation commit that hit a retired or changed head. */
27214
+ const MAX_HEAD_COMMIT_RETRIES = 3;
27215
+ const OBSERVATION_LIFECYCLE_MARKERS = /* @__PURE__ */ new Set([
27216
+ "data-om-observation-start",
27217
+ "data-om-observation-end",
27218
+ "data-om-observation-failed"
27219
+ ]);
27220
+ function isOmPart(part) {
27221
+ return String(part?.type ?? "").startsWith("data-om-");
27222
+ }
27223
+ /**
27224
+ * Index in `message` right after this cycle's start marker (and any OM parts directly after it),
27225
+ * or -1 when the message doesn't carry it.
27226
+ */
27227
+ function afterOwnStartMarker(message, marker) {
27228
+ const cycleId = marker.data?.cycleId;
27229
+ const startType = marker.type.startsWith("data-om-buffering-") ? "data-om-buffering-start" : "data-om-observation-start";
27230
+ if (!cycleId || marker.type === startType) return -1;
27231
+ const parts = message.content?.parts ?? [];
27232
+ const start = parts.findIndex((part) => part?.type === startType && part?.data?.cycleId === cycleId);
27233
+ if (start === -1) return -1;
27234
+ let index = start + 1;
27235
+ while (index < parts.length && isOmPart(parts[index])) index++;
27236
+ return index;
27237
+ }
27238
+ /** True when the message has parts no completed observation boundary covers (OM markers aside). */
27239
+ function hasUnobservedContent(message) {
27240
+ return getUnobservedParts(message).some((part) => !isOmPart(part));
27241
+ }
27242
+ function isObservationLifecycleMarker(marker) {
27243
+ const operationType = marker.data?.operationType;
27244
+ return OBSERVATION_LIFECYCLE_MARKERS.has(marker.type) && operationType !== "reflection";
27245
+ }
26987
27246
  /**
26988
27247
  * Abstract base class for observation strategies.
26989
27248
  *
@@ -27033,17 +27292,35 @@ var ObservationStrategy = class ObservationStrategy {
27033
27292
  const fresh = await this.storage.getObservationalMemory(record.threadId, record.resourceId);
27034
27293
  if (fresh?.lastObservedAt && record.lastObservedAt && fresh.lastObservedAt > record.lastObservedAt) return { observed: false };
27035
27294
  }
27036
- const { messages, existingObservations } = await this.prepare();
27295
+ const { messages, existingObservations, contextRecord } = await this.prepare();
27296
+ if (messages.length === 0) return { observed: false };
27037
27297
  const observationMessages = stripSubconsciousSignals(messages);
27038
27298
  await this.emitStartMarkers(cycleId);
27039
- const output = await this.observe(existingObservations, observationMessages);
27040
- const processed = await this.process(output, existingObservations);
27041
- this.settleObservationCommit(await this.persist(processed));
27299
+ const observerContext = this.deps.prepareObserverContext(existingObservations, contextRecord ?? record);
27300
+ const output = await this.observe(observerContext.context ?? "", observationMessages, observerContext.wasTruncated);
27301
+ let processed = await this.process(output, existingObservations);
27302
+ let committedRecord = record;
27303
+ const outcome = await this.persist(processed);
27304
+ this.settleObservationCommit(outcome?.status === "committed" && !outcome.alreadyCovered);
27305
+ if (outcome?.status === "not-committed") {
27306
+ omDebug(`[OM:observe] cycle ${cycleId} not committed: ${outcome.reason}`);
27307
+ await this.emitFailedMarkers(cycleId, /* @__PURE__ */ new Error(`Observation not committed: ${outcome.reason}`));
27308
+ return {
27309
+ observed: false,
27310
+ usage: output.usage,
27311
+ providerMetadata: output.providerMetadata
27312
+ };
27313
+ }
27314
+ if (outcome?.status === "committed") {
27315
+ processed = outcome.processed;
27316
+ committedRecord = outcome.record;
27317
+ }
27042
27318
  await this.emitEndMarkers(cycleId, processed);
27043
27319
  if (this.needsReflection) await this.deps.reflector.maybeReflect({
27044
27320
  record: {
27045
- ...record,
27046
- activeObservations: processed.observations
27321
+ ...committedRecord,
27322
+ activeObservations: processed.observations,
27323
+ observationTokenCount: processed.observationTokens
27047
27324
  },
27048
27325
  observationTokens: processed.observationTokens,
27049
27326
  threadId,
@@ -27079,7 +27356,8 @@ var ObservationStrategy = class ObservationStrategy {
27079
27356
  threadId
27080
27357
  }
27081
27358
  };
27082
- await this.persistMarkerToStorage(failedMarkerForStorage, threadId, this.opts.resourceId).catch(() => {});
27359
+ const observed = this.getObservedMessagesForThread(threadId);
27360
+ await (observed?.length ? this.persistMarkerToObservedMessage(failedMarkerForStorage, observed, threadId, this.opts.resourceId) : this.persistMarkerToStorage(failedMarkerForStorage, threadId, this.opts.resourceId)).catch(() => {});
27083
27361
  if (abortSignal?.aborted) throw error;
27084
27362
  omError("[OM] Observation failed", error);
27085
27363
  return {
@@ -27106,6 +27384,11 @@ var ObservationStrategy = class ObservationStrategy {
27106
27384
  transient: true
27107
27385
  }).catch(() => {});
27108
27386
  const markerThreadId = marker.data?.threadId ?? this.opts.threadId;
27387
+ const observed = isObservationLifecycleMarker(marker) ? this.getObservedMessagesForThread(markerThreadId) : void 0;
27388
+ if (observed?.length) {
27389
+ await this.persistMarkerToObservedMessage(marker, observed, markerThreadId, this.opts.resourceId);
27390
+ return;
27391
+ }
27109
27392
  if (!await this.persistMarkerToMessage(marker, this.opts.messageList, markerThreadId, this.opts.resourceId)) await this.persistMarkerToStorage(marker, markerThreadId, this.opts.resourceId);
27110
27393
  }
27111
27394
  getObservationMarkerConfig() {
@@ -27189,6 +27472,76 @@ var ObservationStrategy = class ObservationStrategy {
27189
27472
  }
27190
27473
  return `${existingObservations}${lastObservedAt ? ObservationStrategy.createMessageBoundary(lastObservedAt) : "\n\n"}${newThreadSection}`;
27191
27474
  }
27475
+ /**
27476
+ * Commit composed observations to the current head generation.
27477
+ *
27478
+ * The write is conditional on the head text the observations were composed from. When the
27479
+ * target generation was retired by a reflection, or another writer changed the text, the
27480
+ * observations are recomposed against the fresh head and the commit is retried (bounded).
27481
+ * When the fresh head's `observedMessageIds` already include every message this cycle observed
27482
+ * (another instance observed them first), nothing is written and `alreadyCovered` is set, so
27483
+ * callers advance cursors and markers without adding a second copy of the same observations.
27484
+ * The cursor alone is not proof: an activation can move it past a message it never covered.
27485
+ * Returns null when no commit landed — callers must then leave cursors, markers, and the live
27486
+ * context untouched.
27487
+ */
27488
+ async commitActiveObservationsToHead(opts) {
27489
+ let { processed, composedFrom, target } = opts;
27490
+ for (let attempt = 0; attempt <= MAX_HEAD_COMMIT_RETRIES; attempt++) {
27491
+ const result = await this.runCommit(async () => {
27492
+ const head = await getLineageHead(this.storage, target);
27493
+ if (!head) return null;
27494
+ if (head.id !== target.id || (head.activeObservations ?? "") !== composedFrom) {
27495
+ if (this.headCoversCycle(head, opts.cycleMessageIds)) {
27496
+ omDebug(`[OM:observe] head ${head.id} already covers this cycle's messages; skipping the duplicate commit`);
27497
+ target = head;
27498
+ return {
27499
+ applied: true,
27500
+ alreadyCovered: true
27501
+ };
27502
+ }
27503
+ target = head;
27504
+ composedFrom = head.activeObservations ?? "";
27505
+ processed = await opts.recompose(head);
27506
+ }
27507
+ const input = {
27508
+ id: target.id,
27509
+ observations: processed.observations,
27510
+ tokenCount: processed.observationTokens,
27511
+ lastObservedAt: processed.lastObservedAt,
27512
+ observedMessageIds: processed.observedMessageIds,
27513
+ expectedActiveObservations: composedFrom
27514
+ };
27515
+ return typeof this.storage.commitActiveObservations === "function" ? await this.storage.commitActiveObservations(input) : (await this.storage.updateActiveObservations(input), { applied: true });
27516
+ });
27517
+ if (!result) return null;
27518
+ if ("alreadyCovered" in result) return {
27519
+ processed: {
27520
+ ...processed,
27521
+ observations: target.activeObservations ?? "",
27522
+ observationTokens: target.observationTokenCount
27523
+ },
27524
+ record: target,
27525
+ alreadyCovered: true
27526
+ };
27527
+ if (result.applied) return {
27528
+ processed,
27529
+ record: target
27530
+ };
27531
+ omDebug(`[OM:observe] commit to ${target.id} not applied (${result.reason}); retrying against the head`);
27532
+ }
27533
+ return null;
27534
+ }
27535
+ headCoversCycle(head, cycleMessageIds) {
27536
+ if (cycleMessageIds.length === 0) return false;
27537
+ const headIds = new Set(Array.isArray(head.observedMessageIds) ? head.observedMessageIds : []);
27538
+ return cycleMessageIds.every((id) => headIds.has(id));
27539
+ }
27540
+ /** Runs one storage commit in this thread's (or resource's) slot of the process-wide commit queue. */
27541
+ runCommit(op) {
27542
+ const { threadId, resourceId, record } = this.opts;
27543
+ return runOMCommit(getOMLockKey(this.scope, record.threadId ?? threadId, record.resourceId ?? resourceId), op);
27544
+ }
27192
27545
  async indexObservationGroups(observations, threadId, resourceId, observedAt, recordId) {
27193
27546
  if (!resourceId || !this.deps.onIndexObservations) return;
27194
27547
  const groups = parseObservationGroups(observations);
@@ -27207,30 +27560,133 @@ var ObservationStrategy = class ObservationStrategy {
27207
27560
  })));
27208
27561
  }
27209
27562
  /**
27563
+ * Messages this cycle observed for `threadId`, or undefined when the strategy does not
27564
+ * place observation lifecycle markers.
27565
+ */
27566
+ getObservedMessagesForThread(_threadId) {}
27567
+ /**
27568
+ * Persist an observation start/end/failed marker without hiding unobserved content. A completed
27569
+ * end marker tells `getUnobservedMessages` that everything before it in that message is observed
27570
+ * (whatever thread the marker names), so it must only follow content this cycle observed.
27571
+ * Placement, in order:
27572
+ * 1. the live MessageList's newest assistant message (this turn's response or its seed), when the
27573
+ * list belongs to the marker's thread and the cycle observed that message or it has no
27574
+ * unobserved content;
27575
+ * 2. the newest observed assistant message; a stored copy (no live list, or another instance's
27576
+ * message) gets the marker right after the parts this cycle observed (end/failed markers right
27577
+ * after this cycle's start marker), so parts added while the Observer ran stay unobserved;
27578
+ * 3. the newest stored assistant message that is not newer than the observed range, only when it
27579
+ * has nothing unobserved (or already carries this cycle's start marker); otherwise no marker,
27580
+ * and the thread cursor alone records the cycle.
27581
+ * Lifecycle markers never go on user messages.
27582
+ */
27583
+ async persistMarkerToObservedMessage(marker, observed, threadId, resourceId) {
27584
+ const observedIds = new Set(observed.map((m) => m.id));
27585
+ const list = threadId === this.opts.threadId ? this.opts.messageList : void 0;
27586
+ const listMessages = list ? getObservableMessages(list) : [];
27587
+ const live = [...listMessages].reverse().find((m) => m.role === "assistant");
27588
+ if (live && (observedIds.has(live.id) || !hasUnobservedContent(live))) {
27589
+ await this.attachMarker(marker, live, live.content.parts.length, threadId, resourceId);
27590
+ return;
27591
+ }
27592
+ const target = [...observed].reverse().find((m) => m.role === "assistant");
27593
+ if (target) {
27594
+ const fromList = listMessages.find((m) => m.id === target.id);
27595
+ if (fromList) {
27596
+ await this.attachMarker(marker, fromList, fromList.content.parts.length, threadId, resourceId);
27597
+ return;
27598
+ }
27599
+ try {
27600
+ const stored = (await this.storage.listMessagesById({ messageIds: [target.id] })).messages[0];
27601
+ if (stored?.content?.parts && Array.isArray(stored.content.parts)) {
27602
+ const afterStart = afterOwnStartMarker(stored, marker);
27603
+ const index = afterStart !== -1 ? afterStart : this.insertionAfterObserved(stored, target);
27604
+ await this.attachMarker(marker, stored, index, threadId, resourceId);
27605
+ }
27606
+ } catch (e) {
27607
+ omDebug(`[OM:persistMarkerToObservedMessage] failed to load marker target: ${e}`);
27608
+ }
27609
+ return;
27610
+ }
27611
+ const observedTimes = observed.filter((m) => m.createdAt).map((m) => new Date(m.createdAt).getTime());
27612
+ if (observedTimes.length === 0) return;
27613
+ await this.persistMarkerToStorage(marker, threadId, resourceId, {
27614
+ notAfter: new Date(Math.max(...observedTimes)),
27615
+ onlyIfObserved: true
27616
+ });
27617
+ }
27618
+ /** What the cycle observed of each message, captured when it placed its first marker there. */
27619
+ observedParts = /* @__PURE__ */ new Map();
27620
+ /**
27621
+ * Index in `stored` right after the parts the cycle observed in `observed` (and any OM markers
27622
+ * directly after them). Parts are only ever appended, so the observed message's non-marker part
27623
+ * count locates them, counted from the start of the message or, when `observed` is the trimmed
27624
+ * copy `getUnobservedMessages` returns for an already marked message, from just after the stored
27625
+ * copy's last completed boundary. Captured at the cycle's first marker, before the Observer runs,
27626
+ * because `observed` may be a live reference that grows meanwhile. Falls back to the end when the
27627
+ * stored copy has fewer parts than were observed.
27628
+ */
27629
+ insertionAfterObserved(stored, observed) {
27630
+ if (!this.observedParts.has(observed.id)) this.observedParts.set(observed.id, {
27631
+ count: (observed.content?.parts ?? []).filter((part) => !isOmPart(part)).length,
27632
+ observedIsTrimmed: findLastCompletedObservationBoundary(observed) === -1
27633
+ });
27634
+ const { count, observedIsTrimmed } = this.observedParts.get(observed.id);
27635
+ const parts = stored.content.parts;
27636
+ let index = observedIsTrimmed ? findLastCompletedObservationBoundary(stored) + 1 : 0;
27637
+ let seen = 0;
27638
+ while (index < parts.length && seen < count) {
27639
+ if (!isOmPart(parts[index])) seen++;
27640
+ index++;
27641
+ }
27642
+ if (seen < count) return parts.length;
27643
+ while (index < parts.length && isOmPart(parts[index])) index++;
27644
+ return index;
27645
+ }
27646
+ async attachMarker(marker, msg, index, threadId, resourceId) {
27647
+ const parts = msg.content?.parts;
27648
+ if (!parts || !Array.isArray(parts)) return;
27649
+ const markerData = marker.data;
27650
+ if (!(markerData?.cycleId && parts.some((p) => p?.type === marker.type && p?.data?.cycleId === markerData.cycleId))) parts.splice(index, 0, marker);
27651
+ try {
27652
+ await this.messageHistory.persistMessages({
27653
+ messages: [msg],
27654
+ threadId,
27655
+ resourceId
27656
+ });
27657
+ } catch (e) {
27658
+ omDebug(`[OM:persistMarkerToObservedMessage] failed to save marker to DB: ${e}`);
27659
+ }
27660
+ }
27661
+ /**
27210
27662
  * Persist a marker to the last assistant message in storage.
27211
27663
  * Fetches messages directly from the DB so it works even when
27212
27664
  * no MessageList is available (e.g. async buffering ops).
27213
27665
  */
27214
- async persistMarkerToStorage(marker, threadId, resourceId) {
27666
+ async persistMarkerToStorage(marker, threadId, resourceId, opts) {
27215
27667
  try {
27216
- const messages = (await this.storage.listMessages({
27668
+ const assistants = ((await this.storage.listMessages({
27217
27669
  threadId,
27218
27670
  perPage: 20,
27219
27671
  orderBy: {
27220
27672
  field: "createdAt",
27221
27673
  direction: "DESC"
27222
- }
27223
- }))?.messages ?? [];
27224
- for (const msg of messages) if (msg?.role === "assistant" && msg.content?.parts && Array.isArray(msg.content.parts)) {
27225
- const markerData = marker.data;
27226
- if (!(markerData?.cycleId && msg.content.parts.some((p) => p?.type === marker.type && p?.data?.cycleId === markerData.cycleId))) msg.content.parts.push(marker);
27227
- await this.messageHistory.persistMessages({
27228
- messages: [msg],
27229
- threadId,
27230
- resourceId
27231
- });
27232
- return;
27233
- }
27674
+ },
27675
+ ...opts?.notAfter ? { filter: { dateRange: { end: opts.notAfter } } } : {}
27676
+ }))?.messages ?? []).filter((msg) => msg?.role === "assistant" && Array.isArray(msg.content?.parts));
27677
+ const startMessage = opts?.onlyIfObserved || opts?.onCycleStartMessage ? assistants.find((m) => afterOwnStartMarker(m, marker) !== -1) : void 0;
27678
+ const msg = opts?.onCycleStartMessage ? startMessage : startMessage ?? assistants[0];
27679
+ if (!msg) return;
27680
+ const markerData = marker.data;
27681
+ const alreadyPresent = markerData?.cycleId && msg.content.parts.some((p) => p?.type === marker.type && p?.data?.cycleId === markerData.cycleId);
27682
+ const afterStart = afterOwnStartMarker(msg, marker);
27683
+ if (opts?.onlyIfObserved && afterStart === -1 && hasUnobservedContent(msg)) return;
27684
+ if (!alreadyPresent) msg.content.parts.splice(afterStart !== -1 ? afterStart : msg.content.parts.length, 0, marker);
27685
+ await this.messageHistory.persistMessages({
27686
+ messages: [msg],
27687
+ threadId,
27688
+ resourceId
27689
+ });
27234
27690
  } catch (e) {
27235
27691
  omDebug(`[OM:persistMarkerToStorage] failed to save marker to DB: ${e}`);
27236
27692
  }
@@ -27292,10 +27748,15 @@ var SyncObservationStrategy = class extends ObservationStrategy {
27292
27748
  tokensToObserve = 0;
27293
27749
  observerResult;
27294
27750
  priorExtractedValues;
27751
+ /** Head text the processed observations were composed from (the commit's expected text). */
27752
+ composedFrom = "";
27295
27753
  constructor(deps, opts) {
27296
27754
  super(deps, opts);
27297
27755
  this.lastMessage = opts.messages[opts.messages.length - 1];
27298
27756
  }
27757
+ getObservedMessagesForThread(threadId) {
27758
+ return threadId === this.opts.threadId ? this.opts.messages : void 0;
27759
+ }
27299
27760
  get needsLock() {
27300
27761
  return true;
27301
27762
  }
@@ -27330,9 +27791,11 @@ var SyncObservationStrategy = class extends ObservationStrategy {
27330
27791
  }
27331
27792
  }
27332
27793
  this.tokensToObserve = await this.tokenCounter.countMessagesAsync(messages);
27794
+ const freshRecord = await this.storage.getObservationalMemory(record.threadId, record.resourceId);
27333
27795
  return {
27334
27796
  messages,
27335
- existingObservations: (await this.storage.getObservationalMemory(record.threadId, record.resourceId))?.activeObservations ?? record.activeObservations ?? ""
27797
+ existingObservations: freshRecord?.activeObservations ?? record.activeObservations ?? "",
27798
+ contextRecord: freshRecord
27336
27799
  };
27337
27800
  }
27338
27801
  async emitStartMarkers(cycleId) {
@@ -27350,11 +27813,12 @@ var SyncObservationStrategy = class extends ObservationStrategy {
27350
27813
  await this.streamMarker(startMarker);
27351
27814
  }
27352
27815
  }
27353
- async observe(existingObservations, messages) {
27816
+ async observe(observerContext, messages, wasTruncated) {
27354
27817
  const thread = await this.storage.getThreadById({ threadId: this.opts.threadId });
27355
27818
  const omMeta = thread ? getThreadOMMetadata(thread.metadata) : void 0;
27356
27819
  this.priorExtractedValues = getPriorExtractedValues(omMeta, this.observationConfig.extractors);
27357
- const result = await this.deps.observer.call(existingObservations, messages, this.opts.abortSignal, {
27820
+ const result = await this.deps.observer.call(observerContext, messages, this.opts.abortSignal, {
27821
+ wasTruncated,
27358
27822
  requestContext: this.opts.requestContext,
27359
27823
  observabilityContext: this.opts.observabilityContext,
27360
27824
  priorCurrentTask: omMeta?.currentTask,
@@ -27400,20 +27864,14 @@ var SyncObservationStrategy = class extends ObservationStrategy {
27400
27864
  }
27401
27865
  async process(output, existingObservations) {
27402
27866
  const { record, threadId, messages } = this.opts;
27403
- const lastObservedAt = this.getMaxMessageTimestamp(messages);
27404
- const messageRange = this.retrieval ? buildMessageRange(messages) : void 0;
27405
- const newObservations = await this.wrapObservations(output.observations, existingObservations, threadId, lastObservedAt, messageRange);
27406
- const observationTokens = this.tokenCounter.countObservations(newObservations);
27407
- const cycleObservationTokens = this.tokenCounter.countObservations(output.observations);
27408
- const newMessageIds = messages.map((m) => m.id);
27409
- const existingIds = record.observedMessageIds ?? [];
27410
- const observedMessageIds = [.../* @__PURE__ */ new Set([...Array.isArray(existingIds) ? existingIds : [], ...newMessageIds])];
27867
+ this.composedFrom = existingObservations;
27868
+ const processed = await this.compose(output, existingObservations, record);
27411
27869
  this.deps.emitDebugEvent({
27412
27870
  type: "observation_complete",
27413
27871
  timestamp: /* @__PURE__ */ new Date(),
27414
27872
  threadId,
27415
27873
  resourceId: record.resourceId ?? "",
27416
- observations: newObservations,
27874
+ observations: processed.observations,
27417
27875
  rawObserverOutput: output.observations,
27418
27876
  previousObservations: record.activeObservations,
27419
27877
  messages: messages.map((m) => ({
@@ -27422,11 +27880,23 @@ var SyncObservationStrategy = class extends ObservationStrategy {
27422
27880
  })),
27423
27881
  usage: output.usage
27424
27882
  });
27883
+ return processed;
27884
+ }
27885
+ /** Build the observations to commit on top of `existingObservations` (the head text). */
27886
+ async compose(output, existingObservations, baseRecord) {
27887
+ const { threadId, messages } = this.opts;
27888
+ const lastObservedAt = this.getMaxMessageTimestamp(messages);
27889
+ const messageRange = this.retrieval ? buildMessageRange(messages) : void 0;
27890
+ const newObservations = await this.wrapObservations(output.observations, existingObservations, threadId, lastObservedAt, messageRange);
27891
+ const observationTokens = this.tokenCounter.countObservations(newObservations);
27892
+ const cycleObservationTokens = this.tokenCounter.countObservations(output.observations);
27893
+ const newMessageIds = messages.map((m) => m.id);
27894
+ const existingIds = baseRecord.observedMessageIds ?? [];
27425
27895
  return {
27426
27896
  observations: newObservations,
27427
27897
  observationTokens,
27428
27898
  cycleObservationTokens,
27429
- observedMessageIds,
27899
+ observedMessageIds: [.../* @__PURE__ */ new Set([...Array.isArray(existingIds) ? existingIds : [], ...newMessageIds])],
27430
27900
  lastObservedAt,
27431
27901
  suggestedContinuation: output.suggestedContinuation,
27432
27902
  currentTask: output.currentTask,
@@ -27438,12 +27908,27 @@ var SyncObservationStrategy = class extends ObservationStrategy {
27438
27908
  }
27439
27909
  async persist(processed) {
27440
27910
  const { record, threadId, resourceId, messages } = this.opts;
27441
- const thread = await this.storage.getThreadById({ threadId });
27442
- if (!await this.storage.getObservationalMemory(record.threadId, record.resourceId)) {
27911
+ const liveRecord = await getLineageHead(this.storage, record);
27912
+ if (!liveRecord) {
27443
27913
  omDebug(`[OM:sync-obs] skipping persist for thread ${threadId}: observational memory record is gone`);
27444
- return false;
27914
+ return {
27915
+ status: "not-committed",
27916
+ reason: "the observational memory record was cleared"
27917
+ };
27445
27918
  }
27446
- let threadUpdateMarker;
27919
+ const committed = await this.commitActiveObservationsToHead({
27920
+ processed,
27921
+ composedFrom: this.composedFrom,
27922
+ target: liveRecord,
27923
+ recompose: (head) => this.compose(this.observerResult, head.activeObservations ?? "", head),
27924
+ cycleMessageIds: messages.map((m) => m.id)
27925
+ });
27926
+ if (!committed) return {
27927
+ status: "not-committed",
27928
+ reason: "the observational memory head kept changing during the commit"
27929
+ };
27930
+ processed = committed.processed;
27931
+ const thread = committed.alreadyCovered ? null : await this.storage.getThreadById({ threadId });
27447
27932
  if (thread) {
27448
27933
  const oldTitle = thread.title?.trim();
27449
27934
  const newTitle = resolveThreadTitleUpdate(thread, processed.threadTitle);
@@ -27465,23 +27950,20 @@ var SyncObservationStrategy = class extends ObservationStrategy {
27465
27950
  ...shouldUpdateThreadTitle ? { title: newTitle } : {},
27466
27951
  metadata: newMetadata
27467
27952
  });
27468
- if (shouldUpdateThreadTitle) threadUpdateMarker = createThreadUpdateMarker({
27953
+ if (shouldUpdateThreadTitle) await this.streamMarker(createThreadUpdateMarker({
27469
27954
  cycleId: this.cycleId ?? crypto.randomUUID(),
27470
27955
  threadId,
27471
27956
  oldTitle,
27472
27957
  newTitle
27473
- });
27958
+ }));
27474
27959
  }
27475
- if (threadUpdateMarker) await this.streamMarker(threadUpdateMarker);
27476
- await this.storage.updateActiveObservations({
27477
- id: record.id,
27478
- observations: processed.observations,
27479
- tokenCount: processed.observationTokens,
27480
- lastObservedAt: processed.lastObservedAt,
27481
- observedMessageIds: processed.observedMessageIds
27482
- });
27483
- await this.indexObservationGroups(processed.observations, threadId, resourceId, processed.lastObservedAt, record.id);
27484
- return true;
27960
+ if (!committed.alreadyCovered) await this.indexObservationGroups(processed.observations, threadId, resourceId, processed.lastObservedAt, committed.record.id);
27961
+ return {
27962
+ status: "committed",
27963
+ processed,
27964
+ record: committed.record,
27965
+ alreadyCovered: committed.alreadyCovered
27966
+ };
27485
27967
  }
27486
27968
  async emitEndMarkers(cycleId, processed) {
27487
27969
  const actualTokensObserved = await this.tokenCounter.countMessagesAsync(this.opts.messages);
@@ -27525,6 +28007,8 @@ var AsyncBufferObservationStrategy = class extends ObservationStrategy {
27525
28007
  startedAt;
27526
28008
  cycleId;
27527
28009
  priorExtractedValues;
28010
+ /** The generation the chunk was written to (the head when the target was retired). */
28011
+ persistedRecordId;
27528
28012
  constructor(deps, opts) {
27529
28013
  super(deps, opts);
27530
28014
  this.cycleId = opts.cycleId;
@@ -27551,12 +28035,13 @@ var AsyncBufferObservationStrategy = class extends ObservationStrategy {
27551
28035
  };
27552
28036
  }
27553
28037
  async emitStartMarkers(_cycleId) {}
27554
- async observe(existingObservations, messages) {
28038
+ async observe(observerContext, messages, wasTruncated) {
27555
28039
  const thread = await this.storage.getThreadById({ threadId: this.opts.threadId });
27556
28040
  const omMeta = thread ? getThreadOMMetadata(thread.metadata) : void 0;
27557
28041
  this.priorExtractedValues = getPriorExtractedValues(omMeta, this.observationConfig.extractors);
27558
- const result = await this.deps.observer.call(existingObservations, messages, void 0, {
28042
+ const result = await this.deps.observer.call(observerContext, messages, void 0, {
27559
28043
  skipContinuationHints: true,
28044
+ wasTruncated,
27560
28045
  requestContext: this.opts.requestContext,
27561
28046
  observabilityContext: this.opts.observabilityContext,
27562
28047
  priorExtractedValues: this.priorExtractedValues,
@@ -27630,71 +28115,124 @@ var AsyncBufferObservationStrategy = class extends ObservationStrategy {
27630
28115
  };
27631
28116
  }
27632
28117
  async persist(processed) {
27633
- if (!processed.observations) return false;
28118
+ if (!processed.observations) return;
27634
28119
  const { record, threadId, resourceId, messages } = this.opts;
27635
- if (!await this.storage.getObservationalMemory(record.threadId, record.resourceId)) {
28120
+ const liveRecord = await getLineageHead(this.storage, record);
28121
+ if (!liveRecord) {
27636
28122
  omDebug(`[OM:asyncBuffer] skipping persist for thread ${threadId}: observational memory record is gone`);
27637
- return false;
28123
+ return {
28124
+ status: "not-committed",
28125
+ reason: "the observational memory record was cleared"
28126
+ };
27638
28127
  }
27639
28128
  const messageTokens = await this.tokenCounter.countMessagesAsync(messages);
27640
- await withRetry(() => this.storage.updateBufferedObservations({
27641
- id: record.id,
27642
- chunk: {
27643
- cycleId: this.cycleId,
27644
- observations: processed.observations,
27645
- tokenCount: processed.observationTokens,
27646
- messageIds: processed.observedMessageIds,
27647
- messageTokens,
27648
- lastObservedAt: processed.lastObservedAt,
27649
- suggestedContinuation: processed.suggestedContinuation,
27650
- currentTask: processed.currentTask,
27651
- threadTitle: processed.threadTitle,
27652
- extractedValues: processed.extractedValues,
27653
- extractionFailures: processed.extractionFailures
27654
- },
27655
- lastBufferedAtTime: processed.lastObservedAt
27656
- }), {
28129
+ let appendAttempts = 0;
28130
+ let targetId = record.id;
28131
+ const appendResult = await withRetry(() => {
28132
+ appendAttempts++;
28133
+ return this.runCommit(async () => {
28134
+ const head = await getLineageHead(this.storage, record);
28135
+ if (!head) return null;
28136
+ targetId = head.id;
28137
+ const input = {
28138
+ id: targetId,
28139
+ chunk: {
28140
+ cycleId: this.cycleId,
28141
+ observations: processed.observations,
28142
+ tokenCount: processed.observationTokens,
28143
+ messageIds: processed.observedMessageIds,
28144
+ messageTokens,
28145
+ lastObservedAt: processed.lastObservedAt,
28146
+ suggestedContinuation: processed.suggestedContinuation,
28147
+ currentTask: processed.currentTask,
28148
+ threadTitle: processed.threadTitle,
28149
+ extractedValues: processed.extractedValues,
28150
+ extractionFailures: processed.extractionFailures
28151
+ },
28152
+ lastBufferedAtTime: processed.lastObservedAt
28153
+ };
28154
+ return typeof this.storage.appendBufferedObservations === "function" ? this.storage.appendBufferedObservations(input) : this.storage.updateBufferedObservations(input).then(() => ({
28155
+ persisted: true,
28156
+ recordId: input.id
28157
+ }));
28158
+ });
28159
+ }, {
27657
28160
  label: "persist-buffered-observations",
27658
28161
  abortSignal: this.opts.abortSignal
27659
28162
  });
27660
- await this.indexObservationGroups(processed.observations, threadId, resourceId, processed.lastObservedAt, record.id);
27661
- const candidateTitle = processed.threadTitle?.trim();
27662
- const hasValidThreadTitle = !!candidateTitle && candidateTitle.length >= 3;
27663
- if (hasValidThreadTitle || processed.extractedValues) {
27664
- const thread = await this.storage.getThreadById({ threadId });
27665
- if (thread) {
27666
- const oldTitle = thread.title?.trim();
27667
- const newTitle = resolveThreadTitleUpdate(thread, candidateTitle);
27668
- const shouldUpdateThreadTitle = newTitle !== void 0;
27669
- const previousOmMetadata = getThreadOMMetadata(thread.metadata);
27670
- const metadataUpdate = buildThreadMetadataFromExtractedValues(processed.extractors ?? this.observationConfig.extractors, processed.extractedValues);
27671
- const newMetadata = setThreadOMMetadata(thread.metadata, {
27672
- ...hasValidThreadTitle || metadataUpdate.threadTitle ? { threadTitle: metadataUpdate.threadTitle ?? processed.threadTitle } : {},
27673
- extracted: {
27674
- ...previousOmMetadata?.extracted ?? {},
27675
- ...metadataUpdate.extracted ?? {}
27676
- }
27677
- });
27678
- await this.storage.patchThread({
27679
- id: threadId,
27680
- ...shouldUpdateThreadTitle ? { title: newTitle } : {},
27681
- metadata: newMetadata
27682
- });
27683
- if (shouldUpdateThreadTitle) {
27684
- const marker = createThreadUpdateMarker({
27685
- cycleId: this.cycleId,
27686
- threadId,
27687
- oldTitle,
27688
- newTitle
28163
+ if (appendResult === null) {
28164
+ omDebug(`[OM:asyncBuffer] skipping persist for thread ${threadId}: observational memory record was cleared`);
28165
+ return {
28166
+ status: "not-committed",
28167
+ reason: "the observational memory record was cleared"
28168
+ };
28169
+ }
28170
+ let committedRecord = liveRecord;
28171
+ if (!appendResult.persisted) {
28172
+ const head = appendAttempts > 1 ? await getLineageHead(this.storage, record) : null;
28173
+ if (!(!!head && (getBufferedChunks(head).some((chunk) => chunk.cycleId === this.cycleId) || head.activeObservations.includes(processed.observations)))) return {
28174
+ status: "not-committed",
28175
+ reason: "the buffered chunk was already observed"
28176
+ };
28177
+ this.persistedRecordId = head.id;
28178
+ committedRecord = head;
28179
+ } else this.persistedRecordId = appendResult.recordId;
28180
+ await this.emitBufferingEndMarker(processed);
28181
+ await this.opts.onBufferedChunkPersisted?.();
28182
+ if (committedRecord.id !== this.persistedRecordId) {
28183
+ const landedOn = await getLineageRecord(this.storage, record, this.persistedRecordId);
28184
+ if (!landedOn) return {
28185
+ status: "not-committed",
28186
+ reason: "the observational memory record was cleared"
28187
+ };
28188
+ committedRecord = landedOn;
28189
+ }
28190
+ try {
28191
+ await this.indexObservationGroups(processed.observations, threadId, resourceId, processed.lastObservedAt, this.persistedRecordId);
28192
+ const candidateTitle = processed.threadTitle?.trim();
28193
+ const hasValidThreadTitle = !!candidateTitle && candidateTitle.length >= 3;
28194
+ if (hasValidThreadTitle || processed.extractedValues) {
28195
+ const thread = await this.storage.getThreadById({ threadId });
28196
+ if (thread) {
28197
+ const oldTitle = thread.title?.trim();
28198
+ const newTitle = resolveThreadTitleUpdate(thread, candidateTitle);
28199
+ const shouldUpdateThreadTitle = newTitle !== void 0;
28200
+ const previousOmMetadata = getThreadOMMetadata(thread.metadata);
28201
+ const metadataUpdate = buildThreadMetadataFromExtractedValues(processed.extractors ?? this.observationConfig.extractors, processed.extractedValues);
28202
+ const newMetadata = setThreadOMMetadata(thread.metadata, {
28203
+ ...hasValidThreadTitle || metadataUpdate.threadTitle ? { threadTitle: metadataUpdate.threadTitle ?? processed.threadTitle } : {},
28204
+ extracted: {
28205
+ ...previousOmMetadata?.extracted ?? {},
28206
+ ...metadataUpdate.extracted ?? {}
28207
+ }
27689
28208
  });
27690
- await this.streamMarker(marker);
28209
+ await this.storage.patchThread({
28210
+ id: threadId,
28211
+ ...shouldUpdateThreadTitle ? { title: newTitle } : {},
28212
+ metadata: newMetadata
28213
+ });
28214
+ if (shouldUpdateThreadTitle) {
28215
+ const marker = createThreadUpdateMarker({
28216
+ cycleId: this.cycleId,
28217
+ threadId,
28218
+ oldTitle,
28219
+ newTitle
28220
+ });
28221
+ await this.streamMarker(marker);
28222
+ }
27691
28223
  }
27692
28224
  }
28225
+ } catch (error) {
28226
+ omError("[OM] Post-buffer indexing or thread update failed; the buffered chunk is kept", error);
27693
28227
  }
27694
- return true;
28228
+ return {
28229
+ status: "committed",
28230
+ processed,
28231
+ record: committedRecord
28232
+ };
27695
28233
  }
27696
- async emitEndMarkers(_cycleId, processed) {
27697
- if (!processed.observations) return;
28234
+ async emitEndMarkers() {}
28235
+ async emitBufferingEndMarker(processed) {
27698
28236
  const { record, threadId, messages } = this.opts;
27699
28237
  const tokensBuffered = await this.tokenCounter.countMessagesAsync(messages);
27700
28238
  const totalBufferedTokens = getBufferedChunks(await this.storage.getObservationalMemory(record.threadId, record.resourceId)).reduce((sum, c) => sum + (c.tokenCount ?? 0), 0) || processed.observationTokens;
@@ -27704,7 +28242,7 @@ var AsyncBufferObservationStrategy = class extends ObservationStrategy {
27704
28242
  startedAt: this.startedAt,
27705
28243
  tokensBuffered,
27706
28244
  bufferedTokens: totalBufferedTokens,
27707
- recordId: record.id,
28245
+ recordId: this.persistedRecordId ?? record.id,
27708
28246
  threadId,
27709
28247
  observations: processed.observations,
27710
28248
  extractedValues: processed.extractedValues,
@@ -27714,7 +28252,10 @@ var AsyncBufferObservationStrategy = class extends ObservationStrategy {
27714
28252
  ...endMarker,
27715
28253
  transient: true
27716
28254
  }).catch(() => {});
27717
- await this.persistMarkerToStorage(endMarker, threadId, record.resourceId ?? void 0);
28255
+ await this.persistMarkerToStorage(endMarker, threadId, record.resourceId ?? void 0, {
28256
+ notAfter: new Date(this.startedAt),
28257
+ onCycleStartMessage: true
28258
+ });
27718
28259
  }
27719
28260
  async emitFailedMarkers(_cycleId, error) {
27720
28261
  const { record, threadId, messages } = this.opts;
@@ -27733,7 +28274,10 @@ var AsyncBufferObservationStrategy = class extends ObservationStrategy {
27733
28274
  ...failedMarker,
27734
28275
  transient: true
27735
28276
  }).catch(() => {});
27736
- await this.persistMarkerToStorage(failedMarker, threadId, record.resourceId ?? void 0);
28277
+ await this.persistMarkerToStorage(failedMarker, threadId, record.resourceId ?? void 0, {
28278
+ notAfter: new Date(this.startedAt),
28279
+ onCycleStartMessage: true
28280
+ });
27737
28281
  }
27738
28282
  };
27739
28283
  //#endregion
@@ -27756,10 +28300,17 @@ var ResourceScopedObservationStrategy = class extends ObservationStrategy {
27756
28300
  lastBatchProviderMetadata;
27757
28301
  observationResults = [];
27758
28302
  priorMetadataByThread = /* @__PURE__ */ new Map();
28303
+ /** Each observed thread's wrapped section from the last composition. */
28304
+ threadSections = /* @__PURE__ */ new Map();
28305
+ /** Head text the processed observations were composed from (the commit's expected text). */
28306
+ composedFrom = "";
27759
28307
  constructor(deps, opts) {
27760
28308
  super(deps, opts);
27761
28309
  this.resourceId = opts.resourceId;
27762
28310
  }
28311
+ getObservedMessagesForThread(threadId) {
28312
+ return this.threadsWithMessages.get(threadId);
28313
+ }
27763
28314
  get needsLock() {
27764
28315
  return true;
27765
28316
  }
@@ -27846,10 +28397,12 @@ var ResourceScopedObservationStrategy = class extends ObservationStrategy {
27846
28397
  content: typeof m.content === "string" ? m.content : JSON.stringify(m.content)
27847
28398
  }))
27848
28399
  });
27849
- const existingObservations = (await this.storage.getObservationalMemory(null, this.resourceId))?.activeObservations ?? record.activeObservations ?? "";
28400
+ const freshRecord = await this.storage.getObservationalMemory(null, this.resourceId);
28401
+ const existingObservations = freshRecord?.activeObservations ?? record.activeObservations ?? "";
27850
28402
  return {
27851
28403
  messages: Array.from(this.threadsWithMessages.values()).flat(),
27852
- existingObservations
28404
+ existingObservations,
28405
+ contextRecord: freshRecord
27853
28406
  };
27854
28407
  }
27855
28408
  async emitStartMarkers(cycleId) {
@@ -27873,7 +28426,7 @@ var ResourceScopedObservationStrategy = class extends ObservationStrategy {
27873
28426
  }
27874
28427
  }
27875
28428
  }
27876
- async observe(_existingObservations, _messages) {
28429
+ async observe(observerContext, _messages, wasTruncated) {
27877
28430
  const maxTokensPerBatch = this.observationConfig.maxTokensPerBatch ?? OBSERVATIONAL_MEMORY_DEFAULTS.observation.maxTokensPerBatch;
27878
28431
  const orderedThreadIds = this.threadOrder.filter((tid) => this.threadsWithMessages.has(tid));
27879
28432
  const batches = [];
@@ -27899,10 +28452,10 @@ var ResourceScopedObservationStrategy = class extends ObservationStrategy {
27899
28452
  }
27900
28453
  if (currentBatch.threadIds.length > 0) batches.push(currentBatch);
27901
28454
  const batchResults = await Promise.all(batches.map(async (batch) => {
27902
- return this.deps.observer.callMultiThread(_existingObservations, batch.threadMap, batch.threadIds, this.opts.abortSignal, this.opts.requestContext, this.priorMetadataByThread, this.opts.observabilityContext, void 0, {
28455
+ return this.deps.observer.callMultiThread(observerContext, batch.threadMap, batch.threadIds, this.opts.abortSignal, this.opts.requestContext, this.priorMetadataByThread, this.opts.observabilityContext, void 0, {
27903
28456
  resourceId: this.opts.resourceId,
27904
28457
  trigger: this.opts.trigger
27905
- }, this.opts.record.observedTimezone, this.opts.agent, this.opts.currentModel);
28458
+ }, this.opts.record.observedTimezone, this.opts.agent, this.opts.currentModel, wasTruncated);
27906
28459
  }));
27907
28460
  for (const batchResult of batchResults) {
27908
28461
  for (const [threadId, result] of batchResult.results) this.multiThreadResults.set(threadId, result);
@@ -27969,14 +28522,38 @@ var ResourceScopedObservationStrategy = class extends ObservationStrategy {
27969
28522
  }
27970
28523
  });
27971
28524
  }
28525
+ this.composedFrom = existingObservations;
28526
+ const processed = await this.compose(existingObservations, record);
28527
+ for (const [index, { threadId, threadMessages, result }] of this.observationResults.entries()) this.deps.emitDebugEvent({
28528
+ type: "observation_complete",
28529
+ timestamp: /* @__PURE__ */ new Date(),
28530
+ threadId,
28531
+ resourceId: this.resourceId,
28532
+ observations: this.threadSections.get(threadId) ?? "",
28533
+ rawObserverOutput: result.observations,
28534
+ previousObservations: record.activeObservations,
28535
+ messages: threadMessages.map((m) => ({
28536
+ role: m.role,
28537
+ content: typeof m.content === "string" ? m.content : JSON.stringify(m.content)
28538
+ })),
28539
+ usage: index === 0 && this.totalBatchUsage.totalTokens > 0 ? this.totalBatchUsage : void 0
28540
+ });
28541
+ return processed;
28542
+ }
28543
+ /**
28544
+ * Merge every observed thread's section into `existingObservations` (the head text). Resource
28545
+ * scope rewrites an existing same-day `<thread>` section in place, so the result is not an
28546
+ * append of the head text.
28547
+ */
28548
+ async compose(existingObservations, baseRecord) {
27972
28549
  let currentObservations = existingObservations;
27973
28550
  let cycleObservationTokens = 0;
27974
28551
  const threadMetadataUpdates = [];
27975
- for (const obsResult of this.observationResults) {
27976
- const { threadId, threadMessages, result } = obsResult;
28552
+ for (const { threadId, threadMessages, result } of this.observationResults) {
27977
28553
  cycleObservationTokens += this.tokenCounter.countObservations(result.observations);
27978
28554
  const messageRange = this.retrieval ? buildMessageRange(threadMessages) : void 0;
27979
28555
  const threadSection = await this.wrapWithThreadTag(threadId, result.observations, messageRange);
28556
+ this.threadSections.set(threadId, threadSection);
27980
28557
  const threadLastObservedAt = this.getMaxMessageTimestamp(threadMessages);
27981
28558
  currentObservations = this.replaceOrAppendThreadSection(currentObservations, threadId, threadSection, threadLastObservedAt);
27982
28559
  threadMetadataUpdates.push({
@@ -27990,26 +28567,11 @@ var ResourceScopedObservationStrategy = class extends ObservationStrategy {
27990
28567
  extractors: result.extractors,
27991
28568
  lastObservedMessageCursor: getLastObservedMessageCursor(threadMessages)
27992
28569
  });
27993
- const isFirstThread = this.observationResults.indexOf(obsResult) === 0;
27994
- this.deps.emitDebugEvent({
27995
- type: "observation_complete",
27996
- timestamp: /* @__PURE__ */ new Date(),
27997
- threadId,
27998
- resourceId: this.resourceId,
27999
- observations: threadSection,
28000
- rawObserverOutput: result.observations,
28001
- previousObservations: record.activeObservations,
28002
- messages: threadMessages.map((m) => ({
28003
- role: m.role,
28004
- content: typeof m.content === "string" ? m.content : JSON.stringify(m.content)
28005
- })),
28006
- usage: isFirstThread && this.totalBatchUsage.totalTokens > 0 ? this.totalBatchUsage : void 0
28007
- });
28008
28570
  }
28009
28571
  const observedMessages = this.observationResults.flatMap((r) => r.threadMessages);
28010
28572
  const lastObservedAt = this.getMaxMessageTimestamp(observedMessages);
28011
28573
  const newMessageIds = observedMessages.map((m) => m.id);
28012
- const existingIds = record.observedMessageIds ?? [];
28574
+ const existingIds = baseRecord.observedMessageIds ?? [];
28013
28575
  const observedMessageIds = [.../* @__PURE__ */ new Set([...existingIds, ...newMessageIds])];
28014
28576
  const observationTokens = this.tokenCounter.countObservations(currentObservations);
28015
28577
  return {
@@ -28023,8 +28585,25 @@ var ResourceScopedObservationStrategy = class extends ObservationStrategy {
28023
28585
  }
28024
28586
  async persist(processed) {
28025
28587
  const { record, resourceId } = this.opts;
28588
+ const head = await getLineageHead(this.storage, record);
28589
+ if (!head) return {
28590
+ status: "not-committed",
28591
+ reason: "the observational memory record was cleared"
28592
+ };
28593
+ const committed = await this.commitActiveObservationsToHead({
28594
+ processed,
28595
+ composedFrom: this.composedFrom,
28596
+ target: head,
28597
+ recompose: (fresh) => this.compose(fresh.activeObservations ?? "", fresh),
28598
+ cycleMessageIds: this.observationResults.flatMap((r) => r.threadMessages.map((m) => m.id))
28599
+ });
28600
+ if (!committed) return {
28601
+ status: "not-committed",
28602
+ reason: "the observational memory head kept changing during the commit"
28603
+ };
28604
+ processed = committed.processed;
28026
28605
  const threadUpdateMarkers = [];
28027
- if (processed.threadMetadataUpdates) for (const update of processed.threadMetadataUpdates) {
28606
+ if (processed.threadMetadataUpdates && !committed.alreadyCovered) for (const update of processed.threadMetadataUpdates) {
28028
28607
  const thread = await this.storage.getThreadById({ threadId: update.threadId });
28029
28608
  if (thread) {
28030
28609
  const oldTitle = thread.title?.trim();
@@ -28057,15 +28636,13 @@ var ResourceScopedObservationStrategy = class extends ObservationStrategy {
28057
28636
  }
28058
28637
  }
28059
28638
  for (const marker of threadUpdateMarkers) await this.streamMarker(marker);
28060
- await this.storage.updateActiveObservations({
28061
- id: record.id,
28062
- observations: processed.observations,
28063
- tokenCount: processed.observationTokens,
28064
- lastObservedAt: processed.lastObservedAt,
28065
- observedMessageIds: processed.observedMessageIds
28066
- });
28067
- if (resourceId) await Promise.all(this.observationResults.map(({ threadId, threadMessages, result }) => this.indexObservationGroups(result.observations, threadId, resourceId, this.getMaxMessageTimestamp(threadMessages), record.id)));
28068
- return true;
28639
+ if (resourceId && !committed.alreadyCovered) await Promise.all(this.observationResults.map(({ threadId, threadMessages, result }) => this.indexObservationGroups(result.observations, threadId, resourceId, this.getMaxMessageTimestamp(threadMessages), committed.record.id)));
28640
+ return {
28641
+ status: "committed",
28642
+ processed,
28643
+ record: committed.record,
28644
+ alreadyCovered: committed.alreadyCovered
28645
+ };
28069
28646
  }
28070
28647
  async emitEndMarkers(cycleId, processed) {
28071
28648
  for (const obsResult of this.observationResults) {
@@ -28124,7 +28701,8 @@ ObservationStrategy.create = ((om, opts) => {
28124
28701
  observedMessageIds: om.observedMessageIds,
28125
28702
  obscureThreadIds: om.getObscureThreadIds(),
28126
28703
  onIndexObservations: om.onIndexObservations,
28127
- emitDebugEvent: (e) => om.emitDebugEvent(e)
28704
+ emitDebugEvent: (e) => om.emitDebugEvent(e),
28705
+ prepareObserverContext: (observations, record) => om.prepareObserverContext(observations, record)
28128
28706
  };
28129
28707
  if (opts.cycleId) return new AsyncBufferObservationStrategy(deps, opts);
28130
28708
  if (deps.scope === "resource" && opts.resourceId) return new ResourceScopedObservationStrategy(deps, opts);
@@ -28256,8 +28834,8 @@ var ObservationStep = class {
28256
28834
  let reflected = false;
28257
28835
  let didThresholdCleanup = false;
28258
28836
  let observerExchange;
28837
+ const step0Messages = this.stepNumber === 0 ? getObservableMessages(messageList) : [];
28259
28838
  if (this.stepNumber === 0) {
28260
- const step0Messages = getObservableMessages(messageList);
28261
28839
  const activation = await om.activate({
28262
28840
  threadId,
28263
28841
  resourceId,
@@ -28314,7 +28892,17 @@ var ObservationStep = class {
28314
28892
  record: this.turn.record,
28315
28893
  messages: getObservableMessages(messageList)
28316
28894
  });
28317
- if (statusSnapshot.shouldBuffer) {
28895
+ if (statusSnapshot.inAsyncObservationBand && !statusSnapshot.canActivate && !hasIncompleteToolCalls) {
28896
+ await this.turn.refreshRecord();
28897
+ statusSnapshot = await om.getStatus({
28898
+ threadId,
28899
+ resourceId,
28900
+ record: this.turn.record,
28901
+ messages: getObservableMessages(messageList)
28902
+ });
28903
+ }
28904
+ const activateBeforeBuffering = statusSnapshot.inAsyncObservationBand && statusSnapshot.canActivate && !hasIncompleteToolCalls;
28905
+ if (statusSnapshot.shouldBuffer && !activateBeforeBuffering) {
28318
28906
  const allMessages = getObservableMessages(messageList);
28319
28907
  const unobservedMessages = om.getUnobservedMessages(allMessages, statusSnapshot.record);
28320
28908
  const candidates = om.getUnobservedMessages(unobservedMessages, statusSnapshot.record, { excludeBuffered: true });
@@ -28328,8 +28916,10 @@ var ObservationStep = class {
28328
28916
  omDebug(`[OM:buffer] onBufferChunkSealed hook failed: ${error instanceof Error ? error.message : String(error)}`);
28329
28917
  }
28330
28918
  if (this.turn.memory) await this.turn.memory.persistMessages(safeCandidates);
28331
- messageList.removeByIds(safeCandidates.map((msg) => msg.id));
28332
- for (const msg of safeCandidates) messageList.add(msg, "memory");
28919
+ const inputIds = new Set(messageList.get.input.db().map((msg) => msg.id));
28920
+ const sealedOutput = safeCandidates.filter((msg) => !inputIds.has(msg.id));
28921
+ messageList.removeByIds(sealedOutput.map((msg) => msg.id));
28922
+ for (const msg of sealedOutput) messageList.add(msg, "memory");
28333
28923
  }
28334
28924
  if (!deferred) {
28335
28925
  om.trackBackgroundWork(om.buffer({
@@ -28351,7 +28941,8 @@ var ObservationStep = class {
28351
28941
  buffered = true;
28352
28942
  }
28353
28943
  }
28354
- const willObserveNow = statusSnapshot.shouldObserve && !hasIncompleteToolCalls;
28944
+ const bandDefersObservation = statusSnapshot.inAsyncObservationBand && !statusSnapshot.canActivate;
28945
+ const willObserveNow = statusSnapshot.shouldObserve && !hasIncompleteToolCalls && !bandDefersObservation;
28355
28946
  /** In-flight message ids the step-0 cleanup must never remove from live context. */
28356
28947
  let step0PreserveIds;
28357
28948
  if (this.stepNumber > 0 || willObserveNow) {
@@ -28393,7 +28984,7 @@ var ObservationStep = class {
28393
28984
  }
28394
28985
  if (willObserveNow) {
28395
28986
  const preObsGeneration = this.turn.record.generationCount;
28396
- const obsResult = await this.runThresholdObservation();
28987
+ const obsResult = await this.runThresholdObservation(statusSnapshot.inAsyncObservationBand);
28397
28988
  observerExchange = obsResult.observerExchange;
28398
28989
  if (obsResult.succeeded) {
28399
28990
  observed = true;
@@ -28466,12 +29057,12 @@ var ObservationStep = class {
28466
29057
  /**
28467
29058
  * Run the full threshold observation pipeline:
28468
29059
  * waitForBuffering → re-check → activate buffered chunks → reflect → observe
28469
- * (sync fallback when pending tokens are still at or above the threshold)
29060
+ * (sync fallback at blockAfter when async buffering is enabled)
28470
29061
  */
28471
- async runThresholdObservation() {
29062
+ async runThresholdObservation(inAsyncObservationBand) {
28472
29063
  const { threadId, resourceId, messageList } = this.turn;
28473
29064
  const om = this.turn.om;
28474
- await om.waitForBuffering(threadId, resourceId);
29065
+ if (!inAsyncObservationBand) await om.waitForBuffering(threadId, resourceId);
28475
29066
  await this.turn.refreshRecord();
28476
29067
  const observableMessages = this.seededResponseMessage ? getObservableMessages(messageList).filter((msg) => msg.id !== this.turn.responseMessageId) : getObservableMessages(messageList);
28477
29068
  let status = await om.getStatus({
@@ -28500,7 +29091,16 @@ var ObservationStep = class {
28500
29091
  messageList
28501
29092
  });
28502
29093
  this.turn.setRecord(activation.record);
28503
- if (!activation.activated) break;
29094
+ if (!activation.activated) {
29095
+ await this.turn.refreshRecord();
29096
+ status = await om.getStatus({
29097
+ threadId,
29098
+ resourceId,
29099
+ record: this.turn.record,
29100
+ messages: pendingMessages
29101
+ });
29102
+ break;
29103
+ }
28504
29104
  activated = true;
28505
29105
  const ids = new Set(activation.activatedMessageIds ?? []);
28506
29106
  activatedMessageIds.push(...ids);
@@ -28512,11 +29112,17 @@ var ObservationStep = class {
28512
29112
  messages: pendingMessages
28513
29113
  });
28514
29114
  }
29115
+ if (status.shouldObserve && status.canActivate) return {
29116
+ succeeded: activated,
29117
+ record: this.turn.record,
29118
+ activatedMessageIds: activated ? activatedMessageIds : void 0,
29119
+ cleanupMessageIds: activatedMessageIds
29120
+ };
28515
29121
  if (activated) {
28516
- const postActivationRecord = this.turn.record;
29122
+ const preReflectionRecord = this.turn.record;
28517
29123
  await om.reflector.maybeReflect({
28518
- record: postActivationRecord,
28519
- observationTokens: postActivationRecord.observationTokenCount ?? 0,
29124
+ record: preReflectionRecord,
29125
+ observationTokens: preReflectionRecord.observationTokenCount ?? 0,
28520
29126
  threadId,
28521
29127
  writer: this.turn.writer,
28522
29128
  messageList,
@@ -28531,6 +29137,14 @@ var ObservationStep = class {
28531
29137
  }),
28532
29138
  trigger: "turn-sync"
28533
29139
  });
29140
+ await this.turn.refreshRecord();
29141
+ const postActivationRecord = this.turn.record;
29142
+ status = await om.getStatus({
29143
+ threadId,
29144
+ resourceId,
29145
+ record: postActivationRecord,
29146
+ messages: pendingMessages
29147
+ });
28534
29148
  if (!status.shouldObserve) return {
28535
29149
  succeeded: true,
28536
29150
  record: postActivationRecord,
@@ -28538,6 +29152,12 @@ var ObservationStep = class {
28538
29152
  cleanupMessageIds: activatedMessageIds
28539
29153
  };
28540
29154
  }
29155
+ if (status.inAsyncObservationBand || status.canActivate) return {
29156
+ succeeded: activated,
29157
+ record: this.turn.record,
29158
+ activatedMessageIds: activated ? activatedMessageIds : void 0,
29159
+ cleanupMessageIds: activatedMessageIds
29160
+ };
28541
29161
  let obsResult;
28542
29162
  try {
28543
29163
  obsResult = await om.observe({
@@ -28889,13 +29509,18 @@ Take the existing observations and rewrite them to make it easier to continue in
28889
29509
 
28890
29510
  IMPORTANT: your reflections are THE ENTIRETY of the assistants memory. Any information you do not add to your reflections will be immediately forgotten. Make sure you do not leave out anything. Your reflections must assume the assistant knows nothing - your reflections are the ENTIRE memory system.
28891
29511
 
29512
+ The assistant relies on this memory to answer precise questions about what the user said, when they said it, and how things changed over time.
29513
+
28892
29514
  When consolidating observations:
28893
- - Preserve and include dates/times when present (temporal context is critical)
28894
- - Retain the most relevant timestamps (start times, completion times, significant events)
29515
+ - Keep every concrete fact: names, places, dates, numbers, amounts, durations, titles, versions, and who said what. Merge duplicates, but never drop specifics
29516
+ - Keep every dated statement with its date, and retain the timestamps that matter (start times, completion times, significant events)
29517
+ - When the user changed their mind or a fact was updated, keep both the old and new values with their dates and say which is current
29518
+ - Keep what the user explicitly said they have never done, do not know, or refused, and keep every stated preference, rule, and instruction with its meaning intact
29519
+ - Prefer many short factual lines over prose summaries. Do not replace facts with generalities like "discussed travel plans"
28895
29520
  - Combine related items where it makes sense (e.g., "agent called view tool 5 times on file x")
28896
29521
  - Preserve ✅ completion markers — they are memory signals that tell the assistant what is already resolved and help prevent repeated work
28897
29522
  - Preserve the concrete resolved outcome captured by ✅ markers so the assistant knows what exactly is done
28898
- - Condense older observations more aggressively, retain more detail for recent ones
29523
+ - Condense the wording of older observations more than recent ones, but keep their facts
28899
29524
 
28900
29525
  CRITICAL: USER ASSERTIONS vs QUESTIONS
28901
29526
  - "User stated: X" = authoritative assertion (user told us something about themselves)
@@ -29177,6 +29802,25 @@ async function withAbortCheck(fn, abortSignal) {
29177
29802
  * size while still letting them fire sooner than a threshold activation.
29178
29803
  */
29179
29804
  const EARLY_ACTIVATION_SIZE_FLOOR_RATIO = .75;
29805
+ /** A reflection commit that created nothing because the reflected record is no longer the head. */
29806
+ var ReflectionNotAppliedError = class extends Error {
29807
+ constructor(recordId, headId) {
29808
+ super(`Reflection not applied: record ${recordId} is no longer the current generation (head is ${headId})`);
29809
+ this.name = "ReflectionNotAppliedError";
29810
+ }
29811
+ };
29812
+ /**
29813
+ * Whether a reflection commit created the generation the caller asked for. Storage creates
29814
+ * nothing (and returns the current head or the stored record) when the reflected record was
29815
+ * already retired or its observations were rewritten since the reflection read them.
29816
+ *
29817
+ * Adapters that predate `newRecordId` ignore it and do not report `supersededBy`; for those a
29818
+ * returned record other than the reflected one means a new generation was created.
29819
+ */
29820
+ function isReflectionApplied(returned, newRecordId, reflectedRecordId) {
29821
+ if (returned.id === newRecordId) return true;
29822
+ return returned.supersededBy === void 0 && returned.id !== reflectedRecordId;
29823
+ }
29180
29824
  /**
29181
29825
  * Runs the Reflector agent for compressing observations.
29182
29826
  * Handles synchronous reflection, async buffered reflection, and activation.
@@ -29540,14 +30184,14 @@ var ReflectorRunner = class {
29540
30184
  * Perform async buffered reflection — reflects observations and stores to bufferedReflection.
29541
30185
  * Does NOT create a new generation or update activeObservations.
29542
30186
  */
29543
- async doAsyncBufferedReflection(record, _bufferKey, writer, requestContext, observabilityContext, priorExtractedValues, mainAgent, sendSignal, trigger, currentModel) {
30187
+ async doAsyncBufferedReflection(record, bufferKey, writer, requestContext, observabilityContext, priorExtractedValues, mainAgent, sendSignal, trigger, currentModel) {
29544
30188
  const currentRecord = await this.storage.getObservationalMemory(record.threadId, record.resourceId) ?? record;
29545
30189
  const observationTokens = currentRecord.observationTokenCount ?? 0;
29546
30190
  const reflectThreshold = getMaxThreshold(this.getEffectiveReflectionTokens(currentRecord));
29547
30191
  const bufferActivation = this.reflectionConfig.bufferActivation ?? .5;
29548
30192
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
29549
30193
  const cycleId = `reflect-buf-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
29550
- BufferingCoordinator.reflectionBufferCycleIds.set(_bufferKey, cycleId);
30194
+ BufferingCoordinator.reflectionBufferCycleIds.set(bufferKey, cycleId);
29551
30195
  const allLines = (currentRecord.activeObservations ?? "").split("\n");
29552
30196
  const totalLines = allLines.length;
29553
30197
  const avgTokensPerLine = totalLines > 0 ? observationTokens / totalLines : 0;
@@ -29586,13 +30230,24 @@ var ReflectorRunner = class {
29586
30230
  await persistThreadExtractedValues(this.storage, this.reflectionConfig.extractors, currentRecord.threadId ?? void 0, reflectResult.extractedValues);
29587
30231
  const reflectionTokenCount = this.tokenCounter.countObservations(reflectResult.observations);
29588
30232
  omDebug(`[OM:reflect] doAsyncBufferedReflection: reflector returned ${reflectionTokenCount} tokens (${reflectResult.observations?.length} chars), saving to recordId=${currentRecord.id}`);
29589
- await this.storage.updateBufferedReflection({
29590
- id: currentRecord.id,
29591
- reflection: reflectResult.observations,
29592
- tokenCount: reflectionTokenCount,
29593
- inputTokenCount: sliceTokenEstimate,
29594
- reflectedObservationLineCount
29595
- });
30233
+ if (!await runOMCommit(this.buffering.getLockKey(currentRecord.threadId, currentRecord.resourceId), async () => {
30234
+ if ((await this.storage.getObservationalMemory(currentRecord.threadId, currentRecord.resourceId))?.id !== currentRecord.id) return false;
30235
+ await this.storage.updateBufferedReflection({
30236
+ id: currentRecord.id,
30237
+ reflection: reflectResult.observations,
30238
+ tokenCount: reflectionTokenCount,
30239
+ inputTokenCount: sliceTokenEstimate,
30240
+ reflectedObservationLineCount
30241
+ });
30242
+ return true;
30243
+ })) {
30244
+ omDebug(`[OM:reflect] doAsyncBufferedReflection: record ${currentRecord.id} is no longer the head; discarding the buffered reflection`);
30245
+ BufferingCoordinator.lastBufferedBoundary.delete(bufferKey);
30246
+ return {
30247
+ usage: reflectResult.usage,
30248
+ providerMetadata: reflectResult.providerMetadata
30249
+ };
30250
+ }
29596
30251
  omDebug(`[OM:reflect] doAsyncBufferedReflection: bufferedReflection saved with lineCount=${reflectedObservationLineCount}`);
29597
30252
  if (writer) {
29598
30253
  const endMarker = createBufferingEndMarker({
@@ -29671,10 +30326,16 @@ var ReflectorRunner = class {
29671
30326
  }
29672
30327
  }
29673
30328
  omDebug(`[OM:reflect] tryActivateBufferedReflection: activating, beforeTokens=${beforeTokens}, combinedTokenCount=${combinedTokenCount}, reflectedLineCount=${reflectedLineCount}, unreflectedLines=${unreflectedLines.length}`);
29674
- await this.storage.swapBufferedReflectionToActive({
30329
+ const newRecordId = crypto.randomUUID();
30330
+ const swapped = await runOMCommit(lockKey, () => this.storage.swapBufferedReflectionToActive({
29675
30331
  currentRecord: freshRecord,
29676
- tokenCount: combinedTokenCount
29677
- });
30332
+ tokenCount: combinedTokenCount,
30333
+ newRecordId
30334
+ }), { priority: "reflection" });
30335
+ if (!isReflectionApplied(swapped, newRecordId, freshRecord.id)) {
30336
+ omDebug(`[OM:reflect] tryActivateBufferedReflection: buffered reflection on ${freshRecord.id} not applied (head is ${swapped.id})`);
30337
+ return { status: "not-applied" };
30338
+ }
29678
30339
  if (committedContext) await this.notifyReflectionCommitted({
29679
30340
  ...committedContext,
29680
30341
  observations: allLines.slice(0, reflectedLineCount).join("\n").trim(),
@@ -29729,7 +30390,8 @@ var ReflectorRunner = class {
29729
30390
  * @internal Used by observation strategies. Do not call directly.
29730
30391
  */
29731
30392
  async maybeReflect(opts) {
29732
- const { record, observationTokens, writer, abortSignal, mainAgent, sendSignal, sendStateSignal, messageList, currentModel, reflectionHooks, trigger, requestContext, observabilityContext, lastActivityAt, threadId: requestedThreadId } = opts;
30393
+ const { record: callerRecord, observationTokens, writer, abortSignal, mainAgent, sendSignal, sendStateSignal, messageList, currentModel, reflectionHooks, trigger, requestContext, observabilityContext, lastActivityAt, threadId: requestedThreadId } = opts;
30394
+ const record = { ...callerRecord };
29733
30395
  const lockKey = this.buffering.getLockKey(record.threadId, record.resourceId);
29734
30396
  const reflectThreshold = getMaxThreshold(this.getEffectiveReflectionTokens(record));
29735
30397
  const priorExtractedValues = await getThreadExtractedValues(this.storage, this.reflectionConfig.extractors, requestedThreadId ?? record.threadId);
@@ -29783,7 +30445,7 @@ var ReflectorRunner = class {
29783
30445
  abortSignal,
29784
30446
  observabilityContext
29785
30447
  });
29786
- if (activationResult.status === "activated") return;
30448
+ if (activationResult.status === "activated" || activationResult.status === "not-applied") return;
29787
30449
  if (activationResult.status === "suppressed") {
29788
30450
  omDebug(`[OM:reflect] skipping sync fallback / re-buffer after suppressed early ${activationMetadata.triggeredBy} activation (reason=${activationResult.reason})`);
29789
30451
  return;
@@ -29866,13 +30528,19 @@ var ReflectorRunner = class {
29866
30528
  reflectResult.observations = await applyTextTransform(this.hooks, "afterReflection", reflectResult.observations, hookContext);
29867
30529
  reflectionUsage = reflectResult.usage;
29868
30530
  reflectionProviderMetadata = reflectResult.providerMetadata;
29869
- await persistThreadExtractedValues(this.storage, this.reflectionConfig.extractors, record.threadId ?? void 0, reflectResult.extractedValues);
29870
30531
  const reflectionTokenCount = this.tokenCounter.countObservations(reflectResult.observations);
29871
- await this.storage.createReflectionGeneration({
30532
+ const newRecordId = crypto.randomUUID();
30533
+ const committedRecord = await runOMCommit(this.buffering.getLockKey(record.threadId, record.resourceId), () => this.storage.createReflectionGeneration({
29872
30534
  currentRecord: record,
29873
30535
  reflection: reflectResult.observations,
29874
- tokenCount: reflectionTokenCount
29875
- });
30536
+ tokenCount: reflectionTokenCount,
30537
+ newRecordId
30538
+ }), { priority: "reflection" });
30539
+ if (!isReflectionApplied(committedRecord, newRecordId, record.id)) {
30540
+ omDebug(`[OM:reflect] reflection of ${record.id} not applied (head is ${committedRecord.id})`);
30541
+ throw new ReflectionNotAppliedError(record.id, committedRecord.id);
30542
+ }
30543
+ await persistThreadExtractedValues(this.storage, this.reflectionConfig.extractors, record.threadId ?? void 0, reflectResult.extractedValues);
29876
30544
  if (reflectionTokenCount >= reflectThreshold) this.syncReflectionSuppression.set(lockKey, reflectionTokenCount);
29877
30545
  else this.syncReflectionSuppression.delete(lockKey);
29878
30546
  await this.notifyReflectionCommitted({
@@ -29952,6 +30620,7 @@ var ReflectorRunner = class {
29952
30620
  failureKind: failedMarker.data.failureKind,
29953
30621
  error: reflectionError.message
29954
30622
  });
30623
+ if (error instanceof ReflectionNotAppliedError) return;
29955
30624
  if (lifecycleError !== void 0 || abortSignal?.aborted || this.reflectionConfig.failurePolicy !== "continue" || !isOmModelExecutionError(error) || error.failureKind !== "reflector-model") throw error;
29956
30625
  this.syncReflectionSuppression.set(lockKey, observationTokens);
29957
30626
  omError("[OM] Reflection failed", error);
@@ -30295,6 +30964,8 @@ function writeProcessInputStepReproCapture(params) {
30295
30964
  * Returns the parts from the latest step of a message (after the last step-start marker).
30296
30965
  * If no step-start marker exists, returns all parts.
30297
30966
  */
30967
+ /** Re-reads of the head after an activation swap hit a generation retired by a reflection. */
30968
+ const MAX_ACTIVATION_RETIRED_RETRIES = 3;
30298
30969
  function getLatestStepParts(parts) {
30299
30970
  for (let i = parts.length - 1; i >= 0; i--) if (parts[i]?.type === "step-start") return parts.slice(i + 1);
30300
30971
  return parts;
@@ -30488,6 +31159,8 @@ var ObservationalMemory = class ObservationalMemory {
30488
31159
  reflector;
30489
31160
  /** Buffering state coordinator — manages static maps and buffering lifecycle. */
30490
31161
  buffering;
31162
+ /** Unresolved observation blockAfter (multiplier or absolute); resolved per record. */
31163
+ observationBlockAfterSetting;
30491
31164
  shouldObscureThreadIds = false;
30492
31165
  hasher = xxhash();
30493
31166
  mastra;
@@ -30623,6 +31296,7 @@ var ObservationalMemory = class ObservationalMemory {
30623
31296
  else throw new Error("shareTokenBudget requires async buffering to be disabled (this is a temporary limitation). Add observation: { bufferTokens: false } to your config:\n\n observationalMemory: {\n shareTokenBudget: true,\n observation: { bufferTokens: false },\n }\n\nAsync buffering is enabled by default — this opt-out is only needed when using shareTokenBudget.");
30624
31297
  const observationActivateAfterIdle = config.observation?.activateAfterIdle ?? config.activateAfterIdle;
30625
31298
  const observationActivateAfterIdlePath = config.observation?.activateAfterIdle !== void 0 ? "observation.activateAfterIdle" : "activateAfterIdle";
31299
+ this.observationBlockAfterSetting = asyncBufferingDisabled ? void 0 : config.observation?.blockAfter ?? (config.observation?.bufferTokens ?? OBSERVATIONAL_MEMORY_DEFAULTS.observation.bufferTokens ? 1.2 : void 0);
30626
31300
  this.observationConfig = {
30627
31301
  model: observationModel,
30628
31302
  maxRetries: config.observation?.maxRetries ?? RETRY_CONFIG.maxRetries,
@@ -30643,8 +31317,8 @@ var ObservationalMemory = class ObservationalMemory {
30643
31317
  bufferActivation: asyncBufferingDisabled ? void 0 : config.observation?.bufferActivation ?? OBSERVATIONAL_MEMORY_DEFAULTS.observation.bufferActivation,
30644
31318
  activateAfterIdle: parseActivationTTLConfig(observationActivateAfterIdle, observationActivateAfterIdlePath),
30645
31319
  activateOnProviderChange: config.observation?.activateOnProviderChange ?? config.activateOnProviderChange ?? false,
30646
- blockAfter: asyncBufferingDisabled ? void 0 : resolveBlockAfter(config.observation?.blockAfter ?? (config.observation?.bufferTokens ?? OBSERVATIONAL_MEMORY_DEFAULTS.observation.bufferTokens ? 1.2 : void 0), config.observation?.messageTokens ?? OBSERVATIONAL_MEMORY_DEFAULTS.observation.messageTokens),
30647
- previousObserverTokens: config.observation?.previousObserverTokens ?? 2e3,
31320
+ blockAfter: resolveBlockAfter(this.observationBlockAfterSetting, config.observation?.messageTokens ?? OBSERVATIONAL_MEMORY_DEFAULTS.observation.messageTokens),
31321
+ previousObserverTokens: config.observation?.previousObserverTokens ?? false,
30648
31322
  instruction: config.observation?.instruction,
30649
31323
  threadTitle: config.observation?.threadTitle ?? false,
30650
31324
  observeAttachments: config.observation?.observeAttachments ?? [...OBSERVATIONAL_MEMORY_DEFAULTS.observation.observeAttachments],
@@ -30953,6 +31627,13 @@ var ObservationalMemory = class ObservationalMemory {
30953
31627
  return this.observationConfig.messageTokens;
30954
31628
  }
30955
31629
  /**
31630
+ * Resolve the observation blockAfter for a record. A multiplier scales the record's
31631
+ * effective messageTokens (per-record overrides included); an absolute value is used as-is.
31632
+ */
31633
+ getEffectiveObservationBlockAfter(record) {
31634
+ return resolveBlockAfter(this.observationBlockAfterSetting, this.getEffectiveMessageTokens(record));
31635
+ }
31636
+ /**
30956
31637
  * Resolve the effective reflection observationTokens for a record.
30957
31638
  * Only explicit per-record overrides (stored under `_overrides`) win;
30958
31639
  * the initial config snapshot is ignored so instance-level changes
@@ -31066,18 +31747,21 @@ var ObservationalMemory = class ObservationalMemory {
31066
31747
  * the latest user input that triggered it.
31067
31748
  * @internal Used by observation strategies and the reflector. Do not call directly.
31068
31749
  */
31069
- async persistMarkerToStorage(marker, threadId, resourceId) {
31750
+ async persistMarkerToStorage(marker, threadId, resourceId, opts) {
31070
31751
  try {
31071
- const messages = (await this.storage.listMessages({
31752
+ const markerData = marker.data;
31753
+ const targetRole = markerData?.operationType === "reflection" ? "user" : "assistant";
31754
+ const preferred = [...opts?.preferMessages ?? []].reverse().find((m) => m.role === targetRole && (!m.threadId || m.threadId === threadId));
31755
+ const preferredStored = preferred ? (await this.storage.listMessagesById({ messageIds: [preferred.id] })).messages : [];
31756
+ const messages = (preferredStored.length ? { messages: preferredStored } : await this.storage.listMessages({
31072
31757
  threadId,
31073
31758
  perPage: 20,
31074
31759
  orderBy: {
31075
31760
  field: "createdAt",
31076
31761
  direction: "DESC"
31077
- }
31762
+ },
31763
+ ...opts?.notAfter ? { filter: { dateRange: { end: opts.notAfter } } } : {}
31078
31764
  }))?.messages ?? [];
31079
- const markerData = marker.data;
31080
- const targetRole = markerData?.operationType === "reflection" ? "user" : "assistant";
31081
31765
  for (const msg of messages) if (msg?.role === targetRole && msg.content?.parts && Array.isArray(msg.content.parts)) {
31082
31766
  if (!(markerData?.cycleId && msg.content.parts.some((p) => p?.type === marker.type && p?.data?.cycleId === markerData.cycleId))) msg.content.parts.push(marker);
31083
31767
  await this.messageHistory.persistMessages({
@@ -31164,7 +31848,7 @@ var ObservationalMemory = class ObservationalMemory {
31164
31848
  */
31165
31849
  createUnobservedMessage(message) {
31166
31850
  const unobservedParts = getUnobservedParts(message);
31167
- if (unobservedParts.length === 0) return null;
31851
+ if (unobservedParts.every((part) => part?.type?.startsWith("data-om-"))) return null;
31168
31852
  return {
31169
31853
  ...message,
31170
31854
  content: {
@@ -31536,17 +32220,17 @@ ${formattedMessages}
31536
32220
  async startAsyncBufferedObservation(record, threadId, unobservedMessages, lockKey, writer, contextWindowTokens, requestContext, observabilityContext) {
31537
32221
  const bufferKey = this.buffering.getObservationBufferKey(lockKey);
31538
32222
  const currentTokens = contextWindowTokens ?? await this.tokenCounter.countMessagesAsync(unobservedMessages) + (record.pendingMessageTokens ?? 0);
31539
- BufferingCoordinator.lastBufferedBoundary.set(bufferKey, currentTokens);
32223
+ BufferingCoordinator.setObservationBoundary(bufferKey, currentTokens);
31540
32224
  registerOp(record.id, "bufferingObservation");
31541
- this.storage.setBufferingObservationFlag(record.id, true, currentTokens).catch((err) => {
32225
+ runOMCommit(lockKey, () => this.storage.setBufferingObservationFlag(record.id, true, currentTokens)).catch((err) => {
31542
32226
  omError("[OM] Failed to set buffering observation flag", err);
31543
32227
  });
31544
32228
  const asyncOp = this.runAsyncBufferedObservation(record, threadId, unobservedMessages, bufferKey, writer, requestContext, observabilityContext).catch((err) => {
31545
32229
  omError("[OM] async buffering observation failed", err);
31546
32230
  }).finally(() => {
31547
- BufferingCoordinator.asyncBufferingOps.delete(bufferKey);
32231
+ if (BufferingCoordinator.asyncBufferingOps.get(bufferKey) === asyncOp) BufferingCoordinator.asyncBufferingOps.delete(bufferKey);
31548
32232
  unregisterOp(record.id, "bufferingObservation");
31549
- this.storage.setBufferingObservationFlag(record.id, false).catch((err) => {
32233
+ runOMCommit(lockKey, () => this.storage.setBufferingObservationFlag(record.id, false)).catch((err) => {
31550
32234
  omError("[OM] Failed to clear buffering observation flag", err);
31551
32235
  });
31552
32236
  });
@@ -31561,6 +32245,14 @@ ${formattedMessages}
31561
32245
  if (existingOp) try {
31562
32246
  await existingOp;
31563
32247
  } catch {}
32248
+ const releaseChunkWrite = BufferingCoordinator.trackChunkWrite(bufferKey);
32249
+ try {
32250
+ await this.bufferObservationChunk(record, threadId, unobservedMessages, bufferKey, releaseChunkWrite, writer, requestContext, observabilityContext);
32251
+ } finally {
32252
+ releaseChunkWrite();
32253
+ }
32254
+ }
32255
+ async bufferObservationChunk(record, threadId, unobservedMessages, bufferKey, releaseChunkWrite, writer, requestContext, observabilityContext) {
31564
32256
  const freshRecord = await this.storage.getObservationalMemory(record.threadId, record.resourceId);
31565
32257
  if (!freshRecord) return;
31566
32258
  let bufferCursor = BufferingCoordinator.lastBufferedAtTime.get(bufferKey) ?? freshRecord.lastBufferedAtTime ?? null;
@@ -31601,13 +32293,16 @@ ${formattedMessages}
31601
32293
  threadIds: [threadId],
31602
32294
  config: this.getObservationMarkerConfig()
31603
32295
  });
31604
- await this.persistMarkerToStorage(startMarker, threadId, freshRecord.resourceId ?? void 0);
32296
+ await this.persistMarkerToStorage(startMarker, threadId, freshRecord.resourceId ?? void 0, {
32297
+ notAfter: new Date(startedAt),
32298
+ preferMessages: messagesToBuffer
32299
+ });
31605
32300
  if (writer) writer.custom({
31606
32301
  ...startMarker,
31607
32302
  transient: true
31608
32303
  }).catch(() => {});
31609
32304
  omDebug(`[OM:bufferInput] cycleId=${cycleId}, msgCount=${messagesToBuffer.length}, msgTokens=${tokensToBuffer}, ids=${messagesToBuffer.map((m) => `${m.id?.slice(0, 8)}@${m.createdAt ? new Date(m.createdAt).toISOString() : "none"}`).join(",")}`);
31610
- if ((await this.runBufferedObservationCycle({
32305
+ await this.runBufferedObservationCycle({
31611
32306
  threadId,
31612
32307
  resourceId: freshRecord.resourceId ?? void 0,
31613
32308
  trigger: "async-buffer"
@@ -31621,12 +32316,13 @@ ${formattedMessages}
31621
32316
  writer,
31622
32317
  requestContext,
31623
32318
  observabilityContext,
31624
- trigger: "async-buffer"
31625
- }).run()))?.observed) {
31626
- const maxTs = this.getMaxMessageTimestamp(messagesToBuffer);
31627
- const cursor = new Date(maxTs.getTime() + 1);
31628
- BufferingCoordinator.lastBufferedAtTime.set(bufferKey, cursor);
31629
- }
32319
+ trigger: "async-buffer",
32320
+ onBufferedChunkPersisted: async () => {
32321
+ const maxTs = this.getMaxMessageTimestamp(messagesToBuffer);
32322
+ BufferingCoordinator.lastBufferedAtTime.set(bufferKey, new Date(maxTs.getTime() + 1));
32323
+ releaseChunkWrite();
32324
+ }
32325
+ }).run());
31630
32326
  }
31631
32327
  /**
31632
32328
  * Trigger async buffered observation if the token count has crossed a new interval.
@@ -31811,8 +32507,8 @@ ${formattedMessages}
31811
32507
  const { threadId, resourceId, recordId, activatedMessageIds } = opts;
31812
32508
  const lockKey = this.buffering.getLockKey(threadId, resourceId);
31813
32509
  const bufKey = this.buffering.getObservationBufferKey(lockKey);
31814
- BufferingCoordinator.lastBufferedBoundary.set(bufKey, 0);
31815
- await this.storage.setBufferingObservationFlag(recordId, false, 0).catch(() => {});
32510
+ BufferingCoordinator.setObservationBoundary(bufKey, 0);
32511
+ await runOMCommit(lockKey, () => this.storage.setBufferingObservationFlag(recordId, false, 0)).catch(() => {});
31816
32512
  if (activatedMessageIds && activatedMessageIds.length > 0) this.buffering.cleanupStaticMaps(threadId, resourceId, activatedMessageIds);
31817
32513
  }
31818
32514
  /**
@@ -31971,9 +32667,13 @@ ${formattedMessages}
31971
32667
  * @example
31972
32668
  * ```ts
31973
32669
  * const status = await om.getStatus({ threadId });
31974
- * if (status.shouldObserve) {
32670
+ * if (status.shouldObserve && status.canActivate) {
32671
+ * await om.activate({ threadId });
32672
+ * } else if (status.shouldObserve && !status.inAsyncObservationBand) {
32673
+ * // At/above blockAfter (or async buffering disabled): observe synchronously
31975
32674
  * await om.observe({ threadId });
31976
32675
  * } else if (status.shouldBuffer) {
32676
+ * // Below the threshold, or in the threshold→blockAfter band: buffer in the background
31977
32677
  * await om.buffer({ threadId });
31978
32678
  * }
31979
32679
  * if (status.shouldReflect) {
@@ -32003,8 +32703,10 @@ ${formattedMessages}
32003
32703
  const bufferedChunkCount = bufferedChunks.length;
32004
32704
  const bufferedChunkTokens = bufferedChunks.reduce((sum, chunk) => sum + (chunk.messageTokens ?? 0), 0);
32005
32705
  const asyncObservationEnabled = this.buffering.isAsyncObservationEnabled();
32706
+ const observationBlockAfter = asyncObservationEnabled ? this.getEffectiveObservationBlockAfter(record) : void 0;
32707
+ const inAsyncObservationBand = observationBlockAfter !== void 0 && pendingTokens >= threshold && pendingTokens < observationBlockAfter;
32006
32708
  let shouldBuffer = false;
32007
- if (asyncObservationEnabled && pendingTokens < threshold) {
32709
+ if (asyncObservationEnabled && (pendingTokens < threshold || inAsyncObservationBand)) {
32008
32710
  const lockKey = this.buffering.getLockKey(threadId, resourceId);
32009
32711
  shouldBuffer = this.buffering.shouldTriggerAsyncObservation(pendingTokens, lockKey, record, this.storage, threshold);
32010
32712
  }
@@ -32029,6 +32731,8 @@ ${formattedMessages}
32029
32731
  canActivate,
32030
32732
  asyncObservationEnabled,
32031
32733
  asyncReflectionEnabled: this.buffering.isAsyncReflectionEnabled(),
32734
+ observationBlockAfter,
32735
+ inAsyncObservationBand,
32032
32736
  scope: this.scope
32033
32737
  };
32034
32738
  }
@@ -32155,24 +32859,27 @@ ${formattedMessages}
32155
32859
  const currentTokens = opts.pendingTokens ?? record.pendingMessageTokens ?? 0;
32156
32860
  const lockKey = this.buffering.getLockKey(threadId, resourceId);
32157
32861
  const bufferKey = this.buffering.getObservationBufferKey(lockKey);
32158
- BufferingCoordinator.lastBufferedBoundary.set(bufferKey, currentTokens);
32159
- if (record.isBufferingObservation) await this.storage.setBufferingObservationFlag(record.id, false).catch(() => {});
32862
+ const boundaryOwner = Symbol("observation-boundary");
32863
+ BufferingCoordinator.setObservationBoundary(bufferKey, currentTokens, boundaryOwner);
32160
32864
  const existingOp = BufferingCoordinator.asyncBufferingOps.get(bufferKey);
32865
+ let resolveOp;
32866
+ const opPromise = new Promise((resolve) => {
32867
+ resolveOp = resolve;
32868
+ });
32869
+ BufferingCoordinator.asyncBufferingOps.set(bufferKey, opPromise);
32870
+ if (record.isBufferingObservation) await runOMCommit(lockKey, () => this.storage.setBufferingObservationFlag(record.id, false)).catch(() => {});
32161
32871
  if (existingOp) try {
32162
32872
  await existingOp;
32163
32873
  } catch {}
32164
- registerOp(record.id, "bufferingObservation");
32874
+ const opRecordId = record.id;
32875
+ registerOp(opRecordId, "bufferingObservation");
32876
+ const releaseChunkWrite = BufferingCoordinator.trackChunkWrite(bufferKey);
32877
+ const ownsStartBoundary = BufferingCoordinator.ownsObservationBoundary(bufferKey, boundaryOwner);
32165
32878
  inMemoryRecord.isBufferingObservation = true;
32166
- inMemoryRecord.lastBufferedAtTokens = currentTokens;
32167
- this.storage.setBufferingObservationFlag(record.id, true, currentTokens).catch((err) => {
32879
+ if (ownsStartBoundary) inMemoryRecord.lastBufferedAtTokens = currentTokens;
32880
+ runOMCommit(lockKey, () => this.storage.setBufferingObservationFlag(record.id, true, ownsStartBoundary ? currentTokens : void 0)).catch((err) => {
32168
32881
  omError("[OM] Failed to set buffering observation flag", err);
32169
32882
  });
32170
- let resolveOp;
32171
- const opPromise = new Promise((resolve) => {
32172
- resolveOp = resolve;
32173
- });
32174
- BufferingCoordinator.asyncBufferingOps.set(bufferKey, opPromise);
32175
- record = await this.storage.getObservationalMemory(record.threadId, record.resourceId) ?? record;
32176
32883
  const setBufferingState = (isBufferingObservation, lastBufferedAtTokens) => {
32177
32884
  inMemoryRecord.isBufferingObservation = isBufferingObservation;
32178
32885
  record.isBufferingObservation = isBufferingObservation;
@@ -32183,6 +32890,7 @@ ${formattedMessages}
32183
32890
  };
32184
32891
  let flagCleared = false;
32185
32892
  try {
32893
+ record = await this.storage.getObservationalMemory(record.threadId, record.resourceId) ?? record;
32186
32894
  let candidateMessages;
32187
32895
  if (opts.messages) candidateMessages = this.getUnobservedMessages(opts.messages, record, { excludeBuffered: true });
32188
32896
  else {
@@ -32222,12 +32930,29 @@ ${formattedMessages}
32222
32930
  threadIds: [threadId],
32223
32931
  config: this.getObservationMarkerConfig()
32224
32932
  });
32225
- await this.persistMarkerToStorage(startMarker, threadId, record.resourceId ?? void 0);
32933
+ await this.persistMarkerToStorage(startMarker, threadId, record.resourceId ?? void 0, {
32934
+ notAfter: new Date(startedAt),
32935
+ preferMessages: candidateMessages
32936
+ });
32226
32937
  const writer = opts.writer;
32227
32938
  if (writer) writer.custom({
32228
32939
  ...startMarker,
32229
32940
  transient: true
32230
32941
  }).catch(() => {});
32942
+ let chunkRecorded = false;
32943
+ const recordBufferedChunk = async () => {
32944
+ if (chunkRecorded) return;
32945
+ chunkRecorded = true;
32946
+ const ownsBoundary = BufferingCoordinator.ownsObservationBoundary(bufferKey, boundaryOwner);
32947
+ if (ownsBoundary) BufferingCoordinator.setObservationBoundary(bufferKey, newTokens, boundaryOwner);
32948
+ await runOMCommit(lockKey, () => this.storage.setBufferingObservationFlag(record.id, false, ownsBoundary ? newTokens : void 0)).catch(() => {});
32949
+ flagCleared = true;
32950
+ const stillOwnsBoundary = ownsBoundary && BufferingCoordinator.ownsObservationBoundary(bufferKey, boundaryOwner);
32951
+ setBufferingState(false, stillOwnsBoundary ? newTokens : void 0);
32952
+ const maxTimestamp = this.getMaxMessageTimestamp(candidateMessages);
32953
+ BufferingCoordinator.lastBufferedAtTime.set(bufferKey, new Date(maxTimestamp.getTime() + 1));
32954
+ releaseChunkWrite();
32955
+ };
32231
32956
  if (!(await this.runBufferedObservationCycle({
32232
32957
  threadId,
32233
32958
  resourceId: record.resourceId ?? resourceId,
@@ -32246,7 +32971,8 @@ ${formattedMessages}
32246
32971
  requestContext,
32247
32972
  currentModel: opts.currentModel,
32248
32973
  observabilityContext,
32249
- trigger: "async-buffer"
32974
+ trigger: "async-buffer",
32975
+ onBufferedChunkPersisted: recordBufferedChunk
32250
32976
  }).run()))?.observed) return {
32251
32977
  buffered: false,
32252
32978
  record
@@ -32266,13 +32992,7 @@ ${formattedMessages}
32266
32992
  newTokens
32267
32993
  }
32268
32994
  });
32269
- await this.storage.setBufferingObservationFlag(record.id, false, newTokens).catch(() => {});
32270
- flagCleared = true;
32271
- setBufferingState(false, newTokens);
32272
- BufferingCoordinator.lastBufferedBoundary.set(bufferKey, newTokens);
32273
- const maxTimestamp = this.getMaxMessageTimestamp(candidateMessages);
32274
- const cursor = new Date(maxTimestamp.getTime() + 1);
32275
- BufferingCoordinator.lastBufferedAtTime.set(bufferKey, cursor);
32995
+ await recordBufferedChunk();
32276
32996
  return {
32277
32997
  buffered: true,
32278
32998
  record: await this.storage.getObservationalMemory(record.threadId, record.resourceId) ?? record
@@ -32284,12 +33004,13 @@ ${formattedMessages}
32284
33004
  record
32285
33005
  };
32286
33006
  } finally {
32287
- unregisterOp(record.id, "bufferingObservation");
32288
- BufferingCoordinator.asyncBufferingOps.delete(bufferKey);
33007
+ releaseChunkWrite();
33008
+ unregisterOp(opRecordId, "bufferingObservation");
33009
+ if (BufferingCoordinator.asyncBufferingOps.get(bufferKey) === opPromise) BufferingCoordinator.asyncBufferingOps.delete(bufferKey);
32289
33010
  resolveOp();
32290
33011
  if (!flagCleared) {
32291
33012
  setBufferingState(false);
32292
- await this.storage.setBufferingObservationFlag(record.id, false).catch(() => {});
33013
+ await runOMCommit(lockKey, () => this.storage.setBufferingObservationFlag(record.id, false)).catch(() => {});
32293
33014
  }
32294
33015
  }
32295
33016
  }
@@ -32326,8 +33047,8 @@ ${formattedMessages}
32326
33047
  const currentContextTokens = this.tokenCounter.countMessages(unobserved);
32327
33048
  if (currentContextTokens < dbBoundary * .5) {
32328
33049
  omDebug(`[OM:activate] resetting stale lastBufferedBoundary: dbBoundary=${dbBoundary}, currentContextTokens=${currentContextTokens}`);
32329
- BufferingCoordinator.lastBufferedBoundary.set(bufKey, 0);
32330
- await this.storage.setBufferingObservationFlag(record.id, false, 0).catch(() => {});
33050
+ BufferingCoordinator.setObservationBoundary(bufKey, 0);
33051
+ await runOMCommit(lockKey, () => this.storage.setBufferingObservationFlag(record.id, false, 0)).catch(() => {});
32331
33052
  }
32332
33053
  }
32333
33054
  }
@@ -32373,39 +33094,63 @@ ${formattedMessages}
32373
33094
  livePendingTokens ??= status.pendingTokens;
32374
33095
  }
32375
33096
  }
32376
- if (record.isBufferingObservation) {
32377
- const lockKey = this.buffering.getLockKey(threadId, resourceId);
32378
- const bufferKey = this.buffering.getObservationBufferKey(lockKey);
32379
- const asyncOp = BufferingCoordinator.asyncBufferingOps.get(bufferKey);
32380
- if (asyncOp) try {
32381
- await Promise.race([asyncOp, new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error("Timeout")), 6e4))]);
32382
- } catch {}
33097
+ const obsBufferKey = this.buffering.getObservationBufferKey(this.buffering.getLockKey(threadId, resourceId));
33098
+ const pendingChunkWrite = BufferingCoordinator.pendingChunkWrites.get(obsBufferKey);
33099
+ if (pendingChunkWrite) {
33100
+ const blockAfter = this.buffering.isAsyncObservationEnabled() ? this.getEffectiveObservationBlockAfter(record) : void 0;
33101
+ const threshold = calculateDynamicThreshold(this.getEffectiveMessageTokens(record), record.observationTokenCount ?? 0);
33102
+ if (activationTriggeredBy === "threshold" && !(livePendingTokens !== void 0 && blockAfter !== void 0 && livePendingTokens >= threshold && livePendingTokens < blockAfter)) {
33103
+ let timeoutId;
33104
+ await Promise.race([pendingChunkWrite, new Promise((resolve) => timeoutId = setTimeout(resolve, 6e4))]);
33105
+ clearTimeout(timeoutId);
33106
+ }
33107
+ }
33108
+ const commitKey = this.buffering.getLockKey(threadId, resourceId);
33109
+ let freshRecord = null;
33110
+ let freshChunks = [];
33111
+ let activationResult;
33112
+ for (let attempt = 0; attempt <= MAX_ACTIVATION_RETIRED_RETRIES; attempt++) {
33113
+ const swapped = await runOMCommit(commitKey, async () => {
33114
+ const head = await this.storage.getObservationalMemory(record.threadId, record.resourceId);
33115
+ if (!head) return null;
33116
+ const chunks = getBufferedChunks(head);
33117
+ if (!chunks.length) return null;
33118
+ const messageTokensThreshold = getMaxThreshold(this.getEffectiveMessageTokens(head));
33119
+ const activationRatio = resolveActivationRatio(this.observationConfig.bufferActivation ?? .7, messageTokensThreshold);
33120
+ const totalChunkMessageTokens = chunks.reduce((sum, c) => sum + (c.messageTokens ?? 0), 0);
33121
+ const currentPendingTokens = livePendingTokens ?? (head.pendingMessageTokens || totalChunkMessageTokens);
33122
+ const blockAfter = this.getEffectiveObservationBlockAfter(head);
33123
+ const forceMaxActivation = !!(blockAfter && currentPendingTokens >= blockAfter);
33124
+ if (head.pendingMessageTokens !== currentPendingTokens) await this.storage.setPendingMessageTokens(head.id, currentPendingTokens);
33125
+ return {
33126
+ head,
33127
+ chunks,
33128
+ result: await this.storage.swapBufferedToActive({
33129
+ id: head.id,
33130
+ activationRatio,
33131
+ messageTokensThreshold,
33132
+ currentPendingTokens,
33133
+ forceMaxActivation,
33134
+ bufferedChunks: chunks
33135
+ })
33136
+ };
33137
+ });
33138
+ if (!swapped) return {
33139
+ activated: false,
33140
+ record
33141
+ };
33142
+ freshRecord = swapped.head;
33143
+ freshChunks = swapped.chunks;
33144
+ activationResult = swapped.result;
33145
+ if (!activationResult.retired) break;
33146
+ omDebug(`[OM:activate] record ${freshRecord.id} was retired by a reflection; retrying on the head`);
32383
33147
  }
32384
- const freshRecord = await this.storage.getObservationalMemory(record.threadId, record.resourceId);
32385
- if (!freshRecord) return {
33148
+ if (!freshRecord || !activationResult || activationResult.retired) return {
32386
33149
  activated: false,
32387
- record
32388
- };
32389
- const freshChunks = getBufferedChunks(freshRecord);
32390
- if (!freshChunks.length) return {
32391
- activated: false,
32392
- record
33150
+ record: await this.getRecord(threadId, resourceId) ?? record
32393
33151
  };
32394
- const messageTokensThreshold = getMaxThreshold(this.getEffectiveMessageTokens(freshRecord));
32395
- const activationRatio = resolveActivationRatio(this.observationConfig.bufferActivation ?? .7, messageTokensThreshold);
32396
- const totalChunkMessageTokens = freshChunks.reduce((sum, c) => sum + (c.messageTokens ?? 0), 0);
32397
- const currentPendingTokens = livePendingTokens ?? (freshRecord.pendingMessageTokens || totalChunkMessageTokens);
32398
- const forceMaxActivation = !!(this.observationConfig.blockAfter && currentPendingTokens >= this.observationConfig.blockAfter);
32399
- if (freshRecord.pendingMessageTokens !== currentPendingTokens) await this.storage.setPendingMessageTokens(freshRecord.id, currentPendingTokens);
32400
- const activationResult = await this.storage.swapBufferedToActive({
32401
- id: freshRecord.id,
32402
- activationRatio,
32403
- messageTokensThreshold,
32404
- currentPendingTokens,
32405
- forceMaxActivation,
32406
- bufferedChunks: freshChunks
32407
- });
32408
- await this.storage.setBufferingObservationFlag(freshRecord.id, false).catch(() => {});
33152
+ const swappedRecordId = freshRecord.id;
33153
+ await runOMCommit(commitKey, () => this.storage.setBufferingObservationFlag(swappedRecordId, false)).catch(() => {});
32409
33154
  unregisterOp(freshRecord.id, "bufferingObservation");
32410
33155
  const postSwapRecord = await this.storage.getObservationalMemory(record.threadId, record.resourceId);
32411
33156
  if (opts.writer && postSwapRecord && activationResult.activatedCycleIds.length > 0) {
@@ -32650,7 +33395,7 @@ ${formattedMessages}
32650
33395
  * ```
32651
33396
  */
32652
33397
  async reflect(threadId, resourceId, prompt, requestContext, observabilityContext) {
32653
- const record = await this.getOrCreateRecord(threadId, resourceId);
33398
+ const record = { ...await this.getOrCreateRecord(threadId, resourceId) };
32654
33399
  if (!record.activeObservations) return {
32655
33400
  reflected: false,
32656
33401
  record,
@@ -32695,11 +33440,23 @@ ${formattedMessages}
32695
33440
  const reflectResult = await this.reflector.call(await applyTextTransform(this.hooks, "beforeReflection", record.activeObservations, hookContext), prompt, void 0, reflectThreshold, void 0, void 0, void 0, requestContext, priorExtractedValues, observabilityContext, void 0);
32696
33441
  reflectResult.observations = await applyTextTransform(this.hooks, "afterReflection", reflectResult.observations, hookContext);
32697
33442
  const reflectionTokenCount = this.tokenCounter.countObservations(reflectResult.observations);
32698
- await this.storage.createReflectionGeneration({
33443
+ const newRecordId = crypto.randomUUID();
33444
+ const committedRecord = await runOMCommit(this.buffering.getLockKey(threadId, resourceId), () => this.storage.createReflectionGeneration({
32699
33445
  currentRecord: record,
32700
33446
  reflection: reflectResult.observations,
32701
- tokenCount: reflectionTokenCount
32702
- });
33447
+ tokenCount: reflectionTokenCount,
33448
+ newRecordId
33449
+ }), { priority: "reflection" });
33450
+ reflectionUsage = reflectResult.usage;
33451
+ reflectionProviderMetadata = reflectResult.providerMetadata;
33452
+ if (!isReflectionApplied(committedRecord, newRecordId, record.id)) {
33453
+ omDebug(`[OM:reflect] manual reflection of ${record.id} not applied (head is ${committedRecord.id})`);
33454
+ return {
33455
+ reflected: false,
33456
+ record: await this.getOrCreateRecord(threadId, resourceId),
33457
+ usage: reflectResult.usage
33458
+ };
33459
+ }
32703
33460
  if (thread && reflectResult.extractedValues) {
32704
33461
  const metadataUpdate = buildThreadMetadataFromExtractedValues(this.reflectionConfig.extractors, reflectResult.extractedValues);
32705
33462
  const newMetadata = setThreadOMMetadata(thread.metadata, {
@@ -32788,11 +33545,13 @@ ${formattedMessages}
32788
33545
  */
32789
33546
  async updateRecordConfig(threadId, resourceId, config) {
32790
33547
  const ids = this.getStorageIds(threadId, resourceId);
32791
- const record = await this.storage.getObservationalMemory(ids.threadId, ids.resourceId);
32792
- if (!record) throw new Error(`No observational memory record found for thread ${ids.threadId}`);
32793
- await this.storage.updateObservationalMemoryConfig({
32794
- id: record.id,
32795
- config: { _overrides: config }
33548
+ await runOMCommit(this.buffering.getLockKey(threadId, resourceId), async () => {
33549
+ const record = await this.storage.getObservationalMemory(ids.threadId, ids.resourceId);
33550
+ if (!record) throw new Error(`No observational memory record found for thread ${ids.threadId}`);
33551
+ await this.storage.updateObservationalMemoryConfig({
33552
+ id: record.id,
33553
+ config: { _overrides: config }
33554
+ });
32796
33555
  });
32797
33556
  }
32798
33557
  /**
@@ -33186,7 +33945,7 @@ var ObservationalMemoryProcessor = class {
33186
33945
  });
33187
33946
  const finalTotalPending = ctx.status.pendingTokens;
33188
33947
  try {
33189
- await this.engine.getStorage().setPendingMessageTokens(turnRecord.id, finalTotalPending);
33948
+ await runOMCommit(this.engine.buffering.getLockKey(threadId, resourceId), () => this.engine.getStorage().setPendingMessageTokens(turnRecord.id, finalTotalPending));
33190
33949
  this.turn.patchRecord({ pendingMessageTokens: finalTotalPending });
33191
33950
  } catch {}
33192
33951
  if (reproCaptureEnabled) writeProcessInputStepReproCapture({
@@ -35521,6 +36280,7 @@ Notes:
35521
36280
  cloned.id = crypto.randomUUID();
35522
36281
  cloned.createdAt = now;
35523
36282
  cloned.updatedAt = now;
36283
+ cloned.supersededBy = null;
35524
36284
  await memoryStore.insertObservationalMemoryRecord(cloned);
35525
36285
  }
35526
36286
  /**
@@ -35814,4 +36574,4 @@ Notes:
35814
36574
  //#endregion
35815
36575
  export { renderSubconsciousActivity as $, buildObserverSystemPrompt as A, OBSERVATION_CONTEXT_INSTRUCTIONS as B, WorkingMemoryExtractor as C, TokenCounter as D, summarizeConversation as E, parseObserverOutput as F, Subconscious as G, OBSERVATION_CONTEXT_PROMPT_THREAD as H, injectAnchorIds as I, SubconsciousRemindExtractor as J, KnowledgeSemanticIndexCoordinator as K, parseAnchorId as L, formatMessagesForObserver as M, hasCurrentTaskSection as N, OBSERVER_SYSTEM_PROMPT as O, optimizeObservationsForContext as P, publishSubconsciousActivity as Q, stripEphemeralAnchorIds as R, wrapInObservationGroup as S, SUMMARIZE_THREAD_DEFAULTS as T, OBSERVATION_CONTINUATION_HINT as U, OBSERVATION_CONTEXT_PROMPT as V, getObservationContextPrompt as W, SUBCONSCIOUS_ACTIVITY_STATE_ID as X, ModelByInputTokens as Y, buildSubconsciousActivitySnapshot as Z, deriveObservationGroupProvenance as _, extractWorkingMemoryContent as a, renderObservationGroupsForReflection as b, getObservationsAsOf as c, AUTO_MODEL_BY_PROVIDER as d, Extractor as et, resolveAutoModelId as f, combineObservationGroupRanges as g, WorkingMemoryStateProcessor as h, WorkingMemory as i, extractCurrentTask as j, buildObserverPrompt as k, ObservationalMemoryProcessor as l, WORKING_MEMORY_STATE_PROCESSOR_ID as m, MessageHistory$1 as n, extractWorkingMemoryTags as o, WORKING_MEMORY_STATE_ID as p, StaleKnowledgeSemanticIndexError as q, SemanticRecall as r, removeWorkingMemoryTags as s, Memory as t, ObservationalMemory as u, parseObservationGroups as v, deepMergeWorkingMemory as w, stripObservationGroups as x, reconcileObservationGroupsFromReflection as y, OBSERVATIONAL_MEMORY_DEFAULTS as z };
35816
36576
 
35817
- //# sourceMappingURL=src-B9qhQmNI.js.map
36577
+ //# sourceMappingURL=src-DjDnBIS0.js.map