@mastra/server 1.76.0 → 1.77.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4637,7 +4637,7 @@ function splitLines(text) {
4637
4637
  return result;
4638
4638
  }
4639
4639
  //#endregion
4640
- //#region ../memory/dist/src-B9qhQmNI.js
4640
+ //#region ../memory/dist/src-ThciS3vu.js
4641
4641
  var __defProp$3 = Object.defineProperty;
4642
4642
  var __exportAll = (all, no_symbols) => {
4643
4643
  let target = {};
@@ -25063,7 +25063,7 @@ Group related observations (like tool sequences) by indenting:
25063
25063
  * -> applied fix, tests now pass
25064
25064
  * ✅ Tests passing, auth issue resolved
25065
25065
 
25066
- Group observations by date, then list each with 24-hour time.
25066
+ Group observations by date. Within a date, list observations in the order the events happened (not by importance), each with 24-hour time.
25067
25067
 
25068
25068
  <observations>
25069
25069
  Date: Dec 4, 2025
@@ -25436,8 +25436,8 @@ function extractToolResultAttachments(result, counter, attachmentFilter) {
25436
25436
  attachments
25437
25437
  };
25438
25438
  }
25439
- function formatObserverPartLine(title, body, time, previousTime) {
25440
- const timeLabel = time && time !== previousTime ? `(${time})` : "";
25439
+ function formatObserverPartLine(title, body, time, previousTime, alwaysShowTime) {
25440
+ const timeLabel = time && (alwaysShowTime || time !== previousTime) ? `(${time})` : "";
25441
25441
  if (!title) return timeLabel ? `${timeLabel}: ${body}` : body;
25442
25442
  return `${title}${timeLabel ? ` ${timeLabel}` : ""}: ${body}`;
25443
25443
  }
@@ -25462,7 +25462,7 @@ function formatObserverLines(lines, context = {}) {
25462
25462
  previousDate = line.date;
25463
25463
  previousTime = void 0;
25464
25464
  }
25465
- output.push(formatObserverPartLine(line.title, line.body, line.time, previousTime));
25465
+ output.push(formatObserverPartLine(line.title, line.body, line.time, previousTime, line.alwaysShowTime));
25466
25466
  previousTime = line.time || previousTime;
25467
25467
  }
25468
25468
  return {
@@ -25563,14 +25563,15 @@ function formatObserverMessage(msg, counter, toolFormatting, options) {
25563
25563
  const unavailableUrls = getUnavailableAttachmentUrls(msg);
25564
25564
  let lines = [];
25565
25565
  const temporalGapText = isTemporalGapMarker(msg) ? getTemporalGapMarkerText(msg) : void 0;
25566
- const pushLine = (title, body, createdAt) => {
25566
+ const pushLine = (title, body, createdAt, opts) => {
25567
25567
  if (!body) return;
25568
25568
  const normalizedCreatedAt = normalizeObserverCreatedAt(createdAt) ?? messageCreatedAt;
25569
25569
  lines.push({
25570
25570
  date: formatObserverDate(normalizedCreatedAt, timeZone),
25571
25571
  time: formatObserverTime(normalizedCreatedAt, timeZone),
25572
25572
  title,
25573
- body
25573
+ body,
25574
+ alwaysShowTime: opts?.tool === true
25574
25575
  });
25575
25576
  };
25576
25577
  if (temporalGapText) pushLine("", temporalGapText, messageCreatedAt);
@@ -25587,7 +25588,7 @@ function formatObserverMessage(msg, counter, toolFormatting, options) {
25587
25588
  const exchange = toolFormatting.exchanges.get(inv.toolCallId);
25588
25589
  if (exchange && occurrence === exchange.firstOccurrence && exchange.signature && !toolFormatting.emittedCalls.has(inv.toolCallId)) {
25589
25590
  toolFormatting.emittedCalls.add(inv.toolCallId);
25590
- pushLine(`Tool Call ${exchange.signature.toolName}`, formatObserverToolArguments(exchange.signature.args, maxLen), partCreatedAt);
25591
+ pushLine(`Tool Call ${exchange.signature.toolName}`, formatObserverToolArguments(exchange.signature.args, maxLen), partCreatedAt, { tool: true });
25591
25592
  }
25592
25593
  if (inv.state === "result") {
25593
25594
  const { value: resultForObserver } = resolveToolResultValue(part, inv.result);
@@ -25597,17 +25598,17 @@ function formatObserverMessage(msg, counter, toolFormatting, options) {
25597
25598
  toolFormatting.emittedOutcomes.add(inv.toolCallId);
25598
25599
  const hasResult = resultWithoutAttachments !== void 0 && (typeof resultWithoutAttachments !== "string" || resultWithoutAttachments.trim().length > 0);
25599
25600
  const body = inv.isError ? hasResult ? formatToolResultForObserver(resultWithoutAttachments, { maxTokens: maxToolResultTokens }) : inv.errorText?.trim() ? inv.errorText : "Tool execution failed" : hasResult ? formatToolResultForObserver(resultWithoutAttachments, { maxTokens: maxToolResultTokens }) : "[empty result]";
25600
- pushLine(`${inv.isError ? "Tool Error" : "Tool Result"} ${inv.toolName}`, maybeTruncate(body, maxLen), partCreatedAt);
25601
+ pushLine(`${inv.isError ? "Tool Error" : "Tool Result"} ${inv.toolName}`, maybeTruncate(body, maxLen), partCreatedAt, { tool: true });
25601
25602
  }
25602
25603
  return;
25603
25604
  }
25604
25605
  if (exchange?.terminal?.occurrence === occurrence && !toolFormatting.emittedOutcomes.has(inv.toolCallId)) {
25605
25606
  toolFormatting.emittedOutcomes.add(inv.toolCallId);
25606
- if (inv.state === "output-error") pushLine(`Tool Error ${inv.toolName}`, maybeTruncate(inv.errorText?.trim() ? inv.errorText : "Tool execution failed", maxLen), partCreatedAt);
25607
- 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);
25607
+ if (inv.state === "output-error") pushLine(`Tool Error ${inv.toolName}`, maybeTruncate(inv.errorText?.trim() ? inv.errorText : "Tool execution failed", maxLen), partCreatedAt, { tool: true });
25608
+ 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 });
25608
25609
  return;
25609
25610
  }
25610
- if (!exchange) pushLine(`Tool Call ${inv.toolName}`, formatObserverToolArguments(inv.args, maxLen), partCreatedAt);
25611
+ if (!exchange) pushLine(`Tool Call ${inv.toolName}`, formatObserverToolArguments(inv.args, maxLen), partCreatedAt, { tool: true });
25611
25612
  return;
25612
25613
  }
25613
25614
  const partType = part.type;
@@ -25762,13 +25763,17 @@ function surroundObserverHistory(history, contextPrompt, taskInstructions) {
25762
25763
  function buildMultiThreadObserverTaskInstructions(includeThreadTitle) {
25763
25764
  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.`;
25764
25765
  }
25766
+ function buildTruncationNotice(wasTruncated) {
25767
+ if (!wasTruncated) return "";
25768
+ return "Previous observations were truncated for context budget reasons.\nThe main agent still has full memory context outside this observer window.\n\n";
25769
+ }
25765
25770
  function buildMultiThreadObserverContextPrompt(existingObservations, threadOrder, priorMetadataByThread, wasTruncated, includeThreadTitle, extractors = []) {
25766
25771
  let prompt = "";
25767
25772
  if (existingObservations) {
25768
25773
  prompt += `## Previous Observations\n\n${existingObservations}\n\n---\n\n`;
25769
25774
  prompt += "Do not repeat these existing observations. Your new observations will be appended to the existing observations.\n\n";
25770
25775
  }
25771
- const hasTruncatedObservations = wasTruncated ?? false;
25776
+ prompt += buildTruncationNotice(wasTruncated);
25772
25777
  const threadMetadataLines = threadOrder?.map((threadId) => {
25773
25778
  const metadata = priorMetadataByThread?.get(threadId);
25774
25779
  const extractorPriorLines = buildExtractorPriorLines(extractors, metadata?.extracted);
@@ -25782,10 +25787,6 @@ function buildMultiThreadObserverContextPrompt(existingObservations, threadOrder
25782
25787
  }).filter(Boolean).join("\n");
25783
25788
  if (threadMetadataLines) {
25784
25789
  prompt += `## Prior Thread Metadata\n\n${threadMetadataLines}\n\n`;
25785
- if (hasTruncatedObservations) {
25786
- prompt += `Previous observations were truncated for context budget reasons.\n`;
25787
- prompt += `The main agent still has full memory context outside this observer window.\n`;
25788
- }
25789
25790
  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`;
25790
25791
  }
25791
25792
  return prompt;
@@ -25828,7 +25829,7 @@ function parseMultiThreadObserverOutput(output, extractors = []) {
25828
25829
  threadTitle = threadTitleMatch[1].trim();
25829
25830
  observations = observations.replace(/<thread-title>[\s\S]*?<\/thread-title>/i, "");
25830
25831
  }
25831
- observations = sanitizeObservationLines(observations.trim());
25832
+ observations = sortObservationsByTime(sanitizeObservationLines(observations.trim()));
25832
25833
  threads.set(threadId, {
25833
25834
  observations,
25834
25835
  currentTask: currentTask || getStringExtractedValue$1(parsedExtractedValues.values, "current-task"),
@@ -25868,7 +25869,7 @@ function buildObserverContextPrompt(existingObservations, options) {
25868
25869
  prompt += `## Previous Observations\n\n${existingObservations}\n\n---\n\n`;
25869
25870
  prompt += "Do not repeat these existing observations. Your new observations will be appended to the existing observations.\n\n";
25870
25871
  }
25871
- const hasTruncatedObservations = options?.wasTruncated ?? false;
25872
+ prompt += buildTruncationNotice(options?.wasTruncated);
25872
25873
  const priorMetadataLines = [];
25873
25874
  if (options?.priorCurrentTask) priorMetadataLines.push(`- prior current-task: ${options.priorCurrentTask}`);
25874
25875
  if (options?.priorSuggestedResponse) priorMetadataLines.push(`- prior suggested-response: ${options.priorSuggestedResponse}`);
@@ -25876,10 +25877,6 @@ function buildObserverContextPrompt(existingObservations, options) {
25876
25877
  priorMetadataLines.push(...buildExtractorPriorLines(options?.extractors ?? [], options?.priorExtractedValues));
25877
25878
  if (priorMetadataLines.length > 0) {
25878
25879
  prompt += `## Prior Thread Metadata\n\n${priorMetadataLines.join("\n")}\n\n`;
25879
- if (hasTruncatedObservations) {
25880
- prompt += `Previous observations were truncated for context budget reasons.\n`;
25881
- prompt += `The main agent still has full memory context outside this observer window.\n`;
25882
- }
25883
25880
  const titleHint = options?.includeThreadTitle ? ", and thread-title" : "";
25884
25881
  prompt += `Use the prior current-task, suggested-response${titleHint} as continuity hints, then update them based on the new messages.\n\n---\n\n`;
25885
25882
  }
@@ -25910,7 +25907,7 @@ function parseObserverOutput(output, extractors = []) {
25910
25907
  const parsedExtractedValues = parseExtractedValues(output, inlineExtractors);
25911
25908
  const parsed = parseMemorySectionXml(stripExtractorSections(output, inlineExtractors));
25912
25909
  return {
25913
- observations: sanitizeObservationLines(parsed.observations || ""),
25910
+ observations: sortObservationsByTime(sanitizeObservationLines(parsed.observations || "")),
25914
25911
  currentTask: parsed.currentTask || getStringExtractedValue$1(parsedExtractedValues.values, "current-task"),
25915
25912
  suggestedContinuation: parsed.suggestedResponse || getStringExtractedValue$1(parsedExtractedValues.values, "suggested-response"),
25916
25913
  threadTitle: parsed.threadTitle || getStringExtractedValue$1(parsedExtractedValues.values, "thread-title"),
@@ -26004,6 +26001,68 @@ function sanitizeObservationLines(observations) {
26004
26001
  }
26005
26002
  return changed ? lines.join("\n") : observations;
26006
26003
  }
26004
+ const OBSERVATION_TIME = /^[*-]\s+(?:\S+\s+)?\((\d{1,2}):(\d{2})\s*([AaPp])?\.?(?:[Mm]\.?)?\)/;
26005
+ /** Minutes since midnight for each item, or undefined when the group's times can't be ordered safely. */
26006
+ function observationMinutes(items) {
26007
+ const times = items.map((item) => OBSERVATION_TIME.exec(item[0]));
26008
+ if (times.some((t) => !t)) return void 0;
26009
+ const matches = times;
26010
+ const marked = matches.filter((m) => m[3]).length;
26011
+ if (marked > 0 && marked < matches.length) return void 0;
26012
+ if (marked === matches.length) return matches.map((m) => Number(m[1]) % 12 * 60 + (/p/i.test(m[3]) ? 720 : 0) + Number(m[2]));
26013
+ const hours = matches.map((m) => m[1]);
26014
+ const twelveHour = hours.some((h) => /^[1-9]$/.test(h));
26015
+ const twentyFourHour = hours.some((h) => /^(0\d|1[3-9]|2\d)$/.test(h));
26016
+ if (twelveHour && (twentyFourHour || new Set(hours).size > 1)) return void 0;
26017
+ return matches.map((m) => Number(m[1]) * 60 + Number(m[2]));
26018
+ }
26019
+ /**
26020
+ * Put the top-level observations of each "Date:" group in time order, keeping indented
26021
+ * sub-items with their parent. The Observer writes a time on every observation but tends
26022
+ * to list a user's message ahead of the earlier events it follows, which can make a
26023
+ * superseded state read as the latest one. Groups are left as written when any
26024
+ * observation has no time or the times are ambiguous.
26025
+ */
26026
+ function sortObservationsByTime(observations) {
26027
+ if (!observations) return observations;
26028
+ const output = [];
26029
+ let group;
26030
+ const flush = () => {
26031
+ if (!group) return;
26032
+ const minutes = group.sortable && group.items.length > 1 ? observationMinutes(group.items) : void 0;
26033
+ const items = minutes ? group.items.map((item, i) => ({
26034
+ item,
26035
+ minute: minutes[i]
26036
+ })).sort((a, b) => a.minute - b.minute).map((x) => x.item) : group.items;
26037
+ output.push(...group.header, ...items.flat(), ...group.trailing);
26038
+ group = void 0;
26039
+ };
26040
+ for (const line of observations.split("\n")) if (/^\s*Date:\s/.test(line)) {
26041
+ flush();
26042
+ group = {
26043
+ header: [line],
26044
+ items: [],
26045
+ trailing: [],
26046
+ sortable: true
26047
+ };
26048
+ } else if (!group) output.push(line);
26049
+ else if (/^[*-]\s/.test(line)) {
26050
+ group.items.at(-1)?.push(...group.trailing);
26051
+ group.trailing = [];
26052
+ group.items.push([line]);
26053
+ } else if (group.items.length === 0) group.header.push(line);
26054
+ else if (line.trim() === "") group.trailing.push(line);
26055
+ else if (/^\s/.test(line)) {
26056
+ group.items.at(-1).push(...group.trailing, line);
26057
+ group.trailing = [];
26058
+ } else {
26059
+ group.sortable = false;
26060
+ group.items.at(-1).push(...group.trailing, line);
26061
+ group.trailing = [];
26062
+ }
26063
+ flush();
26064
+ return output.join("\n");
26065
+ }
26007
26066
  function analyzeDegenerateRepetition(text) {
26008
26067
  const lines = collapseBoundedLineRuns(text.split("\n"));
26009
26068
  const windowText = lines.filter((line) => line.trim().length >= MIN_DUPLICATE_LINE_CHARS && line.length <= MAX_OBSERVATION_LINE_CHARS).join("\n");
@@ -26538,7 +26597,7 @@ var ObserverRunner = class {
26538
26597
  /**
26539
26598
  * Call the Observer agent for multiple threads in a single batched request.
26540
26599
  */
26541
- async callMultiThread(existingObservations, allMessagesByThread, allThreadOrder, abortSignal, requestContext, priorMetadataByThread, observabilityContext, model, hookContext, timeZone, mainAgent, currentModel) {
26600
+ async callMultiThread(existingObservations, allMessagesByThread, allThreadOrder, abortSignal, requestContext, priorMetadataByThread, observabilityContext, model, hookContext, timeZone, mainAgent, currentModel, wasTruncated) {
26542
26601
  const contextFor = (threadId) => ({
26543
26602
  threadId,
26544
26603
  ...hookContext
@@ -26549,7 +26608,7 @@ var ObserverRunner = class {
26549
26608
  if (filtered.length > 0) messagesByThread.set(threadId, filtered);
26550
26609
  }
26551
26610
  const threadOrder = allThreadOrder.filter((threadId) => messagesByThread.has(threadId));
26552
- const output = await this.callMultiThreadObserver(existingObservations, messagesByThread, threadOrder, abortSignal, requestContext, priorMetadataByThread, observabilityContext, model, timeZone, mainAgent, currentModel);
26611
+ const output = await this.callMultiThreadObserver(existingObservations, messagesByThread, threadOrder, abortSignal, requestContext, priorMetadataByThread, observabilityContext, model, timeZone, mainAgent, currentModel, wasTruncated);
26553
26612
  for (const threadId of allThreadOrder) {
26554
26613
  const threadResult = output.results.get(threadId);
26555
26614
  if (!threadResult) {
@@ -26560,7 +26619,7 @@ var ObserverRunner = class {
26560
26619
  }
26561
26620
  return output;
26562
26621
  }
26563
- async callMultiThreadObserver(existingObservations, messagesByThread, threadOrder, abortSignal, requestContext, priorMetadataByThread, observabilityContext, model, timeZone, mainAgent, currentModel) {
26622
+ async callMultiThreadObserver(existingObservations, messagesByThread, threadOrder, abortSignal, requestContext, priorMetadataByThread, observabilityContext, model, timeZone, mainAgent, currentModel, wasTruncated) {
26564
26623
  if (threadOrder.length === 0) return { results: /* @__PURE__ */ new Map() };
26565
26624
  const inputTokens = Array.from(messagesByThread.values()).reduce((total, messages) => total + this.tokenCounter.countMessages(messages), 0);
26566
26625
  let resolvedModel;
@@ -26606,7 +26665,8 @@ var ObserverRunner = class {
26606
26665
  priorThreadTitle: priorMetadataByThread?.get(threadId)?.threadTitle,
26607
26666
  priorExtractedValues: priorMetadataByThread?.get(threadId)?.extracted,
26608
26667
  model: resolvedModel.model,
26609
- timeZone
26668
+ timeZone,
26669
+ wasTruncated
26610
26670
  });
26611
26671
  results.set(threadId, {
26612
26672
  observations: threadResult.observations,
@@ -26632,7 +26692,7 @@ var ObserverRunner = class {
26632
26692
  const agent = this.createAgent(resolvedModel.model, true, void 0, activeExtractors);
26633
26693
  const internalRequestContext = withOmInternalThreadId(requestContext, agent.id);
26634
26694
  const multiThreadAttachmentFilter = this.resolveAttachmentFilter(resolvedModel.model, requestContext);
26635
- const observerMessages = [buildMultiThreadObserverRequestMessage(existingObservations, messagesByThread, threadOrder, priorMetadataByThread, void 0, this.observationConfig.threadTitle, activeExtractors, {
26695
+ const observerMessages = [buildMultiThreadObserverRequestMessage(existingObservations, messagesByThread, threadOrder, priorMetadataByThread, wasTruncated, this.observationConfig.threadTitle, activeExtractors, {
26636
26696
  attachmentFilter: multiThreadAttachmentFilter,
26637
26697
  timeZone
26638
26698
  })];
@@ -26646,6 +26706,7 @@ var ObserverRunner = class {
26646
26706
  metadata: {
26647
26707
  omThreadCount: threadOrder.length,
26648
26708
  omPreviousObserverTokens: this.observationConfig.previousObserverTokens,
26709
+ omWasTruncated: wasTruncated ?? false,
26649
26710
  omThreadTitleEnabled: this.observationConfig.threadTitle,
26650
26711
  ...resolvedModel.selectedThreshold !== void 0 ? { omSelectedThreshold: resolvedModel.selectedThreshold } : {},
26651
26712
  ...resolvedModel.routingStrategy ? { omRoutingStrategy: resolvedModel.routingStrategy } : {},
@@ -30887,6 +30948,80 @@ function resolveAutoModelId(mainModelId, options) {
30887
30948
  const pick = provider ? models[provider] : void 0;
30888
30949
  return pick ? `${prefix}${pick}` : mainModelId;
30889
30950
  }
30951
+ /** How long an op may hold its key before later ops on that key stop waiting for it. */
30952
+ const OM_COMMIT_STALL_MS = 6e4;
30953
+ /** The per-thread / per-resource key shared by the commit queue and `BufferingCoordinator`. */
30954
+ function getOMLockKey(scope, threadId, resourceId) {
30955
+ if (scope === "resource" && resourceId) return `resource:${resourceId}`;
30956
+ return `thread:${threadId ?? "unknown"}`;
30957
+ }
30958
+ const REGISTRY = Symbol.for("@mastra/memory/om-commit-queue");
30959
+ const { queues, heldKeys } = globalThis[REGISTRY] ??= {
30960
+ queues: /* @__PURE__ */ new Map(),
30961
+ heldKeys: new async_hooks.AsyncLocalStorage()
30962
+ };
30963
+ function holdsKey(key) {
30964
+ const token = heldKeys.getStore()?.get(key);
30965
+ return token !== void 0 && queues.get(key)?.running === token;
30966
+ }
30967
+ function runNext(key, queue) {
30968
+ const next = queue.reflection.shift() ?? queue.normal.shift();
30969
+ if (!next) {
30970
+ queue.running = null;
30971
+ queues.delete(key);
30972
+ return;
30973
+ }
30974
+ next();
30975
+ }
30976
+ /**
30977
+ * Runs `op` once every earlier op on `key` has settled (reflection-priority ops first among those
30978
+ * still waiting) and resolves or rejects with its result. A rejection reaches only this caller.
30979
+ *
30980
+ * Enqueueing on a key from inside an op that holds the same key, directly or through nested ops
30981
+ * on other keys, would deadlock, so it rejects.
30982
+ */
30983
+ function runOMCommit(key, op, { priority = "normal" } = {}) {
30984
+ 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.`));
30985
+ return new Promise((resolve, reject) => {
30986
+ let queue = queues.get(key);
30987
+ if (!queue) {
30988
+ queue = {
30989
+ running: null,
30990
+ reflection: [],
30991
+ normal: []
30992
+ };
30993
+ queues.set(key, queue);
30994
+ }
30995
+ const owner = queue;
30996
+ const start = async_hooks.AsyncResource.bind(() => {
30997
+ const token = Symbol(key);
30998
+ owner.running = token;
30999
+ const held = new Map(heldKeys.getStore());
31000
+ held.set(key, token);
31001
+ let released = false;
31002
+ const release = () => {
31003
+ if (released) return;
31004
+ released = true;
31005
+ clearTimeout(stallTimer);
31006
+ runNext(key, owner);
31007
+ };
31008
+ const stallTimer = setTimeout(() => {
31009
+ 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.`);
31010
+ release();
31011
+ }, OM_COMMIT_STALL_MS);
31012
+ stallTimer.unref?.();
31013
+ heldKeys.run(held, () => Promise.resolve().then(op)).then((value) => {
31014
+ release();
31015
+ resolve(value);
31016
+ }, (error) => {
31017
+ release();
31018
+ reject(error);
31019
+ });
31020
+ });
31021
+ (priority === "reflection" ? owner.reflection : owner.normal).push(start);
31022
+ if (owner.running === null) runNext(key, owner);
31023
+ });
31024
+ }
30890
31025
  const activeOps = /* @__PURE__ */ new Map();
30891
31026
  function opKey(recordId, op) {
30892
31027
  return `${recordId}:${op}`;
@@ -30923,11 +31058,37 @@ var BufferingCoordinator = class BufferingCoordinator {
30923
31058
  */
30924
31059
  static asyncBufferingOps = /* @__PURE__ */ new Map();
30925
31060
  /**
31061
+ * Observation buffer ops that may still append a buffered chunk.
31062
+ * Key format: "obs:{lockKey}"
31063
+ * Value: Promise that resolves once the op has persisted its chunk (or ended without one)
31064
+ *
31065
+ * Above the threshold→blockAfter band, activation waits (bounded) on this phase so the chunk
31066
+ * activates instead of being re-observed by the sync pass. It is not needed for safety: storage
31067
+ * keeps a chunk appended during activation. An op stays in asyncBufferingOps through its
31068
+ * post-persist work (indexing, thread title), which activation never waits for.
31069
+ */
31070
+ static pendingChunkWrites = /* @__PURE__ */ new Map();
31071
+ /**
30926
31072
  * Track the last token boundary at which we started buffering.
30927
31073
  * Key format: "obs:{lockKey}" or "refl:{lockKey}"
30928
31074
  */
30929
31075
  static lastBufferedBoundary = /* @__PURE__ */ new Map();
30930
31076
  /**
31077
+ * The buffer op that set the observation boundary, if one did. Any other write (an activation
31078
+ * reset, another op) replaces or clears it, so an op that outlived it never writes its boundary
31079
+ * back. Key format: "obs:{lockKey}"
31080
+ */
31081
+ static observationBoundaryOwners = /* @__PURE__ */ new Map();
31082
+ /** Set the observation boundary; `owner` is the buffer op setting it, if any. */
31083
+ static setObservationBoundary(bufferKey, tokens, owner) {
31084
+ BufferingCoordinator.lastBufferedBoundary.set(bufferKey, tokens);
31085
+ if (owner) BufferingCoordinator.observationBoundaryOwners.set(bufferKey, owner);
31086
+ else BufferingCoordinator.observationBoundaryOwners.delete(bufferKey);
31087
+ }
31088
+ static ownsObservationBoundary(bufferKey, owner) {
31089
+ return BufferingCoordinator.observationBoundaryOwners.get(bufferKey) === owner;
31090
+ }
31091
+ /**
30931
31092
  * Track the timestamp cursor for buffered messages.
30932
31093
  * Key format: "obs:{lockKey}"
30933
31094
  */
@@ -30943,8 +31104,7 @@ var BufferingCoordinator = class BufferingCoordinator {
30943
31104
  this.scope = opts.scope;
30944
31105
  }
30945
31106
  getLockKey(threadId, resourceId) {
30946
- if (this.scope === "resource" && resourceId) return `resource:${resourceId}`;
30947
- return `thread:${threadId ?? "unknown"}`;
31107
+ return getOMLockKey(this.scope, threadId, resourceId);
30948
31108
  }
30949
31109
  isAsyncObservationEnabled() {
30950
31110
  return this.observationConfig.bufferTokens !== void 0 && this.observationConfig.bufferTokens > 0;
@@ -30962,6 +31122,19 @@ var BufferingCoordinator = class BufferingCoordinator {
30962
31122
  return BufferingCoordinator.asyncBufferingOps.has(bufferKey);
30963
31123
  }
30964
31124
  /**
31125
+ * Mark an observation buffer op as able to append a chunk. Call the returned release
31126
+ * function once it no longer can; releasing more than once is a no-op.
31127
+ */
31128
+ static trackChunkWrite(bufferKey) {
31129
+ let resolve;
31130
+ const pending = new Promise((r) => resolve = r);
31131
+ BufferingCoordinator.pendingChunkWrites.set(bufferKey, pending);
31132
+ return () => {
31133
+ if (BufferingCoordinator.pendingChunkWrites.get(bufferKey) === pending) BufferingCoordinator.pendingChunkWrites.delete(bufferKey);
31134
+ resolve();
31135
+ };
31136
+ }
31137
+ /**
30965
31138
  * Clean up static maps for a thread/resource to prevent memory leaks.
30966
31139
  */
30967
31140
  cleanupStaticMaps(threadId, resourceId, activatedMessageIds) {
@@ -30970,10 +31143,12 @@ var BufferingCoordinator = class BufferingCoordinator {
30970
31143
  const reflBufKey = this.getReflectionBufferKey(lockKey);
30971
31144
  if (activatedMessageIds) {
30972
31145
  BufferingCoordinator.lastBufferedBoundary.delete(obsBufKey);
31146
+ BufferingCoordinator.observationBoundaryOwners.delete(obsBufKey);
30973
31147
  BufferingCoordinator.lastBufferedAtTime.delete(obsBufKey);
30974
31148
  } else {
30975
31149
  BufferingCoordinator.lastBufferedAtTime.delete(obsBufKey);
30976
31150
  BufferingCoordinator.lastBufferedBoundary.delete(obsBufKey);
31151
+ BufferingCoordinator.observationBoundaryOwners.delete(obsBufKey);
30977
31152
  BufferingCoordinator.lastBufferedBoundary.delete(reflBufKey);
30978
31153
  BufferingCoordinator.asyncBufferingOps.delete(obsBufKey);
30979
31154
  BufferingCoordinator.asyncBufferingOps.delete(reflBufKey);
@@ -30988,7 +31163,7 @@ var BufferingCoordinator = class BufferingCoordinator {
30988
31163
  if (record.isBufferingObservation) {
30989
31164
  if (isOpActiveInProcess(record.id, "bufferingObservation")) return false;
30990
31165
  omDebug(`[OM:shouldTriggerAsyncObs] isBufferingObservation=true but stale, clearing`);
30991
- storage?.setBufferingObservationFlag(record.id, false)?.catch(() => {});
31166
+ if (storage) runOMCommit(lockKey, () => storage.setBufferingObservationFlag(record.id, false)).catch(() => {});
30992
31167
  }
30993
31168
  const bufferKey = this.getObservationBufferKey(lockKey);
30994
31169
  if (this.isAsyncBufferingInProgress(bufferKey)) return false;
@@ -31008,7 +31183,7 @@ var BufferingCoordinator = class BufferingCoordinator {
31008
31183
  * Await any in-flight async buffering operations for a given thread/resource.
31009
31184
  */
31010
31185
  static async awaitBuffering(threadId, resourceId, scope, timeoutMs = 3e4) {
31011
- const lockKey = scope === "resource" && resourceId ? `resource:${resourceId}` : `thread:${threadId ?? "unknown"}`;
31186
+ const lockKey = getOMLockKey(scope, threadId, resourceId);
31012
31187
  const obsKey = `obs:${lockKey}`;
31013
31188
  const reflKey = `refl:${lockKey}`;
31014
31189
  const promises = [];
@@ -31300,6 +31475,34 @@ function createThreadUpdateMarker(params) {
31300
31475
  }
31301
31476
  };
31302
31477
  }
31478
+ /**
31479
+ * The current head of `origin`'s lineage (the generations reflections have created from it),
31480
+ * or `null` when that lineage is gone.
31481
+ *
31482
+ * Use this instead of reading the head by thread/resource when committing work that started
31483
+ * from `origin`. Clearing a record (`om.clear`, `Memory.deleteThread`) deletes every generation
31484
+ * of the thread/resource, and the thread can then get a new, unrelated record. Work produced
31485
+ * from the cleared messages must never be committed to it.
31486
+ *
31487
+ * Generation ids are never reused and clearing deletes all of them, so the head belongs to
31488
+ * `origin`'s lineage iff `origin` still exists.
31489
+ */
31490
+ async function getLineageHead(storage, origin) {
31491
+ const head = await storage.getObservationalMemory(origin.threadId, origin.resourceId);
31492
+ if (!head || head.id === origin.id) return head;
31493
+ if (head.generationCount <= origin.generationCount) return null;
31494
+ return (await storage.getObservationalMemoryHistory(origin.threadId, origin.resourceId, head.generationCount - origin.generationCount + 3)).some((record) => record.id === origin.id) ? head : null;
31495
+ }
31496
+ /**
31497
+ * The generation `id` of `origin`'s lineage, or `null` when that lineage is gone or `id` is not
31498
+ * part of it. Unlike {@link getLineageHead}, this keeps naming `id` after later reflections have
31499
+ * superseded it, e.g. to report which generation a write landed on.
31500
+ */
31501
+ async function getLineageRecord(storage, origin, id) {
31502
+ const head = await getLineageHead(storage, origin);
31503
+ if (!head || head.id === id) return head;
31504
+ return (await storage.getObservationalMemoryHistory(origin.threadId, origin.resourceId, head.generationCount - origin.generationCount + 3)).find((record) => record.id === id) ?? null;
31505
+ }
31303
31506
  function isRecord$1(value) {
31304
31507
  return !!value && typeof value === "object" && !Array.isArray(value);
31305
31508
  }
@@ -31442,6 +31645,39 @@ function calculateProjectedMessageRemoval(chunks, bufferActivation, messageToken
31442
31645
  }
31443
31646
  /** Module-level xxhash singleton — loaded once, shared across all strategy instances. */
31444
31647
  const hasherPromise = e();
31648
+ /** Recompose-and-retry rounds for an observation commit that hit a retired or changed head. */
31649
+ const MAX_HEAD_COMMIT_RETRIES = 3;
31650
+ const OBSERVATION_LIFECYCLE_MARKERS = /* @__PURE__ */ new Set([
31651
+ "data-om-observation-start",
31652
+ "data-om-observation-end",
31653
+ "data-om-observation-failed"
31654
+ ]);
31655
+ function isOmPart(part) {
31656
+ return String(part?.type ?? "").startsWith("data-om-");
31657
+ }
31658
+ /**
31659
+ * Index in `message` right after this cycle's start marker (and any OM parts directly after it),
31660
+ * or -1 when the message doesn't carry it.
31661
+ */
31662
+ function afterOwnStartMarker(message, marker) {
31663
+ const cycleId = marker.data?.cycleId;
31664
+ const startType = marker.type.startsWith("data-om-buffering-") ? "data-om-buffering-start" : "data-om-observation-start";
31665
+ if (!cycleId || marker.type === startType) return -1;
31666
+ const parts = message.content?.parts ?? [];
31667
+ const start = parts.findIndex((part) => part?.type === startType && part?.data?.cycleId === cycleId);
31668
+ if (start === -1) return -1;
31669
+ let index = start + 1;
31670
+ while (index < parts.length && isOmPart(parts[index])) index++;
31671
+ return index;
31672
+ }
31673
+ /** True when the message has parts no completed observation boundary covers (OM markers aside). */
31674
+ function hasUnobservedContent(message) {
31675
+ return getUnobservedParts(message).some((part) => !isOmPart(part));
31676
+ }
31677
+ function isObservationLifecycleMarker(marker) {
31678
+ const operationType = marker.data?.operationType;
31679
+ return OBSERVATION_LIFECYCLE_MARKERS.has(marker.type) && operationType !== "reflection";
31680
+ }
31445
31681
  /**
31446
31682
  * Abstract base class for observation strategies.
31447
31683
  *
@@ -31491,17 +31727,35 @@ var ObservationStrategy = class ObservationStrategy {
31491
31727
  const fresh = await this.storage.getObservationalMemory(record.threadId, record.resourceId);
31492
31728
  if (fresh?.lastObservedAt && record.lastObservedAt && fresh.lastObservedAt > record.lastObservedAt) return { observed: false };
31493
31729
  }
31494
- const { messages, existingObservations } = await this.prepare();
31730
+ const { messages, existingObservations, contextRecord } = await this.prepare();
31731
+ if (messages.length === 0) return { observed: false };
31495
31732
  const observationMessages = stripSubconsciousSignals(messages);
31496
31733
  await this.emitStartMarkers(cycleId);
31497
- const output = await this.observe(existingObservations, observationMessages);
31498
- const processed = await this.process(output, existingObservations);
31499
- this.settleObservationCommit(await this.persist(processed));
31734
+ const observerContext = this.deps.prepareObserverContext(existingObservations, contextRecord ?? record);
31735
+ const output = await this.observe(observerContext.context ?? "", observationMessages, observerContext.wasTruncated);
31736
+ let processed = await this.process(output, existingObservations);
31737
+ let committedRecord = record;
31738
+ const outcome = await this.persist(processed);
31739
+ this.settleObservationCommit(outcome?.status === "committed" && !outcome.alreadyCovered);
31740
+ if (outcome?.status === "not-committed") {
31741
+ omDebug(`[OM:observe] cycle ${cycleId} not committed: ${outcome.reason}`);
31742
+ await this.emitFailedMarkers(cycleId, /* @__PURE__ */ new Error(`Observation not committed: ${outcome.reason}`));
31743
+ return {
31744
+ observed: false,
31745
+ usage: output.usage,
31746
+ providerMetadata: output.providerMetadata
31747
+ };
31748
+ }
31749
+ if (outcome?.status === "committed") {
31750
+ processed = outcome.processed;
31751
+ committedRecord = outcome.record;
31752
+ }
31500
31753
  await this.emitEndMarkers(cycleId, processed);
31501
31754
  if (this.needsReflection) await this.deps.reflector.maybeReflect({
31502
31755
  record: {
31503
- ...record,
31504
- activeObservations: processed.observations
31756
+ ...committedRecord,
31757
+ activeObservations: processed.observations,
31758
+ observationTokenCount: processed.observationTokens
31505
31759
  },
31506
31760
  observationTokens: processed.observationTokens,
31507
31761
  threadId,
@@ -31537,7 +31791,8 @@ var ObservationStrategy = class ObservationStrategy {
31537
31791
  threadId
31538
31792
  }
31539
31793
  };
31540
- await this.persistMarkerToStorage(failedMarkerForStorage, threadId, this.opts.resourceId).catch(() => {});
31794
+ const observed = this.getObservedMessagesForThread(threadId);
31795
+ await (observed?.length ? this.persistMarkerToObservedMessage(failedMarkerForStorage, observed, threadId, this.opts.resourceId) : this.persistMarkerToStorage(failedMarkerForStorage, threadId, this.opts.resourceId)).catch(() => {});
31541
31796
  if (abortSignal?.aborted) throw error;
31542
31797
  omError("[OM] Observation failed", error);
31543
31798
  return {
@@ -31564,6 +31819,11 @@ var ObservationStrategy = class ObservationStrategy {
31564
31819
  transient: true
31565
31820
  }).catch(() => {});
31566
31821
  const markerThreadId = marker.data?.threadId ?? this.opts.threadId;
31822
+ const observed = isObservationLifecycleMarker(marker) ? this.getObservedMessagesForThread(markerThreadId) : void 0;
31823
+ if (observed?.length) {
31824
+ await this.persistMarkerToObservedMessage(marker, observed, markerThreadId, this.opts.resourceId);
31825
+ return;
31826
+ }
31567
31827
  if (!await this.persistMarkerToMessage(marker, this.opts.messageList, markerThreadId, this.opts.resourceId)) await this.persistMarkerToStorage(marker, markerThreadId, this.opts.resourceId);
31568
31828
  }
31569
31829
  getObservationMarkerConfig() {
@@ -31647,6 +31907,76 @@ var ObservationStrategy = class ObservationStrategy {
31647
31907
  }
31648
31908
  return `${existingObservations}${lastObservedAt ? ObservationStrategy.createMessageBoundary(lastObservedAt) : "\n\n"}${newThreadSection}`;
31649
31909
  }
31910
+ /**
31911
+ * Commit composed observations to the current head generation.
31912
+ *
31913
+ * The write is conditional on the head text the observations were composed from. When the
31914
+ * target generation was retired by a reflection, or another writer changed the text, the
31915
+ * observations are recomposed against the fresh head and the commit is retried (bounded).
31916
+ * When the fresh head's `observedMessageIds` already include every message this cycle observed
31917
+ * (another instance observed them first), nothing is written and `alreadyCovered` is set, so
31918
+ * callers advance cursors and markers without adding a second copy of the same observations.
31919
+ * The cursor alone is not proof: an activation can move it past a message it never covered.
31920
+ * Returns null when no commit landed — callers must then leave cursors, markers, and the live
31921
+ * context untouched.
31922
+ */
31923
+ async commitActiveObservationsToHead(opts) {
31924
+ let { processed, composedFrom, target } = opts;
31925
+ for (let attempt = 0; attempt <= MAX_HEAD_COMMIT_RETRIES; attempt++) {
31926
+ const result = await this.runCommit(async () => {
31927
+ const head = await getLineageHead(this.storage, target);
31928
+ if (!head) return null;
31929
+ if (head.id !== target.id || (head.activeObservations ?? "") !== composedFrom) {
31930
+ if (this.headCoversCycle(head, opts.cycleMessageIds)) {
31931
+ omDebug(`[OM:observe] head ${head.id} already covers this cycle's messages; skipping the duplicate commit`);
31932
+ target = head;
31933
+ return {
31934
+ applied: true,
31935
+ alreadyCovered: true
31936
+ };
31937
+ }
31938
+ target = head;
31939
+ composedFrom = head.activeObservations ?? "";
31940
+ processed = await opts.recompose(head);
31941
+ }
31942
+ const input = {
31943
+ id: target.id,
31944
+ observations: processed.observations,
31945
+ tokenCount: processed.observationTokens,
31946
+ lastObservedAt: processed.lastObservedAt,
31947
+ observedMessageIds: processed.observedMessageIds,
31948
+ expectedActiveObservations: composedFrom
31949
+ };
31950
+ return typeof this.storage.commitActiveObservations === "function" ? await this.storage.commitActiveObservations(input) : (await this.storage.updateActiveObservations(input), { applied: true });
31951
+ });
31952
+ if (!result) return null;
31953
+ if ("alreadyCovered" in result) return {
31954
+ processed: {
31955
+ ...processed,
31956
+ observations: target.activeObservations ?? "",
31957
+ observationTokens: target.observationTokenCount
31958
+ },
31959
+ record: target,
31960
+ alreadyCovered: true
31961
+ };
31962
+ if (result.applied) return {
31963
+ processed,
31964
+ record: target
31965
+ };
31966
+ omDebug(`[OM:observe] commit to ${target.id} not applied (${result.reason}); retrying against the head`);
31967
+ }
31968
+ return null;
31969
+ }
31970
+ headCoversCycle(head, cycleMessageIds) {
31971
+ if (cycleMessageIds.length === 0) return false;
31972
+ const headIds = new Set(Array.isArray(head.observedMessageIds) ? head.observedMessageIds : []);
31973
+ return cycleMessageIds.every((id) => headIds.has(id));
31974
+ }
31975
+ /** Runs one storage commit in this thread's (or resource's) slot of the process-wide commit queue. */
31976
+ runCommit(op) {
31977
+ const { threadId, resourceId, record } = this.opts;
31978
+ return runOMCommit(getOMLockKey(this.scope, record.threadId ?? threadId, record.resourceId ?? resourceId), op);
31979
+ }
31650
31980
  async indexObservationGroups(observations, threadId, resourceId, observedAt, recordId) {
31651
31981
  if (!resourceId || !this.deps.onIndexObservations) return;
31652
31982
  const groups = parseObservationGroups(observations);
@@ -31665,30 +31995,133 @@ var ObservationStrategy = class ObservationStrategy {
31665
31995
  })));
31666
31996
  }
31667
31997
  /**
31998
+ * Messages this cycle observed for `threadId`, or undefined when the strategy does not
31999
+ * place observation lifecycle markers.
32000
+ */
32001
+ getObservedMessagesForThread(_threadId) {}
32002
+ /**
32003
+ * Persist an observation start/end/failed marker without hiding unobserved content. A completed
32004
+ * end marker tells `getUnobservedMessages` that everything before it in that message is observed
32005
+ * (whatever thread the marker names), so it must only follow content this cycle observed.
32006
+ * Placement, in order:
32007
+ * 1. the live MessageList's newest assistant message (this turn's response or its seed), when the
32008
+ * list belongs to the marker's thread and the cycle observed that message or it has no
32009
+ * unobserved content;
32010
+ * 2. the newest observed assistant message; a stored copy (no live list, or another instance's
32011
+ * message) gets the marker right after the parts this cycle observed (end/failed markers right
32012
+ * after this cycle's start marker), so parts added while the Observer ran stay unobserved;
32013
+ * 3. the newest stored assistant message that is not newer than the observed range, only when it
32014
+ * has nothing unobserved (or already carries this cycle's start marker); otherwise no marker,
32015
+ * and the thread cursor alone records the cycle.
32016
+ * Lifecycle markers never go on user messages.
32017
+ */
32018
+ async persistMarkerToObservedMessage(marker, observed, threadId, resourceId) {
32019
+ const observedIds = new Set(observed.map((m) => m.id));
32020
+ const list = threadId === this.opts.threadId ? this.opts.messageList : void 0;
32021
+ const listMessages = list ? getObservableMessages(list) : [];
32022
+ const live = [...listMessages].reverse().find((m) => m.role === "assistant");
32023
+ if (live && (observedIds.has(live.id) || !hasUnobservedContent(live))) {
32024
+ await this.attachMarker(marker, live, live.content.parts.length, threadId, resourceId);
32025
+ return;
32026
+ }
32027
+ const target = [...observed].reverse().find((m) => m.role === "assistant");
32028
+ if (target) {
32029
+ const fromList = listMessages.find((m) => m.id === target.id);
32030
+ if (fromList) {
32031
+ await this.attachMarker(marker, fromList, fromList.content.parts.length, threadId, resourceId);
32032
+ return;
32033
+ }
32034
+ try {
32035
+ const stored = (await this.storage.listMessagesById({ messageIds: [target.id] })).messages[0];
32036
+ if (stored?.content?.parts && Array.isArray(stored.content.parts)) {
32037
+ const afterStart = afterOwnStartMarker(stored, marker);
32038
+ const index = afterStart !== -1 ? afterStart : this.insertionAfterObserved(stored, target);
32039
+ await this.attachMarker(marker, stored, index, threadId, resourceId);
32040
+ }
32041
+ } catch (e) {
32042
+ omDebug(`[OM:persistMarkerToObservedMessage] failed to load marker target: ${e}`);
32043
+ }
32044
+ return;
32045
+ }
32046
+ const observedTimes = observed.filter((m) => m.createdAt).map((m) => new Date(m.createdAt).getTime());
32047
+ if (observedTimes.length === 0) return;
32048
+ await this.persistMarkerToStorage(marker, threadId, resourceId, {
32049
+ notAfter: new Date(Math.max(...observedTimes)),
32050
+ onlyIfObserved: true
32051
+ });
32052
+ }
32053
+ /** What the cycle observed of each message, captured when it placed its first marker there. */
32054
+ observedParts = /* @__PURE__ */ new Map();
32055
+ /**
32056
+ * Index in `stored` right after the parts the cycle observed in `observed` (and any OM markers
32057
+ * directly after them). Parts are only ever appended, so the observed message's non-marker part
32058
+ * count locates them, counted from the start of the message or, when `observed` is the trimmed
32059
+ * copy `getUnobservedMessages` returns for an already marked message, from just after the stored
32060
+ * copy's last completed boundary. Captured at the cycle's first marker, before the Observer runs,
32061
+ * because `observed` may be a live reference that grows meanwhile. Falls back to the end when the
32062
+ * stored copy has fewer parts than were observed.
32063
+ */
32064
+ insertionAfterObserved(stored, observed) {
32065
+ if (!this.observedParts.has(observed.id)) this.observedParts.set(observed.id, {
32066
+ count: (observed.content?.parts ?? []).filter((part) => !isOmPart(part)).length,
32067
+ observedIsTrimmed: findLastCompletedObservationBoundary(observed) === -1
32068
+ });
32069
+ const { count, observedIsTrimmed } = this.observedParts.get(observed.id);
32070
+ const parts = stored.content.parts;
32071
+ let index = observedIsTrimmed ? findLastCompletedObservationBoundary(stored) + 1 : 0;
32072
+ let seen = 0;
32073
+ while (index < parts.length && seen < count) {
32074
+ if (!isOmPart(parts[index])) seen++;
32075
+ index++;
32076
+ }
32077
+ if (seen < count) return parts.length;
32078
+ while (index < parts.length && isOmPart(parts[index])) index++;
32079
+ return index;
32080
+ }
32081
+ async attachMarker(marker, msg, index, threadId, resourceId) {
32082
+ const parts = msg.content?.parts;
32083
+ if (!parts || !Array.isArray(parts)) return;
32084
+ const markerData = marker.data;
32085
+ if (!(markerData?.cycleId && parts.some((p) => p?.type === marker.type && p?.data?.cycleId === markerData.cycleId))) parts.splice(index, 0, marker);
32086
+ try {
32087
+ await this.messageHistory.persistMessages({
32088
+ messages: [msg],
32089
+ threadId,
32090
+ resourceId
32091
+ });
32092
+ } catch (e) {
32093
+ omDebug(`[OM:persistMarkerToObservedMessage] failed to save marker to DB: ${e}`);
32094
+ }
32095
+ }
32096
+ /**
31668
32097
  * Persist a marker to the last assistant message in storage.
31669
32098
  * Fetches messages directly from the DB so it works even when
31670
32099
  * no MessageList is available (e.g. async buffering ops).
31671
32100
  */
31672
- async persistMarkerToStorage(marker, threadId, resourceId) {
32101
+ async persistMarkerToStorage(marker, threadId, resourceId, opts) {
31673
32102
  try {
31674
- const messages = (await this.storage.listMessages({
32103
+ const assistants = ((await this.storage.listMessages({
31675
32104
  threadId,
31676
32105
  perPage: 20,
31677
32106
  orderBy: {
31678
32107
  field: "createdAt",
31679
32108
  direction: "DESC"
31680
- }
31681
- }))?.messages ?? [];
31682
- for (const msg of messages) if (msg?.role === "assistant" && msg.content?.parts && Array.isArray(msg.content.parts)) {
31683
- const markerData = marker.data;
31684
- if (!(markerData?.cycleId && msg.content.parts.some((p) => p?.type === marker.type && p?.data?.cycleId === markerData.cycleId))) msg.content.parts.push(marker);
31685
- await this.messageHistory.persistMessages({
31686
- messages: [msg],
31687
- threadId,
31688
- resourceId
31689
- });
31690
- return;
31691
- }
32109
+ },
32110
+ ...opts?.notAfter ? { filter: { dateRange: { end: opts.notAfter } } } : {}
32111
+ }))?.messages ?? []).filter((msg) => msg?.role === "assistant" && Array.isArray(msg.content?.parts));
32112
+ const startMessage = opts?.onlyIfObserved || opts?.onCycleStartMessage ? assistants.find((m) => afterOwnStartMarker(m, marker) !== -1) : void 0;
32113
+ const msg = opts?.onCycleStartMessage ? startMessage : startMessage ?? assistants[0];
32114
+ if (!msg) return;
32115
+ const markerData = marker.data;
32116
+ const alreadyPresent = markerData?.cycleId && msg.content.parts.some((p) => p?.type === marker.type && p?.data?.cycleId === markerData.cycleId);
32117
+ const afterStart = afterOwnStartMarker(msg, marker);
32118
+ if (opts?.onlyIfObserved && afterStart === -1 && hasUnobservedContent(msg)) return;
32119
+ if (!alreadyPresent) msg.content.parts.splice(afterStart !== -1 ? afterStart : msg.content.parts.length, 0, marker);
32120
+ await this.messageHistory.persistMessages({
32121
+ messages: [msg],
32122
+ threadId,
32123
+ resourceId
32124
+ });
31692
32125
  } catch (e) {
31693
32126
  omDebug(`[OM:persistMarkerToStorage] failed to save marker to DB: ${e}`);
31694
32127
  }
@@ -31746,10 +32179,15 @@ var SyncObservationStrategy = class extends ObservationStrategy {
31746
32179
  tokensToObserve = 0;
31747
32180
  observerResult;
31748
32181
  priorExtractedValues;
32182
+ /** Head text the processed observations were composed from (the commit's expected text). */
32183
+ composedFrom = "";
31749
32184
  constructor(deps, opts) {
31750
32185
  super(deps, opts);
31751
32186
  this.lastMessage = opts.messages[opts.messages.length - 1];
31752
32187
  }
32188
+ getObservedMessagesForThread(threadId) {
32189
+ return threadId === this.opts.threadId ? this.opts.messages : void 0;
32190
+ }
31753
32191
  get needsLock() {
31754
32192
  return true;
31755
32193
  }
@@ -31784,9 +32222,11 @@ var SyncObservationStrategy = class extends ObservationStrategy {
31784
32222
  }
31785
32223
  }
31786
32224
  this.tokensToObserve = await this.tokenCounter.countMessagesAsync(messages);
32225
+ const freshRecord = await this.storage.getObservationalMemory(record.threadId, record.resourceId);
31787
32226
  return {
31788
32227
  messages,
31789
- existingObservations: (await this.storage.getObservationalMemory(record.threadId, record.resourceId))?.activeObservations ?? record.activeObservations ?? ""
32228
+ existingObservations: freshRecord?.activeObservations ?? record.activeObservations ?? "",
32229
+ contextRecord: freshRecord
31790
32230
  };
31791
32231
  }
31792
32232
  async emitStartMarkers(cycleId) {
@@ -31804,11 +32244,12 @@ var SyncObservationStrategy = class extends ObservationStrategy {
31804
32244
  await this.streamMarker(startMarker);
31805
32245
  }
31806
32246
  }
31807
- async observe(existingObservations, messages) {
32247
+ async observe(observerContext, messages, wasTruncated) {
31808
32248
  const thread = await this.storage.getThreadById({ threadId: this.opts.threadId });
31809
32249
  const omMeta = thread ? (0, _mastra_core_memory.getThreadOMMetadata)(thread.metadata) : void 0;
31810
32250
  this.priorExtractedValues = getPriorExtractedValues(omMeta, this.observationConfig.extractors);
31811
- const result = await this.deps.observer.call(existingObservations, messages, this.opts.abortSignal, {
32251
+ const result = await this.deps.observer.call(observerContext, messages, this.opts.abortSignal, {
32252
+ wasTruncated,
31812
32253
  requestContext: this.opts.requestContext,
31813
32254
  observabilityContext: this.opts.observabilityContext,
31814
32255
  priorCurrentTask: omMeta?.currentTask,
@@ -31854,20 +32295,14 @@ var SyncObservationStrategy = class extends ObservationStrategy {
31854
32295
  }
31855
32296
  async process(output, existingObservations) {
31856
32297
  const { record, threadId, messages } = this.opts;
31857
- const lastObservedAt = this.getMaxMessageTimestamp(messages);
31858
- const messageRange = this.retrieval ? buildMessageRange(messages) : void 0;
31859
- const newObservations = await this.wrapObservations(output.observations, existingObservations, threadId, lastObservedAt, messageRange);
31860
- const observationTokens = this.tokenCounter.countObservations(newObservations);
31861
- const cycleObservationTokens = this.tokenCounter.countObservations(output.observations);
31862
- const newMessageIds = messages.map((m) => m.id);
31863
- const existingIds = record.observedMessageIds ?? [];
31864
- const observedMessageIds = [.../* @__PURE__ */ new Set([...Array.isArray(existingIds) ? existingIds : [], ...newMessageIds])];
32298
+ this.composedFrom = existingObservations;
32299
+ const processed = await this.compose(output, existingObservations, record);
31865
32300
  this.deps.emitDebugEvent({
31866
32301
  type: "observation_complete",
31867
32302
  timestamp: /* @__PURE__ */ new Date(),
31868
32303
  threadId,
31869
32304
  resourceId: record.resourceId ?? "",
31870
- observations: newObservations,
32305
+ observations: processed.observations,
31871
32306
  rawObserverOutput: output.observations,
31872
32307
  previousObservations: record.activeObservations,
31873
32308
  messages: messages.map((m) => ({
@@ -31876,11 +32311,23 @@ var SyncObservationStrategy = class extends ObservationStrategy {
31876
32311
  })),
31877
32312
  usage: output.usage
31878
32313
  });
32314
+ return processed;
32315
+ }
32316
+ /** Build the observations to commit on top of `existingObservations` (the head text). */
32317
+ async compose(output, existingObservations, baseRecord) {
32318
+ const { threadId, messages } = this.opts;
32319
+ const lastObservedAt = this.getMaxMessageTimestamp(messages);
32320
+ const messageRange = this.retrieval ? buildMessageRange(messages) : void 0;
32321
+ const newObservations = await this.wrapObservations(output.observations, existingObservations, threadId, lastObservedAt, messageRange);
32322
+ const observationTokens = this.tokenCounter.countObservations(newObservations);
32323
+ const cycleObservationTokens = this.tokenCounter.countObservations(output.observations);
32324
+ const newMessageIds = messages.map((m) => m.id);
32325
+ const existingIds = baseRecord.observedMessageIds ?? [];
31879
32326
  return {
31880
32327
  observations: newObservations,
31881
32328
  observationTokens,
31882
32329
  cycleObservationTokens,
31883
- observedMessageIds,
32330
+ observedMessageIds: [.../* @__PURE__ */ new Set([...Array.isArray(existingIds) ? existingIds : [], ...newMessageIds])],
31884
32331
  lastObservedAt,
31885
32332
  suggestedContinuation: output.suggestedContinuation,
31886
32333
  currentTask: output.currentTask,
@@ -31892,12 +32339,27 @@ var SyncObservationStrategy = class extends ObservationStrategy {
31892
32339
  }
31893
32340
  async persist(processed) {
31894
32341
  const { record, threadId, resourceId, messages } = this.opts;
31895
- const thread = await this.storage.getThreadById({ threadId });
31896
- if (!await this.storage.getObservationalMemory(record.threadId, record.resourceId)) {
32342
+ const liveRecord = await getLineageHead(this.storage, record);
32343
+ if (!liveRecord) {
31897
32344
  omDebug(`[OM:sync-obs] skipping persist for thread ${threadId}: observational memory record is gone`);
31898
- return false;
32345
+ return {
32346
+ status: "not-committed",
32347
+ reason: "the observational memory record was cleared"
32348
+ };
31899
32349
  }
31900
- let threadUpdateMarker;
32350
+ const committed = await this.commitActiveObservationsToHead({
32351
+ processed,
32352
+ composedFrom: this.composedFrom,
32353
+ target: liveRecord,
32354
+ recompose: (head) => this.compose(this.observerResult, head.activeObservations ?? "", head),
32355
+ cycleMessageIds: messages.map((m) => m.id)
32356
+ });
32357
+ if (!committed) return {
32358
+ status: "not-committed",
32359
+ reason: "the observational memory head kept changing during the commit"
32360
+ };
32361
+ processed = committed.processed;
32362
+ const thread = committed.alreadyCovered ? null : await this.storage.getThreadById({ threadId });
31901
32363
  if (thread) {
31902
32364
  const oldTitle = thread.title?.trim();
31903
32365
  const newTitle = resolveThreadTitleUpdate(thread, processed.threadTitle);
@@ -31919,23 +32381,20 @@ var SyncObservationStrategy = class extends ObservationStrategy {
31919
32381
  ...shouldUpdateThreadTitle ? { title: newTitle } : {},
31920
32382
  metadata: newMetadata
31921
32383
  });
31922
- if (shouldUpdateThreadTitle) threadUpdateMarker = createThreadUpdateMarker({
32384
+ if (shouldUpdateThreadTitle) await this.streamMarker(createThreadUpdateMarker({
31923
32385
  cycleId: this.cycleId ?? crypto.randomUUID(),
31924
32386
  threadId,
31925
32387
  oldTitle,
31926
32388
  newTitle
31927
- });
32389
+ }));
31928
32390
  }
31929
- if (threadUpdateMarker) await this.streamMarker(threadUpdateMarker);
31930
- await this.storage.updateActiveObservations({
31931
- id: record.id,
31932
- observations: processed.observations,
31933
- tokenCount: processed.observationTokens,
31934
- lastObservedAt: processed.lastObservedAt,
31935
- observedMessageIds: processed.observedMessageIds
31936
- });
31937
- await this.indexObservationGroups(processed.observations, threadId, resourceId, processed.lastObservedAt, record.id);
31938
- return true;
32391
+ if (!committed.alreadyCovered) await this.indexObservationGroups(processed.observations, threadId, resourceId, processed.lastObservedAt, committed.record.id);
32392
+ return {
32393
+ status: "committed",
32394
+ processed,
32395
+ record: committed.record,
32396
+ alreadyCovered: committed.alreadyCovered
32397
+ };
31939
32398
  }
31940
32399
  async emitEndMarkers(cycleId, processed) {
31941
32400
  const actualTokensObserved = await this.tokenCounter.countMessagesAsync(this.opts.messages);
@@ -31977,6 +32436,8 @@ var AsyncBufferObservationStrategy = class extends ObservationStrategy {
31977
32436
  startedAt;
31978
32437
  cycleId;
31979
32438
  priorExtractedValues;
32439
+ /** The generation the chunk was written to (the head when the target was retired). */
32440
+ persistedRecordId;
31980
32441
  constructor(deps, opts) {
31981
32442
  super(deps, opts);
31982
32443
  this.cycleId = opts.cycleId;
@@ -32003,12 +32464,13 @@ var AsyncBufferObservationStrategy = class extends ObservationStrategy {
32003
32464
  };
32004
32465
  }
32005
32466
  async emitStartMarkers(_cycleId) {}
32006
- async observe(existingObservations, messages) {
32467
+ async observe(observerContext, messages, wasTruncated) {
32007
32468
  const thread = await this.storage.getThreadById({ threadId: this.opts.threadId });
32008
32469
  const omMeta = thread ? (0, _mastra_core_memory.getThreadOMMetadata)(thread.metadata) : void 0;
32009
32470
  this.priorExtractedValues = getPriorExtractedValues(omMeta, this.observationConfig.extractors);
32010
- const result = await this.deps.observer.call(existingObservations, messages, void 0, {
32471
+ const result = await this.deps.observer.call(observerContext, messages, void 0, {
32011
32472
  skipContinuationHints: true,
32473
+ wasTruncated,
32012
32474
  requestContext: this.opts.requestContext,
32013
32475
  observabilityContext: this.opts.observabilityContext,
32014
32476
  priorExtractedValues: this.priorExtractedValues,
@@ -32082,71 +32544,124 @@ var AsyncBufferObservationStrategy = class extends ObservationStrategy {
32082
32544
  };
32083
32545
  }
32084
32546
  async persist(processed) {
32085
- if (!processed.observations) return false;
32547
+ if (!processed.observations) return;
32086
32548
  const { record, threadId, resourceId, messages } = this.opts;
32087
- if (!await this.storage.getObservationalMemory(record.threadId, record.resourceId)) {
32549
+ const liveRecord = await getLineageHead(this.storage, record);
32550
+ if (!liveRecord) {
32088
32551
  omDebug(`[OM:asyncBuffer] skipping persist for thread ${threadId}: observational memory record is gone`);
32089
- return false;
32552
+ return {
32553
+ status: "not-committed",
32554
+ reason: "the observational memory record was cleared"
32555
+ };
32090
32556
  }
32091
32557
  const messageTokens = await this.tokenCounter.countMessagesAsync(messages);
32092
- await withRetry(() => this.storage.updateBufferedObservations({
32093
- id: record.id,
32094
- chunk: {
32095
- cycleId: this.cycleId,
32096
- observations: processed.observations,
32097
- tokenCount: processed.observationTokens,
32098
- messageIds: processed.observedMessageIds,
32099
- messageTokens,
32100
- lastObservedAt: processed.lastObservedAt,
32101
- suggestedContinuation: processed.suggestedContinuation,
32102
- currentTask: processed.currentTask,
32103
- threadTitle: processed.threadTitle,
32104
- extractedValues: processed.extractedValues,
32105
- extractionFailures: processed.extractionFailures
32106
- },
32107
- lastBufferedAtTime: processed.lastObservedAt
32108
- }), {
32558
+ let appendAttempts = 0;
32559
+ let targetId = record.id;
32560
+ const appendResult = await withRetry(() => {
32561
+ appendAttempts++;
32562
+ return this.runCommit(async () => {
32563
+ const head = await getLineageHead(this.storage, record);
32564
+ if (!head) return null;
32565
+ targetId = head.id;
32566
+ const input = {
32567
+ id: targetId,
32568
+ chunk: {
32569
+ cycleId: this.cycleId,
32570
+ observations: processed.observations,
32571
+ tokenCount: processed.observationTokens,
32572
+ messageIds: processed.observedMessageIds,
32573
+ messageTokens,
32574
+ lastObservedAt: processed.lastObservedAt,
32575
+ suggestedContinuation: processed.suggestedContinuation,
32576
+ currentTask: processed.currentTask,
32577
+ threadTitle: processed.threadTitle,
32578
+ extractedValues: processed.extractedValues,
32579
+ extractionFailures: processed.extractionFailures
32580
+ },
32581
+ lastBufferedAtTime: processed.lastObservedAt
32582
+ };
32583
+ return typeof this.storage.appendBufferedObservations === "function" ? this.storage.appendBufferedObservations(input) : this.storage.updateBufferedObservations(input).then(() => ({
32584
+ persisted: true,
32585
+ recordId: input.id
32586
+ }));
32587
+ });
32588
+ }, {
32109
32589
  label: "persist-buffered-observations",
32110
32590
  abortSignal: this.opts.abortSignal
32111
32591
  });
32112
- await this.indexObservationGroups(processed.observations, threadId, resourceId, processed.lastObservedAt, record.id);
32113
- const candidateTitle = processed.threadTitle?.trim();
32114
- const hasValidThreadTitle = !!candidateTitle && candidateTitle.length >= 3;
32115
- if (hasValidThreadTitle || processed.extractedValues) {
32116
- const thread = await this.storage.getThreadById({ threadId });
32117
- if (thread) {
32118
- const oldTitle = thread.title?.trim();
32119
- const newTitle = resolveThreadTitleUpdate(thread, candidateTitle);
32120
- const shouldUpdateThreadTitle = newTitle !== void 0;
32121
- const previousOmMetadata = (0, _mastra_core_memory.getThreadOMMetadata)(thread.metadata);
32122
- const metadataUpdate = buildThreadMetadataFromExtractedValues(processed.extractors ?? this.observationConfig.extractors, processed.extractedValues);
32123
- const newMetadata = (0, _mastra_core_memory.setThreadOMMetadata)(thread.metadata, {
32124
- ...hasValidThreadTitle || metadataUpdate.threadTitle ? { threadTitle: metadataUpdate.threadTitle ?? processed.threadTitle } : {},
32125
- extracted: {
32126
- ...previousOmMetadata?.extracted ?? {},
32127
- ...metadataUpdate.extracted ?? {}
32128
- }
32129
- });
32130
- await this.storage.patchThread({
32131
- id: threadId,
32132
- ...shouldUpdateThreadTitle ? { title: newTitle } : {},
32133
- metadata: newMetadata
32134
- });
32135
- if (shouldUpdateThreadTitle) {
32136
- const marker = createThreadUpdateMarker({
32137
- cycleId: this.cycleId,
32138
- threadId,
32139
- oldTitle,
32140
- newTitle
32592
+ if (appendResult === null) {
32593
+ omDebug(`[OM:asyncBuffer] skipping persist for thread ${threadId}: observational memory record was cleared`);
32594
+ return {
32595
+ status: "not-committed",
32596
+ reason: "the observational memory record was cleared"
32597
+ };
32598
+ }
32599
+ let committedRecord = liveRecord;
32600
+ if (!appendResult.persisted) {
32601
+ const head = appendAttempts > 1 ? await getLineageHead(this.storage, record) : null;
32602
+ if (!(!!head && (getBufferedChunks(head).some((chunk) => chunk.cycleId === this.cycleId) || head.activeObservations.includes(processed.observations)))) return {
32603
+ status: "not-committed",
32604
+ reason: "the buffered chunk was already observed"
32605
+ };
32606
+ this.persistedRecordId = head.id;
32607
+ committedRecord = head;
32608
+ } else this.persistedRecordId = appendResult.recordId;
32609
+ await this.emitBufferingEndMarker(processed);
32610
+ await this.opts.onBufferedChunkPersisted?.();
32611
+ if (committedRecord.id !== this.persistedRecordId) {
32612
+ const landedOn = await getLineageRecord(this.storage, record, this.persistedRecordId);
32613
+ if (!landedOn) return {
32614
+ status: "not-committed",
32615
+ reason: "the observational memory record was cleared"
32616
+ };
32617
+ committedRecord = landedOn;
32618
+ }
32619
+ try {
32620
+ await this.indexObservationGroups(processed.observations, threadId, resourceId, processed.lastObservedAt, this.persistedRecordId);
32621
+ const candidateTitle = processed.threadTitle?.trim();
32622
+ const hasValidThreadTitle = !!candidateTitle && candidateTitle.length >= 3;
32623
+ if (hasValidThreadTitle || processed.extractedValues) {
32624
+ const thread = await this.storage.getThreadById({ threadId });
32625
+ if (thread) {
32626
+ const oldTitle = thread.title?.trim();
32627
+ const newTitle = resolveThreadTitleUpdate(thread, candidateTitle);
32628
+ const shouldUpdateThreadTitle = newTitle !== void 0;
32629
+ const previousOmMetadata = (0, _mastra_core_memory.getThreadOMMetadata)(thread.metadata);
32630
+ const metadataUpdate = buildThreadMetadataFromExtractedValues(processed.extractors ?? this.observationConfig.extractors, processed.extractedValues);
32631
+ const newMetadata = (0, _mastra_core_memory.setThreadOMMetadata)(thread.metadata, {
32632
+ ...hasValidThreadTitle || metadataUpdate.threadTitle ? { threadTitle: metadataUpdate.threadTitle ?? processed.threadTitle } : {},
32633
+ extracted: {
32634
+ ...previousOmMetadata?.extracted ?? {},
32635
+ ...metadataUpdate.extracted ?? {}
32636
+ }
32637
+ });
32638
+ await this.storage.patchThread({
32639
+ id: threadId,
32640
+ ...shouldUpdateThreadTitle ? { title: newTitle } : {},
32641
+ metadata: newMetadata
32141
32642
  });
32142
- await this.streamMarker(marker);
32643
+ if (shouldUpdateThreadTitle) {
32644
+ const marker = createThreadUpdateMarker({
32645
+ cycleId: this.cycleId,
32646
+ threadId,
32647
+ oldTitle,
32648
+ newTitle
32649
+ });
32650
+ await this.streamMarker(marker);
32651
+ }
32143
32652
  }
32144
32653
  }
32654
+ } catch (error) {
32655
+ omError("[OM] Post-buffer indexing or thread update failed; the buffered chunk is kept", error);
32145
32656
  }
32146
- return true;
32657
+ return {
32658
+ status: "committed",
32659
+ processed,
32660
+ record: committedRecord
32661
+ };
32147
32662
  }
32148
- async emitEndMarkers(_cycleId, processed) {
32149
- if (!processed.observations) return;
32663
+ async emitEndMarkers() {}
32664
+ async emitBufferingEndMarker(processed) {
32150
32665
  const { record, threadId, messages } = this.opts;
32151
32666
  const tokensBuffered = await this.tokenCounter.countMessagesAsync(messages);
32152
32667
  const totalBufferedTokens = getBufferedChunks(await this.storage.getObservationalMemory(record.threadId, record.resourceId)).reduce((sum, c) => sum + (c.tokenCount ?? 0), 0) || processed.observationTokens;
@@ -32156,7 +32671,7 @@ var AsyncBufferObservationStrategy = class extends ObservationStrategy {
32156
32671
  startedAt: this.startedAt,
32157
32672
  tokensBuffered,
32158
32673
  bufferedTokens: totalBufferedTokens,
32159
- recordId: record.id,
32674
+ recordId: this.persistedRecordId ?? record.id,
32160
32675
  threadId,
32161
32676
  observations: processed.observations,
32162
32677
  extractedValues: processed.extractedValues,
@@ -32166,7 +32681,10 @@ var AsyncBufferObservationStrategy = class extends ObservationStrategy {
32166
32681
  ...endMarker,
32167
32682
  transient: true
32168
32683
  }).catch(() => {});
32169
- await this.persistMarkerToStorage(endMarker, threadId, record.resourceId ?? void 0);
32684
+ await this.persistMarkerToStorage(endMarker, threadId, record.resourceId ?? void 0, {
32685
+ notAfter: new Date(this.startedAt),
32686
+ onCycleStartMessage: true
32687
+ });
32170
32688
  }
32171
32689
  async emitFailedMarkers(_cycleId, error) {
32172
32690
  const { record, threadId, messages } = this.opts;
@@ -32185,7 +32703,10 @@ var AsyncBufferObservationStrategy = class extends ObservationStrategy {
32185
32703
  ...failedMarker,
32186
32704
  transient: true
32187
32705
  }).catch(() => {});
32188
- await this.persistMarkerToStorage(failedMarker, threadId, record.resourceId ?? void 0);
32706
+ await this.persistMarkerToStorage(failedMarker, threadId, record.resourceId ?? void 0, {
32707
+ notAfter: new Date(this.startedAt),
32708
+ onCycleStartMessage: true
32709
+ });
32189
32710
  }
32190
32711
  };
32191
32712
  var ResourceScopedObservationStrategy = class extends ObservationStrategy {
@@ -32206,10 +32727,17 @@ var ResourceScopedObservationStrategy = class extends ObservationStrategy {
32206
32727
  lastBatchProviderMetadata;
32207
32728
  observationResults = [];
32208
32729
  priorMetadataByThread = /* @__PURE__ */ new Map();
32730
+ /** Each observed thread's wrapped section from the last composition. */
32731
+ threadSections = /* @__PURE__ */ new Map();
32732
+ /** Head text the processed observations were composed from (the commit's expected text). */
32733
+ composedFrom = "";
32209
32734
  constructor(deps, opts) {
32210
32735
  super(deps, opts);
32211
32736
  this.resourceId = opts.resourceId;
32212
32737
  }
32738
+ getObservedMessagesForThread(threadId) {
32739
+ return this.threadsWithMessages.get(threadId);
32740
+ }
32213
32741
  get needsLock() {
32214
32742
  return true;
32215
32743
  }
@@ -32296,10 +32824,12 @@ var ResourceScopedObservationStrategy = class extends ObservationStrategy {
32296
32824
  content: typeof m.content === "string" ? m.content : JSON.stringify(m.content)
32297
32825
  }))
32298
32826
  });
32299
- const existingObservations = (await this.storage.getObservationalMemory(null, this.resourceId))?.activeObservations ?? record.activeObservations ?? "";
32827
+ const freshRecord = await this.storage.getObservationalMemory(null, this.resourceId);
32828
+ const existingObservations = freshRecord?.activeObservations ?? record.activeObservations ?? "";
32300
32829
  return {
32301
32830
  messages: Array.from(this.threadsWithMessages.values()).flat(),
32302
- existingObservations
32831
+ existingObservations,
32832
+ contextRecord: freshRecord
32303
32833
  };
32304
32834
  }
32305
32835
  async emitStartMarkers(cycleId) {
@@ -32323,7 +32853,7 @@ var ResourceScopedObservationStrategy = class extends ObservationStrategy {
32323
32853
  }
32324
32854
  }
32325
32855
  }
32326
- async observe(_existingObservations, _messages) {
32856
+ async observe(observerContext, _messages, wasTruncated) {
32327
32857
  const maxTokensPerBatch = this.observationConfig.maxTokensPerBatch ?? OBSERVATIONAL_MEMORY_DEFAULTS.observation.maxTokensPerBatch;
32328
32858
  const orderedThreadIds = this.threadOrder.filter((tid) => this.threadsWithMessages.has(tid));
32329
32859
  const batches = [];
@@ -32349,10 +32879,10 @@ var ResourceScopedObservationStrategy = class extends ObservationStrategy {
32349
32879
  }
32350
32880
  if (currentBatch.threadIds.length > 0) batches.push(currentBatch);
32351
32881
  const batchResults = await Promise.all(batches.map(async (batch) => {
32352
- return this.deps.observer.callMultiThread(_existingObservations, batch.threadMap, batch.threadIds, this.opts.abortSignal, this.opts.requestContext, this.priorMetadataByThread, this.opts.observabilityContext, void 0, {
32882
+ return this.deps.observer.callMultiThread(observerContext, batch.threadMap, batch.threadIds, this.opts.abortSignal, this.opts.requestContext, this.priorMetadataByThread, this.opts.observabilityContext, void 0, {
32353
32883
  resourceId: this.opts.resourceId,
32354
32884
  trigger: this.opts.trigger
32355
- }, this.opts.record.observedTimezone, this.opts.agent, this.opts.currentModel);
32885
+ }, this.opts.record.observedTimezone, this.opts.agent, this.opts.currentModel, wasTruncated);
32356
32886
  }));
32357
32887
  for (const batchResult of batchResults) {
32358
32888
  for (const [threadId, result] of batchResult.results) this.multiThreadResults.set(threadId, result);
@@ -32419,14 +32949,38 @@ var ResourceScopedObservationStrategy = class extends ObservationStrategy {
32419
32949
  }
32420
32950
  });
32421
32951
  }
32952
+ this.composedFrom = existingObservations;
32953
+ const processed = await this.compose(existingObservations, record);
32954
+ for (const [index, { threadId, threadMessages, result }] of this.observationResults.entries()) this.deps.emitDebugEvent({
32955
+ type: "observation_complete",
32956
+ timestamp: /* @__PURE__ */ new Date(),
32957
+ threadId,
32958
+ resourceId: this.resourceId,
32959
+ observations: this.threadSections.get(threadId) ?? "",
32960
+ rawObserverOutput: result.observations,
32961
+ previousObservations: record.activeObservations,
32962
+ messages: threadMessages.map((m) => ({
32963
+ role: m.role,
32964
+ content: typeof m.content === "string" ? m.content : JSON.stringify(m.content)
32965
+ })),
32966
+ usage: index === 0 && this.totalBatchUsage.totalTokens > 0 ? this.totalBatchUsage : void 0
32967
+ });
32968
+ return processed;
32969
+ }
32970
+ /**
32971
+ * Merge every observed thread's section into `existingObservations` (the head text). Resource
32972
+ * scope rewrites an existing same-day `<thread>` section in place, so the result is not an
32973
+ * append of the head text.
32974
+ */
32975
+ async compose(existingObservations, baseRecord) {
32422
32976
  let currentObservations = existingObservations;
32423
32977
  let cycleObservationTokens = 0;
32424
32978
  const threadMetadataUpdates = [];
32425
- for (const obsResult of this.observationResults) {
32426
- const { threadId, threadMessages, result } = obsResult;
32979
+ for (const { threadId, threadMessages, result } of this.observationResults) {
32427
32980
  cycleObservationTokens += this.tokenCounter.countObservations(result.observations);
32428
32981
  const messageRange = this.retrieval ? buildMessageRange(threadMessages) : void 0;
32429
32982
  const threadSection = await this.wrapWithThreadTag(threadId, result.observations, messageRange);
32983
+ this.threadSections.set(threadId, threadSection);
32430
32984
  const threadLastObservedAt = this.getMaxMessageTimestamp(threadMessages);
32431
32985
  currentObservations = this.replaceOrAppendThreadSection(currentObservations, threadId, threadSection, threadLastObservedAt);
32432
32986
  threadMetadataUpdates.push({
@@ -32440,26 +32994,11 @@ var ResourceScopedObservationStrategy = class extends ObservationStrategy {
32440
32994
  extractors: result.extractors,
32441
32995
  lastObservedMessageCursor: getLastObservedMessageCursor(threadMessages)
32442
32996
  });
32443
- const isFirstThread = this.observationResults.indexOf(obsResult) === 0;
32444
- this.deps.emitDebugEvent({
32445
- type: "observation_complete",
32446
- timestamp: /* @__PURE__ */ new Date(),
32447
- threadId,
32448
- resourceId: this.resourceId,
32449
- observations: threadSection,
32450
- rawObserverOutput: result.observations,
32451
- previousObservations: record.activeObservations,
32452
- messages: threadMessages.map((m) => ({
32453
- role: m.role,
32454
- content: typeof m.content === "string" ? m.content : JSON.stringify(m.content)
32455
- })),
32456
- usage: isFirstThread && this.totalBatchUsage.totalTokens > 0 ? this.totalBatchUsage : void 0
32457
- });
32458
32997
  }
32459
32998
  const observedMessages = this.observationResults.flatMap((r) => r.threadMessages);
32460
32999
  const lastObservedAt = this.getMaxMessageTimestamp(observedMessages);
32461
33000
  const newMessageIds = observedMessages.map((m) => m.id);
32462
- const existingIds = record.observedMessageIds ?? [];
33001
+ const existingIds = baseRecord.observedMessageIds ?? [];
32463
33002
  const observedMessageIds = [.../* @__PURE__ */ new Set([...existingIds, ...newMessageIds])];
32464
33003
  const observationTokens = this.tokenCounter.countObservations(currentObservations);
32465
33004
  return {
@@ -32473,8 +33012,25 @@ var ResourceScopedObservationStrategy = class extends ObservationStrategy {
32473
33012
  }
32474
33013
  async persist(processed) {
32475
33014
  const { record, resourceId } = this.opts;
33015
+ const head = await getLineageHead(this.storage, record);
33016
+ if (!head) return {
33017
+ status: "not-committed",
33018
+ reason: "the observational memory record was cleared"
33019
+ };
33020
+ const committed = await this.commitActiveObservationsToHead({
33021
+ processed,
33022
+ composedFrom: this.composedFrom,
33023
+ target: head,
33024
+ recompose: (fresh) => this.compose(fresh.activeObservations ?? "", fresh),
33025
+ cycleMessageIds: this.observationResults.flatMap((r) => r.threadMessages.map((m) => m.id))
33026
+ });
33027
+ if (!committed) return {
33028
+ status: "not-committed",
33029
+ reason: "the observational memory head kept changing during the commit"
33030
+ };
33031
+ processed = committed.processed;
32476
33032
  const threadUpdateMarkers = [];
32477
- if (processed.threadMetadataUpdates) for (const update of processed.threadMetadataUpdates) {
33033
+ if (processed.threadMetadataUpdates && !committed.alreadyCovered) for (const update of processed.threadMetadataUpdates) {
32478
33034
  const thread = await this.storage.getThreadById({ threadId: update.threadId });
32479
33035
  if (thread) {
32480
33036
  const oldTitle = thread.title?.trim();
@@ -32507,15 +33063,13 @@ var ResourceScopedObservationStrategy = class extends ObservationStrategy {
32507
33063
  }
32508
33064
  }
32509
33065
  for (const marker of threadUpdateMarkers) await this.streamMarker(marker);
32510
- await this.storage.updateActiveObservations({
32511
- id: record.id,
32512
- observations: processed.observations,
32513
- tokenCount: processed.observationTokens,
32514
- lastObservedAt: processed.lastObservedAt,
32515
- observedMessageIds: processed.observedMessageIds
32516
- });
32517
- if (resourceId) await Promise.all(this.observationResults.map(({ threadId, threadMessages, result }) => this.indexObservationGroups(result.observations, threadId, resourceId, this.getMaxMessageTimestamp(threadMessages), record.id)));
32518
- return true;
33066
+ 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)));
33067
+ return {
33068
+ status: "committed",
33069
+ processed,
33070
+ record: committed.record,
33071
+ alreadyCovered: committed.alreadyCovered
33072
+ };
32519
33073
  }
32520
33074
  async emitEndMarkers(cycleId, processed) {
32521
33075
  for (const obsResult of this.observationResults) {
@@ -32572,7 +33126,8 @@ ObservationStrategy.create = ((om, opts) => {
32572
33126
  observedMessageIds: om.observedMessageIds,
32573
33127
  obscureThreadIds: om.getObscureThreadIds(),
32574
33128
  onIndexObservations: om.onIndexObservations,
32575
- emitDebugEvent: (e) => om.emitDebugEvent(e)
33129
+ emitDebugEvent: (e) => om.emitDebugEvent(e),
33130
+ prepareObserverContext: (observations, record) => om.prepareObserverContext(observations, record)
32576
33131
  };
32577
33132
  if (opts.cycleId) return new AsyncBufferObservationStrategy(deps, opts);
32578
33133
  if (deps.scope === "resource" && opts.resourceId) return new ResourceScopedObservationStrategy(deps, opts);
@@ -32698,8 +33253,8 @@ var ObservationStep = class {
32698
33253
  let reflected = false;
32699
33254
  let didThresholdCleanup = false;
32700
33255
  let observerExchange;
33256
+ const step0Messages = this.stepNumber === 0 ? getObservableMessages(messageList) : [];
32701
33257
  if (this.stepNumber === 0) {
32702
- const step0Messages = getObservableMessages(messageList);
32703
33258
  const activation = await om.activate({
32704
33259
  threadId,
32705
33260
  resourceId,
@@ -32756,7 +33311,17 @@ var ObservationStep = class {
32756
33311
  record: this.turn.record,
32757
33312
  messages: getObservableMessages(messageList)
32758
33313
  });
32759
- if (statusSnapshot.shouldBuffer) {
33314
+ if (statusSnapshot.inAsyncObservationBand && !statusSnapshot.canActivate && !hasIncompleteToolCalls) {
33315
+ await this.turn.refreshRecord();
33316
+ statusSnapshot = await om.getStatus({
33317
+ threadId,
33318
+ resourceId,
33319
+ record: this.turn.record,
33320
+ messages: getObservableMessages(messageList)
33321
+ });
33322
+ }
33323
+ const activateBeforeBuffering = statusSnapshot.inAsyncObservationBand && statusSnapshot.canActivate && !hasIncompleteToolCalls;
33324
+ if (statusSnapshot.shouldBuffer && !activateBeforeBuffering) {
32760
33325
  const allMessages = getObservableMessages(messageList);
32761
33326
  const unobservedMessages = om.getUnobservedMessages(allMessages, statusSnapshot.record);
32762
33327
  const candidates = om.getUnobservedMessages(unobservedMessages, statusSnapshot.record, { excludeBuffered: true });
@@ -32770,8 +33335,10 @@ var ObservationStep = class {
32770
33335
  omDebug(`[OM:buffer] onBufferChunkSealed hook failed: ${error instanceof Error ? error.message : String(error)}`);
32771
33336
  }
32772
33337
  if (this.turn.memory) await this.turn.memory.persistMessages(safeCandidates);
32773
- messageList.removeByIds(safeCandidates.map((msg) => msg.id));
32774
- for (const msg of safeCandidates) messageList.add(msg, "memory");
33338
+ const inputIds = new Set(messageList.get.input.db().map((msg) => msg.id));
33339
+ const sealedOutput = safeCandidates.filter((msg) => !inputIds.has(msg.id));
33340
+ messageList.removeByIds(sealedOutput.map((msg) => msg.id));
33341
+ for (const msg of sealedOutput) messageList.add(msg, "memory");
32775
33342
  }
32776
33343
  if (!deferred) {
32777
33344
  om.trackBackgroundWork(om.buffer({
@@ -32793,7 +33360,8 @@ var ObservationStep = class {
32793
33360
  buffered = true;
32794
33361
  }
32795
33362
  }
32796
- const willObserveNow = statusSnapshot.shouldObserve && !hasIncompleteToolCalls;
33363
+ const bandDefersObservation = statusSnapshot.inAsyncObservationBand && !statusSnapshot.canActivate;
33364
+ const willObserveNow = statusSnapshot.shouldObserve && !hasIncompleteToolCalls && !bandDefersObservation;
32797
33365
  /** In-flight message ids the step-0 cleanup must never remove from live context. */
32798
33366
  let step0PreserveIds;
32799
33367
  if (this.stepNumber > 0 || willObserveNow) {
@@ -32835,7 +33403,7 @@ var ObservationStep = class {
32835
33403
  }
32836
33404
  if (willObserveNow) {
32837
33405
  const preObsGeneration = this.turn.record.generationCount;
32838
- const obsResult = await this.runThresholdObservation();
33406
+ const obsResult = await this.runThresholdObservation(statusSnapshot.inAsyncObservationBand);
32839
33407
  observerExchange = obsResult.observerExchange;
32840
33408
  if (obsResult.succeeded) {
32841
33409
  observed = true;
@@ -32908,12 +33476,12 @@ var ObservationStep = class {
32908
33476
  /**
32909
33477
  * Run the full threshold observation pipeline:
32910
33478
  * waitForBuffering → re-check → activate buffered chunks → reflect → observe
32911
- * (sync fallback when pending tokens are still at or above the threshold)
33479
+ * (sync fallback at blockAfter when async buffering is enabled)
32912
33480
  */
32913
- async runThresholdObservation() {
33481
+ async runThresholdObservation(inAsyncObservationBand) {
32914
33482
  const { threadId, resourceId, messageList } = this.turn;
32915
33483
  const om = this.turn.om;
32916
- await om.waitForBuffering(threadId, resourceId);
33484
+ if (!inAsyncObservationBand) await om.waitForBuffering(threadId, resourceId);
32917
33485
  await this.turn.refreshRecord();
32918
33486
  const observableMessages = this.seededResponseMessage ? getObservableMessages(messageList).filter((msg) => msg.id !== this.turn.responseMessageId) : getObservableMessages(messageList);
32919
33487
  let status = await om.getStatus({
@@ -32942,7 +33510,16 @@ var ObservationStep = class {
32942
33510
  messageList
32943
33511
  });
32944
33512
  this.turn.setRecord(activation.record);
32945
- if (!activation.activated) break;
33513
+ if (!activation.activated) {
33514
+ await this.turn.refreshRecord();
33515
+ status = await om.getStatus({
33516
+ threadId,
33517
+ resourceId,
33518
+ record: this.turn.record,
33519
+ messages: pendingMessages
33520
+ });
33521
+ break;
33522
+ }
32946
33523
  activated = true;
32947
33524
  const ids = new Set(activation.activatedMessageIds ?? []);
32948
33525
  activatedMessageIds.push(...ids);
@@ -32954,11 +33531,17 @@ var ObservationStep = class {
32954
33531
  messages: pendingMessages
32955
33532
  });
32956
33533
  }
33534
+ if (status.shouldObserve && status.canActivate) return {
33535
+ succeeded: activated,
33536
+ record: this.turn.record,
33537
+ activatedMessageIds: activated ? activatedMessageIds : void 0,
33538
+ cleanupMessageIds: activatedMessageIds
33539
+ };
32957
33540
  if (activated) {
32958
- const postActivationRecord = this.turn.record;
33541
+ const preReflectionRecord = this.turn.record;
32959
33542
  await om.reflector.maybeReflect({
32960
- record: postActivationRecord,
32961
- observationTokens: postActivationRecord.observationTokenCount ?? 0,
33543
+ record: preReflectionRecord,
33544
+ observationTokens: preReflectionRecord.observationTokenCount ?? 0,
32962
33545
  threadId,
32963
33546
  writer: this.turn.writer,
32964
33547
  messageList,
@@ -32973,6 +33556,14 @@ var ObservationStep = class {
32973
33556
  }),
32974
33557
  trigger: "turn-sync"
32975
33558
  });
33559
+ await this.turn.refreshRecord();
33560
+ const postActivationRecord = this.turn.record;
33561
+ status = await om.getStatus({
33562
+ threadId,
33563
+ resourceId,
33564
+ record: postActivationRecord,
33565
+ messages: pendingMessages
33566
+ });
32976
33567
  if (!status.shouldObserve) return {
32977
33568
  succeeded: true,
32978
33569
  record: postActivationRecord,
@@ -32980,6 +33571,12 @@ var ObservationStep = class {
32980
33571
  cleanupMessageIds: activatedMessageIds
32981
33572
  };
32982
33573
  }
33574
+ if (status.inAsyncObservationBand || status.canActivate) return {
33575
+ succeeded: activated,
33576
+ record: this.turn.record,
33577
+ activatedMessageIds: activated ? activatedMessageIds : void 0,
33578
+ cleanupMessageIds: activatedMessageIds
33579
+ };
32983
33580
  let obsResult;
32984
33581
  try {
32985
33582
  obsResult = await om.observe({
@@ -33327,13 +33924,18 @@ Take the existing observations and rewrite them to make it easier to continue in
33327
33924
 
33328
33925
  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.
33329
33926
 
33927
+ 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.
33928
+
33330
33929
  When consolidating observations:
33331
- - Preserve and include dates/times when present (temporal context is critical)
33332
- - Retain the most relevant timestamps (start times, completion times, significant events)
33930
+ - Keep every concrete fact: names, places, dates, numbers, amounts, durations, titles, versions, and who said what. Merge duplicates, but never drop specifics
33931
+ - Keep every dated statement with its date, and retain the timestamps that matter (start times, completion times, significant events)
33932
+ - 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
33933
+ - 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
33934
+ - Prefer many short factual lines over prose summaries. Do not replace facts with generalities like "discussed travel plans"
33333
33935
  - Combine related items where it makes sense (e.g., "agent called view tool 5 times on file x")
33334
33936
  - Preserve ✅ completion markers — they are memory signals that tell the assistant what is already resolved and help prevent repeated work
33335
33937
  - Preserve the concrete resolved outcome captured by ✅ markers so the assistant knows what exactly is done
33336
- - Condense older observations more aggressively, retain more detail for recent ones
33938
+ - Condense the wording of older observations more than recent ones, but keep their facts
33337
33939
 
33338
33940
  CRITICAL: USER ASSERTIONS vs QUESTIONS
33339
33941
  - "User stated: X" = authoritative assertion (user told us something about themselves)
@@ -33613,6 +34215,25 @@ async function withAbortCheck(fn, abortSignal) {
33613
34215
  * size while still letting them fire sooner than a threshold activation.
33614
34216
  */
33615
34217
  const EARLY_ACTIVATION_SIZE_FLOOR_RATIO = .75;
34218
+ /** A reflection commit that created nothing because the reflected record is no longer the head. */
34219
+ var ReflectionNotAppliedError = class extends Error {
34220
+ constructor(recordId, headId) {
34221
+ super(`Reflection not applied: record ${recordId} is no longer the current generation (head is ${headId})`);
34222
+ this.name = "ReflectionNotAppliedError";
34223
+ }
34224
+ };
34225
+ /**
34226
+ * Whether a reflection commit created the generation the caller asked for. Storage creates
34227
+ * nothing (and returns the current head or the stored record) when the reflected record was
34228
+ * already retired or its observations were rewritten since the reflection read them.
34229
+ *
34230
+ * Adapters that predate `newRecordId` ignore it and do not report `supersededBy`; for those a
34231
+ * returned record other than the reflected one means a new generation was created.
34232
+ */
34233
+ function isReflectionApplied(returned, newRecordId, reflectedRecordId) {
34234
+ if (returned.id === newRecordId) return true;
34235
+ return returned.supersededBy === void 0 && returned.id !== reflectedRecordId;
34236
+ }
33616
34237
  /**
33617
34238
  * Runs the Reflector agent for compressing observations.
33618
34239
  * Handles synchronous reflection, async buffered reflection, and activation.
@@ -33976,14 +34597,14 @@ var ReflectorRunner = class {
33976
34597
  * Perform async buffered reflection — reflects observations and stores to bufferedReflection.
33977
34598
  * Does NOT create a new generation or update activeObservations.
33978
34599
  */
33979
- async doAsyncBufferedReflection(record, _bufferKey, writer, requestContext, observabilityContext, priorExtractedValues, mainAgent, sendSignal, trigger, currentModel) {
34600
+ async doAsyncBufferedReflection(record, bufferKey, writer, requestContext, observabilityContext, priorExtractedValues, mainAgent, sendSignal, trigger, currentModel) {
33980
34601
  const currentRecord = await this.storage.getObservationalMemory(record.threadId, record.resourceId) ?? record;
33981
34602
  const observationTokens = currentRecord.observationTokenCount ?? 0;
33982
34603
  const reflectThreshold = getMaxThreshold(this.getEffectiveReflectionTokens(currentRecord));
33983
34604
  const bufferActivation = this.reflectionConfig.bufferActivation ?? .5;
33984
34605
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
33985
34606
  const cycleId = `reflect-buf-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
33986
- BufferingCoordinator.reflectionBufferCycleIds.set(_bufferKey, cycleId);
34607
+ BufferingCoordinator.reflectionBufferCycleIds.set(bufferKey, cycleId);
33987
34608
  const allLines = (currentRecord.activeObservations ?? "").split("\n");
33988
34609
  const totalLines = allLines.length;
33989
34610
  const avgTokensPerLine = totalLines > 0 ? observationTokens / totalLines : 0;
@@ -34022,13 +34643,24 @@ var ReflectorRunner = class {
34022
34643
  await persistThreadExtractedValues(this.storage, this.reflectionConfig.extractors, currentRecord.threadId ?? void 0, reflectResult.extractedValues);
34023
34644
  const reflectionTokenCount = this.tokenCounter.countObservations(reflectResult.observations);
34024
34645
  omDebug(`[OM:reflect] doAsyncBufferedReflection: reflector returned ${reflectionTokenCount} tokens (${reflectResult.observations?.length} chars), saving to recordId=${currentRecord.id}`);
34025
- await this.storage.updateBufferedReflection({
34026
- id: currentRecord.id,
34027
- reflection: reflectResult.observations,
34028
- tokenCount: reflectionTokenCount,
34029
- inputTokenCount: sliceTokenEstimate,
34030
- reflectedObservationLineCount
34031
- });
34646
+ if (!await runOMCommit(this.buffering.getLockKey(currentRecord.threadId, currentRecord.resourceId), async () => {
34647
+ if ((await this.storage.getObservationalMemory(currentRecord.threadId, currentRecord.resourceId))?.id !== currentRecord.id) return false;
34648
+ await this.storage.updateBufferedReflection({
34649
+ id: currentRecord.id,
34650
+ reflection: reflectResult.observations,
34651
+ tokenCount: reflectionTokenCount,
34652
+ inputTokenCount: sliceTokenEstimate,
34653
+ reflectedObservationLineCount
34654
+ });
34655
+ return true;
34656
+ })) {
34657
+ omDebug(`[OM:reflect] doAsyncBufferedReflection: record ${currentRecord.id} is no longer the head; discarding the buffered reflection`);
34658
+ BufferingCoordinator.lastBufferedBoundary.delete(bufferKey);
34659
+ return {
34660
+ usage: reflectResult.usage,
34661
+ providerMetadata: reflectResult.providerMetadata
34662
+ };
34663
+ }
34032
34664
  omDebug(`[OM:reflect] doAsyncBufferedReflection: bufferedReflection saved with lineCount=${reflectedObservationLineCount}`);
34033
34665
  if (writer) {
34034
34666
  const endMarker = createBufferingEndMarker({
@@ -34107,10 +34739,16 @@ var ReflectorRunner = class {
34107
34739
  }
34108
34740
  }
34109
34741
  omDebug(`[OM:reflect] tryActivateBufferedReflection: activating, beforeTokens=${beforeTokens}, combinedTokenCount=${combinedTokenCount}, reflectedLineCount=${reflectedLineCount}, unreflectedLines=${unreflectedLines.length}`);
34110
- await this.storage.swapBufferedReflectionToActive({
34742
+ const newRecordId = crypto.randomUUID();
34743
+ const swapped = await runOMCommit(lockKey, () => this.storage.swapBufferedReflectionToActive({
34111
34744
  currentRecord: freshRecord,
34112
- tokenCount: combinedTokenCount
34113
- });
34745
+ tokenCount: combinedTokenCount,
34746
+ newRecordId
34747
+ }), { priority: "reflection" });
34748
+ if (!isReflectionApplied(swapped, newRecordId, freshRecord.id)) {
34749
+ omDebug(`[OM:reflect] tryActivateBufferedReflection: buffered reflection on ${freshRecord.id} not applied (head is ${swapped.id})`);
34750
+ return { status: "not-applied" };
34751
+ }
34114
34752
  if (committedContext) await this.notifyReflectionCommitted({
34115
34753
  ...committedContext,
34116
34754
  observations: allLines.slice(0, reflectedLineCount).join("\n").trim(),
@@ -34165,7 +34803,8 @@ var ReflectorRunner = class {
34165
34803
  * @internal Used by observation strategies. Do not call directly.
34166
34804
  */
34167
34805
  async maybeReflect(opts) {
34168
- const { record, observationTokens, writer, abortSignal, mainAgent, sendSignal, sendStateSignal, messageList, currentModel, reflectionHooks, trigger, requestContext, observabilityContext, lastActivityAt, threadId: requestedThreadId } = opts;
34806
+ const { record: callerRecord, observationTokens, writer, abortSignal, mainAgent, sendSignal, sendStateSignal, messageList, currentModel, reflectionHooks, trigger, requestContext, observabilityContext, lastActivityAt, threadId: requestedThreadId } = opts;
34807
+ const record = { ...callerRecord };
34169
34808
  const lockKey = this.buffering.getLockKey(record.threadId, record.resourceId);
34170
34809
  const reflectThreshold = getMaxThreshold(this.getEffectiveReflectionTokens(record));
34171
34810
  const priorExtractedValues = await getThreadExtractedValues(this.storage, this.reflectionConfig.extractors, requestedThreadId ?? record.threadId);
@@ -34219,7 +34858,7 @@ var ReflectorRunner = class {
34219
34858
  abortSignal,
34220
34859
  observabilityContext
34221
34860
  });
34222
- if (activationResult.status === "activated") return;
34861
+ if (activationResult.status === "activated" || activationResult.status === "not-applied") return;
34223
34862
  if (activationResult.status === "suppressed") {
34224
34863
  omDebug(`[OM:reflect] skipping sync fallback / re-buffer after suppressed early ${activationMetadata.triggeredBy} activation (reason=${activationResult.reason})`);
34225
34864
  return;
@@ -34302,13 +34941,19 @@ var ReflectorRunner = class {
34302
34941
  reflectResult.observations = await applyTextTransform(this.hooks, "afterReflection", reflectResult.observations, hookContext);
34303
34942
  reflectionUsage = reflectResult.usage;
34304
34943
  reflectionProviderMetadata = reflectResult.providerMetadata;
34305
- await persistThreadExtractedValues(this.storage, this.reflectionConfig.extractors, record.threadId ?? void 0, reflectResult.extractedValues);
34306
34944
  const reflectionTokenCount = this.tokenCounter.countObservations(reflectResult.observations);
34307
- await this.storage.createReflectionGeneration({
34945
+ const newRecordId = crypto.randomUUID();
34946
+ const committedRecord = await runOMCommit(this.buffering.getLockKey(record.threadId, record.resourceId), () => this.storage.createReflectionGeneration({
34308
34947
  currentRecord: record,
34309
34948
  reflection: reflectResult.observations,
34310
- tokenCount: reflectionTokenCount
34311
- });
34949
+ tokenCount: reflectionTokenCount,
34950
+ newRecordId
34951
+ }), { priority: "reflection" });
34952
+ if (!isReflectionApplied(committedRecord, newRecordId, record.id)) {
34953
+ omDebug(`[OM:reflect] reflection of ${record.id} not applied (head is ${committedRecord.id})`);
34954
+ throw new ReflectionNotAppliedError(record.id, committedRecord.id);
34955
+ }
34956
+ await persistThreadExtractedValues(this.storage, this.reflectionConfig.extractors, record.threadId ?? void 0, reflectResult.extractedValues);
34312
34957
  if (reflectionTokenCount >= reflectThreshold) this.syncReflectionSuppression.set(lockKey, reflectionTokenCount);
34313
34958
  else this.syncReflectionSuppression.delete(lockKey);
34314
34959
  await this.notifyReflectionCommitted({
@@ -34388,6 +35033,7 @@ var ReflectorRunner = class {
34388
35033
  failureKind: failedMarker.data.failureKind,
34389
35034
  error: reflectionError.message
34390
35035
  });
35036
+ if (error instanceof ReflectionNotAppliedError) return;
34391
35037
  if (lifecycleError !== void 0 || abortSignal?.aborted || this.reflectionConfig.failurePolicy !== "continue" || !isOmModelExecutionError(error) || error.failureKind !== "reflector-model") throw error;
34392
35038
  this.syncReflectionSuppression.set(lockKey, observationTokens);
34393
35039
  omError("[OM] Reflection failed", error);
@@ -34727,6 +35373,8 @@ function writeProcessInputStepReproCapture(params) {
34727
35373
  * Returns the parts from the latest step of a message (after the last step-start marker).
34728
35374
  * If no step-start marker exists, returns all parts.
34729
35375
  */
35376
+ /** Re-reads of the head after an activation swap hit a generation retired by a reflection. */
35377
+ const MAX_ACTIVATION_RETIRED_RETRIES = 3;
34730
35378
  function getLatestStepParts(parts) {
34731
35379
  for (let i = parts.length - 1; i >= 0; i--) if (parts[i]?.type === "step-start") return parts.slice(i + 1);
34732
35380
  return parts;
@@ -34920,6 +35568,8 @@ var ObservationalMemory = class ObservationalMemory {
34920
35568
  reflector;
34921
35569
  /** Buffering state coordinator — manages static maps and buffering lifecycle. */
34922
35570
  buffering;
35571
+ /** Unresolved observation blockAfter (multiplier or absolute); resolved per record. */
35572
+ observationBlockAfterSetting;
34923
35573
  shouldObscureThreadIds = false;
34924
35574
  hasher = e();
34925
35575
  mastra;
@@ -35055,6 +35705,7 @@ var ObservationalMemory = class ObservationalMemory {
35055
35705
  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.");
35056
35706
  const observationActivateAfterIdle = config.observation?.activateAfterIdle ?? config.activateAfterIdle;
35057
35707
  const observationActivateAfterIdlePath = config.observation?.activateAfterIdle !== void 0 ? "observation.activateAfterIdle" : "activateAfterIdle";
35708
+ this.observationBlockAfterSetting = asyncBufferingDisabled ? void 0 : config.observation?.blockAfter ?? (config.observation?.bufferTokens ?? OBSERVATIONAL_MEMORY_DEFAULTS.observation.bufferTokens ? 1.2 : void 0);
35058
35709
  this.observationConfig = {
35059
35710
  model: observationModel,
35060
35711
  maxRetries: config.observation?.maxRetries ?? RETRY_CONFIG.maxRetries,
@@ -35075,8 +35726,8 @@ var ObservationalMemory = class ObservationalMemory {
35075
35726
  bufferActivation: asyncBufferingDisabled ? void 0 : config.observation?.bufferActivation ?? OBSERVATIONAL_MEMORY_DEFAULTS.observation.bufferActivation,
35076
35727
  activateAfterIdle: parseActivationTTLConfig(observationActivateAfterIdle, observationActivateAfterIdlePath),
35077
35728
  activateOnProviderChange: config.observation?.activateOnProviderChange ?? config.activateOnProviderChange ?? false,
35078
- 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),
35079
- previousObserverTokens: config.observation?.previousObserverTokens ?? 2e3,
35729
+ blockAfter: resolveBlockAfter(this.observationBlockAfterSetting, config.observation?.messageTokens ?? OBSERVATIONAL_MEMORY_DEFAULTS.observation.messageTokens),
35730
+ previousObserverTokens: config.observation?.previousObserverTokens ?? false,
35080
35731
  instruction: config.observation?.instruction,
35081
35732
  threadTitle: config.observation?.threadTitle ?? false,
35082
35733
  observeAttachments: config.observation?.observeAttachments ?? [...OBSERVATIONAL_MEMORY_DEFAULTS.observation.observeAttachments],
@@ -35385,6 +36036,13 @@ var ObservationalMemory = class ObservationalMemory {
35385
36036
  return this.observationConfig.messageTokens;
35386
36037
  }
35387
36038
  /**
36039
+ * Resolve the observation blockAfter for a record. A multiplier scales the record's
36040
+ * effective messageTokens (per-record overrides included); an absolute value is used as-is.
36041
+ */
36042
+ getEffectiveObservationBlockAfter(record) {
36043
+ return resolveBlockAfter(this.observationBlockAfterSetting, this.getEffectiveMessageTokens(record));
36044
+ }
36045
+ /**
35388
36046
  * Resolve the effective reflection observationTokens for a record.
35389
36047
  * Only explicit per-record overrides (stored under `_overrides`) win;
35390
36048
  * the initial config snapshot is ignored so instance-level changes
@@ -35498,18 +36156,21 @@ var ObservationalMemory = class ObservationalMemory {
35498
36156
  * the latest user input that triggered it.
35499
36157
  * @internal Used by observation strategies and the reflector. Do not call directly.
35500
36158
  */
35501
- async persistMarkerToStorage(marker, threadId, resourceId) {
36159
+ async persistMarkerToStorage(marker, threadId, resourceId, opts) {
35502
36160
  try {
35503
- const messages = (await this.storage.listMessages({
36161
+ const markerData = marker.data;
36162
+ const targetRole = markerData?.operationType === "reflection" ? "user" : "assistant";
36163
+ const preferred = [...opts?.preferMessages ?? []].reverse().find((m) => m.role === targetRole && (!m.threadId || m.threadId === threadId));
36164
+ const preferredStored = preferred ? (await this.storage.listMessagesById({ messageIds: [preferred.id] })).messages : [];
36165
+ const messages = (preferredStored.length ? { messages: preferredStored } : await this.storage.listMessages({
35504
36166
  threadId,
35505
36167
  perPage: 20,
35506
36168
  orderBy: {
35507
36169
  field: "createdAt",
35508
36170
  direction: "DESC"
35509
- }
36171
+ },
36172
+ ...opts?.notAfter ? { filter: { dateRange: { end: opts.notAfter } } } : {}
35510
36173
  }))?.messages ?? [];
35511
- const markerData = marker.data;
35512
- const targetRole = markerData?.operationType === "reflection" ? "user" : "assistant";
35513
36174
  for (const msg of messages) if (msg?.role === targetRole && msg.content?.parts && Array.isArray(msg.content.parts)) {
35514
36175
  if (!(markerData?.cycleId && msg.content.parts.some((p) => p?.type === marker.type && p?.data?.cycleId === markerData.cycleId))) msg.content.parts.push(marker);
35515
36176
  await this.messageHistory.persistMessages({
@@ -35596,7 +36257,7 @@ var ObservationalMemory = class ObservationalMemory {
35596
36257
  */
35597
36258
  createUnobservedMessage(message) {
35598
36259
  const unobservedParts = getUnobservedParts(message);
35599
- if (unobservedParts.length === 0) return null;
36260
+ if (unobservedParts.every((part) => part?.type?.startsWith("data-om-"))) return null;
35600
36261
  return {
35601
36262
  ...message,
35602
36263
  content: {
@@ -35968,17 +36629,17 @@ ${formattedMessages}
35968
36629
  async startAsyncBufferedObservation(record, threadId, unobservedMessages, lockKey, writer, contextWindowTokens, requestContext, observabilityContext) {
35969
36630
  const bufferKey = this.buffering.getObservationBufferKey(lockKey);
35970
36631
  const currentTokens = contextWindowTokens ?? await this.tokenCounter.countMessagesAsync(unobservedMessages) + (record.pendingMessageTokens ?? 0);
35971
- BufferingCoordinator.lastBufferedBoundary.set(bufferKey, currentTokens);
36632
+ BufferingCoordinator.setObservationBoundary(bufferKey, currentTokens);
35972
36633
  registerOp(record.id, "bufferingObservation");
35973
- this.storage.setBufferingObservationFlag(record.id, true, currentTokens).catch((err) => {
36634
+ runOMCommit(lockKey, () => this.storage.setBufferingObservationFlag(record.id, true, currentTokens)).catch((err) => {
35974
36635
  omError("[OM] Failed to set buffering observation flag", err);
35975
36636
  });
35976
36637
  const asyncOp = this.runAsyncBufferedObservation(record, threadId, unobservedMessages, bufferKey, writer, requestContext, observabilityContext).catch((err) => {
35977
36638
  omError("[OM] async buffering observation failed", err);
35978
36639
  }).finally(() => {
35979
- BufferingCoordinator.asyncBufferingOps.delete(bufferKey);
36640
+ if (BufferingCoordinator.asyncBufferingOps.get(bufferKey) === asyncOp) BufferingCoordinator.asyncBufferingOps.delete(bufferKey);
35980
36641
  unregisterOp(record.id, "bufferingObservation");
35981
- this.storage.setBufferingObservationFlag(record.id, false).catch((err) => {
36642
+ runOMCommit(lockKey, () => this.storage.setBufferingObservationFlag(record.id, false)).catch((err) => {
35982
36643
  omError("[OM] Failed to clear buffering observation flag", err);
35983
36644
  });
35984
36645
  });
@@ -35993,6 +36654,14 @@ ${formattedMessages}
35993
36654
  if (existingOp) try {
35994
36655
  await existingOp;
35995
36656
  } catch {}
36657
+ const releaseChunkWrite = BufferingCoordinator.trackChunkWrite(bufferKey);
36658
+ try {
36659
+ await this.bufferObservationChunk(record, threadId, unobservedMessages, bufferKey, releaseChunkWrite, writer, requestContext, observabilityContext);
36660
+ } finally {
36661
+ releaseChunkWrite();
36662
+ }
36663
+ }
36664
+ async bufferObservationChunk(record, threadId, unobservedMessages, bufferKey, releaseChunkWrite, writer, requestContext, observabilityContext) {
35996
36665
  const freshRecord = await this.storage.getObservationalMemory(record.threadId, record.resourceId);
35997
36666
  if (!freshRecord) return;
35998
36667
  let bufferCursor = BufferingCoordinator.lastBufferedAtTime.get(bufferKey) ?? freshRecord.lastBufferedAtTime ?? null;
@@ -36033,13 +36702,16 @@ ${formattedMessages}
36033
36702
  threadIds: [threadId],
36034
36703
  config: this.getObservationMarkerConfig()
36035
36704
  });
36036
- await this.persistMarkerToStorage(startMarker, threadId, freshRecord.resourceId ?? void 0);
36705
+ await this.persistMarkerToStorage(startMarker, threadId, freshRecord.resourceId ?? void 0, {
36706
+ notAfter: new Date(startedAt),
36707
+ preferMessages: messagesToBuffer
36708
+ });
36037
36709
  if (writer) writer.custom({
36038
36710
  ...startMarker,
36039
36711
  transient: true
36040
36712
  }).catch(() => {});
36041
36713
  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(",")}`);
36042
- if ((await this.runBufferedObservationCycle({
36714
+ await this.runBufferedObservationCycle({
36043
36715
  threadId,
36044
36716
  resourceId: freshRecord.resourceId ?? void 0,
36045
36717
  trigger: "async-buffer"
@@ -36053,12 +36725,13 @@ ${formattedMessages}
36053
36725
  writer,
36054
36726
  requestContext,
36055
36727
  observabilityContext,
36056
- trigger: "async-buffer"
36057
- }).run()))?.observed) {
36058
- const maxTs = this.getMaxMessageTimestamp(messagesToBuffer);
36059
- const cursor = new Date(maxTs.getTime() + 1);
36060
- BufferingCoordinator.lastBufferedAtTime.set(bufferKey, cursor);
36061
- }
36728
+ trigger: "async-buffer",
36729
+ onBufferedChunkPersisted: async () => {
36730
+ const maxTs = this.getMaxMessageTimestamp(messagesToBuffer);
36731
+ BufferingCoordinator.lastBufferedAtTime.set(bufferKey, new Date(maxTs.getTime() + 1));
36732
+ releaseChunkWrite();
36733
+ }
36734
+ }).run());
36062
36735
  }
36063
36736
  /**
36064
36737
  * Trigger async buffered observation if the token count has crossed a new interval.
@@ -36243,8 +36916,8 @@ ${formattedMessages}
36243
36916
  const { threadId, resourceId, recordId, activatedMessageIds } = opts;
36244
36917
  const lockKey = this.buffering.getLockKey(threadId, resourceId);
36245
36918
  const bufKey = this.buffering.getObservationBufferKey(lockKey);
36246
- BufferingCoordinator.lastBufferedBoundary.set(bufKey, 0);
36247
- await this.storage.setBufferingObservationFlag(recordId, false, 0).catch(() => {});
36919
+ BufferingCoordinator.setObservationBoundary(bufKey, 0);
36920
+ await runOMCommit(lockKey, () => this.storage.setBufferingObservationFlag(recordId, false, 0)).catch(() => {});
36248
36921
  if (activatedMessageIds && activatedMessageIds.length > 0) this.buffering.cleanupStaticMaps(threadId, resourceId, activatedMessageIds);
36249
36922
  }
36250
36923
  /**
@@ -36403,9 +37076,13 @@ ${formattedMessages}
36403
37076
  * @example
36404
37077
  * ```ts
36405
37078
  * const status = await om.getStatus({ threadId });
36406
- * if (status.shouldObserve) {
37079
+ * if (status.shouldObserve && status.canActivate) {
37080
+ * await om.activate({ threadId });
37081
+ * } else if (status.shouldObserve && !status.inAsyncObservationBand) {
37082
+ * // At/above blockAfter (or async buffering disabled): observe synchronously
36407
37083
  * await om.observe({ threadId });
36408
37084
  * } else if (status.shouldBuffer) {
37085
+ * // Below the threshold, or in the threshold→blockAfter band: buffer in the background
36409
37086
  * await om.buffer({ threadId });
36410
37087
  * }
36411
37088
  * if (status.shouldReflect) {
@@ -36435,8 +37112,10 @@ ${formattedMessages}
36435
37112
  const bufferedChunkCount = bufferedChunks.length;
36436
37113
  const bufferedChunkTokens = bufferedChunks.reduce((sum, chunk) => sum + (chunk.messageTokens ?? 0), 0);
36437
37114
  const asyncObservationEnabled = this.buffering.isAsyncObservationEnabled();
37115
+ const observationBlockAfter = asyncObservationEnabled ? this.getEffectiveObservationBlockAfter(record) : void 0;
37116
+ const inAsyncObservationBand = observationBlockAfter !== void 0 && pendingTokens >= threshold && pendingTokens < observationBlockAfter;
36438
37117
  let shouldBuffer = false;
36439
- if (asyncObservationEnabled && pendingTokens < threshold) {
37118
+ if (asyncObservationEnabled && (pendingTokens < threshold || inAsyncObservationBand)) {
36440
37119
  const lockKey = this.buffering.getLockKey(threadId, resourceId);
36441
37120
  shouldBuffer = this.buffering.shouldTriggerAsyncObservation(pendingTokens, lockKey, record, this.storage, threshold);
36442
37121
  }
@@ -36461,6 +37140,8 @@ ${formattedMessages}
36461
37140
  canActivate,
36462
37141
  asyncObservationEnabled,
36463
37142
  asyncReflectionEnabled: this.buffering.isAsyncReflectionEnabled(),
37143
+ observationBlockAfter,
37144
+ inAsyncObservationBand,
36464
37145
  scope: this.scope
36465
37146
  };
36466
37147
  }
@@ -36587,24 +37268,27 @@ ${formattedMessages}
36587
37268
  const currentTokens = opts.pendingTokens ?? record.pendingMessageTokens ?? 0;
36588
37269
  const lockKey = this.buffering.getLockKey(threadId, resourceId);
36589
37270
  const bufferKey = this.buffering.getObservationBufferKey(lockKey);
36590
- BufferingCoordinator.lastBufferedBoundary.set(bufferKey, currentTokens);
36591
- if (record.isBufferingObservation) await this.storage.setBufferingObservationFlag(record.id, false).catch(() => {});
37271
+ const boundaryOwner = Symbol("observation-boundary");
37272
+ BufferingCoordinator.setObservationBoundary(bufferKey, currentTokens, boundaryOwner);
36592
37273
  const existingOp = BufferingCoordinator.asyncBufferingOps.get(bufferKey);
37274
+ let resolveOp;
37275
+ const opPromise = new Promise((resolve) => {
37276
+ resolveOp = resolve;
37277
+ });
37278
+ BufferingCoordinator.asyncBufferingOps.set(bufferKey, opPromise);
37279
+ if (record.isBufferingObservation) await runOMCommit(lockKey, () => this.storage.setBufferingObservationFlag(record.id, false)).catch(() => {});
36593
37280
  if (existingOp) try {
36594
37281
  await existingOp;
36595
37282
  } catch {}
36596
- registerOp(record.id, "bufferingObservation");
37283
+ const opRecordId = record.id;
37284
+ registerOp(opRecordId, "bufferingObservation");
37285
+ const releaseChunkWrite = BufferingCoordinator.trackChunkWrite(bufferKey);
37286
+ const ownsStartBoundary = BufferingCoordinator.ownsObservationBoundary(bufferKey, boundaryOwner);
36597
37287
  inMemoryRecord.isBufferingObservation = true;
36598
- inMemoryRecord.lastBufferedAtTokens = currentTokens;
36599
- this.storage.setBufferingObservationFlag(record.id, true, currentTokens).catch((err) => {
37288
+ if (ownsStartBoundary) inMemoryRecord.lastBufferedAtTokens = currentTokens;
37289
+ runOMCommit(lockKey, () => this.storage.setBufferingObservationFlag(record.id, true, ownsStartBoundary ? currentTokens : void 0)).catch((err) => {
36600
37290
  omError("[OM] Failed to set buffering observation flag", err);
36601
37291
  });
36602
- let resolveOp;
36603
- const opPromise = new Promise((resolve) => {
36604
- resolveOp = resolve;
36605
- });
36606
- BufferingCoordinator.asyncBufferingOps.set(bufferKey, opPromise);
36607
- record = await this.storage.getObservationalMemory(record.threadId, record.resourceId) ?? record;
36608
37292
  const setBufferingState = (isBufferingObservation, lastBufferedAtTokens) => {
36609
37293
  inMemoryRecord.isBufferingObservation = isBufferingObservation;
36610
37294
  record.isBufferingObservation = isBufferingObservation;
@@ -36615,6 +37299,7 @@ ${formattedMessages}
36615
37299
  };
36616
37300
  let flagCleared = false;
36617
37301
  try {
37302
+ record = await this.storage.getObservationalMemory(record.threadId, record.resourceId) ?? record;
36618
37303
  let candidateMessages;
36619
37304
  if (opts.messages) candidateMessages = this.getUnobservedMessages(opts.messages, record, { excludeBuffered: true });
36620
37305
  else {
@@ -36654,12 +37339,29 @@ ${formattedMessages}
36654
37339
  threadIds: [threadId],
36655
37340
  config: this.getObservationMarkerConfig()
36656
37341
  });
36657
- await this.persistMarkerToStorage(startMarker, threadId, record.resourceId ?? void 0);
37342
+ await this.persistMarkerToStorage(startMarker, threadId, record.resourceId ?? void 0, {
37343
+ notAfter: new Date(startedAt),
37344
+ preferMessages: candidateMessages
37345
+ });
36658
37346
  const writer = opts.writer;
36659
37347
  if (writer) writer.custom({
36660
37348
  ...startMarker,
36661
37349
  transient: true
36662
37350
  }).catch(() => {});
37351
+ let chunkRecorded = false;
37352
+ const recordBufferedChunk = async () => {
37353
+ if (chunkRecorded) return;
37354
+ chunkRecorded = true;
37355
+ const ownsBoundary = BufferingCoordinator.ownsObservationBoundary(bufferKey, boundaryOwner);
37356
+ if (ownsBoundary) BufferingCoordinator.setObservationBoundary(bufferKey, newTokens, boundaryOwner);
37357
+ await runOMCommit(lockKey, () => this.storage.setBufferingObservationFlag(record.id, false, ownsBoundary ? newTokens : void 0)).catch(() => {});
37358
+ flagCleared = true;
37359
+ const stillOwnsBoundary = ownsBoundary && BufferingCoordinator.ownsObservationBoundary(bufferKey, boundaryOwner);
37360
+ setBufferingState(false, stillOwnsBoundary ? newTokens : void 0);
37361
+ const maxTimestamp = this.getMaxMessageTimestamp(candidateMessages);
37362
+ BufferingCoordinator.lastBufferedAtTime.set(bufferKey, new Date(maxTimestamp.getTime() + 1));
37363
+ releaseChunkWrite();
37364
+ };
36663
37365
  if (!(await this.runBufferedObservationCycle({
36664
37366
  threadId,
36665
37367
  resourceId: record.resourceId ?? resourceId,
@@ -36678,7 +37380,8 @@ ${formattedMessages}
36678
37380
  requestContext,
36679
37381
  currentModel: opts.currentModel,
36680
37382
  observabilityContext,
36681
- trigger: "async-buffer"
37383
+ trigger: "async-buffer",
37384
+ onBufferedChunkPersisted: recordBufferedChunk
36682
37385
  }).run()))?.observed) return {
36683
37386
  buffered: false,
36684
37387
  record
@@ -36698,13 +37401,7 @@ ${formattedMessages}
36698
37401
  newTokens
36699
37402
  }
36700
37403
  });
36701
- await this.storage.setBufferingObservationFlag(record.id, false, newTokens).catch(() => {});
36702
- flagCleared = true;
36703
- setBufferingState(false, newTokens);
36704
- BufferingCoordinator.lastBufferedBoundary.set(bufferKey, newTokens);
36705
- const maxTimestamp = this.getMaxMessageTimestamp(candidateMessages);
36706
- const cursor = new Date(maxTimestamp.getTime() + 1);
36707
- BufferingCoordinator.lastBufferedAtTime.set(bufferKey, cursor);
37404
+ await recordBufferedChunk();
36708
37405
  return {
36709
37406
  buffered: true,
36710
37407
  record: await this.storage.getObservationalMemory(record.threadId, record.resourceId) ?? record
@@ -36716,12 +37413,13 @@ ${formattedMessages}
36716
37413
  record
36717
37414
  };
36718
37415
  } finally {
36719
- unregisterOp(record.id, "bufferingObservation");
36720
- BufferingCoordinator.asyncBufferingOps.delete(bufferKey);
37416
+ releaseChunkWrite();
37417
+ unregisterOp(opRecordId, "bufferingObservation");
37418
+ if (BufferingCoordinator.asyncBufferingOps.get(bufferKey) === opPromise) BufferingCoordinator.asyncBufferingOps.delete(bufferKey);
36721
37419
  resolveOp();
36722
37420
  if (!flagCleared) {
36723
37421
  setBufferingState(false);
36724
- await this.storage.setBufferingObservationFlag(record.id, false).catch(() => {});
37422
+ await runOMCommit(lockKey, () => this.storage.setBufferingObservationFlag(record.id, false)).catch(() => {});
36725
37423
  }
36726
37424
  }
36727
37425
  }
@@ -36758,8 +37456,8 @@ ${formattedMessages}
36758
37456
  const currentContextTokens = this.tokenCounter.countMessages(unobserved);
36759
37457
  if (currentContextTokens < dbBoundary * .5) {
36760
37458
  omDebug(`[OM:activate] resetting stale lastBufferedBoundary: dbBoundary=${dbBoundary}, currentContextTokens=${currentContextTokens}`);
36761
- BufferingCoordinator.lastBufferedBoundary.set(bufKey, 0);
36762
- await this.storage.setBufferingObservationFlag(record.id, false, 0).catch(() => {});
37459
+ BufferingCoordinator.setObservationBoundary(bufKey, 0);
37460
+ await runOMCommit(lockKey, () => this.storage.setBufferingObservationFlag(record.id, false, 0)).catch(() => {});
36763
37461
  }
36764
37462
  }
36765
37463
  }
@@ -36805,39 +37503,63 @@ ${formattedMessages}
36805
37503
  livePendingTokens ??= status.pendingTokens;
36806
37504
  }
36807
37505
  }
36808
- if (record.isBufferingObservation) {
36809
- const lockKey = this.buffering.getLockKey(threadId, resourceId);
36810
- const bufferKey = this.buffering.getObservationBufferKey(lockKey);
36811
- const asyncOp = BufferingCoordinator.asyncBufferingOps.get(bufferKey);
36812
- if (asyncOp) try {
36813
- await Promise.race([asyncOp, new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error("Timeout")), 6e4))]);
36814
- } catch {}
37506
+ const obsBufferKey = this.buffering.getObservationBufferKey(this.buffering.getLockKey(threadId, resourceId));
37507
+ const pendingChunkWrite = BufferingCoordinator.pendingChunkWrites.get(obsBufferKey);
37508
+ if (pendingChunkWrite) {
37509
+ const blockAfter = this.buffering.isAsyncObservationEnabled() ? this.getEffectiveObservationBlockAfter(record) : void 0;
37510
+ const threshold = calculateDynamicThreshold(this.getEffectiveMessageTokens(record), record.observationTokenCount ?? 0);
37511
+ if (activationTriggeredBy === "threshold" && !(livePendingTokens !== void 0 && blockAfter !== void 0 && livePendingTokens >= threshold && livePendingTokens < blockAfter)) {
37512
+ let timeoutId;
37513
+ await Promise.race([pendingChunkWrite, new Promise((resolve) => timeoutId = setTimeout(resolve, 6e4))]);
37514
+ clearTimeout(timeoutId);
37515
+ }
37516
+ }
37517
+ const commitKey = this.buffering.getLockKey(threadId, resourceId);
37518
+ let freshRecord = null;
37519
+ let freshChunks = [];
37520
+ let activationResult;
37521
+ for (let attempt = 0; attempt <= MAX_ACTIVATION_RETIRED_RETRIES; attempt++) {
37522
+ const swapped = await runOMCommit(commitKey, async () => {
37523
+ const head = await this.storage.getObservationalMemory(record.threadId, record.resourceId);
37524
+ if (!head) return null;
37525
+ const chunks = getBufferedChunks(head);
37526
+ if (!chunks.length) return null;
37527
+ const messageTokensThreshold = getMaxThreshold(this.getEffectiveMessageTokens(head));
37528
+ const activationRatio = resolveActivationRatio(this.observationConfig.bufferActivation ?? .7, messageTokensThreshold);
37529
+ const totalChunkMessageTokens = chunks.reduce((sum, c) => sum + (c.messageTokens ?? 0), 0);
37530
+ const currentPendingTokens = livePendingTokens ?? (head.pendingMessageTokens || totalChunkMessageTokens);
37531
+ const blockAfter = this.getEffectiveObservationBlockAfter(head);
37532
+ const forceMaxActivation = !!(blockAfter && currentPendingTokens >= blockAfter);
37533
+ if (head.pendingMessageTokens !== currentPendingTokens) await this.storage.setPendingMessageTokens(head.id, currentPendingTokens);
37534
+ return {
37535
+ head,
37536
+ chunks,
37537
+ result: await this.storage.swapBufferedToActive({
37538
+ id: head.id,
37539
+ activationRatio,
37540
+ messageTokensThreshold,
37541
+ currentPendingTokens,
37542
+ forceMaxActivation,
37543
+ bufferedChunks: chunks
37544
+ })
37545
+ };
37546
+ });
37547
+ if (!swapped) return {
37548
+ activated: false,
37549
+ record
37550
+ };
37551
+ freshRecord = swapped.head;
37552
+ freshChunks = swapped.chunks;
37553
+ activationResult = swapped.result;
37554
+ if (!activationResult.retired) break;
37555
+ omDebug(`[OM:activate] record ${freshRecord.id} was retired by a reflection; retrying on the head`);
36815
37556
  }
36816
- const freshRecord = await this.storage.getObservationalMemory(record.threadId, record.resourceId);
36817
- if (!freshRecord) return {
37557
+ if (!freshRecord || !activationResult || activationResult.retired) return {
36818
37558
  activated: false,
36819
- record
37559
+ record: await this.getRecord(threadId, resourceId) ?? record
36820
37560
  };
36821
- const freshChunks = getBufferedChunks(freshRecord);
36822
- if (!freshChunks.length) return {
36823
- activated: false,
36824
- record
36825
- };
36826
- const messageTokensThreshold = getMaxThreshold(this.getEffectiveMessageTokens(freshRecord));
36827
- const activationRatio = resolveActivationRatio(this.observationConfig.bufferActivation ?? .7, messageTokensThreshold);
36828
- const totalChunkMessageTokens = freshChunks.reduce((sum, c) => sum + (c.messageTokens ?? 0), 0);
36829
- const currentPendingTokens = livePendingTokens ?? (freshRecord.pendingMessageTokens || totalChunkMessageTokens);
36830
- const forceMaxActivation = !!(this.observationConfig.blockAfter && currentPendingTokens >= this.observationConfig.blockAfter);
36831
- if (freshRecord.pendingMessageTokens !== currentPendingTokens) await this.storage.setPendingMessageTokens(freshRecord.id, currentPendingTokens);
36832
- const activationResult = await this.storage.swapBufferedToActive({
36833
- id: freshRecord.id,
36834
- activationRatio,
36835
- messageTokensThreshold,
36836
- currentPendingTokens,
36837
- forceMaxActivation,
36838
- bufferedChunks: freshChunks
36839
- });
36840
- await this.storage.setBufferingObservationFlag(freshRecord.id, false).catch(() => {});
37561
+ const swappedRecordId = freshRecord.id;
37562
+ await runOMCommit(commitKey, () => this.storage.setBufferingObservationFlag(swappedRecordId, false)).catch(() => {});
36841
37563
  unregisterOp(freshRecord.id, "bufferingObservation");
36842
37564
  const postSwapRecord = await this.storage.getObservationalMemory(record.threadId, record.resourceId);
36843
37565
  if (opts.writer && postSwapRecord && activationResult.activatedCycleIds.length > 0) {
@@ -37082,7 +37804,7 @@ ${formattedMessages}
37082
37804
  * ```
37083
37805
  */
37084
37806
  async reflect(threadId, resourceId, prompt, requestContext, observabilityContext) {
37085
- const record = await this.getOrCreateRecord(threadId, resourceId);
37807
+ const record = { ...await this.getOrCreateRecord(threadId, resourceId) };
37086
37808
  if (!record.activeObservations) return {
37087
37809
  reflected: false,
37088
37810
  record,
@@ -37127,11 +37849,23 @@ ${formattedMessages}
37127
37849
  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);
37128
37850
  reflectResult.observations = await applyTextTransform(this.hooks, "afterReflection", reflectResult.observations, hookContext);
37129
37851
  const reflectionTokenCount = this.tokenCounter.countObservations(reflectResult.observations);
37130
- await this.storage.createReflectionGeneration({
37852
+ const newRecordId = crypto.randomUUID();
37853
+ const committedRecord = await runOMCommit(this.buffering.getLockKey(threadId, resourceId), () => this.storage.createReflectionGeneration({
37131
37854
  currentRecord: record,
37132
37855
  reflection: reflectResult.observations,
37133
- tokenCount: reflectionTokenCount
37134
- });
37856
+ tokenCount: reflectionTokenCount,
37857
+ newRecordId
37858
+ }), { priority: "reflection" });
37859
+ reflectionUsage = reflectResult.usage;
37860
+ reflectionProviderMetadata = reflectResult.providerMetadata;
37861
+ if (!isReflectionApplied(committedRecord, newRecordId, record.id)) {
37862
+ omDebug(`[OM:reflect] manual reflection of ${record.id} not applied (head is ${committedRecord.id})`);
37863
+ return {
37864
+ reflected: false,
37865
+ record: await this.getOrCreateRecord(threadId, resourceId),
37866
+ usage: reflectResult.usage
37867
+ };
37868
+ }
37135
37869
  if (thread && reflectResult.extractedValues) {
37136
37870
  const metadataUpdate = buildThreadMetadataFromExtractedValues(this.reflectionConfig.extractors, reflectResult.extractedValues);
37137
37871
  const newMetadata = (0, _mastra_core_memory.setThreadOMMetadata)(thread.metadata, {
@@ -37220,11 +37954,13 @@ ${formattedMessages}
37220
37954
  */
37221
37955
  async updateRecordConfig(threadId, resourceId, config) {
37222
37956
  const ids = this.getStorageIds(threadId, resourceId);
37223
- const record = await this.storage.getObservationalMemory(ids.threadId, ids.resourceId);
37224
- if (!record) throw new Error(`No observational memory record found for thread ${ids.threadId}`);
37225
- await this.storage.updateObservationalMemoryConfig({
37226
- id: record.id,
37227
- config: { _overrides: config }
37957
+ await runOMCommit(this.buffering.getLockKey(threadId, resourceId), async () => {
37958
+ const record = await this.storage.getObservationalMemory(ids.threadId, ids.resourceId);
37959
+ if (!record) throw new Error(`No observational memory record found for thread ${ids.threadId}`);
37960
+ await this.storage.updateObservationalMemoryConfig({
37961
+ id: record.id,
37962
+ config: { _overrides: config }
37963
+ });
37228
37964
  });
37229
37965
  }
37230
37966
  /**
@@ -37614,7 +38350,7 @@ var ObservationalMemoryProcessor = class {
37614
38350
  });
37615
38351
  const finalTotalPending = ctx.status.pendingTokens;
37616
38352
  try {
37617
- await this.engine.getStorage().setPendingMessageTokens(turnRecord.id, finalTotalPending);
38353
+ await runOMCommit(this.engine.buffering.getLockKey(threadId, resourceId), () => this.engine.getStorage().setPendingMessageTokens(turnRecord.id, finalTotalPending));
37618
38354
  this.turn.patchRecord({ pendingMessageTokens: finalTotalPending });
37619
38355
  } catch {}
37620
38356
  if (reproCaptureEnabled) writeProcessInputStepReproCapture({
@@ -39928,6 +40664,7 @@ Notes:
39928
40664
  cloned.id = crypto.randomUUID();
39929
40665
  cloned.createdAt = now;
39930
40666
  cloned.updatedAt = now;
40667
+ cloned.supersededBy = null;
39931
40668
  await memoryStore.insertObservationalMemoryRecord(cloned);
39932
40669
  }
39933
40670
  /**
@@ -51002,4 +51739,4 @@ const agentBuilderWorkflows = {
51002
51739
  //#endregion
51003
51740
  exports.agentBuilderWorkflows = agentBuilderWorkflows;
51004
51741
 
51005
- //# sourceMappingURL=dist-C8tgsOw_.cjs.map
51742
+ //# sourceMappingURL=dist-BOW7V2e6.cjs.map