@mastra/server 1.76.0 → 1.77.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dist/{datasets-C2aXB8At.cjs → datasets-BdD8JWhx.cjs} +64 -7
  2. package/dist/datasets-BdD8JWhx.cjs.map +1 -0
  3. package/dist/{datasets-CDQDQxos.js → datasets-CKwh-k4I.js} +53 -8
  4. package/dist/datasets-CKwh-k4I.js.map +1 -0
  5. package/dist/{dist-C68HDUAO.js → dist-B6B4Cxda.js} +1060 -304
  6. package/dist/dist-B6B4Cxda.js.map +1 -0
  7. package/dist/{dist-C8tgsOw_.cjs → dist-BGfXR4Xj.cjs} +1059 -303
  8. package/dist/dist-BGfXR4Xj.cjs.map +1 -0
  9. package/dist/docs/SKILL.md +1 -1
  10. package/dist/docs/assets/SOURCE_MAP.json +1 -1
  11. package/dist/docs/references/docs-server-custom-adapters.md +1 -1
  12. package/dist/docs/references/reference-server-create-route.md +1 -1
  13. package/dist/docs/references/reference-server-routes.md +1 -1
  14. package/dist/server/handlers/agent-builder.cjs +1 -1
  15. package/dist/server/handlers/agent-builder.js +1 -1
  16. package/dist/server/handlers/datasets.cjs +27 -15
  17. package/dist/server/handlers/datasets.cjs.map +1 -1
  18. package/dist/server/handlers/datasets.d.ts +1032 -38
  19. package/dist/server/handlers/datasets.d.ts.map +1 -1
  20. package/dist/server/handlers/datasets.js +27 -15
  21. package/dist/server/handlers/datasets.js.map +1 -1
  22. package/dist/server/schemas/datasets.d.ts +430 -11
  23. package/dist/server/schemas/datasets.d.ts.map +1 -1
  24. package/dist/server/schemas/index.cjs +3 -1
  25. package/dist/server/schemas/index.js +2 -2
  26. package/dist/server/server-adapter/routes/datasets.d.ts +1032 -38
  27. package/dist/server/server-adapter/routes/datasets.d.ts.map +1 -1
  28. package/package.json +6 -6
  29. package/dist/datasets-C2aXB8At.cjs.map +0 -1
  30. package/dist/datasets-CDQDQxos.js.map +0 -1
  31. package/dist/dist-C68HDUAO.js.map +0 -1
  32. package/dist/dist-C8tgsOw_.cjs.map +0 -1
@@ -27,7 +27,7 @@ import { tmpdir } from "os";
27
27
  import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "path";
28
28
  import { createHash } from "crypto";
29
29
  import { channel, tracingChannel } from "diagnostics_channel";
30
- import { AsyncLocalStorage } from "async_hooks";
30
+ import { AsyncLocalStorage, AsyncResource } from "async_hooks";
31
31
  import { exec, execFile, spawn } from "child_process";
32
32
  import { copyFile, mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from "fs/promises";
33
33
  //#region ../../../../../setup-pnpm/node_modules/.bin/store/v11/links/@/async-mutex/0.5.0/4b32f2ae13cd0d7acb694455f7c2810d59202098580506b1730c21c7ab02798f/node_modules/async-mutex/index.mjs
@@ -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-DjDnBIS0.js
4641
4641
  var __defProp$3 = Object.defineProperty;
4642
4642
  var __exportAll = (all, no_symbols) => {
4643
4643
  let target = {};
@@ -23752,8 +23752,12 @@ function formatOmError(error) {
23752
23752
  function isAbortError$5(error, abortSignal) {
23753
23753
  return abortSignal?.aborted === true || hasAbortInChain(error);
23754
23754
  }
23755
+ function hasEmptyObjectValue(object, extractor) {
23756
+ const value = object[extractor.slug];
23757
+ return typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length === 0;
23758
+ }
23755
23759
  function shouldRetryEmptyStructuredObject(object, extractors) {
23756
- return Object.keys(object).length === 0 && extractors.some((extractor) => extractor.retryStructuredExtractionOnEmptyObject);
23760
+ return extractors.some((extractor) => extractor.retryStructuredExtractionOnEmptyObject && (Object.keys(object).length === 0 || hasEmptyObjectValue(object, extractor)));
23757
23761
  }
23758
23762
  async function extractStructuredValues(opts) {
23759
23763
  const structuredExtractors = (opts.extractors ?? []).filter((extractor) => extractor.mode === "structured");
@@ -23827,17 +23831,28 @@ ${extractorInstructions}${priorLines.length > 0 ? `\n\n## Prior Extracted Values
23827
23831
  }
23828
23832
  }
23829
23833
  if (retryEmptyObject) try {
23830
- object = await streamWithStructuredOutput(coreFeatures.has("json-prompt-injection:inline") ? "inline" : true);
23834
+ const fallbackJsonPromptInjection = coreFeatures.has("json-prompt-injection:inline") ? "inline" : true;
23835
+ object = {
23836
+ ...object,
23837
+ ...await streamWithStructuredOutput(fallbackJsonPromptInjection)
23838
+ };
23831
23839
  } catch (fallbackError) {
23832
23840
  if (isAbortError$5(fallbackError, opts.abortSignal)) throw fallbackError;
23833
23841
  const message = fallbackError instanceof Error ? fallbackError.message : String(fallbackError);
23834
- return {
23842
+ if (Object.keys(object).length === 0) return {
23835
23843
  values,
23836
23844
  failures: structuredExtractors.map((extractor) => ({
23837
23845
  slug: extractor.slug,
23838
23846
  error: message
23839
23847
  }))
23840
23848
  };
23849
+ for (const extractor of structuredExtractors) if (extractor.retryStructuredExtractionOnEmptyObject && hasEmptyObjectValue(object, extractor)) {
23850
+ failures.push({
23851
+ slug: extractor.slug,
23852
+ error: message
23853
+ });
23854
+ delete object[extractor.slug];
23855
+ }
23841
23856
  }
23842
23857
  for (const extractor of structuredExtractors) {
23843
23858
  const value = object[extractor.slug];
@@ -25063,7 +25078,7 @@ Group related observations (like tool sequences) by indenting:
25063
25078
  * -> applied fix, tests now pass
25064
25079
  * ✅ Tests passing, auth issue resolved
25065
25080
 
25066
- Group observations by date, then list each with 24-hour time.
25081
+ Group observations by date. Within a date, list observations in the order the events happened (not by importance), each with 24-hour time.
25067
25082
 
25068
25083
  <observations>
25069
25084
  Date: Dec 4, 2025
@@ -25436,8 +25451,8 @@ function extractToolResultAttachments(result, counter, attachmentFilter) {
25436
25451
  attachments
25437
25452
  };
25438
25453
  }
25439
- function formatObserverPartLine(title, body, time, previousTime) {
25440
- const timeLabel = time && time !== previousTime ? `(${time})` : "";
25454
+ function formatObserverPartLine(title, body, time, previousTime, alwaysShowTime) {
25455
+ const timeLabel = time && (alwaysShowTime || time !== previousTime) ? `(${time})` : "";
25441
25456
  if (!title) return timeLabel ? `${timeLabel}: ${body}` : body;
25442
25457
  return `${title}${timeLabel ? ` ${timeLabel}` : ""}: ${body}`;
25443
25458
  }
@@ -25462,7 +25477,7 @@ function formatObserverLines(lines, context = {}) {
25462
25477
  previousDate = line.date;
25463
25478
  previousTime = void 0;
25464
25479
  }
25465
- output.push(formatObserverPartLine(line.title, line.body, line.time, previousTime));
25480
+ output.push(formatObserverPartLine(line.title, line.body, line.time, previousTime, line.alwaysShowTime));
25466
25481
  previousTime = line.time || previousTime;
25467
25482
  }
25468
25483
  return {
@@ -25563,14 +25578,15 @@ function formatObserverMessage(msg, counter, toolFormatting, options) {
25563
25578
  const unavailableUrls = getUnavailableAttachmentUrls(msg);
25564
25579
  let lines = [];
25565
25580
  const temporalGapText = isTemporalGapMarker(msg) ? getTemporalGapMarkerText(msg) : void 0;
25566
- const pushLine = (title, body, createdAt) => {
25581
+ const pushLine = (title, body, createdAt, opts) => {
25567
25582
  if (!body) return;
25568
25583
  const normalizedCreatedAt = normalizeObserverCreatedAt(createdAt) ?? messageCreatedAt;
25569
25584
  lines.push({
25570
25585
  date: formatObserverDate(normalizedCreatedAt, timeZone),
25571
25586
  time: formatObserverTime(normalizedCreatedAt, timeZone),
25572
25587
  title,
25573
- body
25588
+ body,
25589
+ alwaysShowTime: opts?.tool === true
25574
25590
  });
25575
25591
  };
25576
25592
  if (temporalGapText) pushLine("", temporalGapText, messageCreatedAt);
@@ -25587,7 +25603,7 @@ function formatObserverMessage(msg, counter, toolFormatting, options) {
25587
25603
  const exchange = toolFormatting.exchanges.get(inv.toolCallId);
25588
25604
  if (exchange && occurrence === exchange.firstOccurrence && exchange.signature && !toolFormatting.emittedCalls.has(inv.toolCallId)) {
25589
25605
  toolFormatting.emittedCalls.add(inv.toolCallId);
25590
- pushLine(`Tool Call ${exchange.signature.toolName}`, formatObserverToolArguments(exchange.signature.args, maxLen), partCreatedAt);
25606
+ pushLine(`Tool Call ${exchange.signature.toolName}`, formatObserverToolArguments(exchange.signature.args, maxLen), partCreatedAt, { tool: true });
25591
25607
  }
25592
25608
  if (inv.state === "result") {
25593
25609
  const { value: resultForObserver } = resolveToolResultValue(part, inv.result);
@@ -25597,17 +25613,17 @@ function formatObserverMessage(msg, counter, toolFormatting, options) {
25597
25613
  toolFormatting.emittedOutcomes.add(inv.toolCallId);
25598
25614
  const hasResult = resultWithoutAttachments !== void 0 && (typeof resultWithoutAttachments !== "string" || resultWithoutAttachments.trim().length > 0);
25599
25615
  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);
25616
+ pushLine(`${inv.isError ? "Tool Error" : "Tool Result"} ${inv.toolName}`, maybeTruncate(body, maxLen), partCreatedAt, { tool: true });
25601
25617
  }
25602
25618
  return;
25603
25619
  }
25604
25620
  if (exchange?.terminal?.occurrence === occurrence && !toolFormatting.emittedOutcomes.has(inv.toolCallId)) {
25605
25621
  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);
25622
+ if (inv.state === "output-error") pushLine(`Tool Error ${inv.toolName}`, maybeTruncate(inv.errorText?.trim() ? inv.errorText : "Tool execution failed", maxLen), partCreatedAt, { tool: true });
25623
+ 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
25624
  return;
25609
25625
  }
25610
- if (!exchange) pushLine(`Tool Call ${inv.toolName}`, formatObserverToolArguments(inv.args, maxLen), partCreatedAt);
25626
+ if (!exchange) pushLine(`Tool Call ${inv.toolName}`, formatObserverToolArguments(inv.args, maxLen), partCreatedAt, { tool: true });
25611
25627
  return;
25612
25628
  }
25613
25629
  const partType = part.type;
@@ -25762,13 +25778,17 @@ function surroundObserverHistory(history, contextPrompt, taskInstructions) {
25762
25778
  function buildMultiThreadObserverTaskInstructions(includeThreadTitle) {
25763
25779
  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
25780
  }
25781
+ function buildTruncationNotice(wasTruncated) {
25782
+ if (!wasTruncated) return "";
25783
+ return "Previous observations were truncated for context budget reasons.\nThe main agent still has full memory context outside this observer window.\n\n";
25784
+ }
25765
25785
  function buildMultiThreadObserverContextPrompt(existingObservations, threadOrder, priorMetadataByThread, wasTruncated, includeThreadTitle, extractors = []) {
25766
25786
  let prompt = "";
25767
25787
  if (existingObservations) {
25768
25788
  prompt += `## Previous Observations\n\n${existingObservations}\n\n---\n\n`;
25769
25789
  prompt += "Do not repeat these existing observations. Your new observations will be appended to the existing observations.\n\n";
25770
25790
  }
25771
- const hasTruncatedObservations = wasTruncated ?? false;
25791
+ prompt += buildTruncationNotice(wasTruncated);
25772
25792
  const threadMetadataLines = threadOrder?.map((threadId) => {
25773
25793
  const metadata = priorMetadataByThread?.get(threadId);
25774
25794
  const extractorPriorLines = buildExtractorPriorLines(extractors, metadata?.extracted);
@@ -25782,10 +25802,6 @@ function buildMultiThreadObserverContextPrompt(existingObservations, threadOrder
25782
25802
  }).filter(Boolean).join("\n");
25783
25803
  if (threadMetadataLines) {
25784
25804
  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
25805
  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
25806
  }
25791
25807
  return prompt;
@@ -25828,7 +25844,7 @@ function parseMultiThreadObserverOutput(output, extractors = []) {
25828
25844
  threadTitle = threadTitleMatch[1].trim();
25829
25845
  observations = observations.replace(/<thread-title>[\s\S]*?<\/thread-title>/i, "");
25830
25846
  }
25831
- observations = sanitizeObservationLines(observations.trim());
25847
+ observations = sortObservationsByTime(sanitizeObservationLines(observations.trim()));
25832
25848
  threads.set(threadId, {
25833
25849
  observations,
25834
25850
  currentTask: currentTask || getStringExtractedValue$1(parsedExtractedValues.values, "current-task"),
@@ -25868,7 +25884,7 @@ function buildObserverContextPrompt(existingObservations, options) {
25868
25884
  prompt += `## Previous Observations\n\n${existingObservations}\n\n---\n\n`;
25869
25885
  prompt += "Do not repeat these existing observations. Your new observations will be appended to the existing observations.\n\n";
25870
25886
  }
25871
- const hasTruncatedObservations = options?.wasTruncated ?? false;
25887
+ prompt += buildTruncationNotice(options?.wasTruncated);
25872
25888
  const priorMetadataLines = [];
25873
25889
  if (options?.priorCurrentTask) priorMetadataLines.push(`- prior current-task: ${options.priorCurrentTask}`);
25874
25890
  if (options?.priorSuggestedResponse) priorMetadataLines.push(`- prior suggested-response: ${options.priorSuggestedResponse}`);
@@ -25876,10 +25892,6 @@ function buildObserverContextPrompt(existingObservations, options) {
25876
25892
  priorMetadataLines.push(...buildExtractorPriorLines(options?.extractors ?? [], options?.priorExtractedValues));
25877
25893
  if (priorMetadataLines.length > 0) {
25878
25894
  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
25895
  const titleHint = options?.includeThreadTitle ? ", and thread-title" : "";
25884
25896
  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
25897
  }
@@ -25910,7 +25922,7 @@ function parseObserverOutput(output, extractors = []) {
25910
25922
  const parsedExtractedValues = parseExtractedValues(output, inlineExtractors);
25911
25923
  const parsed = parseMemorySectionXml(stripExtractorSections(output, inlineExtractors));
25912
25924
  return {
25913
- observations: sanitizeObservationLines(parsed.observations || ""),
25925
+ observations: sortObservationsByTime(sanitizeObservationLines(parsed.observations || "")),
25914
25926
  currentTask: parsed.currentTask || getStringExtractedValue$1(parsedExtractedValues.values, "current-task"),
25915
25927
  suggestedContinuation: parsed.suggestedResponse || getStringExtractedValue$1(parsedExtractedValues.values, "suggested-response"),
25916
25928
  threadTitle: parsed.threadTitle || getStringExtractedValue$1(parsedExtractedValues.values, "thread-title"),
@@ -26004,6 +26016,68 @@ function sanitizeObservationLines(observations) {
26004
26016
  }
26005
26017
  return changed ? lines.join("\n") : observations;
26006
26018
  }
26019
+ const OBSERVATION_TIME = /^[*-]\s+(?:\S+\s+)?\((\d{1,2}):(\d{2})\s*([AaPp])?\.?(?:[Mm]\.?)?\)/;
26020
+ /** Minutes since midnight for each item, or undefined when the group's times can't be ordered safely. */
26021
+ function observationMinutes(items) {
26022
+ const times = items.map((item) => OBSERVATION_TIME.exec(item[0]));
26023
+ if (times.some((t) => !t)) return void 0;
26024
+ const matches = times;
26025
+ const marked = matches.filter((m) => m[3]).length;
26026
+ if (marked > 0 && marked < matches.length) return void 0;
26027
+ if (marked === matches.length) return matches.map((m) => Number(m[1]) % 12 * 60 + (/p/i.test(m[3]) ? 720 : 0) + Number(m[2]));
26028
+ const hours = matches.map((m) => m[1]);
26029
+ const twelveHour = hours.some((h) => /^[1-9]$/.test(h));
26030
+ const twentyFourHour = hours.some((h) => /^(0\d|1[3-9]|2\d)$/.test(h));
26031
+ if (twelveHour && (twentyFourHour || new Set(hours).size > 1)) return void 0;
26032
+ return matches.map((m) => Number(m[1]) * 60 + Number(m[2]));
26033
+ }
26034
+ /**
26035
+ * Put the top-level observations of each "Date:" group in time order, keeping indented
26036
+ * sub-items with their parent. The Observer writes a time on every observation but tends
26037
+ * to list a user's message ahead of the earlier events it follows, which can make a
26038
+ * superseded state read as the latest one. Groups are left as written when any
26039
+ * observation has no time or the times are ambiguous.
26040
+ */
26041
+ function sortObservationsByTime(observations) {
26042
+ if (!observations) return observations;
26043
+ const output = [];
26044
+ let group;
26045
+ const flush = () => {
26046
+ if (!group) return;
26047
+ const minutes = group.sortable && group.items.length > 1 ? observationMinutes(group.items) : void 0;
26048
+ const items = minutes ? group.items.map((item, i) => ({
26049
+ item,
26050
+ minute: minutes[i]
26051
+ })).sort((a, b) => a.minute - b.minute).map((x) => x.item) : group.items;
26052
+ output.push(...group.header, ...items.flat(), ...group.trailing);
26053
+ group = void 0;
26054
+ };
26055
+ for (const line of observations.split("\n")) if (/^\s*Date:\s/.test(line)) {
26056
+ flush();
26057
+ group = {
26058
+ header: [line],
26059
+ items: [],
26060
+ trailing: [],
26061
+ sortable: true
26062
+ };
26063
+ } else if (!group) output.push(line);
26064
+ else if (/^[*-]\s/.test(line)) {
26065
+ group.items.at(-1)?.push(...group.trailing);
26066
+ group.trailing = [];
26067
+ group.items.push([line]);
26068
+ } else if (group.items.length === 0) group.header.push(line);
26069
+ else if (line.trim() === "") group.trailing.push(line);
26070
+ else if (/^\s/.test(line)) {
26071
+ group.items.at(-1).push(...group.trailing, line);
26072
+ group.trailing = [];
26073
+ } else {
26074
+ group.sortable = false;
26075
+ group.items.at(-1).push(...group.trailing, line);
26076
+ group.trailing = [];
26077
+ }
26078
+ flush();
26079
+ return output.join("\n");
26080
+ }
26007
26081
  function analyzeDegenerateRepetition(text) {
26008
26082
  const lines = collapseBoundedLineRuns(text.split("\n"));
26009
26083
  const windowText = lines.filter((line) => line.trim().length >= MIN_DUPLICATE_LINE_CHARS && line.length <= MAX_OBSERVATION_LINE_CHARS).join("\n");
@@ -26538,7 +26612,7 @@ var ObserverRunner = class {
26538
26612
  /**
26539
26613
  * Call the Observer agent for multiple threads in a single batched request.
26540
26614
  */
26541
- async callMultiThread(existingObservations, allMessagesByThread, allThreadOrder, abortSignal, requestContext, priorMetadataByThread, observabilityContext, model, hookContext, timeZone, mainAgent, currentModel) {
26615
+ async callMultiThread(existingObservations, allMessagesByThread, allThreadOrder, abortSignal, requestContext, priorMetadataByThread, observabilityContext, model, hookContext, timeZone, mainAgent, currentModel, wasTruncated) {
26542
26616
  const contextFor = (threadId) => ({
26543
26617
  threadId,
26544
26618
  ...hookContext
@@ -26549,7 +26623,7 @@ var ObserverRunner = class {
26549
26623
  if (filtered.length > 0) messagesByThread.set(threadId, filtered);
26550
26624
  }
26551
26625
  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);
26626
+ const output = await this.callMultiThreadObserver(existingObservations, messagesByThread, threadOrder, abortSignal, requestContext, priorMetadataByThread, observabilityContext, model, timeZone, mainAgent, currentModel, wasTruncated);
26553
26627
  for (const threadId of allThreadOrder) {
26554
26628
  const threadResult = output.results.get(threadId);
26555
26629
  if (!threadResult) {
@@ -26560,7 +26634,7 @@ var ObserverRunner = class {
26560
26634
  }
26561
26635
  return output;
26562
26636
  }
26563
- async callMultiThreadObserver(existingObservations, messagesByThread, threadOrder, abortSignal, requestContext, priorMetadataByThread, observabilityContext, model, timeZone, mainAgent, currentModel) {
26637
+ async callMultiThreadObserver(existingObservations, messagesByThread, threadOrder, abortSignal, requestContext, priorMetadataByThread, observabilityContext, model, timeZone, mainAgent, currentModel, wasTruncated) {
26564
26638
  if (threadOrder.length === 0) return { results: /* @__PURE__ */ new Map() };
26565
26639
  const inputTokens = Array.from(messagesByThread.values()).reduce((total, messages) => total + this.tokenCounter.countMessages(messages), 0);
26566
26640
  let resolvedModel;
@@ -26606,7 +26680,8 @@ var ObserverRunner = class {
26606
26680
  priorThreadTitle: priorMetadataByThread?.get(threadId)?.threadTitle,
26607
26681
  priorExtractedValues: priorMetadataByThread?.get(threadId)?.extracted,
26608
26682
  model: resolvedModel.model,
26609
- timeZone
26683
+ timeZone,
26684
+ wasTruncated
26610
26685
  });
26611
26686
  results.set(threadId, {
26612
26687
  observations: threadResult.observations,
@@ -26632,7 +26707,7 @@ var ObserverRunner = class {
26632
26707
  const agent = this.createAgent(resolvedModel.model, true, void 0, activeExtractors);
26633
26708
  const internalRequestContext = withOmInternalThreadId(requestContext, agent.id);
26634
26709
  const multiThreadAttachmentFilter = this.resolveAttachmentFilter(resolvedModel.model, requestContext);
26635
- const observerMessages = [buildMultiThreadObserverRequestMessage(existingObservations, messagesByThread, threadOrder, priorMetadataByThread, void 0, this.observationConfig.threadTitle, activeExtractors, {
26710
+ const observerMessages = [buildMultiThreadObserverRequestMessage(existingObservations, messagesByThread, threadOrder, priorMetadataByThread, wasTruncated, this.observationConfig.threadTitle, activeExtractors, {
26636
26711
  attachmentFilter: multiThreadAttachmentFilter,
26637
26712
  timeZone
26638
26713
  })];
@@ -26646,6 +26721,7 @@ var ObserverRunner = class {
26646
26721
  metadata: {
26647
26722
  omThreadCount: threadOrder.length,
26648
26723
  omPreviousObserverTokens: this.observationConfig.previousObserverTokens,
26724
+ omWasTruncated: wasTruncated ?? false,
26649
26725
  omThreadTitleEnabled: this.observationConfig.threadTitle,
26650
26726
  ...resolvedModel.selectedThreshold !== void 0 ? { omSelectedThreshold: resolvedModel.selectedThreshold } : {},
26651
26727
  ...resolvedModel.routingStrategy ? { omRoutingStrategy: resolvedModel.routingStrategy } : {},
@@ -28568,6 +28644,9 @@ async function validateAgainstConfiguredSchema(schema, value) {
28568
28644
  }).join("; ");
28569
28645
  throw new Error(`Working memory update does not match the configured schema, so it was not saved: ${details}`);
28570
28646
  }
28647
+ function isEmptyPlainObject(value) {
28648
+ return typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length === 0;
28649
+ }
28571
28650
  async function getWorkingMemoryDetails(context) {
28572
28651
  const memory = context.memory;
28573
28652
  const memoryConfig = parseMemoryRequestContext(context.requestContext)?.memoryConfig;
@@ -28615,8 +28694,9 @@ var WorkingMemoryExtractor = class extends Extractor {
28615
28694
  const configuredSchema = memory.getMergedThreadConfig(memoryConfig ?? {}).workingMemory?.schema;
28616
28695
  let document = current;
28617
28696
  if (configuredSchema) {
28618
- if (current === null) return;
28697
+ if (current === null || isEmptyPlainObject(current)) return;
28619
28698
  document = await validateAgainstConfiguredSchema(configuredSchema, current);
28699
+ if (document === null || isEmptyPlainObject(document)) return;
28620
28700
  }
28621
28701
  const workingMemory = typeof document === "string" ? document : JSON.stringify(document) ?? "";
28622
28702
  if (!workingMemory.trim()) return;
@@ -30887,6 +30967,80 @@ function resolveAutoModelId(mainModelId, options) {
30887
30967
  const pick = provider ? models[provider] : void 0;
30888
30968
  return pick ? `${prefix}${pick}` : mainModelId;
30889
30969
  }
30970
+ /** How long an op may hold its key before later ops on that key stop waiting for it. */
30971
+ const OM_COMMIT_STALL_MS = 6e4;
30972
+ /** The per-thread / per-resource key shared by the commit queue and `BufferingCoordinator`. */
30973
+ function getOMLockKey(scope, threadId, resourceId) {
30974
+ if (scope === "resource" && resourceId) return `resource:${resourceId}`;
30975
+ return `thread:${threadId ?? "unknown"}`;
30976
+ }
30977
+ const REGISTRY = Symbol.for("@mastra/memory/om-commit-queue");
30978
+ const { queues, heldKeys } = globalThis[REGISTRY] ??= {
30979
+ queues: /* @__PURE__ */ new Map(),
30980
+ heldKeys: new AsyncLocalStorage()
30981
+ };
30982
+ function holdsKey(key) {
30983
+ const token = heldKeys.getStore()?.get(key);
30984
+ return token !== void 0 && queues.get(key)?.running === token;
30985
+ }
30986
+ function runNext(key, queue) {
30987
+ const next = queue.reflection.shift() ?? queue.normal.shift();
30988
+ if (!next) {
30989
+ queue.running = null;
30990
+ queues.delete(key);
30991
+ return;
30992
+ }
30993
+ next();
30994
+ }
30995
+ /**
30996
+ * Runs `op` once every earlier op on `key` has settled (reflection-priority ops first among those
30997
+ * still waiting) and resolves or rejects with its result. A rejection reaches only this caller.
30998
+ *
30999
+ * Enqueueing on a key from inside an op that holds the same key, directly or through nested ops
31000
+ * on other keys, would deadlock, so it rejects.
31001
+ */
31002
+ function runOMCommit(key, op, { priority = "normal" } = {}) {
31003
+ 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.`));
31004
+ return new Promise((resolve, reject) => {
31005
+ let queue = queues.get(key);
31006
+ if (!queue) {
31007
+ queue = {
31008
+ running: null,
31009
+ reflection: [],
31010
+ normal: []
31011
+ };
31012
+ queues.set(key, queue);
31013
+ }
31014
+ const owner = queue;
31015
+ const start = AsyncResource.bind(() => {
31016
+ const token = Symbol(key);
31017
+ owner.running = token;
31018
+ const held = new Map(heldKeys.getStore());
31019
+ held.set(key, token);
31020
+ let released = false;
31021
+ const release = () => {
31022
+ if (released) return;
31023
+ released = true;
31024
+ clearTimeout(stallTimer);
31025
+ runNext(key, owner);
31026
+ };
31027
+ const stallTimer = setTimeout(() => {
31028
+ 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.`);
31029
+ release();
31030
+ }, OM_COMMIT_STALL_MS);
31031
+ stallTimer.unref?.();
31032
+ heldKeys.run(held, () => Promise.resolve().then(op)).then((value) => {
31033
+ release();
31034
+ resolve(value);
31035
+ }, (error) => {
31036
+ release();
31037
+ reject(error);
31038
+ });
31039
+ });
31040
+ (priority === "reflection" ? owner.reflection : owner.normal).push(start);
31041
+ if (owner.running === null) runNext(key, owner);
31042
+ });
31043
+ }
30890
31044
  const activeOps = /* @__PURE__ */ new Map();
30891
31045
  function opKey(recordId, op) {
30892
31046
  return `${recordId}:${op}`;
@@ -30923,11 +31077,37 @@ var BufferingCoordinator = class BufferingCoordinator {
30923
31077
  */
30924
31078
  static asyncBufferingOps = /* @__PURE__ */ new Map();
30925
31079
  /**
31080
+ * Observation buffer ops that may still append a buffered chunk.
31081
+ * Key format: "obs:{lockKey}"
31082
+ * Value: Promise that resolves once the op has persisted its chunk (or ended without one)
31083
+ *
31084
+ * Above the threshold→blockAfter band, activation waits (bounded) on this phase so the chunk
31085
+ * activates instead of being re-observed by the sync pass. It is not needed for safety: storage
31086
+ * keeps a chunk appended during activation. An op stays in asyncBufferingOps through its
31087
+ * post-persist work (indexing, thread title), which activation never waits for.
31088
+ */
31089
+ static pendingChunkWrites = /* @__PURE__ */ new Map();
31090
+ /**
30926
31091
  * Track the last token boundary at which we started buffering.
30927
31092
  * Key format: "obs:{lockKey}" or "refl:{lockKey}"
30928
31093
  */
30929
31094
  static lastBufferedBoundary = /* @__PURE__ */ new Map();
30930
31095
  /**
31096
+ * The buffer op that set the observation boundary, if one did. Any other write (an activation
31097
+ * reset, another op) replaces or clears it, so an op that outlived it never writes its boundary
31098
+ * back. Key format: "obs:{lockKey}"
31099
+ */
31100
+ static observationBoundaryOwners = /* @__PURE__ */ new Map();
31101
+ /** Set the observation boundary; `owner` is the buffer op setting it, if any. */
31102
+ static setObservationBoundary(bufferKey, tokens, owner) {
31103
+ BufferingCoordinator.lastBufferedBoundary.set(bufferKey, tokens);
31104
+ if (owner) BufferingCoordinator.observationBoundaryOwners.set(bufferKey, owner);
31105
+ else BufferingCoordinator.observationBoundaryOwners.delete(bufferKey);
31106
+ }
31107
+ static ownsObservationBoundary(bufferKey, owner) {
31108
+ return BufferingCoordinator.observationBoundaryOwners.get(bufferKey) === owner;
31109
+ }
31110
+ /**
30931
31111
  * Track the timestamp cursor for buffered messages.
30932
31112
  * Key format: "obs:{lockKey}"
30933
31113
  */
@@ -30943,8 +31123,7 @@ var BufferingCoordinator = class BufferingCoordinator {
30943
31123
  this.scope = opts.scope;
30944
31124
  }
30945
31125
  getLockKey(threadId, resourceId) {
30946
- if (this.scope === "resource" && resourceId) return `resource:${resourceId}`;
30947
- return `thread:${threadId ?? "unknown"}`;
31126
+ return getOMLockKey(this.scope, threadId, resourceId);
30948
31127
  }
30949
31128
  isAsyncObservationEnabled() {
30950
31129
  return this.observationConfig.bufferTokens !== void 0 && this.observationConfig.bufferTokens > 0;
@@ -30962,6 +31141,19 @@ var BufferingCoordinator = class BufferingCoordinator {
30962
31141
  return BufferingCoordinator.asyncBufferingOps.has(bufferKey);
30963
31142
  }
30964
31143
  /**
31144
+ * Mark an observation buffer op as able to append a chunk. Call the returned release
31145
+ * function once it no longer can; releasing more than once is a no-op.
31146
+ */
31147
+ static trackChunkWrite(bufferKey) {
31148
+ let resolve;
31149
+ const pending = new Promise((r) => resolve = r);
31150
+ BufferingCoordinator.pendingChunkWrites.set(bufferKey, pending);
31151
+ return () => {
31152
+ if (BufferingCoordinator.pendingChunkWrites.get(bufferKey) === pending) BufferingCoordinator.pendingChunkWrites.delete(bufferKey);
31153
+ resolve();
31154
+ };
31155
+ }
31156
+ /**
30965
31157
  * Clean up static maps for a thread/resource to prevent memory leaks.
30966
31158
  */
30967
31159
  cleanupStaticMaps(threadId, resourceId, activatedMessageIds) {
@@ -30970,10 +31162,12 @@ var BufferingCoordinator = class BufferingCoordinator {
30970
31162
  const reflBufKey = this.getReflectionBufferKey(lockKey);
30971
31163
  if (activatedMessageIds) {
30972
31164
  BufferingCoordinator.lastBufferedBoundary.delete(obsBufKey);
31165
+ BufferingCoordinator.observationBoundaryOwners.delete(obsBufKey);
30973
31166
  BufferingCoordinator.lastBufferedAtTime.delete(obsBufKey);
30974
31167
  } else {
30975
31168
  BufferingCoordinator.lastBufferedAtTime.delete(obsBufKey);
30976
31169
  BufferingCoordinator.lastBufferedBoundary.delete(obsBufKey);
31170
+ BufferingCoordinator.observationBoundaryOwners.delete(obsBufKey);
30977
31171
  BufferingCoordinator.lastBufferedBoundary.delete(reflBufKey);
30978
31172
  BufferingCoordinator.asyncBufferingOps.delete(obsBufKey);
30979
31173
  BufferingCoordinator.asyncBufferingOps.delete(reflBufKey);
@@ -30988,7 +31182,7 @@ var BufferingCoordinator = class BufferingCoordinator {
30988
31182
  if (record.isBufferingObservation) {
30989
31183
  if (isOpActiveInProcess(record.id, "bufferingObservation")) return false;
30990
31184
  omDebug(`[OM:shouldTriggerAsyncObs] isBufferingObservation=true but stale, clearing`);
30991
- storage?.setBufferingObservationFlag(record.id, false)?.catch(() => {});
31185
+ if (storage) runOMCommit(lockKey, () => storage.setBufferingObservationFlag(record.id, false)).catch(() => {});
30992
31186
  }
30993
31187
  const bufferKey = this.getObservationBufferKey(lockKey);
30994
31188
  if (this.isAsyncBufferingInProgress(bufferKey)) return false;
@@ -31008,7 +31202,7 @@ var BufferingCoordinator = class BufferingCoordinator {
31008
31202
  * Await any in-flight async buffering operations for a given thread/resource.
31009
31203
  */
31010
31204
  static async awaitBuffering(threadId, resourceId, scope, timeoutMs = 3e4) {
31011
- const lockKey = scope === "resource" && resourceId ? `resource:${resourceId}` : `thread:${threadId ?? "unknown"}`;
31205
+ const lockKey = getOMLockKey(scope, threadId, resourceId);
31012
31206
  const obsKey = `obs:${lockKey}`;
31013
31207
  const reflKey = `refl:${lockKey}`;
31014
31208
  const promises = [];
@@ -31300,6 +31494,34 @@ function createThreadUpdateMarker(params) {
31300
31494
  }
31301
31495
  };
31302
31496
  }
31497
+ /**
31498
+ * The current head of `origin`'s lineage (the generations reflections have created from it),
31499
+ * or `null` when that lineage is gone.
31500
+ *
31501
+ * Use this instead of reading the head by thread/resource when committing work that started
31502
+ * from `origin`. Clearing a record (`om.clear`, `Memory.deleteThread`) deletes every generation
31503
+ * of the thread/resource, and the thread can then get a new, unrelated record. Work produced
31504
+ * from the cleared messages must never be committed to it.
31505
+ *
31506
+ * Generation ids are never reused and clearing deletes all of them, so the head belongs to
31507
+ * `origin`'s lineage iff `origin` still exists.
31508
+ */
31509
+ async function getLineageHead(storage, origin) {
31510
+ const head = await storage.getObservationalMemory(origin.threadId, origin.resourceId);
31511
+ if (!head || head.id === origin.id) return head;
31512
+ if (head.generationCount <= origin.generationCount) return null;
31513
+ return (await storage.getObservationalMemoryHistory(origin.threadId, origin.resourceId, head.generationCount - origin.generationCount + 3)).some((record) => record.id === origin.id) ? head : null;
31514
+ }
31515
+ /**
31516
+ * The generation `id` of `origin`'s lineage, or `null` when that lineage is gone or `id` is not
31517
+ * part of it. Unlike {@link getLineageHead}, this keeps naming `id` after later reflections have
31518
+ * superseded it, e.g. to report which generation a write landed on.
31519
+ */
31520
+ async function getLineageRecord(storage, origin, id) {
31521
+ const head = await getLineageHead(storage, origin);
31522
+ if (!head || head.id === id) return head;
31523
+ return (await storage.getObservationalMemoryHistory(origin.threadId, origin.resourceId, head.generationCount - origin.generationCount + 3)).find((record) => record.id === id) ?? null;
31524
+ }
31303
31525
  function isRecord$1(value) {
31304
31526
  return !!value && typeof value === "object" && !Array.isArray(value);
31305
31527
  }
@@ -31442,6 +31664,39 @@ function calculateProjectedMessageRemoval(chunks, bufferActivation, messageToken
31442
31664
  }
31443
31665
  /** Module-level xxhash singleton — loaded once, shared across all strategy instances. */
31444
31666
  const hasherPromise = e();
31667
+ /** Recompose-and-retry rounds for an observation commit that hit a retired or changed head. */
31668
+ const MAX_HEAD_COMMIT_RETRIES = 3;
31669
+ const OBSERVATION_LIFECYCLE_MARKERS = /* @__PURE__ */ new Set([
31670
+ "data-om-observation-start",
31671
+ "data-om-observation-end",
31672
+ "data-om-observation-failed"
31673
+ ]);
31674
+ function isOmPart(part) {
31675
+ return String(part?.type ?? "").startsWith("data-om-");
31676
+ }
31677
+ /**
31678
+ * Index in `message` right after this cycle's start marker (and any OM parts directly after it),
31679
+ * or -1 when the message doesn't carry it.
31680
+ */
31681
+ function afterOwnStartMarker(message, marker) {
31682
+ const cycleId = marker.data?.cycleId;
31683
+ const startType = marker.type.startsWith("data-om-buffering-") ? "data-om-buffering-start" : "data-om-observation-start";
31684
+ if (!cycleId || marker.type === startType) return -1;
31685
+ const parts = message.content?.parts ?? [];
31686
+ const start = parts.findIndex((part) => part?.type === startType && part?.data?.cycleId === cycleId);
31687
+ if (start === -1) return -1;
31688
+ let index = start + 1;
31689
+ while (index < parts.length && isOmPart(parts[index])) index++;
31690
+ return index;
31691
+ }
31692
+ /** True when the message has parts no completed observation boundary covers (OM markers aside). */
31693
+ function hasUnobservedContent(message) {
31694
+ return getUnobservedParts(message).some((part) => !isOmPart(part));
31695
+ }
31696
+ function isObservationLifecycleMarker(marker) {
31697
+ const operationType = marker.data?.operationType;
31698
+ return OBSERVATION_LIFECYCLE_MARKERS.has(marker.type) && operationType !== "reflection";
31699
+ }
31445
31700
  /**
31446
31701
  * Abstract base class for observation strategies.
31447
31702
  *
@@ -31491,17 +31746,35 @@ var ObservationStrategy = class ObservationStrategy {
31491
31746
  const fresh = await this.storage.getObservationalMemory(record.threadId, record.resourceId);
31492
31747
  if (fresh?.lastObservedAt && record.lastObservedAt && fresh.lastObservedAt > record.lastObservedAt) return { observed: false };
31493
31748
  }
31494
- const { messages, existingObservations } = await this.prepare();
31749
+ const { messages, existingObservations, contextRecord } = await this.prepare();
31750
+ if (messages.length === 0) return { observed: false };
31495
31751
  const observationMessages = stripSubconsciousSignals(messages);
31496
31752
  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));
31753
+ const observerContext = this.deps.prepareObserverContext(existingObservations, contextRecord ?? record);
31754
+ const output = await this.observe(observerContext.context ?? "", observationMessages, observerContext.wasTruncated);
31755
+ let processed = await this.process(output, existingObservations);
31756
+ let committedRecord = record;
31757
+ const outcome = await this.persist(processed);
31758
+ this.settleObservationCommit(outcome?.status === "committed" && !outcome.alreadyCovered);
31759
+ if (outcome?.status === "not-committed") {
31760
+ omDebug(`[OM:observe] cycle ${cycleId} not committed: ${outcome.reason}`);
31761
+ await this.emitFailedMarkers(cycleId, /* @__PURE__ */ new Error(`Observation not committed: ${outcome.reason}`));
31762
+ return {
31763
+ observed: false,
31764
+ usage: output.usage,
31765
+ providerMetadata: output.providerMetadata
31766
+ };
31767
+ }
31768
+ if (outcome?.status === "committed") {
31769
+ processed = outcome.processed;
31770
+ committedRecord = outcome.record;
31771
+ }
31500
31772
  await this.emitEndMarkers(cycleId, processed);
31501
31773
  if (this.needsReflection) await this.deps.reflector.maybeReflect({
31502
31774
  record: {
31503
- ...record,
31504
- activeObservations: processed.observations
31775
+ ...committedRecord,
31776
+ activeObservations: processed.observations,
31777
+ observationTokenCount: processed.observationTokens
31505
31778
  },
31506
31779
  observationTokens: processed.observationTokens,
31507
31780
  threadId,
@@ -31537,7 +31810,8 @@ var ObservationStrategy = class ObservationStrategy {
31537
31810
  threadId
31538
31811
  }
31539
31812
  };
31540
- await this.persistMarkerToStorage(failedMarkerForStorage, threadId, this.opts.resourceId).catch(() => {});
31813
+ const observed = this.getObservedMessagesForThread(threadId);
31814
+ await (observed?.length ? this.persistMarkerToObservedMessage(failedMarkerForStorage, observed, threadId, this.opts.resourceId) : this.persistMarkerToStorage(failedMarkerForStorage, threadId, this.opts.resourceId)).catch(() => {});
31541
31815
  if (abortSignal?.aborted) throw error;
31542
31816
  omError("[OM] Observation failed", error);
31543
31817
  return {
@@ -31564,6 +31838,11 @@ var ObservationStrategy = class ObservationStrategy {
31564
31838
  transient: true
31565
31839
  }).catch(() => {});
31566
31840
  const markerThreadId = marker.data?.threadId ?? this.opts.threadId;
31841
+ const observed = isObservationLifecycleMarker(marker) ? this.getObservedMessagesForThread(markerThreadId) : void 0;
31842
+ if (observed?.length) {
31843
+ await this.persistMarkerToObservedMessage(marker, observed, markerThreadId, this.opts.resourceId);
31844
+ return;
31845
+ }
31567
31846
  if (!await this.persistMarkerToMessage(marker, this.opts.messageList, markerThreadId, this.opts.resourceId)) await this.persistMarkerToStorage(marker, markerThreadId, this.opts.resourceId);
31568
31847
  }
31569
31848
  getObservationMarkerConfig() {
@@ -31647,6 +31926,76 @@ var ObservationStrategy = class ObservationStrategy {
31647
31926
  }
31648
31927
  return `${existingObservations}${lastObservedAt ? ObservationStrategy.createMessageBoundary(lastObservedAt) : "\n\n"}${newThreadSection}`;
31649
31928
  }
31929
+ /**
31930
+ * Commit composed observations to the current head generation.
31931
+ *
31932
+ * The write is conditional on the head text the observations were composed from. When the
31933
+ * target generation was retired by a reflection, or another writer changed the text, the
31934
+ * observations are recomposed against the fresh head and the commit is retried (bounded).
31935
+ * When the fresh head's `observedMessageIds` already include every message this cycle observed
31936
+ * (another instance observed them first), nothing is written and `alreadyCovered` is set, so
31937
+ * callers advance cursors and markers without adding a second copy of the same observations.
31938
+ * The cursor alone is not proof: an activation can move it past a message it never covered.
31939
+ * Returns null when no commit landed — callers must then leave cursors, markers, and the live
31940
+ * context untouched.
31941
+ */
31942
+ async commitActiveObservationsToHead(opts) {
31943
+ let { processed, composedFrom, target } = opts;
31944
+ for (let attempt = 0; attempt <= MAX_HEAD_COMMIT_RETRIES; attempt++) {
31945
+ const result = await this.runCommit(async () => {
31946
+ const head = await getLineageHead(this.storage, target);
31947
+ if (!head) return null;
31948
+ if (head.id !== target.id || (head.activeObservations ?? "") !== composedFrom) {
31949
+ if (this.headCoversCycle(head, opts.cycleMessageIds)) {
31950
+ omDebug(`[OM:observe] head ${head.id} already covers this cycle's messages; skipping the duplicate commit`);
31951
+ target = head;
31952
+ return {
31953
+ applied: true,
31954
+ alreadyCovered: true
31955
+ };
31956
+ }
31957
+ target = head;
31958
+ composedFrom = head.activeObservations ?? "";
31959
+ processed = await opts.recompose(head);
31960
+ }
31961
+ const input = {
31962
+ id: target.id,
31963
+ observations: processed.observations,
31964
+ tokenCount: processed.observationTokens,
31965
+ lastObservedAt: processed.lastObservedAt,
31966
+ observedMessageIds: processed.observedMessageIds,
31967
+ expectedActiveObservations: composedFrom
31968
+ };
31969
+ return typeof this.storage.commitActiveObservations === "function" ? await this.storage.commitActiveObservations(input) : (await this.storage.updateActiveObservations(input), { applied: true });
31970
+ });
31971
+ if (!result) return null;
31972
+ if ("alreadyCovered" in result) return {
31973
+ processed: {
31974
+ ...processed,
31975
+ observations: target.activeObservations ?? "",
31976
+ observationTokens: target.observationTokenCount
31977
+ },
31978
+ record: target,
31979
+ alreadyCovered: true
31980
+ };
31981
+ if (result.applied) return {
31982
+ processed,
31983
+ record: target
31984
+ };
31985
+ omDebug(`[OM:observe] commit to ${target.id} not applied (${result.reason}); retrying against the head`);
31986
+ }
31987
+ return null;
31988
+ }
31989
+ headCoversCycle(head, cycleMessageIds) {
31990
+ if (cycleMessageIds.length === 0) return false;
31991
+ const headIds = new Set(Array.isArray(head.observedMessageIds) ? head.observedMessageIds : []);
31992
+ return cycleMessageIds.every((id) => headIds.has(id));
31993
+ }
31994
+ /** Runs one storage commit in this thread's (or resource's) slot of the process-wide commit queue. */
31995
+ runCommit(op) {
31996
+ const { threadId, resourceId, record } = this.opts;
31997
+ return runOMCommit(getOMLockKey(this.scope, record.threadId ?? threadId, record.resourceId ?? resourceId), op);
31998
+ }
31650
31999
  async indexObservationGroups(observations, threadId, resourceId, observedAt, recordId) {
31651
32000
  if (!resourceId || !this.deps.onIndexObservations) return;
31652
32001
  const groups = parseObservationGroups(observations);
@@ -31665,30 +32014,133 @@ var ObservationStrategy = class ObservationStrategy {
31665
32014
  })));
31666
32015
  }
31667
32016
  /**
32017
+ * Messages this cycle observed for `threadId`, or undefined when the strategy does not
32018
+ * place observation lifecycle markers.
32019
+ */
32020
+ getObservedMessagesForThread(_threadId) {}
32021
+ /**
32022
+ * Persist an observation start/end/failed marker without hiding unobserved content. A completed
32023
+ * end marker tells `getUnobservedMessages` that everything before it in that message is observed
32024
+ * (whatever thread the marker names), so it must only follow content this cycle observed.
32025
+ * Placement, in order:
32026
+ * 1. the live MessageList's newest assistant message (this turn's response or its seed), when the
32027
+ * list belongs to the marker's thread and the cycle observed that message or it has no
32028
+ * unobserved content;
32029
+ * 2. the newest observed assistant message; a stored copy (no live list, or another instance's
32030
+ * message) gets the marker right after the parts this cycle observed (end/failed markers right
32031
+ * after this cycle's start marker), so parts added while the Observer ran stay unobserved;
32032
+ * 3. the newest stored assistant message that is not newer than the observed range, only when it
32033
+ * has nothing unobserved (or already carries this cycle's start marker); otherwise no marker,
32034
+ * and the thread cursor alone records the cycle.
32035
+ * Lifecycle markers never go on user messages.
32036
+ */
32037
+ async persistMarkerToObservedMessage(marker, observed, threadId, resourceId) {
32038
+ const observedIds = new Set(observed.map((m) => m.id));
32039
+ const list = threadId === this.opts.threadId ? this.opts.messageList : void 0;
32040
+ const listMessages = list ? getObservableMessages(list) : [];
32041
+ const live = [...listMessages].reverse().find((m) => m.role === "assistant");
32042
+ if (live && (observedIds.has(live.id) || !hasUnobservedContent(live))) {
32043
+ await this.attachMarker(marker, live, live.content.parts.length, threadId, resourceId);
32044
+ return;
32045
+ }
32046
+ const target = [...observed].reverse().find((m) => m.role === "assistant");
32047
+ if (target) {
32048
+ const fromList = listMessages.find((m) => m.id === target.id);
32049
+ if (fromList) {
32050
+ await this.attachMarker(marker, fromList, fromList.content.parts.length, threadId, resourceId);
32051
+ return;
32052
+ }
32053
+ try {
32054
+ const stored = (await this.storage.listMessagesById({ messageIds: [target.id] })).messages[0];
32055
+ if (stored?.content?.parts && Array.isArray(stored.content.parts)) {
32056
+ const afterStart = afterOwnStartMarker(stored, marker);
32057
+ const index = afterStart !== -1 ? afterStart : this.insertionAfterObserved(stored, target);
32058
+ await this.attachMarker(marker, stored, index, threadId, resourceId);
32059
+ }
32060
+ } catch (e) {
32061
+ omDebug(`[OM:persistMarkerToObservedMessage] failed to load marker target: ${e}`);
32062
+ }
32063
+ return;
32064
+ }
32065
+ const observedTimes = observed.filter((m) => m.createdAt).map((m) => new Date(m.createdAt).getTime());
32066
+ if (observedTimes.length === 0) return;
32067
+ await this.persistMarkerToStorage(marker, threadId, resourceId, {
32068
+ notAfter: new Date(Math.max(...observedTimes)),
32069
+ onlyIfObserved: true
32070
+ });
32071
+ }
32072
+ /** What the cycle observed of each message, captured when it placed its first marker there. */
32073
+ observedParts = /* @__PURE__ */ new Map();
32074
+ /**
32075
+ * Index in `stored` right after the parts the cycle observed in `observed` (and any OM markers
32076
+ * directly after them). Parts are only ever appended, so the observed message's non-marker part
32077
+ * count locates them, counted from the start of the message or, when `observed` is the trimmed
32078
+ * copy `getUnobservedMessages` returns for an already marked message, from just after the stored
32079
+ * copy's last completed boundary. Captured at the cycle's first marker, before the Observer runs,
32080
+ * because `observed` may be a live reference that grows meanwhile. Falls back to the end when the
32081
+ * stored copy has fewer parts than were observed.
32082
+ */
32083
+ insertionAfterObserved(stored, observed) {
32084
+ if (!this.observedParts.has(observed.id)) this.observedParts.set(observed.id, {
32085
+ count: (observed.content?.parts ?? []).filter((part) => !isOmPart(part)).length,
32086
+ observedIsTrimmed: findLastCompletedObservationBoundary(observed) === -1
32087
+ });
32088
+ const { count, observedIsTrimmed } = this.observedParts.get(observed.id);
32089
+ const parts = stored.content.parts;
32090
+ let index = observedIsTrimmed ? findLastCompletedObservationBoundary(stored) + 1 : 0;
32091
+ let seen = 0;
32092
+ while (index < parts.length && seen < count) {
32093
+ if (!isOmPart(parts[index])) seen++;
32094
+ index++;
32095
+ }
32096
+ if (seen < count) return parts.length;
32097
+ while (index < parts.length && isOmPart(parts[index])) index++;
32098
+ return index;
32099
+ }
32100
+ async attachMarker(marker, msg, index, threadId, resourceId) {
32101
+ const parts = msg.content?.parts;
32102
+ if (!parts || !Array.isArray(parts)) return;
32103
+ const markerData = marker.data;
32104
+ if (!(markerData?.cycleId && parts.some((p) => p?.type === marker.type && p?.data?.cycleId === markerData.cycleId))) parts.splice(index, 0, marker);
32105
+ try {
32106
+ await this.messageHistory.persistMessages({
32107
+ messages: [msg],
32108
+ threadId,
32109
+ resourceId
32110
+ });
32111
+ } catch (e) {
32112
+ omDebug(`[OM:persistMarkerToObservedMessage] failed to save marker to DB: ${e}`);
32113
+ }
32114
+ }
32115
+ /**
31668
32116
  * Persist a marker to the last assistant message in storage.
31669
32117
  * Fetches messages directly from the DB so it works even when
31670
32118
  * no MessageList is available (e.g. async buffering ops).
31671
32119
  */
31672
- async persistMarkerToStorage(marker, threadId, resourceId) {
32120
+ async persistMarkerToStorage(marker, threadId, resourceId, opts) {
31673
32121
  try {
31674
- const messages = (await this.storage.listMessages({
32122
+ const assistants = ((await this.storage.listMessages({
31675
32123
  threadId,
31676
32124
  perPage: 20,
31677
32125
  orderBy: {
31678
32126
  field: "createdAt",
31679
32127
  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
- }
32128
+ },
32129
+ ...opts?.notAfter ? { filter: { dateRange: { end: opts.notAfter } } } : {}
32130
+ }))?.messages ?? []).filter((msg) => msg?.role === "assistant" && Array.isArray(msg.content?.parts));
32131
+ const startMessage = opts?.onlyIfObserved || opts?.onCycleStartMessage ? assistants.find((m) => afterOwnStartMarker(m, marker) !== -1) : void 0;
32132
+ const msg = opts?.onCycleStartMessage ? startMessage : startMessage ?? assistants[0];
32133
+ if (!msg) return;
32134
+ const markerData = marker.data;
32135
+ const alreadyPresent = markerData?.cycleId && msg.content.parts.some((p) => p?.type === marker.type && p?.data?.cycleId === markerData.cycleId);
32136
+ const afterStart = afterOwnStartMarker(msg, marker);
32137
+ if (opts?.onlyIfObserved && afterStart === -1 && hasUnobservedContent(msg)) return;
32138
+ if (!alreadyPresent) msg.content.parts.splice(afterStart !== -1 ? afterStart : msg.content.parts.length, 0, marker);
32139
+ await this.messageHistory.persistMessages({
32140
+ messages: [msg],
32141
+ threadId,
32142
+ resourceId
32143
+ });
31692
32144
  } catch (e) {
31693
32145
  omDebug(`[OM:persistMarkerToStorage] failed to save marker to DB: ${e}`);
31694
32146
  }
@@ -31746,10 +32198,15 @@ var SyncObservationStrategy = class extends ObservationStrategy {
31746
32198
  tokensToObserve = 0;
31747
32199
  observerResult;
31748
32200
  priorExtractedValues;
32201
+ /** Head text the processed observations were composed from (the commit's expected text). */
32202
+ composedFrom = "";
31749
32203
  constructor(deps, opts) {
31750
32204
  super(deps, opts);
31751
32205
  this.lastMessage = opts.messages[opts.messages.length - 1];
31752
32206
  }
32207
+ getObservedMessagesForThread(threadId) {
32208
+ return threadId === this.opts.threadId ? this.opts.messages : void 0;
32209
+ }
31753
32210
  get needsLock() {
31754
32211
  return true;
31755
32212
  }
@@ -31784,9 +32241,11 @@ var SyncObservationStrategy = class extends ObservationStrategy {
31784
32241
  }
31785
32242
  }
31786
32243
  this.tokensToObserve = await this.tokenCounter.countMessagesAsync(messages);
32244
+ const freshRecord = await this.storage.getObservationalMemory(record.threadId, record.resourceId);
31787
32245
  return {
31788
32246
  messages,
31789
- existingObservations: (await this.storage.getObservationalMemory(record.threadId, record.resourceId))?.activeObservations ?? record.activeObservations ?? ""
32247
+ existingObservations: freshRecord?.activeObservations ?? record.activeObservations ?? "",
32248
+ contextRecord: freshRecord
31790
32249
  };
31791
32250
  }
31792
32251
  async emitStartMarkers(cycleId) {
@@ -31804,11 +32263,12 @@ var SyncObservationStrategy = class extends ObservationStrategy {
31804
32263
  await this.streamMarker(startMarker);
31805
32264
  }
31806
32265
  }
31807
- async observe(existingObservations, messages) {
32266
+ async observe(observerContext, messages, wasTruncated) {
31808
32267
  const thread = await this.storage.getThreadById({ threadId: this.opts.threadId });
31809
32268
  const omMeta = thread ? getThreadOMMetadata(thread.metadata) : void 0;
31810
32269
  this.priorExtractedValues = getPriorExtractedValues(omMeta, this.observationConfig.extractors);
31811
- const result = await this.deps.observer.call(existingObservations, messages, this.opts.abortSignal, {
32270
+ const result = await this.deps.observer.call(observerContext, messages, this.opts.abortSignal, {
32271
+ wasTruncated,
31812
32272
  requestContext: this.opts.requestContext,
31813
32273
  observabilityContext: this.opts.observabilityContext,
31814
32274
  priorCurrentTask: omMeta?.currentTask,
@@ -31854,20 +32314,14 @@ var SyncObservationStrategy = class extends ObservationStrategy {
31854
32314
  }
31855
32315
  async process(output, existingObservations) {
31856
32316
  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])];
32317
+ this.composedFrom = existingObservations;
32318
+ const processed = await this.compose(output, existingObservations, record);
31865
32319
  this.deps.emitDebugEvent({
31866
32320
  type: "observation_complete",
31867
32321
  timestamp: /* @__PURE__ */ new Date(),
31868
32322
  threadId,
31869
32323
  resourceId: record.resourceId ?? "",
31870
- observations: newObservations,
32324
+ observations: processed.observations,
31871
32325
  rawObserverOutput: output.observations,
31872
32326
  previousObservations: record.activeObservations,
31873
32327
  messages: messages.map((m) => ({
@@ -31876,11 +32330,23 @@ var SyncObservationStrategy = class extends ObservationStrategy {
31876
32330
  })),
31877
32331
  usage: output.usage
31878
32332
  });
32333
+ return processed;
32334
+ }
32335
+ /** Build the observations to commit on top of `existingObservations` (the head text). */
32336
+ async compose(output, existingObservations, baseRecord) {
32337
+ const { threadId, messages } = this.opts;
32338
+ const lastObservedAt = this.getMaxMessageTimestamp(messages);
32339
+ const messageRange = this.retrieval ? buildMessageRange(messages) : void 0;
32340
+ const newObservations = await this.wrapObservations(output.observations, existingObservations, threadId, lastObservedAt, messageRange);
32341
+ const observationTokens = this.tokenCounter.countObservations(newObservations);
32342
+ const cycleObservationTokens = this.tokenCounter.countObservations(output.observations);
32343
+ const newMessageIds = messages.map((m) => m.id);
32344
+ const existingIds = baseRecord.observedMessageIds ?? [];
31879
32345
  return {
31880
32346
  observations: newObservations,
31881
32347
  observationTokens,
31882
32348
  cycleObservationTokens,
31883
- observedMessageIds,
32349
+ observedMessageIds: [.../* @__PURE__ */ new Set([...Array.isArray(existingIds) ? existingIds : [], ...newMessageIds])],
31884
32350
  lastObservedAt,
31885
32351
  suggestedContinuation: output.suggestedContinuation,
31886
32352
  currentTask: output.currentTask,
@@ -31892,12 +32358,27 @@ var SyncObservationStrategy = class extends ObservationStrategy {
31892
32358
  }
31893
32359
  async persist(processed) {
31894
32360
  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)) {
32361
+ const liveRecord = await getLineageHead(this.storage, record);
32362
+ if (!liveRecord) {
31897
32363
  omDebug(`[OM:sync-obs] skipping persist for thread ${threadId}: observational memory record is gone`);
31898
- return false;
32364
+ return {
32365
+ status: "not-committed",
32366
+ reason: "the observational memory record was cleared"
32367
+ };
31899
32368
  }
31900
- let threadUpdateMarker;
32369
+ const committed = await this.commitActiveObservationsToHead({
32370
+ processed,
32371
+ composedFrom: this.composedFrom,
32372
+ target: liveRecord,
32373
+ recompose: (head) => this.compose(this.observerResult, head.activeObservations ?? "", head),
32374
+ cycleMessageIds: messages.map((m) => m.id)
32375
+ });
32376
+ if (!committed) return {
32377
+ status: "not-committed",
32378
+ reason: "the observational memory head kept changing during the commit"
32379
+ };
32380
+ processed = committed.processed;
32381
+ const thread = committed.alreadyCovered ? null : await this.storage.getThreadById({ threadId });
31901
32382
  if (thread) {
31902
32383
  const oldTitle = thread.title?.trim();
31903
32384
  const newTitle = resolveThreadTitleUpdate(thread, processed.threadTitle);
@@ -31919,23 +32400,20 @@ var SyncObservationStrategy = class extends ObservationStrategy {
31919
32400
  ...shouldUpdateThreadTitle ? { title: newTitle } : {},
31920
32401
  metadata: newMetadata
31921
32402
  });
31922
- if (shouldUpdateThreadTitle) threadUpdateMarker = createThreadUpdateMarker({
32403
+ if (shouldUpdateThreadTitle) await this.streamMarker(createThreadUpdateMarker({
31923
32404
  cycleId: this.cycleId ?? crypto.randomUUID(),
31924
32405
  threadId,
31925
32406
  oldTitle,
31926
32407
  newTitle
31927
- });
32408
+ }));
31928
32409
  }
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;
32410
+ if (!committed.alreadyCovered) await this.indexObservationGroups(processed.observations, threadId, resourceId, processed.lastObservedAt, committed.record.id);
32411
+ return {
32412
+ status: "committed",
32413
+ processed,
32414
+ record: committed.record,
32415
+ alreadyCovered: committed.alreadyCovered
32416
+ };
31939
32417
  }
31940
32418
  async emitEndMarkers(cycleId, processed) {
31941
32419
  const actualTokensObserved = await this.tokenCounter.countMessagesAsync(this.opts.messages);
@@ -31977,6 +32455,8 @@ var AsyncBufferObservationStrategy = class extends ObservationStrategy {
31977
32455
  startedAt;
31978
32456
  cycleId;
31979
32457
  priorExtractedValues;
32458
+ /** The generation the chunk was written to (the head when the target was retired). */
32459
+ persistedRecordId;
31980
32460
  constructor(deps, opts) {
31981
32461
  super(deps, opts);
31982
32462
  this.cycleId = opts.cycleId;
@@ -32003,12 +32483,13 @@ var AsyncBufferObservationStrategy = class extends ObservationStrategy {
32003
32483
  };
32004
32484
  }
32005
32485
  async emitStartMarkers(_cycleId) {}
32006
- async observe(existingObservations, messages) {
32486
+ async observe(observerContext, messages, wasTruncated) {
32007
32487
  const thread = await this.storage.getThreadById({ threadId: this.opts.threadId });
32008
32488
  const omMeta = thread ? getThreadOMMetadata(thread.metadata) : void 0;
32009
32489
  this.priorExtractedValues = getPriorExtractedValues(omMeta, this.observationConfig.extractors);
32010
- const result = await this.deps.observer.call(existingObservations, messages, void 0, {
32490
+ const result = await this.deps.observer.call(observerContext, messages, void 0, {
32011
32491
  skipContinuationHints: true,
32492
+ wasTruncated,
32012
32493
  requestContext: this.opts.requestContext,
32013
32494
  observabilityContext: this.opts.observabilityContext,
32014
32495
  priorExtractedValues: this.priorExtractedValues,
@@ -32082,71 +32563,124 @@ var AsyncBufferObservationStrategy = class extends ObservationStrategy {
32082
32563
  };
32083
32564
  }
32084
32565
  async persist(processed) {
32085
- if (!processed.observations) return false;
32566
+ if (!processed.observations) return;
32086
32567
  const { record, threadId, resourceId, messages } = this.opts;
32087
- if (!await this.storage.getObservationalMemory(record.threadId, record.resourceId)) {
32568
+ const liveRecord = await getLineageHead(this.storage, record);
32569
+ if (!liveRecord) {
32088
32570
  omDebug(`[OM:asyncBuffer] skipping persist for thread ${threadId}: observational memory record is gone`);
32089
- return false;
32571
+ return {
32572
+ status: "not-committed",
32573
+ reason: "the observational memory record was cleared"
32574
+ };
32090
32575
  }
32091
32576
  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
- }), {
32577
+ let appendAttempts = 0;
32578
+ let targetId = record.id;
32579
+ const appendResult = await withRetry(() => {
32580
+ appendAttempts++;
32581
+ return this.runCommit(async () => {
32582
+ const head = await getLineageHead(this.storage, record);
32583
+ if (!head) return null;
32584
+ targetId = head.id;
32585
+ const input = {
32586
+ id: targetId,
32587
+ chunk: {
32588
+ cycleId: this.cycleId,
32589
+ observations: processed.observations,
32590
+ tokenCount: processed.observationTokens,
32591
+ messageIds: processed.observedMessageIds,
32592
+ messageTokens,
32593
+ lastObservedAt: processed.lastObservedAt,
32594
+ suggestedContinuation: processed.suggestedContinuation,
32595
+ currentTask: processed.currentTask,
32596
+ threadTitle: processed.threadTitle,
32597
+ extractedValues: processed.extractedValues,
32598
+ extractionFailures: processed.extractionFailures
32599
+ },
32600
+ lastBufferedAtTime: processed.lastObservedAt
32601
+ };
32602
+ return typeof this.storage.appendBufferedObservations === "function" ? this.storage.appendBufferedObservations(input) : this.storage.updateBufferedObservations(input).then(() => ({
32603
+ persisted: true,
32604
+ recordId: input.id
32605
+ }));
32606
+ });
32607
+ }, {
32109
32608
  label: "persist-buffered-observations",
32110
32609
  abortSignal: this.opts.abortSignal
32111
32610
  });
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 = getThreadOMMetadata(thread.metadata);
32122
- const metadataUpdate = buildThreadMetadataFromExtractedValues(processed.extractors ?? this.observationConfig.extractors, processed.extractedValues);
32123
- const newMetadata = 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
32611
+ if (appendResult === null) {
32612
+ omDebug(`[OM:asyncBuffer] skipping persist for thread ${threadId}: observational memory record was cleared`);
32613
+ return {
32614
+ status: "not-committed",
32615
+ reason: "the observational memory record was cleared"
32616
+ };
32617
+ }
32618
+ let committedRecord = liveRecord;
32619
+ if (!appendResult.persisted) {
32620
+ const head = appendAttempts > 1 ? await getLineageHead(this.storage, record) : null;
32621
+ if (!(!!head && (getBufferedChunks(head).some((chunk) => chunk.cycleId === this.cycleId) || head.activeObservations.includes(processed.observations)))) return {
32622
+ status: "not-committed",
32623
+ reason: "the buffered chunk was already observed"
32624
+ };
32625
+ this.persistedRecordId = head.id;
32626
+ committedRecord = head;
32627
+ } else this.persistedRecordId = appendResult.recordId;
32628
+ await this.emitBufferingEndMarker(processed);
32629
+ await this.opts.onBufferedChunkPersisted?.();
32630
+ if (committedRecord.id !== this.persistedRecordId) {
32631
+ const landedOn = await getLineageRecord(this.storage, record, this.persistedRecordId);
32632
+ if (!landedOn) return {
32633
+ status: "not-committed",
32634
+ reason: "the observational memory record was cleared"
32635
+ };
32636
+ committedRecord = landedOn;
32637
+ }
32638
+ try {
32639
+ await this.indexObservationGroups(processed.observations, threadId, resourceId, processed.lastObservedAt, this.persistedRecordId);
32640
+ const candidateTitle = processed.threadTitle?.trim();
32641
+ const hasValidThreadTitle = !!candidateTitle && candidateTitle.length >= 3;
32642
+ if (hasValidThreadTitle || processed.extractedValues) {
32643
+ const thread = await this.storage.getThreadById({ threadId });
32644
+ if (thread) {
32645
+ const oldTitle = thread.title?.trim();
32646
+ const newTitle = resolveThreadTitleUpdate(thread, candidateTitle);
32647
+ const shouldUpdateThreadTitle = newTitle !== void 0;
32648
+ const previousOmMetadata = getThreadOMMetadata(thread.metadata);
32649
+ const metadataUpdate = buildThreadMetadataFromExtractedValues(processed.extractors ?? this.observationConfig.extractors, processed.extractedValues);
32650
+ const newMetadata = setThreadOMMetadata(thread.metadata, {
32651
+ ...hasValidThreadTitle || metadataUpdate.threadTitle ? { threadTitle: metadataUpdate.threadTitle ?? processed.threadTitle } : {},
32652
+ extracted: {
32653
+ ...previousOmMetadata?.extracted ?? {},
32654
+ ...metadataUpdate.extracted ?? {}
32655
+ }
32656
+ });
32657
+ await this.storage.patchThread({
32658
+ id: threadId,
32659
+ ...shouldUpdateThreadTitle ? { title: newTitle } : {},
32660
+ metadata: newMetadata
32141
32661
  });
32142
- await this.streamMarker(marker);
32662
+ if (shouldUpdateThreadTitle) {
32663
+ const marker = createThreadUpdateMarker({
32664
+ cycleId: this.cycleId,
32665
+ threadId,
32666
+ oldTitle,
32667
+ newTitle
32668
+ });
32669
+ await this.streamMarker(marker);
32670
+ }
32143
32671
  }
32144
32672
  }
32673
+ } catch (error) {
32674
+ omError("[OM] Post-buffer indexing or thread update failed; the buffered chunk is kept", error);
32145
32675
  }
32146
- return true;
32676
+ return {
32677
+ status: "committed",
32678
+ processed,
32679
+ record: committedRecord
32680
+ };
32147
32681
  }
32148
- async emitEndMarkers(_cycleId, processed) {
32149
- if (!processed.observations) return;
32682
+ async emitEndMarkers() {}
32683
+ async emitBufferingEndMarker(processed) {
32150
32684
  const { record, threadId, messages } = this.opts;
32151
32685
  const tokensBuffered = await this.tokenCounter.countMessagesAsync(messages);
32152
32686
  const totalBufferedTokens = getBufferedChunks(await this.storage.getObservationalMemory(record.threadId, record.resourceId)).reduce((sum, c) => sum + (c.tokenCount ?? 0), 0) || processed.observationTokens;
@@ -32156,7 +32690,7 @@ var AsyncBufferObservationStrategy = class extends ObservationStrategy {
32156
32690
  startedAt: this.startedAt,
32157
32691
  tokensBuffered,
32158
32692
  bufferedTokens: totalBufferedTokens,
32159
- recordId: record.id,
32693
+ recordId: this.persistedRecordId ?? record.id,
32160
32694
  threadId,
32161
32695
  observations: processed.observations,
32162
32696
  extractedValues: processed.extractedValues,
@@ -32166,7 +32700,10 @@ var AsyncBufferObservationStrategy = class extends ObservationStrategy {
32166
32700
  ...endMarker,
32167
32701
  transient: true
32168
32702
  }).catch(() => {});
32169
- await this.persistMarkerToStorage(endMarker, threadId, record.resourceId ?? void 0);
32703
+ await this.persistMarkerToStorage(endMarker, threadId, record.resourceId ?? void 0, {
32704
+ notAfter: new Date(this.startedAt),
32705
+ onCycleStartMessage: true
32706
+ });
32170
32707
  }
32171
32708
  async emitFailedMarkers(_cycleId, error) {
32172
32709
  const { record, threadId, messages } = this.opts;
@@ -32185,7 +32722,10 @@ var AsyncBufferObservationStrategy = class extends ObservationStrategy {
32185
32722
  ...failedMarker,
32186
32723
  transient: true
32187
32724
  }).catch(() => {});
32188
- await this.persistMarkerToStorage(failedMarker, threadId, record.resourceId ?? void 0);
32725
+ await this.persistMarkerToStorage(failedMarker, threadId, record.resourceId ?? void 0, {
32726
+ notAfter: new Date(this.startedAt),
32727
+ onCycleStartMessage: true
32728
+ });
32189
32729
  }
32190
32730
  };
32191
32731
  var ResourceScopedObservationStrategy = class extends ObservationStrategy {
@@ -32206,10 +32746,17 @@ var ResourceScopedObservationStrategy = class extends ObservationStrategy {
32206
32746
  lastBatchProviderMetadata;
32207
32747
  observationResults = [];
32208
32748
  priorMetadataByThread = /* @__PURE__ */ new Map();
32749
+ /** Each observed thread's wrapped section from the last composition. */
32750
+ threadSections = /* @__PURE__ */ new Map();
32751
+ /** Head text the processed observations were composed from (the commit's expected text). */
32752
+ composedFrom = "";
32209
32753
  constructor(deps, opts) {
32210
32754
  super(deps, opts);
32211
32755
  this.resourceId = opts.resourceId;
32212
32756
  }
32757
+ getObservedMessagesForThread(threadId) {
32758
+ return this.threadsWithMessages.get(threadId);
32759
+ }
32213
32760
  get needsLock() {
32214
32761
  return true;
32215
32762
  }
@@ -32296,10 +32843,12 @@ var ResourceScopedObservationStrategy = class extends ObservationStrategy {
32296
32843
  content: typeof m.content === "string" ? m.content : JSON.stringify(m.content)
32297
32844
  }))
32298
32845
  });
32299
- const existingObservations = (await this.storage.getObservationalMemory(null, this.resourceId))?.activeObservations ?? record.activeObservations ?? "";
32846
+ const freshRecord = await this.storage.getObservationalMemory(null, this.resourceId);
32847
+ const existingObservations = freshRecord?.activeObservations ?? record.activeObservations ?? "";
32300
32848
  return {
32301
32849
  messages: Array.from(this.threadsWithMessages.values()).flat(),
32302
- existingObservations
32850
+ existingObservations,
32851
+ contextRecord: freshRecord
32303
32852
  };
32304
32853
  }
32305
32854
  async emitStartMarkers(cycleId) {
@@ -32323,7 +32872,7 @@ var ResourceScopedObservationStrategy = class extends ObservationStrategy {
32323
32872
  }
32324
32873
  }
32325
32874
  }
32326
- async observe(_existingObservations, _messages) {
32875
+ async observe(observerContext, _messages, wasTruncated) {
32327
32876
  const maxTokensPerBatch = this.observationConfig.maxTokensPerBatch ?? OBSERVATIONAL_MEMORY_DEFAULTS.observation.maxTokensPerBatch;
32328
32877
  const orderedThreadIds = this.threadOrder.filter((tid) => this.threadsWithMessages.has(tid));
32329
32878
  const batches = [];
@@ -32349,10 +32898,10 @@ var ResourceScopedObservationStrategy = class extends ObservationStrategy {
32349
32898
  }
32350
32899
  if (currentBatch.threadIds.length > 0) batches.push(currentBatch);
32351
32900
  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, {
32901
+ return this.deps.observer.callMultiThread(observerContext, batch.threadMap, batch.threadIds, this.opts.abortSignal, this.opts.requestContext, this.priorMetadataByThread, this.opts.observabilityContext, void 0, {
32353
32902
  resourceId: this.opts.resourceId,
32354
32903
  trigger: this.opts.trigger
32355
- }, this.opts.record.observedTimezone, this.opts.agent, this.opts.currentModel);
32904
+ }, this.opts.record.observedTimezone, this.opts.agent, this.opts.currentModel, wasTruncated);
32356
32905
  }));
32357
32906
  for (const batchResult of batchResults) {
32358
32907
  for (const [threadId, result] of batchResult.results) this.multiThreadResults.set(threadId, result);
@@ -32419,14 +32968,38 @@ var ResourceScopedObservationStrategy = class extends ObservationStrategy {
32419
32968
  }
32420
32969
  });
32421
32970
  }
32971
+ this.composedFrom = existingObservations;
32972
+ const processed = await this.compose(existingObservations, record);
32973
+ for (const [index, { threadId, threadMessages, result }] of this.observationResults.entries()) this.deps.emitDebugEvent({
32974
+ type: "observation_complete",
32975
+ timestamp: /* @__PURE__ */ new Date(),
32976
+ threadId,
32977
+ resourceId: this.resourceId,
32978
+ observations: this.threadSections.get(threadId) ?? "",
32979
+ rawObserverOutput: result.observations,
32980
+ previousObservations: record.activeObservations,
32981
+ messages: threadMessages.map((m) => ({
32982
+ role: m.role,
32983
+ content: typeof m.content === "string" ? m.content : JSON.stringify(m.content)
32984
+ })),
32985
+ usage: index === 0 && this.totalBatchUsage.totalTokens > 0 ? this.totalBatchUsage : void 0
32986
+ });
32987
+ return processed;
32988
+ }
32989
+ /**
32990
+ * Merge every observed thread's section into `existingObservations` (the head text). Resource
32991
+ * scope rewrites an existing same-day `<thread>` section in place, so the result is not an
32992
+ * append of the head text.
32993
+ */
32994
+ async compose(existingObservations, baseRecord) {
32422
32995
  let currentObservations = existingObservations;
32423
32996
  let cycleObservationTokens = 0;
32424
32997
  const threadMetadataUpdates = [];
32425
- for (const obsResult of this.observationResults) {
32426
- const { threadId, threadMessages, result } = obsResult;
32998
+ for (const { threadId, threadMessages, result } of this.observationResults) {
32427
32999
  cycleObservationTokens += this.tokenCounter.countObservations(result.observations);
32428
33000
  const messageRange = this.retrieval ? buildMessageRange(threadMessages) : void 0;
32429
33001
  const threadSection = await this.wrapWithThreadTag(threadId, result.observations, messageRange);
33002
+ this.threadSections.set(threadId, threadSection);
32430
33003
  const threadLastObservedAt = this.getMaxMessageTimestamp(threadMessages);
32431
33004
  currentObservations = this.replaceOrAppendThreadSection(currentObservations, threadId, threadSection, threadLastObservedAt);
32432
33005
  threadMetadataUpdates.push({
@@ -32440,26 +33013,11 @@ var ResourceScopedObservationStrategy = class extends ObservationStrategy {
32440
33013
  extractors: result.extractors,
32441
33014
  lastObservedMessageCursor: getLastObservedMessageCursor(threadMessages)
32442
33015
  });
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
33016
  }
32459
33017
  const observedMessages = this.observationResults.flatMap((r) => r.threadMessages);
32460
33018
  const lastObservedAt = this.getMaxMessageTimestamp(observedMessages);
32461
33019
  const newMessageIds = observedMessages.map((m) => m.id);
32462
- const existingIds = record.observedMessageIds ?? [];
33020
+ const existingIds = baseRecord.observedMessageIds ?? [];
32463
33021
  const observedMessageIds = [.../* @__PURE__ */ new Set([...existingIds, ...newMessageIds])];
32464
33022
  const observationTokens = this.tokenCounter.countObservations(currentObservations);
32465
33023
  return {
@@ -32473,8 +33031,25 @@ var ResourceScopedObservationStrategy = class extends ObservationStrategy {
32473
33031
  }
32474
33032
  async persist(processed) {
32475
33033
  const { record, resourceId } = this.opts;
33034
+ const head = await getLineageHead(this.storage, record);
33035
+ if (!head) return {
33036
+ status: "not-committed",
33037
+ reason: "the observational memory record was cleared"
33038
+ };
33039
+ const committed = await this.commitActiveObservationsToHead({
33040
+ processed,
33041
+ composedFrom: this.composedFrom,
33042
+ target: head,
33043
+ recompose: (fresh) => this.compose(fresh.activeObservations ?? "", fresh),
33044
+ cycleMessageIds: this.observationResults.flatMap((r) => r.threadMessages.map((m) => m.id))
33045
+ });
33046
+ if (!committed) return {
33047
+ status: "not-committed",
33048
+ reason: "the observational memory head kept changing during the commit"
33049
+ };
33050
+ processed = committed.processed;
32476
33051
  const threadUpdateMarkers = [];
32477
- if (processed.threadMetadataUpdates) for (const update of processed.threadMetadataUpdates) {
33052
+ if (processed.threadMetadataUpdates && !committed.alreadyCovered) for (const update of processed.threadMetadataUpdates) {
32478
33053
  const thread = await this.storage.getThreadById({ threadId: update.threadId });
32479
33054
  if (thread) {
32480
33055
  const oldTitle = thread.title?.trim();
@@ -32507,15 +33082,13 @@ var ResourceScopedObservationStrategy = class extends ObservationStrategy {
32507
33082
  }
32508
33083
  }
32509
33084
  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;
33085
+ 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)));
33086
+ return {
33087
+ status: "committed",
33088
+ processed,
33089
+ record: committed.record,
33090
+ alreadyCovered: committed.alreadyCovered
33091
+ };
32519
33092
  }
32520
33093
  async emitEndMarkers(cycleId, processed) {
32521
33094
  for (const obsResult of this.observationResults) {
@@ -32572,7 +33145,8 @@ ObservationStrategy.create = ((om, opts) => {
32572
33145
  observedMessageIds: om.observedMessageIds,
32573
33146
  obscureThreadIds: om.getObscureThreadIds(),
32574
33147
  onIndexObservations: om.onIndexObservations,
32575
- emitDebugEvent: (e) => om.emitDebugEvent(e)
33148
+ emitDebugEvent: (e) => om.emitDebugEvent(e),
33149
+ prepareObserverContext: (observations, record) => om.prepareObserverContext(observations, record)
32576
33150
  };
32577
33151
  if (opts.cycleId) return new AsyncBufferObservationStrategy(deps, opts);
32578
33152
  if (deps.scope === "resource" && opts.resourceId) return new ResourceScopedObservationStrategy(deps, opts);
@@ -32698,8 +33272,8 @@ var ObservationStep = class {
32698
33272
  let reflected = false;
32699
33273
  let didThresholdCleanup = false;
32700
33274
  let observerExchange;
33275
+ const step0Messages = this.stepNumber === 0 ? getObservableMessages(messageList) : [];
32701
33276
  if (this.stepNumber === 0) {
32702
- const step0Messages = getObservableMessages(messageList);
32703
33277
  const activation = await om.activate({
32704
33278
  threadId,
32705
33279
  resourceId,
@@ -32756,7 +33330,17 @@ var ObservationStep = class {
32756
33330
  record: this.turn.record,
32757
33331
  messages: getObservableMessages(messageList)
32758
33332
  });
32759
- if (statusSnapshot.shouldBuffer) {
33333
+ if (statusSnapshot.inAsyncObservationBand && !statusSnapshot.canActivate && !hasIncompleteToolCalls) {
33334
+ await this.turn.refreshRecord();
33335
+ statusSnapshot = await om.getStatus({
33336
+ threadId,
33337
+ resourceId,
33338
+ record: this.turn.record,
33339
+ messages: getObservableMessages(messageList)
33340
+ });
33341
+ }
33342
+ const activateBeforeBuffering = statusSnapshot.inAsyncObservationBand && statusSnapshot.canActivate && !hasIncompleteToolCalls;
33343
+ if (statusSnapshot.shouldBuffer && !activateBeforeBuffering) {
32760
33344
  const allMessages = getObservableMessages(messageList);
32761
33345
  const unobservedMessages = om.getUnobservedMessages(allMessages, statusSnapshot.record);
32762
33346
  const candidates = om.getUnobservedMessages(unobservedMessages, statusSnapshot.record, { excludeBuffered: true });
@@ -32770,8 +33354,10 @@ var ObservationStep = class {
32770
33354
  omDebug(`[OM:buffer] onBufferChunkSealed hook failed: ${error instanceof Error ? error.message : String(error)}`);
32771
33355
  }
32772
33356
  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");
33357
+ const inputIds = new Set(messageList.get.input.db().map((msg) => msg.id));
33358
+ const sealedOutput = safeCandidates.filter((msg) => !inputIds.has(msg.id));
33359
+ messageList.removeByIds(sealedOutput.map((msg) => msg.id));
33360
+ for (const msg of sealedOutput) messageList.add(msg, "memory");
32775
33361
  }
32776
33362
  if (!deferred) {
32777
33363
  om.trackBackgroundWork(om.buffer({
@@ -32793,7 +33379,8 @@ var ObservationStep = class {
32793
33379
  buffered = true;
32794
33380
  }
32795
33381
  }
32796
- const willObserveNow = statusSnapshot.shouldObserve && !hasIncompleteToolCalls;
33382
+ const bandDefersObservation = statusSnapshot.inAsyncObservationBand && !statusSnapshot.canActivate;
33383
+ const willObserveNow = statusSnapshot.shouldObserve && !hasIncompleteToolCalls && !bandDefersObservation;
32797
33384
  /** In-flight message ids the step-0 cleanup must never remove from live context. */
32798
33385
  let step0PreserveIds;
32799
33386
  if (this.stepNumber > 0 || willObserveNow) {
@@ -32835,7 +33422,7 @@ var ObservationStep = class {
32835
33422
  }
32836
33423
  if (willObserveNow) {
32837
33424
  const preObsGeneration = this.turn.record.generationCount;
32838
- const obsResult = await this.runThresholdObservation();
33425
+ const obsResult = await this.runThresholdObservation(statusSnapshot.inAsyncObservationBand);
32839
33426
  observerExchange = obsResult.observerExchange;
32840
33427
  if (obsResult.succeeded) {
32841
33428
  observed = true;
@@ -32908,12 +33495,12 @@ var ObservationStep = class {
32908
33495
  /**
32909
33496
  * Run the full threshold observation pipeline:
32910
33497
  * waitForBuffering → re-check → activate buffered chunks → reflect → observe
32911
- * (sync fallback when pending tokens are still at or above the threshold)
33498
+ * (sync fallback at blockAfter when async buffering is enabled)
32912
33499
  */
32913
- async runThresholdObservation() {
33500
+ async runThresholdObservation(inAsyncObservationBand) {
32914
33501
  const { threadId, resourceId, messageList } = this.turn;
32915
33502
  const om = this.turn.om;
32916
- await om.waitForBuffering(threadId, resourceId);
33503
+ if (!inAsyncObservationBand) await om.waitForBuffering(threadId, resourceId);
32917
33504
  await this.turn.refreshRecord();
32918
33505
  const observableMessages = this.seededResponseMessage ? getObservableMessages(messageList).filter((msg) => msg.id !== this.turn.responseMessageId) : getObservableMessages(messageList);
32919
33506
  let status = await om.getStatus({
@@ -32942,7 +33529,16 @@ var ObservationStep = class {
32942
33529
  messageList
32943
33530
  });
32944
33531
  this.turn.setRecord(activation.record);
32945
- if (!activation.activated) break;
33532
+ if (!activation.activated) {
33533
+ await this.turn.refreshRecord();
33534
+ status = await om.getStatus({
33535
+ threadId,
33536
+ resourceId,
33537
+ record: this.turn.record,
33538
+ messages: pendingMessages
33539
+ });
33540
+ break;
33541
+ }
32946
33542
  activated = true;
32947
33543
  const ids = new Set(activation.activatedMessageIds ?? []);
32948
33544
  activatedMessageIds.push(...ids);
@@ -32954,11 +33550,17 @@ var ObservationStep = class {
32954
33550
  messages: pendingMessages
32955
33551
  });
32956
33552
  }
33553
+ if (status.shouldObserve && status.canActivate) return {
33554
+ succeeded: activated,
33555
+ record: this.turn.record,
33556
+ activatedMessageIds: activated ? activatedMessageIds : void 0,
33557
+ cleanupMessageIds: activatedMessageIds
33558
+ };
32957
33559
  if (activated) {
32958
- const postActivationRecord = this.turn.record;
33560
+ const preReflectionRecord = this.turn.record;
32959
33561
  await om.reflector.maybeReflect({
32960
- record: postActivationRecord,
32961
- observationTokens: postActivationRecord.observationTokenCount ?? 0,
33562
+ record: preReflectionRecord,
33563
+ observationTokens: preReflectionRecord.observationTokenCount ?? 0,
32962
33564
  threadId,
32963
33565
  writer: this.turn.writer,
32964
33566
  messageList,
@@ -32973,6 +33575,14 @@ var ObservationStep = class {
32973
33575
  }),
32974
33576
  trigger: "turn-sync"
32975
33577
  });
33578
+ await this.turn.refreshRecord();
33579
+ const postActivationRecord = this.turn.record;
33580
+ status = await om.getStatus({
33581
+ threadId,
33582
+ resourceId,
33583
+ record: postActivationRecord,
33584
+ messages: pendingMessages
33585
+ });
32976
33586
  if (!status.shouldObserve) return {
32977
33587
  succeeded: true,
32978
33588
  record: postActivationRecord,
@@ -32980,6 +33590,12 @@ var ObservationStep = class {
32980
33590
  cleanupMessageIds: activatedMessageIds
32981
33591
  };
32982
33592
  }
33593
+ if (status.inAsyncObservationBand || status.canActivate) return {
33594
+ succeeded: activated,
33595
+ record: this.turn.record,
33596
+ activatedMessageIds: activated ? activatedMessageIds : void 0,
33597
+ cleanupMessageIds: activatedMessageIds
33598
+ };
32983
33599
  let obsResult;
32984
33600
  try {
32985
33601
  obsResult = await om.observe({
@@ -33327,13 +33943,18 @@ Take the existing observations and rewrite them to make it easier to continue in
33327
33943
 
33328
33944
  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
33945
 
33946
+ 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.
33947
+
33330
33948
  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)
33949
+ - Keep every concrete fact: names, places, dates, numbers, amounts, durations, titles, versions, and who said what. Merge duplicates, but never drop specifics
33950
+ - Keep every dated statement with its date, and retain the timestamps that matter (start times, completion times, significant events)
33951
+ - 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
33952
+ - 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
33953
+ - Prefer many short factual lines over prose summaries. Do not replace facts with generalities like "discussed travel plans"
33333
33954
  - Combine related items where it makes sense (e.g., "agent called view tool 5 times on file x")
33334
33955
  - Preserve ✅ completion markers — they are memory signals that tell the assistant what is already resolved and help prevent repeated work
33335
33956
  - 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
33957
+ - Condense the wording of older observations more than recent ones, but keep their facts
33337
33958
 
33338
33959
  CRITICAL: USER ASSERTIONS vs QUESTIONS
33339
33960
  - "User stated: X" = authoritative assertion (user told us something about themselves)
@@ -33613,6 +34234,25 @@ async function withAbortCheck(fn, abortSignal) {
33613
34234
  * size while still letting them fire sooner than a threshold activation.
33614
34235
  */
33615
34236
  const EARLY_ACTIVATION_SIZE_FLOOR_RATIO = .75;
34237
+ /** A reflection commit that created nothing because the reflected record is no longer the head. */
34238
+ var ReflectionNotAppliedError = class extends Error {
34239
+ constructor(recordId, headId) {
34240
+ super(`Reflection not applied: record ${recordId} is no longer the current generation (head is ${headId})`);
34241
+ this.name = "ReflectionNotAppliedError";
34242
+ }
34243
+ };
34244
+ /**
34245
+ * Whether a reflection commit created the generation the caller asked for. Storage creates
34246
+ * nothing (and returns the current head or the stored record) when the reflected record was
34247
+ * already retired or its observations were rewritten since the reflection read them.
34248
+ *
34249
+ * Adapters that predate `newRecordId` ignore it and do not report `supersededBy`; for those a
34250
+ * returned record other than the reflected one means a new generation was created.
34251
+ */
34252
+ function isReflectionApplied(returned, newRecordId, reflectedRecordId) {
34253
+ if (returned.id === newRecordId) return true;
34254
+ return returned.supersededBy === void 0 && returned.id !== reflectedRecordId;
34255
+ }
33616
34256
  /**
33617
34257
  * Runs the Reflector agent for compressing observations.
33618
34258
  * Handles synchronous reflection, async buffered reflection, and activation.
@@ -33976,14 +34616,14 @@ var ReflectorRunner = class {
33976
34616
  * Perform async buffered reflection — reflects observations and stores to bufferedReflection.
33977
34617
  * Does NOT create a new generation or update activeObservations.
33978
34618
  */
33979
- async doAsyncBufferedReflection(record, _bufferKey, writer, requestContext, observabilityContext, priorExtractedValues, mainAgent, sendSignal, trigger, currentModel) {
34619
+ async doAsyncBufferedReflection(record, bufferKey, writer, requestContext, observabilityContext, priorExtractedValues, mainAgent, sendSignal, trigger, currentModel) {
33980
34620
  const currentRecord = await this.storage.getObservationalMemory(record.threadId, record.resourceId) ?? record;
33981
34621
  const observationTokens = currentRecord.observationTokenCount ?? 0;
33982
34622
  const reflectThreshold = getMaxThreshold(this.getEffectiveReflectionTokens(currentRecord));
33983
34623
  const bufferActivation = this.reflectionConfig.bufferActivation ?? .5;
33984
34624
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
33985
34625
  const cycleId = `reflect-buf-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
33986
- BufferingCoordinator.reflectionBufferCycleIds.set(_bufferKey, cycleId);
34626
+ BufferingCoordinator.reflectionBufferCycleIds.set(bufferKey, cycleId);
33987
34627
  const allLines = (currentRecord.activeObservations ?? "").split("\n");
33988
34628
  const totalLines = allLines.length;
33989
34629
  const avgTokensPerLine = totalLines > 0 ? observationTokens / totalLines : 0;
@@ -34022,13 +34662,24 @@ var ReflectorRunner = class {
34022
34662
  await persistThreadExtractedValues(this.storage, this.reflectionConfig.extractors, currentRecord.threadId ?? void 0, reflectResult.extractedValues);
34023
34663
  const reflectionTokenCount = this.tokenCounter.countObservations(reflectResult.observations);
34024
34664
  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
- });
34665
+ if (!await runOMCommit(this.buffering.getLockKey(currentRecord.threadId, currentRecord.resourceId), async () => {
34666
+ if ((await this.storage.getObservationalMemory(currentRecord.threadId, currentRecord.resourceId))?.id !== currentRecord.id) return false;
34667
+ await this.storage.updateBufferedReflection({
34668
+ id: currentRecord.id,
34669
+ reflection: reflectResult.observations,
34670
+ tokenCount: reflectionTokenCount,
34671
+ inputTokenCount: sliceTokenEstimate,
34672
+ reflectedObservationLineCount
34673
+ });
34674
+ return true;
34675
+ })) {
34676
+ omDebug(`[OM:reflect] doAsyncBufferedReflection: record ${currentRecord.id} is no longer the head; discarding the buffered reflection`);
34677
+ BufferingCoordinator.lastBufferedBoundary.delete(bufferKey);
34678
+ return {
34679
+ usage: reflectResult.usage,
34680
+ providerMetadata: reflectResult.providerMetadata
34681
+ };
34682
+ }
34032
34683
  omDebug(`[OM:reflect] doAsyncBufferedReflection: bufferedReflection saved with lineCount=${reflectedObservationLineCount}`);
34033
34684
  if (writer) {
34034
34685
  const endMarker = createBufferingEndMarker({
@@ -34107,10 +34758,16 @@ var ReflectorRunner = class {
34107
34758
  }
34108
34759
  }
34109
34760
  omDebug(`[OM:reflect] tryActivateBufferedReflection: activating, beforeTokens=${beforeTokens}, combinedTokenCount=${combinedTokenCount}, reflectedLineCount=${reflectedLineCount}, unreflectedLines=${unreflectedLines.length}`);
34110
- await this.storage.swapBufferedReflectionToActive({
34761
+ const newRecordId = crypto.randomUUID();
34762
+ const swapped = await runOMCommit(lockKey, () => this.storage.swapBufferedReflectionToActive({
34111
34763
  currentRecord: freshRecord,
34112
- tokenCount: combinedTokenCount
34113
- });
34764
+ tokenCount: combinedTokenCount,
34765
+ newRecordId
34766
+ }), { priority: "reflection" });
34767
+ if (!isReflectionApplied(swapped, newRecordId, freshRecord.id)) {
34768
+ omDebug(`[OM:reflect] tryActivateBufferedReflection: buffered reflection on ${freshRecord.id} not applied (head is ${swapped.id})`);
34769
+ return { status: "not-applied" };
34770
+ }
34114
34771
  if (committedContext) await this.notifyReflectionCommitted({
34115
34772
  ...committedContext,
34116
34773
  observations: allLines.slice(0, reflectedLineCount).join("\n").trim(),
@@ -34165,7 +34822,8 @@ var ReflectorRunner = class {
34165
34822
  * @internal Used by observation strategies. Do not call directly.
34166
34823
  */
34167
34824
  async maybeReflect(opts) {
34168
- const { record, observationTokens, writer, abortSignal, mainAgent, sendSignal, sendStateSignal, messageList, currentModel, reflectionHooks, trigger, requestContext, observabilityContext, lastActivityAt, threadId: requestedThreadId } = opts;
34825
+ const { record: callerRecord, observationTokens, writer, abortSignal, mainAgent, sendSignal, sendStateSignal, messageList, currentModel, reflectionHooks, trigger, requestContext, observabilityContext, lastActivityAt, threadId: requestedThreadId } = opts;
34826
+ const record = { ...callerRecord };
34169
34827
  const lockKey = this.buffering.getLockKey(record.threadId, record.resourceId);
34170
34828
  const reflectThreshold = getMaxThreshold(this.getEffectiveReflectionTokens(record));
34171
34829
  const priorExtractedValues = await getThreadExtractedValues(this.storage, this.reflectionConfig.extractors, requestedThreadId ?? record.threadId);
@@ -34219,7 +34877,7 @@ var ReflectorRunner = class {
34219
34877
  abortSignal,
34220
34878
  observabilityContext
34221
34879
  });
34222
- if (activationResult.status === "activated") return;
34880
+ if (activationResult.status === "activated" || activationResult.status === "not-applied") return;
34223
34881
  if (activationResult.status === "suppressed") {
34224
34882
  omDebug(`[OM:reflect] skipping sync fallback / re-buffer after suppressed early ${activationMetadata.triggeredBy} activation (reason=${activationResult.reason})`);
34225
34883
  return;
@@ -34302,13 +34960,19 @@ var ReflectorRunner = class {
34302
34960
  reflectResult.observations = await applyTextTransform(this.hooks, "afterReflection", reflectResult.observations, hookContext);
34303
34961
  reflectionUsage = reflectResult.usage;
34304
34962
  reflectionProviderMetadata = reflectResult.providerMetadata;
34305
- await persistThreadExtractedValues(this.storage, this.reflectionConfig.extractors, record.threadId ?? void 0, reflectResult.extractedValues);
34306
34963
  const reflectionTokenCount = this.tokenCounter.countObservations(reflectResult.observations);
34307
- await this.storage.createReflectionGeneration({
34964
+ const newRecordId = crypto.randomUUID();
34965
+ const committedRecord = await runOMCommit(this.buffering.getLockKey(record.threadId, record.resourceId), () => this.storage.createReflectionGeneration({
34308
34966
  currentRecord: record,
34309
34967
  reflection: reflectResult.observations,
34310
- tokenCount: reflectionTokenCount
34311
- });
34968
+ tokenCount: reflectionTokenCount,
34969
+ newRecordId
34970
+ }), { priority: "reflection" });
34971
+ if (!isReflectionApplied(committedRecord, newRecordId, record.id)) {
34972
+ omDebug(`[OM:reflect] reflection of ${record.id} not applied (head is ${committedRecord.id})`);
34973
+ throw new ReflectionNotAppliedError(record.id, committedRecord.id);
34974
+ }
34975
+ await persistThreadExtractedValues(this.storage, this.reflectionConfig.extractors, record.threadId ?? void 0, reflectResult.extractedValues);
34312
34976
  if (reflectionTokenCount >= reflectThreshold) this.syncReflectionSuppression.set(lockKey, reflectionTokenCount);
34313
34977
  else this.syncReflectionSuppression.delete(lockKey);
34314
34978
  await this.notifyReflectionCommitted({
@@ -34388,6 +35052,7 @@ var ReflectorRunner = class {
34388
35052
  failureKind: failedMarker.data.failureKind,
34389
35053
  error: reflectionError.message
34390
35054
  });
35055
+ if (error instanceof ReflectionNotAppliedError) return;
34391
35056
  if (lifecycleError !== void 0 || abortSignal?.aborted || this.reflectionConfig.failurePolicy !== "continue" || !isOmModelExecutionError(error) || error.failureKind !== "reflector-model") throw error;
34392
35057
  this.syncReflectionSuppression.set(lockKey, observationTokens);
34393
35058
  omError("[OM] Reflection failed", error);
@@ -34727,6 +35392,8 @@ function writeProcessInputStepReproCapture(params) {
34727
35392
  * Returns the parts from the latest step of a message (after the last step-start marker).
34728
35393
  * If no step-start marker exists, returns all parts.
34729
35394
  */
35395
+ /** Re-reads of the head after an activation swap hit a generation retired by a reflection. */
35396
+ const MAX_ACTIVATION_RETIRED_RETRIES = 3;
34730
35397
  function getLatestStepParts(parts) {
34731
35398
  for (let i = parts.length - 1; i >= 0; i--) if (parts[i]?.type === "step-start") return parts.slice(i + 1);
34732
35399
  return parts;
@@ -34920,6 +35587,8 @@ var ObservationalMemory = class ObservationalMemory {
34920
35587
  reflector;
34921
35588
  /** Buffering state coordinator — manages static maps and buffering lifecycle. */
34922
35589
  buffering;
35590
+ /** Unresolved observation blockAfter (multiplier or absolute); resolved per record. */
35591
+ observationBlockAfterSetting;
34923
35592
  shouldObscureThreadIds = false;
34924
35593
  hasher = e();
34925
35594
  mastra;
@@ -35055,6 +35724,7 @@ var ObservationalMemory = class ObservationalMemory {
35055
35724
  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
35725
  const observationActivateAfterIdle = config.observation?.activateAfterIdle ?? config.activateAfterIdle;
35057
35726
  const observationActivateAfterIdlePath = config.observation?.activateAfterIdle !== void 0 ? "observation.activateAfterIdle" : "activateAfterIdle";
35727
+ this.observationBlockAfterSetting = asyncBufferingDisabled ? void 0 : config.observation?.blockAfter ?? (config.observation?.bufferTokens ?? OBSERVATIONAL_MEMORY_DEFAULTS.observation.bufferTokens ? 1.2 : void 0);
35058
35728
  this.observationConfig = {
35059
35729
  model: observationModel,
35060
35730
  maxRetries: config.observation?.maxRetries ?? RETRY_CONFIG.maxRetries,
@@ -35075,8 +35745,8 @@ var ObservationalMemory = class ObservationalMemory {
35075
35745
  bufferActivation: asyncBufferingDisabled ? void 0 : config.observation?.bufferActivation ?? OBSERVATIONAL_MEMORY_DEFAULTS.observation.bufferActivation,
35076
35746
  activateAfterIdle: parseActivationTTLConfig(observationActivateAfterIdle, observationActivateAfterIdlePath),
35077
35747
  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,
35748
+ blockAfter: resolveBlockAfter(this.observationBlockAfterSetting, config.observation?.messageTokens ?? OBSERVATIONAL_MEMORY_DEFAULTS.observation.messageTokens),
35749
+ previousObserverTokens: config.observation?.previousObserverTokens ?? false,
35080
35750
  instruction: config.observation?.instruction,
35081
35751
  threadTitle: config.observation?.threadTitle ?? false,
35082
35752
  observeAttachments: config.observation?.observeAttachments ?? [...OBSERVATIONAL_MEMORY_DEFAULTS.observation.observeAttachments],
@@ -35385,6 +36055,13 @@ var ObservationalMemory = class ObservationalMemory {
35385
36055
  return this.observationConfig.messageTokens;
35386
36056
  }
35387
36057
  /**
36058
+ * Resolve the observation blockAfter for a record. A multiplier scales the record's
36059
+ * effective messageTokens (per-record overrides included); an absolute value is used as-is.
36060
+ */
36061
+ getEffectiveObservationBlockAfter(record) {
36062
+ return resolveBlockAfter(this.observationBlockAfterSetting, this.getEffectiveMessageTokens(record));
36063
+ }
36064
+ /**
35388
36065
  * Resolve the effective reflection observationTokens for a record.
35389
36066
  * Only explicit per-record overrides (stored under `_overrides`) win;
35390
36067
  * the initial config snapshot is ignored so instance-level changes
@@ -35498,18 +36175,21 @@ var ObservationalMemory = class ObservationalMemory {
35498
36175
  * the latest user input that triggered it.
35499
36176
  * @internal Used by observation strategies and the reflector. Do not call directly.
35500
36177
  */
35501
- async persistMarkerToStorage(marker, threadId, resourceId) {
36178
+ async persistMarkerToStorage(marker, threadId, resourceId, opts) {
35502
36179
  try {
35503
- const messages = (await this.storage.listMessages({
36180
+ const markerData = marker.data;
36181
+ const targetRole = markerData?.operationType === "reflection" ? "user" : "assistant";
36182
+ const preferred = [...opts?.preferMessages ?? []].reverse().find((m) => m.role === targetRole && (!m.threadId || m.threadId === threadId));
36183
+ const preferredStored = preferred ? (await this.storage.listMessagesById({ messageIds: [preferred.id] })).messages : [];
36184
+ const messages = (preferredStored.length ? { messages: preferredStored } : await this.storage.listMessages({
35504
36185
  threadId,
35505
36186
  perPage: 20,
35506
36187
  orderBy: {
35507
36188
  field: "createdAt",
35508
36189
  direction: "DESC"
35509
- }
36190
+ },
36191
+ ...opts?.notAfter ? { filter: { dateRange: { end: opts.notAfter } } } : {}
35510
36192
  }))?.messages ?? [];
35511
- const markerData = marker.data;
35512
- const targetRole = markerData?.operationType === "reflection" ? "user" : "assistant";
35513
36193
  for (const msg of messages) if (msg?.role === targetRole && msg.content?.parts && Array.isArray(msg.content.parts)) {
35514
36194
  if (!(markerData?.cycleId && msg.content.parts.some((p) => p?.type === marker.type && p?.data?.cycleId === markerData.cycleId))) msg.content.parts.push(marker);
35515
36195
  await this.messageHistory.persistMessages({
@@ -35596,7 +36276,7 @@ var ObservationalMemory = class ObservationalMemory {
35596
36276
  */
35597
36277
  createUnobservedMessage(message) {
35598
36278
  const unobservedParts = getUnobservedParts(message);
35599
- if (unobservedParts.length === 0) return null;
36279
+ if (unobservedParts.every((part) => part?.type?.startsWith("data-om-"))) return null;
35600
36280
  return {
35601
36281
  ...message,
35602
36282
  content: {
@@ -35968,17 +36648,17 @@ ${formattedMessages}
35968
36648
  async startAsyncBufferedObservation(record, threadId, unobservedMessages, lockKey, writer, contextWindowTokens, requestContext, observabilityContext) {
35969
36649
  const bufferKey = this.buffering.getObservationBufferKey(lockKey);
35970
36650
  const currentTokens = contextWindowTokens ?? await this.tokenCounter.countMessagesAsync(unobservedMessages) + (record.pendingMessageTokens ?? 0);
35971
- BufferingCoordinator.lastBufferedBoundary.set(bufferKey, currentTokens);
36651
+ BufferingCoordinator.setObservationBoundary(bufferKey, currentTokens);
35972
36652
  registerOp(record.id, "bufferingObservation");
35973
- this.storage.setBufferingObservationFlag(record.id, true, currentTokens).catch((err) => {
36653
+ runOMCommit(lockKey, () => this.storage.setBufferingObservationFlag(record.id, true, currentTokens)).catch((err) => {
35974
36654
  omError("[OM] Failed to set buffering observation flag", err);
35975
36655
  });
35976
36656
  const asyncOp = this.runAsyncBufferedObservation(record, threadId, unobservedMessages, bufferKey, writer, requestContext, observabilityContext).catch((err) => {
35977
36657
  omError("[OM] async buffering observation failed", err);
35978
36658
  }).finally(() => {
35979
- BufferingCoordinator.asyncBufferingOps.delete(bufferKey);
36659
+ if (BufferingCoordinator.asyncBufferingOps.get(bufferKey) === asyncOp) BufferingCoordinator.asyncBufferingOps.delete(bufferKey);
35980
36660
  unregisterOp(record.id, "bufferingObservation");
35981
- this.storage.setBufferingObservationFlag(record.id, false).catch((err) => {
36661
+ runOMCommit(lockKey, () => this.storage.setBufferingObservationFlag(record.id, false)).catch((err) => {
35982
36662
  omError("[OM] Failed to clear buffering observation flag", err);
35983
36663
  });
35984
36664
  });
@@ -35993,6 +36673,14 @@ ${formattedMessages}
35993
36673
  if (existingOp) try {
35994
36674
  await existingOp;
35995
36675
  } catch {}
36676
+ const releaseChunkWrite = BufferingCoordinator.trackChunkWrite(bufferKey);
36677
+ try {
36678
+ await this.bufferObservationChunk(record, threadId, unobservedMessages, bufferKey, releaseChunkWrite, writer, requestContext, observabilityContext);
36679
+ } finally {
36680
+ releaseChunkWrite();
36681
+ }
36682
+ }
36683
+ async bufferObservationChunk(record, threadId, unobservedMessages, bufferKey, releaseChunkWrite, writer, requestContext, observabilityContext) {
35996
36684
  const freshRecord = await this.storage.getObservationalMemory(record.threadId, record.resourceId);
35997
36685
  if (!freshRecord) return;
35998
36686
  let bufferCursor = BufferingCoordinator.lastBufferedAtTime.get(bufferKey) ?? freshRecord.lastBufferedAtTime ?? null;
@@ -36033,13 +36721,16 @@ ${formattedMessages}
36033
36721
  threadIds: [threadId],
36034
36722
  config: this.getObservationMarkerConfig()
36035
36723
  });
36036
- await this.persistMarkerToStorage(startMarker, threadId, freshRecord.resourceId ?? void 0);
36724
+ await this.persistMarkerToStorage(startMarker, threadId, freshRecord.resourceId ?? void 0, {
36725
+ notAfter: new Date(startedAt),
36726
+ preferMessages: messagesToBuffer
36727
+ });
36037
36728
  if (writer) writer.custom({
36038
36729
  ...startMarker,
36039
36730
  transient: true
36040
36731
  }).catch(() => {});
36041
36732
  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({
36733
+ await this.runBufferedObservationCycle({
36043
36734
  threadId,
36044
36735
  resourceId: freshRecord.resourceId ?? void 0,
36045
36736
  trigger: "async-buffer"
@@ -36053,12 +36744,13 @@ ${formattedMessages}
36053
36744
  writer,
36054
36745
  requestContext,
36055
36746
  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
- }
36747
+ trigger: "async-buffer",
36748
+ onBufferedChunkPersisted: async () => {
36749
+ const maxTs = this.getMaxMessageTimestamp(messagesToBuffer);
36750
+ BufferingCoordinator.lastBufferedAtTime.set(bufferKey, new Date(maxTs.getTime() + 1));
36751
+ releaseChunkWrite();
36752
+ }
36753
+ }).run());
36062
36754
  }
36063
36755
  /**
36064
36756
  * Trigger async buffered observation if the token count has crossed a new interval.
@@ -36243,8 +36935,8 @@ ${formattedMessages}
36243
36935
  const { threadId, resourceId, recordId, activatedMessageIds } = opts;
36244
36936
  const lockKey = this.buffering.getLockKey(threadId, resourceId);
36245
36937
  const bufKey = this.buffering.getObservationBufferKey(lockKey);
36246
- BufferingCoordinator.lastBufferedBoundary.set(bufKey, 0);
36247
- await this.storage.setBufferingObservationFlag(recordId, false, 0).catch(() => {});
36938
+ BufferingCoordinator.setObservationBoundary(bufKey, 0);
36939
+ await runOMCommit(lockKey, () => this.storage.setBufferingObservationFlag(recordId, false, 0)).catch(() => {});
36248
36940
  if (activatedMessageIds && activatedMessageIds.length > 0) this.buffering.cleanupStaticMaps(threadId, resourceId, activatedMessageIds);
36249
36941
  }
36250
36942
  /**
@@ -36403,9 +37095,13 @@ ${formattedMessages}
36403
37095
  * @example
36404
37096
  * ```ts
36405
37097
  * const status = await om.getStatus({ threadId });
36406
- * if (status.shouldObserve) {
37098
+ * if (status.shouldObserve && status.canActivate) {
37099
+ * await om.activate({ threadId });
37100
+ * } else if (status.shouldObserve && !status.inAsyncObservationBand) {
37101
+ * // At/above blockAfter (or async buffering disabled): observe synchronously
36407
37102
  * await om.observe({ threadId });
36408
37103
  * } else if (status.shouldBuffer) {
37104
+ * // Below the threshold, or in the threshold→blockAfter band: buffer in the background
36409
37105
  * await om.buffer({ threadId });
36410
37106
  * }
36411
37107
  * if (status.shouldReflect) {
@@ -36435,8 +37131,10 @@ ${formattedMessages}
36435
37131
  const bufferedChunkCount = bufferedChunks.length;
36436
37132
  const bufferedChunkTokens = bufferedChunks.reduce((sum, chunk) => sum + (chunk.messageTokens ?? 0), 0);
36437
37133
  const asyncObservationEnabled = this.buffering.isAsyncObservationEnabled();
37134
+ const observationBlockAfter = asyncObservationEnabled ? this.getEffectiveObservationBlockAfter(record) : void 0;
37135
+ const inAsyncObservationBand = observationBlockAfter !== void 0 && pendingTokens >= threshold && pendingTokens < observationBlockAfter;
36438
37136
  let shouldBuffer = false;
36439
- if (asyncObservationEnabled && pendingTokens < threshold) {
37137
+ if (asyncObservationEnabled && (pendingTokens < threshold || inAsyncObservationBand)) {
36440
37138
  const lockKey = this.buffering.getLockKey(threadId, resourceId);
36441
37139
  shouldBuffer = this.buffering.shouldTriggerAsyncObservation(pendingTokens, lockKey, record, this.storage, threshold);
36442
37140
  }
@@ -36461,6 +37159,8 @@ ${formattedMessages}
36461
37159
  canActivate,
36462
37160
  asyncObservationEnabled,
36463
37161
  asyncReflectionEnabled: this.buffering.isAsyncReflectionEnabled(),
37162
+ observationBlockAfter,
37163
+ inAsyncObservationBand,
36464
37164
  scope: this.scope
36465
37165
  };
36466
37166
  }
@@ -36587,24 +37287,27 @@ ${formattedMessages}
36587
37287
  const currentTokens = opts.pendingTokens ?? record.pendingMessageTokens ?? 0;
36588
37288
  const lockKey = this.buffering.getLockKey(threadId, resourceId);
36589
37289
  const bufferKey = this.buffering.getObservationBufferKey(lockKey);
36590
- BufferingCoordinator.lastBufferedBoundary.set(bufferKey, currentTokens);
36591
- if (record.isBufferingObservation) await this.storage.setBufferingObservationFlag(record.id, false).catch(() => {});
37290
+ const boundaryOwner = Symbol("observation-boundary");
37291
+ BufferingCoordinator.setObservationBoundary(bufferKey, currentTokens, boundaryOwner);
36592
37292
  const existingOp = BufferingCoordinator.asyncBufferingOps.get(bufferKey);
37293
+ let resolveOp;
37294
+ const opPromise = new Promise((resolve) => {
37295
+ resolveOp = resolve;
37296
+ });
37297
+ BufferingCoordinator.asyncBufferingOps.set(bufferKey, opPromise);
37298
+ if (record.isBufferingObservation) await runOMCommit(lockKey, () => this.storage.setBufferingObservationFlag(record.id, false)).catch(() => {});
36593
37299
  if (existingOp) try {
36594
37300
  await existingOp;
36595
37301
  } catch {}
36596
- registerOp(record.id, "bufferingObservation");
37302
+ const opRecordId = record.id;
37303
+ registerOp(opRecordId, "bufferingObservation");
37304
+ const releaseChunkWrite = BufferingCoordinator.trackChunkWrite(bufferKey);
37305
+ const ownsStartBoundary = BufferingCoordinator.ownsObservationBoundary(bufferKey, boundaryOwner);
36597
37306
  inMemoryRecord.isBufferingObservation = true;
36598
- inMemoryRecord.lastBufferedAtTokens = currentTokens;
36599
- this.storage.setBufferingObservationFlag(record.id, true, currentTokens).catch((err) => {
37307
+ if (ownsStartBoundary) inMemoryRecord.lastBufferedAtTokens = currentTokens;
37308
+ runOMCommit(lockKey, () => this.storage.setBufferingObservationFlag(record.id, true, ownsStartBoundary ? currentTokens : void 0)).catch((err) => {
36600
37309
  omError("[OM] Failed to set buffering observation flag", err);
36601
37310
  });
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
37311
  const setBufferingState = (isBufferingObservation, lastBufferedAtTokens) => {
36609
37312
  inMemoryRecord.isBufferingObservation = isBufferingObservation;
36610
37313
  record.isBufferingObservation = isBufferingObservation;
@@ -36615,6 +37318,7 @@ ${formattedMessages}
36615
37318
  };
36616
37319
  let flagCleared = false;
36617
37320
  try {
37321
+ record = await this.storage.getObservationalMemory(record.threadId, record.resourceId) ?? record;
36618
37322
  let candidateMessages;
36619
37323
  if (opts.messages) candidateMessages = this.getUnobservedMessages(opts.messages, record, { excludeBuffered: true });
36620
37324
  else {
@@ -36654,12 +37358,29 @@ ${formattedMessages}
36654
37358
  threadIds: [threadId],
36655
37359
  config: this.getObservationMarkerConfig()
36656
37360
  });
36657
- await this.persistMarkerToStorage(startMarker, threadId, record.resourceId ?? void 0);
37361
+ await this.persistMarkerToStorage(startMarker, threadId, record.resourceId ?? void 0, {
37362
+ notAfter: new Date(startedAt),
37363
+ preferMessages: candidateMessages
37364
+ });
36658
37365
  const writer = opts.writer;
36659
37366
  if (writer) writer.custom({
36660
37367
  ...startMarker,
36661
37368
  transient: true
36662
37369
  }).catch(() => {});
37370
+ let chunkRecorded = false;
37371
+ const recordBufferedChunk = async () => {
37372
+ if (chunkRecorded) return;
37373
+ chunkRecorded = true;
37374
+ const ownsBoundary = BufferingCoordinator.ownsObservationBoundary(bufferKey, boundaryOwner);
37375
+ if (ownsBoundary) BufferingCoordinator.setObservationBoundary(bufferKey, newTokens, boundaryOwner);
37376
+ await runOMCommit(lockKey, () => this.storage.setBufferingObservationFlag(record.id, false, ownsBoundary ? newTokens : void 0)).catch(() => {});
37377
+ flagCleared = true;
37378
+ const stillOwnsBoundary = ownsBoundary && BufferingCoordinator.ownsObservationBoundary(bufferKey, boundaryOwner);
37379
+ setBufferingState(false, stillOwnsBoundary ? newTokens : void 0);
37380
+ const maxTimestamp = this.getMaxMessageTimestamp(candidateMessages);
37381
+ BufferingCoordinator.lastBufferedAtTime.set(bufferKey, new Date(maxTimestamp.getTime() + 1));
37382
+ releaseChunkWrite();
37383
+ };
36663
37384
  if (!(await this.runBufferedObservationCycle({
36664
37385
  threadId,
36665
37386
  resourceId: record.resourceId ?? resourceId,
@@ -36678,7 +37399,8 @@ ${formattedMessages}
36678
37399
  requestContext,
36679
37400
  currentModel: opts.currentModel,
36680
37401
  observabilityContext,
36681
- trigger: "async-buffer"
37402
+ trigger: "async-buffer",
37403
+ onBufferedChunkPersisted: recordBufferedChunk
36682
37404
  }).run()))?.observed) return {
36683
37405
  buffered: false,
36684
37406
  record
@@ -36698,13 +37420,7 @@ ${formattedMessages}
36698
37420
  newTokens
36699
37421
  }
36700
37422
  });
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);
37423
+ await recordBufferedChunk();
36708
37424
  return {
36709
37425
  buffered: true,
36710
37426
  record: await this.storage.getObservationalMemory(record.threadId, record.resourceId) ?? record
@@ -36716,12 +37432,13 @@ ${formattedMessages}
36716
37432
  record
36717
37433
  };
36718
37434
  } finally {
36719
- unregisterOp(record.id, "bufferingObservation");
36720
- BufferingCoordinator.asyncBufferingOps.delete(bufferKey);
37435
+ releaseChunkWrite();
37436
+ unregisterOp(opRecordId, "bufferingObservation");
37437
+ if (BufferingCoordinator.asyncBufferingOps.get(bufferKey) === opPromise) BufferingCoordinator.asyncBufferingOps.delete(bufferKey);
36721
37438
  resolveOp();
36722
37439
  if (!flagCleared) {
36723
37440
  setBufferingState(false);
36724
- await this.storage.setBufferingObservationFlag(record.id, false).catch(() => {});
37441
+ await runOMCommit(lockKey, () => this.storage.setBufferingObservationFlag(record.id, false)).catch(() => {});
36725
37442
  }
36726
37443
  }
36727
37444
  }
@@ -36758,8 +37475,8 @@ ${formattedMessages}
36758
37475
  const currentContextTokens = this.tokenCounter.countMessages(unobserved);
36759
37476
  if (currentContextTokens < dbBoundary * .5) {
36760
37477
  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(() => {});
37478
+ BufferingCoordinator.setObservationBoundary(bufKey, 0);
37479
+ await runOMCommit(lockKey, () => this.storage.setBufferingObservationFlag(record.id, false, 0)).catch(() => {});
36763
37480
  }
36764
37481
  }
36765
37482
  }
@@ -36805,39 +37522,63 @@ ${formattedMessages}
36805
37522
  livePendingTokens ??= status.pendingTokens;
36806
37523
  }
36807
37524
  }
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 {}
37525
+ const obsBufferKey = this.buffering.getObservationBufferKey(this.buffering.getLockKey(threadId, resourceId));
37526
+ const pendingChunkWrite = BufferingCoordinator.pendingChunkWrites.get(obsBufferKey);
37527
+ if (pendingChunkWrite) {
37528
+ const blockAfter = this.buffering.isAsyncObservationEnabled() ? this.getEffectiveObservationBlockAfter(record) : void 0;
37529
+ const threshold = calculateDynamicThreshold(this.getEffectiveMessageTokens(record), record.observationTokenCount ?? 0);
37530
+ if (activationTriggeredBy === "threshold" && !(livePendingTokens !== void 0 && blockAfter !== void 0 && livePendingTokens >= threshold && livePendingTokens < blockAfter)) {
37531
+ let timeoutId;
37532
+ await Promise.race([pendingChunkWrite, new Promise((resolve) => timeoutId = setTimeout(resolve, 6e4))]);
37533
+ clearTimeout(timeoutId);
37534
+ }
37535
+ }
37536
+ const commitKey = this.buffering.getLockKey(threadId, resourceId);
37537
+ let freshRecord = null;
37538
+ let freshChunks = [];
37539
+ let activationResult;
37540
+ for (let attempt = 0; attempt <= MAX_ACTIVATION_RETIRED_RETRIES; attempt++) {
37541
+ const swapped = await runOMCommit(commitKey, async () => {
37542
+ const head = await this.storage.getObservationalMemory(record.threadId, record.resourceId);
37543
+ if (!head) return null;
37544
+ const chunks = getBufferedChunks(head);
37545
+ if (!chunks.length) return null;
37546
+ const messageTokensThreshold = getMaxThreshold(this.getEffectiveMessageTokens(head));
37547
+ const activationRatio = resolveActivationRatio(this.observationConfig.bufferActivation ?? .7, messageTokensThreshold);
37548
+ const totalChunkMessageTokens = chunks.reduce((sum, c) => sum + (c.messageTokens ?? 0), 0);
37549
+ const currentPendingTokens = livePendingTokens ?? (head.pendingMessageTokens || totalChunkMessageTokens);
37550
+ const blockAfter = this.getEffectiveObservationBlockAfter(head);
37551
+ const forceMaxActivation = !!(blockAfter && currentPendingTokens >= blockAfter);
37552
+ if (head.pendingMessageTokens !== currentPendingTokens) await this.storage.setPendingMessageTokens(head.id, currentPendingTokens);
37553
+ return {
37554
+ head,
37555
+ chunks,
37556
+ result: await this.storage.swapBufferedToActive({
37557
+ id: head.id,
37558
+ activationRatio,
37559
+ messageTokensThreshold,
37560
+ currentPendingTokens,
37561
+ forceMaxActivation,
37562
+ bufferedChunks: chunks
37563
+ })
37564
+ };
37565
+ });
37566
+ if (!swapped) return {
37567
+ activated: false,
37568
+ record
37569
+ };
37570
+ freshRecord = swapped.head;
37571
+ freshChunks = swapped.chunks;
37572
+ activationResult = swapped.result;
37573
+ if (!activationResult.retired) break;
37574
+ omDebug(`[OM:activate] record ${freshRecord.id} was retired by a reflection; retrying on the head`);
36815
37575
  }
36816
- const freshRecord = await this.storage.getObservationalMemory(record.threadId, record.resourceId);
36817
- if (!freshRecord) return {
37576
+ if (!freshRecord || !activationResult || activationResult.retired) return {
36818
37577
  activated: false,
36819
- record
37578
+ record: await this.getRecord(threadId, resourceId) ?? record
36820
37579
  };
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(() => {});
37580
+ const swappedRecordId = freshRecord.id;
37581
+ await runOMCommit(commitKey, () => this.storage.setBufferingObservationFlag(swappedRecordId, false)).catch(() => {});
36841
37582
  unregisterOp(freshRecord.id, "bufferingObservation");
36842
37583
  const postSwapRecord = await this.storage.getObservationalMemory(record.threadId, record.resourceId);
36843
37584
  if (opts.writer && postSwapRecord && activationResult.activatedCycleIds.length > 0) {
@@ -37082,7 +37823,7 @@ ${formattedMessages}
37082
37823
  * ```
37083
37824
  */
37084
37825
  async reflect(threadId, resourceId, prompt, requestContext, observabilityContext) {
37085
- const record = await this.getOrCreateRecord(threadId, resourceId);
37826
+ const record = { ...await this.getOrCreateRecord(threadId, resourceId) };
37086
37827
  if (!record.activeObservations) return {
37087
37828
  reflected: false,
37088
37829
  record,
@@ -37127,11 +37868,23 @@ ${formattedMessages}
37127
37868
  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
37869
  reflectResult.observations = await applyTextTransform(this.hooks, "afterReflection", reflectResult.observations, hookContext);
37129
37870
  const reflectionTokenCount = this.tokenCounter.countObservations(reflectResult.observations);
37130
- await this.storage.createReflectionGeneration({
37871
+ const newRecordId = crypto.randomUUID();
37872
+ const committedRecord = await runOMCommit(this.buffering.getLockKey(threadId, resourceId), () => this.storage.createReflectionGeneration({
37131
37873
  currentRecord: record,
37132
37874
  reflection: reflectResult.observations,
37133
- tokenCount: reflectionTokenCount
37134
- });
37875
+ tokenCount: reflectionTokenCount,
37876
+ newRecordId
37877
+ }), { priority: "reflection" });
37878
+ reflectionUsage = reflectResult.usage;
37879
+ reflectionProviderMetadata = reflectResult.providerMetadata;
37880
+ if (!isReflectionApplied(committedRecord, newRecordId, record.id)) {
37881
+ omDebug(`[OM:reflect] manual reflection of ${record.id} not applied (head is ${committedRecord.id})`);
37882
+ return {
37883
+ reflected: false,
37884
+ record: await this.getOrCreateRecord(threadId, resourceId),
37885
+ usage: reflectResult.usage
37886
+ };
37887
+ }
37135
37888
  if (thread && reflectResult.extractedValues) {
37136
37889
  const metadataUpdate = buildThreadMetadataFromExtractedValues(this.reflectionConfig.extractors, reflectResult.extractedValues);
37137
37890
  const newMetadata = setThreadOMMetadata(thread.metadata, {
@@ -37220,11 +37973,13 @@ ${formattedMessages}
37220
37973
  */
37221
37974
  async updateRecordConfig(threadId, resourceId, config) {
37222
37975
  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 }
37976
+ await runOMCommit(this.buffering.getLockKey(threadId, resourceId), async () => {
37977
+ const record = await this.storage.getObservationalMemory(ids.threadId, ids.resourceId);
37978
+ if (!record) throw new Error(`No observational memory record found for thread ${ids.threadId}`);
37979
+ await this.storage.updateObservationalMemoryConfig({
37980
+ id: record.id,
37981
+ config: { _overrides: config }
37982
+ });
37228
37983
  });
37229
37984
  }
37230
37985
  /**
@@ -37614,7 +38369,7 @@ var ObservationalMemoryProcessor = class {
37614
38369
  });
37615
38370
  const finalTotalPending = ctx.status.pendingTokens;
37616
38371
  try {
37617
- await this.engine.getStorage().setPendingMessageTokens(turnRecord.id, finalTotalPending);
38372
+ await runOMCommit(this.engine.buffering.getLockKey(threadId, resourceId), () => this.engine.getStorage().setPendingMessageTokens(turnRecord.id, finalTotalPending));
37618
38373
  this.turn.patchRecord({ pendingMessageTokens: finalTotalPending });
37619
38374
  } catch {}
37620
38375
  if (reproCaptureEnabled) writeProcessInputStepReproCapture({
@@ -39928,6 +40683,7 @@ Notes:
39928
40683
  cloned.id = crypto.randomUUID();
39929
40684
  cloned.createdAt = now;
39930
40685
  cloned.updatedAt = now;
40686
+ cloned.supersededBy = null;
39931
40687
  await memoryStore.insertObservationalMemoryRecord(cloned);
39932
40688
  }
39933
40689
  /**
@@ -50999,4 +51755,4 @@ const agentBuilderWorkflows = {
50999
51755
  //#endregion
51000
51756
  export { agentBuilderWorkflows };
51001
51757
 
51002
- //# sourceMappingURL=dist-C68HDUAO.js.map
51758
+ //# sourceMappingURL=dist-B6B4Cxda.js.map