@mastra/server 1.73.1-alpha.0 → 1.74.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4637,7 +4637,7 @@ function splitLines(text) {
4637
4637
  return result;
4638
4638
  }
4639
4639
  //#endregion
4640
- //#region ../memory/dist/src-C9z_nVe-.js
4640
+ //#region ../memory/dist/src-CPCWMt3d.js
4641
4641
  var __defProp$3 = Object.defineProperty;
4642
4642
  var __exportAll = (all, no_symbols) => {
4643
4643
  let target = {};
@@ -22961,6 +22961,8 @@ const OBSERVATION_CONTEXT_INSTRUCTIONS = `IMPORTANT: When responding, reference
22961
22961
 
22962
22962
  KNOWLEDGE UPDATES: When asked about current state (e.g., "where do I currently...", "what is my current..."), always prefer the MOST RECENT information. Observations include dates - if you see conflicting information, the newer observation supersedes the older one. Look for phrases like "will start", "is switching", "changed to", "moved to" as indicators that previous information has been updated.
22963
22963
 
22964
+ USER STATEMENTS VS ASSISTANT SUGGESTIONS: Treat what the user said about their own life, plans, decisions, and dates as authoritative, unless data or their own later messages say otherwise. Treat what the assistant said, such as proposed schedules, example dates, and recommendations, as suggestions rather than facts about what happened, unless the user adopted or confirmed them. What the assistant did, such as editing a file, running a command, or calling a tool, is a record of what happened.
22965
+
22964
22966
  PLANNED ACTIONS: If the user stated they planned to do something (e.g., "I'm going to...", "I'm looking forward to...", "I will...") and the date they planned to do it is now in the past (check the relative time like "3 weeks ago"), assume they completed the action unless there's evidence they didn't. For example, if someone said "I'll start my new diet on Monday" and that was 2 weeks ago, assume they started the diet.
22965
22967
 
22966
22968
  MOST RECENT USER INPUT: Treat the most recent user message as the highest-priority signal for what to do next. Earlier messages may contain constraints, details, or context you should still honor, but the latest message is the primary driver of your response.
@@ -22981,30 +22983,19 @@ SYSTEM REMINDERS: Messages wrapped in <system-reminder>...</system-reminder> con
22981
22983
  * @param searchEnabled - Whether semantic search (`retrieval: { vector: true }`) is
22982
22984
  * available. When false, the guidance only covers \`threads\`/\`messages\` browsing so
22983
22985
  * the agent is not steered toward a mode that cannot work.
22986
+ * @param observationPagingEnabled - Whether the storage adapter supports original
22987
+ * observation-group paging. Only used when search is enabled.
22984
22988
  */
22985
- function getRetrievalInstructions(scope = "resource", customInstructions, searchEnabled = true) {
22989
+ function getRetrievalInstructions(scope = "resource", customInstructions, searchEnabled = true, observationPagingEnabled = searchEnabled) {
22986
22990
  const isResource = scope === "resource";
22987
- const base = `## Recall — looking up source messages
22988
-
22989
- Your memory is comprised of observations which are sometimes wrapped in <observation-group> xml tags containing ranges like <observation-group range="startId:endId">. These ranges point back to the raw messages that each observation group was derived from. The original messages are still available — use the **recall** tool to retrieve them.
22990
-
22991
- ### When to use recall
22992
- - The user asks you to **repeat, show, or reproduce** something from a past conversation
22993
- - The user asks for **exact content** — code, text, quotes, error messages, URLs, file paths, specific numbers
22994
- - Your observations mention something but your memory lacks the detail needed to fully answer (e.g. you know a blog post was shared but only have a summary of it)
22995
- - You want to **verify or expand on** an observation before responding${isResource ? `
22996
- - The user references another conversation that your observations don't cover — even if you have no observations yet, their other threads may contain it` : ""}
22997
-
22998
- **Default to using recall when the user references specific past content.** Your observations capture the gist, not the details. If there's any doubt whether your memory is complete enough, use recall.
22999
-
23000
- ${isResource ? searchEnabled ? `### Choosing a mode
22991
+ const modeSection = isResource ? searchEnabled ? `### Choosing a mode
23001
22992
  The recall tool works across ALL of this user's conversation threads, not just the current one.
23002
22993
 
23003
- - Use \`mode: "search"\` with a \`query\` when you don't know which thread contains the answer. Each result includes its thread ID and the raw message IDs it came from, which you can use as a \`cursor\`.
23004
- - Use \`mode: "messages"\` when you already know the thread — pass \`threadId\` to read another thread, or a \`cursor\` from an observation-group range or a search result.
22994
+ - Use \`mode: "search"\` with a \`query\` to find relevant history, even when you already know the thread. Each result includes its thread ID, observation group ID, and source message range.
22995
+ - Use \`mode: "messages"\` for original wording or details behind an observation — pass \`threadId\` to read another thread and a \`cursor\` from the source message range.
23005
22996
  - Use \`mode: "threads"\` to list the user's threads (IDs, titles, dates) when you need to discover where something was discussed. Use \`before\`/\`after\` to narrow by date.
23006
22997
 
23007
- **If search results look irrelevant, do not give up.** Search only covers content that has been indexed — a short or recent conversation may exist in raw message history before any observation of it was created. When search returns nothing suitable but the user is clearly referring to a past conversation, call \`mode: "threads"\` to find candidate threads (titles and dates are strong clues), then read them with \`mode: "messages"\`. If a search result already gives you a thread ID, go straight to \`mode: "messages"\`.` : `### Choosing a mode
22998
+ **If search results look irrelevant, do not give up.** Search only covers content that has been indexed — a short or recent conversation may exist in raw message history before any observation of it was created. When search returns nothing suitable but the user is clearly referring to a past conversation, call \`mode: "threads"\` to find candidate threads (titles and dates are strong clues), then read them with \`mode: "messages"\`. Use a useful hit as an entry point to the surrounding history rather than treating it as the whole answer.` : `### Choosing a mode
23008
22999
  The recall tool works across ALL of this user's conversation threads, not just the current one.
23009
23000
 
23010
23001
  - Use \`mode: "threads"\` to list the user's threads (IDs, titles, dates) when you need to discover where something was discussed. Use \`before\`/\`after\` to narrow by date.
@@ -23015,7 +23006,62 @@ The recall tool is limited to the current conversation thread.
23015
23006
 
23016
23007
  - Use \`mode: "messages"\` (default) to page through this thread's message history near a cursor.${searchEnabled ? `
23017
23008
  - Use \`mode: "search"\` with a \`query\` to find messages by content within this thread.` : ""}
23018
- - Use \`mode: "threads"\` to get the current thread's ID, title, and dates.`}
23009
+ - Use \`mode: "threads"\` to get the current thread's ID, title, and dates.`;
23010
+ const pagingEnabled = searchEnabled && observationPagingEnabled;
23011
+ const lookupSteps = [
23012
+ `**Search to locate.** Run a few differently worded queries rather than relying on one. When you roughly know when something happened, or need events from different periods, repeat the search with \`after\`/\`before\` date windows. Date filters apply to when an observation was recorded, not necessarily to dates mentioned inside it, so widen or remove them when nothing fits. If nothing useful comes up, try the user's message verbatim as the search query.`,
23013
+ ...pagingEnabled ? [`**Page observations for context.** Open a relevant hit with \`mode: "observations"\` and its \`groupId\` to read the full group and the dated conclusions and decisions around it, without loading every tool call or diff. Do this for truncated excerpts, for dates you need to pin down, and for events that unfolded over several turns. To understand what led to an event and what followed, page both before and after the anchor.`] : [],
23014
+ `**Read source messages to confirm.** When exact wording, numbers, code, who said what, or conflicting accounts matter, read the raw messages from the hit's source range. The range connects the summary view to the raw-message view.`
23015
+ ];
23016
+ const searchSection = searchEnabled ? `### Finding evidence
23017
+ Search matches are entry points, not a complete timeline. Similarity selects the matches; they are displayed by observation date. A missing search hit is not evidence that an event did not happen.
23018
+
23019
+ ${pagingEnabled ? "Observations and messages are two views of the same conversation history, not separate archives. Use search to find an entry point, observations for breadth, and messages for depth:" : "Use search to find an entry point and messages for depth:"}
23020
+ ${lookupSteps.map((step, i) => `${i + 1}. ${step}`).join("\n")}
23021
+
23022
+ When searches keep returning the same groups, often as already-in-context references, stop rephrasing: ${pagingEnabled ? "page from those groups or read their source messages" : "read their source messages"} instead.
23023
+
23024
+ "Excerpt already in current context" marks a hit whose text an earlier search result already shows. "Group already in current context" marks a group that is already in your observations or whose source messages are still in the conversation. "Source range overlaps current context" marks a group whose source messages are still in the conversation. Search keeps these references and tries lower-ranked matches to fill the requested number of excerpts. This backfill is bounded: fewer excerpts do not mean history is exhausted. Use the evidence already present rather than repeating the same lookup. You can still ${pagingEnabled ? "page a referenced group" : "read a referenced group's source messages"} when you need all of it, or search again if that context is no longer available.
23025
+
23026
+ If search still finds nothing useful, browse raw messages${isResource ? " or discover other threads" : " in this thread"} before concluding the information is unavailable. Raw history may exist for threads that have no observations yet.` : "";
23027
+ const base = `## Recall — looking up source messages
23028
+
23029
+ Your memory contains observation groups with IDs and source message ranges, shown as \`## Group\` headings with \`_range: startId:endId\` or as \`<observation-group>\` tags. Use the **recall** tool to recover retained original observations and source messages.
23030
+
23031
+ Historical notes may describe older versions of tools. Use the currently provided tool schema and these instructions for available modes and arguments; treat recalled tool descriptions as evidence about past behavior, not the current tool contract.
23032
+
23033
+ ### Reflections are lossy
23034
+ Groups marked \`kind="reflection"\` (rendered as \`_kind: reflection_\`) are lossy summaries. Use them for a broad understanding of what happened, not as an exhaustive record. If something is absent from a reflection, that does not mean it did not happen. Use recall to check the original evidence before drawing that conclusion. Original observations can omit details too; read the source messages when precise wording or evidence matters.
23035
+
23036
+ ### When to use recall
23037
+ - The user asks you to **repeat, show, or reproduce** something from a past conversation
23038
+ - The user asks for **exact content** — code, text, quotes, error messages, URLs, file paths, specific numbers
23039
+ - Your observations mention something but your memory lacks the detail needed to fully answer (e.g. you know a blog post was shared but only have a summary of it)
23040
+ - You want to **verify or expand on** an observation before responding
23041
+ - The answer depends on historical dates, order, or duration: verify both event dates and that they refer to the events the user means. Distinguish a plan, an actual start, a later update, and a repeated mention instead of choosing a nearby date from a summary
23042
+ - An observation records something the assistant proposed, such as a schedule, date, or plan, and your observations don't show what the user decided. Read the raw messages around it to find the user's decision before treating the proposal as what happened
23043
+ - Relevant details are missing, ambiguous, or conflicting in the current observations${isResource ? `
23044
+ - The user references another conversation that your observations don't cover — even if you have no observations yet, their other threads may contain it` : ""}
23045
+
23046
+ **Default to using recall when the user references specific past content.** Your observations capture the gist, not the details. If there's any doubt whether your memory is complete enough, use recall.
23047
+
23048
+ For questions about what was discussed or decided and why, start with recall when the original evidence is not already visible. Current source code or general documentation can establish what happens now, but not necessarily the past discussion or rationale. Retrieve the recorded decisions first; inspect current code separately if the answer also depends on today's implementation. Distinguish recorded reasons from your own inference, and use source messages when observation summaries omit the rationale.
23049
+
23050
+ ${[
23051
+ modeSection,
23052
+ searchSection,
23053
+ pagingEnabled ? `### Paging original observations
23054
+ Use \`mode: "observations"\` around a relevant search hit to fill in missing details, check earlier or later events, and recover context omitted from a reflection or search result.
23055
+
23056
+ - Copy the hit's \`groupId\`${isResource ? " and `threadId`" : ""} exactly as shown. Omit \`direction\` to read the full anchor group and following groups, including text truncated in search.
23057
+ - Use \`direction: "before"\` or \`direction: "after"\` to read groups strictly before or after that anchor. Pages contain 5 groups by default; \`limit\` allows up to 20. Dense groups can be large; use \`limit: 1\` or \`limit: 2\` for a quick skim.
23058
+ - Follow the returned continuation calls, using the first or last group ID on each page as the next anchor. \`groupId\` is an observation cursor; \`cursor\` is a raw message ID. Do not interchange them.
23059
+ - \`hasMore\` reports whether more groups exist in the requested direction (after when direction is omitted). A full page can still have \`hasMore: false\`; stop in that direction without making an empty follow-up call. Explicit start/end markers describe retained observation history, not whether older raw messages exist. When newer messages have not been observed yet, the end marker says how many and gives the \`mode: "messages"\` call that reads them.
23060
+ - Search results mark where other observation groups may sit between two hits from the same thread. Page from those hits instead of repeatedly searching for the same isolated hit.
23061
+ - These pages contain original observations, not reflections. For exact wording or details absent from the observations, use \`mode: "messages"\` with a message ID from the group's \`_range\`.
23062
+
23063
+ Stop once the relevant evidence is sufficient. If retained history is incomplete, say what is unknown rather than guessing.` : ""
23064
+ ].filter(Boolean).join("\n\n")}
23019
23065
 
23020
23066
  ### How to use recall with a cursor
23021
23067
  Each range has the format \`startId:endId\` where both are message IDs separated by a colon.
@@ -23026,8 +23072,8 @@ Each range has the format \`startId:endId\` where both are message IDs separated
23026
23072
  4. If the first page doesn't have what you need, increment the page number to keep paginating.
23027
23073
  5. Check \`hasNextPage\`/\`hasPrevPage\` in the result to know if more pages exist in each direction.
23028
23074
 
23029
- ### Detail levels
23030
- By default recall returns **low** detail: truncated text and tool names only. Each message shows its ID and each part has a positional index like \`[p0]\`, \`[p1]\`, etc.
23075
+ ### Raw-message detail levels
23076
+ In \`mode: "messages"\`, recall returns **low** detail by default: truncated text and tool names only. Each message shows its ID and each part has a positional index like \`[p0]\`, \`[p1]\`, etc.
23031
23077
 
23032
23078
  - Use \`detail: "high"\` to get full message content including tool arguments and results. This will only return the high detail version of a single message part at a time.
23033
23079
  - Use \`partIndex\` with a cursor to fetch a single part at full detail — for example, to read one specific tool result or code block without loading every part.
@@ -23044,8 +23090,8 @@ If a single part is larger than the token budget, the \`partIndex\` result is \`
23044
23090
 
23045
23091
  ### When recall is NOT needed
23046
23092
  - The user is asking for a high-level summary and your observations already cover it
23047
- - The question is about general preferences or facts that don't require source text
23048
- ${isResource ? `- No relevant observation range exists AND ${searchEnabled ? "`search`/`threads`" : "`threads`"} turned up nothing — but remember that raw history may exist for threads that have no observations yet, so check before concluding the information is unavailable` : `- There is no relevant range in your observations for the topic`}
23093
+ - The question is general knowledge that doesn't depend on this user's history
23094
+ - The necessary original evidence is already visible, unambiguous, and not contradicted by other observations; do not repeat a lookup just to call the tool
23049
23095
 
23050
23096
  Observation groups with range IDs and your recall tool allows you to think back and remember details you're fuzzy on.`;
23051
23097
  const custom = customInstructions?.trim();
@@ -23688,6 +23734,10 @@ function calendarDayIn(instant, timeZone) {
23688
23734
  const part = (type) => Number(parts.find((p) => p.type === type)?.value);
23689
23735
  return Date.UTC(part("year"), part("month") - 1, part("day")) / DAY_MS;
23690
23736
  }
23737
+ /** Format an instant relative to today, using the record's calendar time zone. */
23738
+ function formatRelativeTime(date, currentDate, timeZone) {
23739
+ return formatRelativeDays(calendarDayIn(currentDate, timeZone) - calendarDayIn(date, timeZone));
23740
+ }
23691
23741
  function formatRelativeDays(diffDays) {
23692
23742
  if (diffDays < 0) {
23693
23743
  const futureDays = Math.abs(diffDays);
@@ -28263,6 +28313,676 @@ var WorkingMemoryExtractor = class extends Extractor {
28263
28313
  });
28264
28314
  }
28265
28315
  };
28316
+ /**
28317
+ * Find the index of the last completed observation boundary (end marker) in a message's parts.
28318
+ * Returns -1 if no completed observation is found.
28319
+ */
28320
+ function findLastCompletedObservationBoundary(message) {
28321
+ const parts = message.content?.parts;
28322
+ if (!parts || !Array.isArray(parts)) return -1;
28323
+ for (let i = parts.length - 1; i >= 0; i--) {
28324
+ const part = parts[i];
28325
+ if (part?.type === "data-om-observation-end" && part.data?.operationType !== "reflection") return i;
28326
+ }
28327
+ return -1;
28328
+ }
28329
+ /**
28330
+ * Get unobserved parts from a message.
28331
+ * If the message has a completed observation (start + end), only return parts after the end.
28332
+ * If observation is in progress (start without end), include parts before the start.
28333
+ * Otherwise, return all parts.
28334
+ */
28335
+ function getUnobservedParts(message) {
28336
+ const parts = message.content?.parts;
28337
+ if (!parts || !Array.isArray(parts)) return [];
28338
+ const endMarkerIndex = findLastCompletedObservationBoundary(message);
28339
+ if (endMarkerIndex === -1) return parts.filter((p) => {
28340
+ return p?.type !== "data-om-observation-start";
28341
+ });
28342
+ return parts.slice(endMarkerIndex + 1).filter((p) => {
28343
+ return !p?.type?.startsWith("data-om-observation-");
28344
+ });
28345
+ }
28346
+ /**
28347
+ * Compute a cursor pointing at the latest message by createdAt.
28348
+ * Used to derive a stable observation boundary for replay pruning.
28349
+ */
28350
+ function getLastObservedMessageCursor(messages) {
28351
+ let latest;
28352
+ for (const msg of messages) {
28353
+ if (!msg?.id || !msg.createdAt) continue;
28354
+ if (!latest || new Date(msg.createdAt).getTime() > new Date(latest.createdAt).getTime()) latest = msg;
28355
+ }
28356
+ return latest ? {
28357
+ createdAt: new Date(latest.createdAt).toISOString(),
28358
+ id: latest.id
28359
+ } : void 0;
28360
+ }
28361
+ /**
28362
+ * Check if a message is at or before a cursor (by createdAt then id).
28363
+ */
28364
+ function isMessageAtOrBeforeCursor(msg, cursor) {
28365
+ if (!msg.createdAt) return false;
28366
+ const msgIso = new Date(msg.createdAt).toISOString();
28367
+ if (msgIso < cursor.createdAt) return true;
28368
+ if (msgIso === cursor.createdAt && msg.id === cursor.id) return true;
28369
+ return false;
28370
+ }
28371
+ /**
28372
+ * Return the toolCallId of a tool-invocation part in the given state, or undefined.
28373
+ */
28374
+ function getToolInvocationId(part, state) {
28375
+ if (part?.type !== "tool-invocation") return void 0;
28376
+ if (part.toolInvocation.state !== state) return void 0;
28377
+ return part.toolInvocation.toolCallId || void 0;
28378
+ }
28379
+ function collectPendingToolResultIds(allMessages, observedIds) {
28380
+ const pendingToolResultIds = /* @__PURE__ */ new Set();
28381
+ for (const msg of allMessages) {
28382
+ if (!msg?.id) continue;
28383
+ if (observedIds?.has(msg.id)) continue;
28384
+ const parts = msg.content?.parts;
28385
+ if (!parts || !Array.isArray(parts)) continue;
28386
+ for (const part of parts) {
28387
+ const id = getToolInvocationId(part, "result");
28388
+ if (id) pendingToolResultIds.add(id);
28389
+ }
28390
+ }
28391
+ return pendingToolResultIds;
28392
+ }
28393
+ function hasPendingToolResultPair(message, pendingToolResultIds) {
28394
+ if (pendingToolResultIds.size === 0) return false;
28395
+ const parts = message.content?.parts;
28396
+ if (!parts || !Array.isArray(parts)) return false;
28397
+ return parts.some((part) => {
28398
+ const id = getToolInvocationId(part, "call");
28399
+ return id !== void 0 && pendingToolResultIds.has(id);
28400
+ });
28401
+ }
28402
+ function collectPendingToolResultIdsAfterMarker(allMessages, markerMessageIndex, markerMessage) {
28403
+ const pendingToolResultIds = /* @__PURE__ */ new Set();
28404
+ const unobservedMarkerParts = getUnobservedParts(markerMessage);
28405
+ if (unobservedMarkerParts.length > 0) collectPendingToolResultIds([{
28406
+ ...markerMessage,
28407
+ content: {
28408
+ ...markerMessage.content,
28409
+ parts: unobservedMarkerParts
28410
+ }
28411
+ }], void 0).forEach((id) => pendingToolResultIds.add(id));
28412
+ collectPendingToolResultIds(allMessages.slice(markerMessageIndex + 1), void 0).forEach((id) => pendingToolResultIds.add(id));
28413
+ return pendingToolResultIds;
28414
+ }
28415
+ function getUnobservedPartsPreservingToolCallPairs(message) {
28416
+ const parts = message.content?.parts;
28417
+ if (!parts || !Array.isArray(parts)) return [];
28418
+ const endMarkerIndex = findLastCompletedObservationBoundary(message);
28419
+ if (endMarkerIndex === -1) return getUnobservedParts(message);
28420
+ const unobservedParts = getUnobservedParts(message);
28421
+ const unobservedResultIds = /* @__PURE__ */ new Set();
28422
+ for (const part of unobservedParts) {
28423
+ const id = getToolInvocationId(part, "result");
28424
+ if (id) unobservedResultIds.add(id);
28425
+ }
28426
+ if (unobservedResultIds.size === 0) return unobservedParts;
28427
+ const preservedCalls = parts.slice(0, endMarkerIndex).filter((part) => {
28428
+ const id = getToolInvocationId(part, "call");
28429
+ return id !== void 0 && unobservedResultIds.has(id);
28430
+ });
28431
+ return preservedCalls.length > 0 ? [...preservedCalls, ...unobservedParts] : unobservedParts;
28432
+ }
28433
+ /**
28434
+ * Get the messages Observational Memory is allowed to work with.
28435
+ *
28436
+ * Messages supplied through the `context` option and the synthetic `om-continuation`
28437
+ * message are prompt-only input. Core's persistence contract already treats context as
28438
+ * never-persist, while the continuation remains memory-sourced so it can stay in the live
28439
+ * actor prompt.
28440
+ *
28441
+ * OM builds its windows from `get.all.db()`, which includes both categories, and then seals
28442
+ * and persists candidates directly. Excluding them here keeps OM's observation, buffering,
28443
+ * persistence, and token accounting consistent with their ephemeral contract.
28444
+ */
28445
+ function getObservableMessages(messageList) {
28446
+ const contextMessageIds = messageList.makeMessageSourceChecker().context;
28447
+ return messageList.get.all.db().filter((message) => {
28448
+ return message.id !== "om-continuation" && !contextMessageIds.has(message.id);
28449
+ });
28450
+ }
28451
+ /**
28452
+ * Safely extract buffered observation chunks from a record.
28453
+ * Handles both array and JSON-string formats, returning empty array if malformed.
28454
+ */
28455
+ /**
28456
+ * Filter out already-observed messages from the in-memory context.
28457
+ * Uses marker-boundary pruning (preferred) or record-based fallback.
28458
+ *
28459
+ * The `fallbackCursor` is optional — callers that need it should resolve it
28460
+ * from thread metadata before calling this function.
28461
+ */
28462
+ function filterObservedMessages(opts) {
28463
+ const { messageList, record } = opts;
28464
+ const allMessages = getObservableMessages(messageList);
28465
+ const useMarkerBoundaryPruning = opts.useMarkerBoundaryPruning ?? true;
28466
+ const preserveMessageIds = opts.preserveMessageIds ?? /* @__PURE__ */ new Set();
28467
+ const observedIds = new Set(Array.isArray(record?.observedMessageIds) ? record.observedMessageIds : []);
28468
+ let markerMessageIndex = -1;
28469
+ let markerMessage = null;
28470
+ for (let i = allMessages.length - 1; i >= 0; i--) {
28471
+ const msg = allMessages[i];
28472
+ if (!msg) continue;
28473
+ if (findLastCompletedObservationBoundary(msg) !== -1) {
28474
+ markerMessageIndex = i;
28475
+ markerMessage = msg;
28476
+ break;
28477
+ }
28478
+ }
28479
+ if (useMarkerBoundaryPruning && markerMessage && markerMessageIndex !== -1) {
28480
+ const pendingToolResultIds = collectPendingToolResultIdsAfterMarker(allMessages, markerMessageIndex, markerMessage);
28481
+ const messagesToRemove = [];
28482
+ for (let i = 0; i < markerMessageIndex; i++) {
28483
+ const msg = allMessages[i];
28484
+ if (msg?.id && msg.id !== "om-continuation" && !preserveMessageIds.has(msg.id) && !hasPendingToolResultPair(msg, pendingToolResultIds)) messagesToRemove.push(msg.id);
28485
+ }
28486
+ if (messagesToRemove.length > 0) messageList.removeByIds(messagesToRemove);
28487
+ const unobserved = getUnobservedPartsPreservingToolCallPairs(markerMessage);
28488
+ if (unobserved.length === 0) {
28489
+ if (markerMessage.id) messageList.removeByIds([markerMessage.id]);
28490
+ } else if (unobserved.length < (markerMessage.content?.parts?.length ?? 0)) markerMessage.content.parts = unobserved;
28491
+ } else if (record) {
28492
+ const pendingToolResultIds = collectPendingToolResultIds(allMessages, observedIds);
28493
+ const derivedCursor = opts.fallbackCursor ?? getLastObservedMessageCursor(allMessages.filter((msg) => !!msg?.id && observedIds.has(msg.id) && !!msg.createdAt));
28494
+ const lastObservedAt = record.lastObservedAt;
28495
+ const messagesToRemove = [];
28496
+ for (const msg of allMessages) {
28497
+ if (!msg?.id || msg.id === "om-continuation" || preserveMessageIds.has(msg.id)) continue;
28498
+ const hasPendingPair = hasPendingToolResultPair(msg, pendingToolResultIds);
28499
+ if (observedIds.has(msg.id)) {
28500
+ if (hasPendingPair) continue;
28501
+ messagesToRemove.push(msg.id);
28502
+ continue;
28503
+ }
28504
+ if (derivedCursor && isMessageAtOrBeforeCursor(msg, derivedCursor)) {
28505
+ if (hasPendingPair) continue;
28506
+ messagesToRemove.push(msg.id);
28507
+ continue;
28508
+ }
28509
+ if (lastObservedAt && msg.createdAt) {
28510
+ if (new Date(msg.createdAt) <= lastObservedAt) {
28511
+ if (hasPendingPair) continue;
28512
+ messagesToRemove.push(msg.id);
28513
+ }
28514
+ }
28515
+ }
28516
+ if (messagesToRemove.length > 0) messageList.removeByIds(messagesToRemove);
28517
+ }
28518
+ }
28519
+ function getBufferedChunks(record) {
28520
+ if (!record?.bufferedObservationChunks) return [];
28521
+ if (Array.isArray(record.bufferedObservationChunks)) return record.bufferedObservationChunks;
28522
+ if (typeof record.bufferedObservationChunks === "string") try {
28523
+ const parsed = JSON.parse(record.bufferedObservationChunks);
28524
+ return Array.isArray(parsed) ? parsed : [];
28525
+ } catch {
28526
+ return [];
28527
+ }
28528
+ return [];
28529
+ }
28530
+ /**
28531
+ * Sort threads by their oldest unobserved message.
28532
+ * Returns thread IDs in order from oldest to most recent.
28533
+ */
28534
+ /**
28535
+ * Combine active and buffered observations for the buffering observer context.
28536
+ */
28537
+ function combineObservationsForBuffering(activeObservations, bufferedObservations) {
28538
+ if (!activeObservations && !bufferedObservations) return void 0;
28539
+ if (!activeObservations) return bufferedObservations;
28540
+ if (!bufferedObservations) return activeObservations;
28541
+ return `${activeObservations}\n\n--- BUFFERED (pending activation) ---\n\n${bufferedObservations}`;
28542
+ }
28543
+ function sortThreadsByOldestMessage(messagesByThread) {
28544
+ return Array.from(messagesByThread.entries()).map(([threadId, messages]) => ({
28545
+ threadId,
28546
+ oldestTimestamp: Math.min(...messages.map((m) => m.createdAt ? new Date(m.createdAt).getTime() : Date.now()))
28547
+ })).sort((a, b) => a.oldestTimestamp - b.oldestTimestamp).map((t) => t.threadId);
28548
+ }
28549
+ /**
28550
+ * Strip any thread tags that the Observer might have added.
28551
+ * Thread attribution is handled externally by the system, not by the Observer.
28552
+ */
28553
+ function stripThreadTags(observations) {
28554
+ return observations.replace(/<\/?thread\b[^>]{0,1024}>/gi, "").trim();
28555
+ }
28556
+ const OBSERVATION_GROUP_OPEN = "<observation-group";
28557
+ const OBSERVATION_GROUP_CLOSE = "</observation-group>";
28558
+ const ATTRIBUTE_PATTERN = /([\w][\w-]*)="([^"]*)"/g;
28559
+ const REFLECTION_GROUP_SPLIT_PATTERN = /^##\s+Group\s+/m;
28560
+ function parseObservationGroupAttributes(attributeString) {
28561
+ const attributes = {};
28562
+ for (const match of attributeString.matchAll(ATTRIBUTE_PATTERN)) {
28563
+ const [, key, value] = match;
28564
+ if (key && value !== void 0) attributes[key] = value;
28565
+ }
28566
+ return attributes;
28567
+ }
28568
+ function isLineStart(observations, index) {
28569
+ return index === 0 || observations[index - 1] === "\n";
28570
+ }
28571
+ function scanObservationGroupTags(observations) {
28572
+ const tags = [];
28573
+ const incompleteOpenings = [];
28574
+ let cursor = 0;
28575
+ let closeFloor = 0;
28576
+ let nestedFloor = -1;
28577
+ while (cursor < observations.length) {
28578
+ const start = observations.indexOf(OBSERVATION_GROUP_OPEN, cursor);
28579
+ if (start === -1) break;
28580
+ const attributesStart = start + 18;
28581
+ if (!/\s/.test(observations[attributesStart] ?? "")) {
28582
+ cursor = attributesStart;
28583
+ continue;
28584
+ }
28585
+ const openEnd = observations.indexOf(">", attributesStart);
28586
+ if (openEnd === -1) break;
28587
+ const closeStart = observations.indexOf(OBSERVATION_GROUP_CLOSE, Math.max(openEnd + 1, closeFloor));
28588
+ if (closeStart === -1) {
28589
+ if (isLineStart(observations, start)) incompleteOpenings.push({
28590
+ start,
28591
+ end: openEnd + 1
28592
+ });
28593
+ closeFloor = observations.length;
28594
+ cursor = openEnd + 1;
28595
+ continue;
28596
+ }
28597
+ if (nestedFloor <= openEnd) {
28598
+ const lineOpen = observations.indexOf(`\n${OBSERVATION_GROUP_OPEN}`, openEnd);
28599
+ nestedFloor = lineOpen === -1 ? observations.length : lineOpen + 1;
28600
+ }
28601
+ const nestedStart = nestedFloor;
28602
+ if (nestedStart < closeStart) {
28603
+ incompleteOpenings.push({
28604
+ start,
28605
+ end: openEnd + 1
28606
+ });
28607
+ closeFloor = closeStart;
28608
+ cursor = nestedStart;
28609
+ continue;
28610
+ }
28611
+ tags.push({
28612
+ start,
28613
+ end: closeStart + 20,
28614
+ attributeString: observations.slice(attributesStart, openEnd),
28615
+ content: observations.slice(openEnd + 1, closeStart)
28616
+ });
28617
+ closeFloor = closeStart;
28618
+ cursor = closeStart + 20;
28619
+ }
28620
+ return {
28621
+ tags,
28622
+ incompleteOpenings
28623
+ };
28624
+ }
28625
+ function replaceObservationGroupTags(observations, replace) {
28626
+ const { tags, incompleteOpenings } = scanObservationGroupTags(observations);
28627
+ const parts = [];
28628
+ let cursor = 0;
28629
+ let openingIndex = 0;
28630
+ const skipIncompleteOpeningsBefore = (limit) => {
28631
+ while (openingIndex < incompleteOpenings.length && incompleteOpenings[openingIndex].start < limit) {
28632
+ const opening = incompleteOpenings[openingIndex];
28633
+ openingIndex++;
28634
+ if (opening.start < cursor) continue;
28635
+ parts.push(observations.slice(cursor, opening.start));
28636
+ cursor = opening.end;
28637
+ }
28638
+ };
28639
+ for (const tag of tags) {
28640
+ skipIncompleteOpeningsBefore(tag.start);
28641
+ parts.push(observations.slice(cursor, tag.start), replace(tag));
28642
+ cursor = tag.end;
28643
+ }
28644
+ skipIncompleteOpeningsBefore(Number.POSITIVE_INFINITY);
28645
+ parts.push(observations.slice(cursor));
28646
+ return parts.join("");
28647
+ }
28648
+ function parseReflectionObservationGroupSections(content) {
28649
+ const normalizedContent = content.trim();
28650
+ if (!normalizedContent || !REFLECTION_GROUP_SPLIT_PATTERN.test(normalizedContent)) return [];
28651
+ return normalizedContent.split(REFLECTION_GROUP_SPLIT_PATTERN).map((section) => section.trim()).filter(Boolean).map((section) => {
28652
+ const newlineIndex = section.indexOf("\n");
28653
+ return {
28654
+ heading: (newlineIndex >= 0 ? section.slice(0, newlineIndex) : section).trim(),
28655
+ body: stripReflectionGroupMetadata((newlineIndex >= 0 ? section.slice(newlineIndex + 1) : "").trim())
28656
+ };
28657
+ });
28658
+ }
28659
+ function stripReflectionGroupMetadata(body) {
28660
+ return body.replace(/^_range:\s*`[^`]*`_\s*\n?/m, "").trim();
28661
+ }
28662
+ function generateAnchorId() {
28663
+ return Buffer.from(globalThis.crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(8))).toString("hex");
28664
+ }
28665
+ function wrapInObservationGroup(observations, range, id = generateAnchorId(), _sourceGroupIds, kind) {
28666
+ const content = observations.trim();
28667
+ return `<observation-group id="${id}" range="${range}"${kind ? ` kind="${kind}"` : ""}>\n${content}\n</observation-group>`;
28668
+ }
28669
+ function parseObservationGroups(observations) {
28670
+ if (!observations) return [];
28671
+ const groups = [];
28672
+ for (const tag of scanObservationGroupTags(observations).tags) {
28673
+ const attributes = parseObservationGroupAttributes(tag.attributeString);
28674
+ const id = attributes.id;
28675
+ const range = attributes.range;
28676
+ if (!id || !range) continue;
28677
+ groups.push({
28678
+ id,
28679
+ range,
28680
+ kind: attributes.kind,
28681
+ content: tag.content.trim()
28682
+ });
28683
+ }
28684
+ return groups;
28685
+ }
28686
+ function stripObservationGroups(observations) {
28687
+ if (!observations) return observations;
28688
+ return replaceObservationGroupTags(observations, (tag) => tag.content.trim()).replace(/\n{3,}/g, "\n\n").trim();
28689
+ }
28690
+ function getRangeSegments(range) {
28691
+ return range.split(",").map((segment) => segment.trim()).filter(Boolean);
28692
+ }
28693
+ function getRangeEndpoints(segment) {
28694
+ const parts = segment.split(":").map((part) => part.trim());
28695
+ if (parts.length !== 2 || !parts[0] || !parts[1]) return null;
28696
+ return {
28697
+ start: parts[0],
28698
+ end: parts[1]
28699
+ };
28700
+ }
28701
+ /**
28702
+ * `buildMessageRange` produces opaque UUID endpoints, so those segments cannot be ordered by value.
28703
+ * Spanning from the first start to the last end keeps reflected metadata bounded — the full segment
28704
+ * list is deliberately not preserved (#14791). Segments that are not `start:end` pairs are left as
28705
+ * they are rather than being joined into a fabricated span.
28706
+ */
28707
+ function spanRangeSegments(segments) {
28708
+ const endpoints = segments.map(getRangeEndpoints);
28709
+ if (endpoints.some((endpoint) => endpoint === null)) return segments.join(",");
28710
+ const first = endpoints[0];
28711
+ const last = endpoints.at(-1);
28712
+ return `${first.start}:${last.end}`;
28713
+ }
28714
+ function combineObservationGroupRanges(groups) {
28715
+ const segments = Array.from(new Set(groups.flatMap((group) => getRangeSegments(group.range))));
28716
+ if (segments.length === 0) return "";
28717
+ const numericEndpoints = [];
28718
+ if (!segments.every((segment) => {
28719
+ const endpoints = getRangeEndpoints(segment);
28720
+ if (!endpoints || !/^\d+$/.test(endpoints.start) || !/^\d+$/.test(endpoints.end)) return false;
28721
+ const start = {
28722
+ label: endpoints.start,
28723
+ value: Number(endpoints.start)
28724
+ };
28725
+ const end = {
28726
+ label: endpoints.end,
28727
+ value: Number(endpoints.end)
28728
+ };
28729
+ if (!Number.isSafeInteger(start.value) || !Number.isSafeInteger(end.value)) return false;
28730
+ numericEndpoints.push(start.value <= end.value ? {
28731
+ start,
28732
+ end
28733
+ } : {
28734
+ start: end,
28735
+ end: start
28736
+ });
28737
+ return true;
28738
+ })) return spanRangeSegments(segments);
28739
+ const first = numericEndpoints.reduce((lowest, pair) => pair.start.value < lowest.start.value ? pair : lowest);
28740
+ const last = numericEndpoints.reduce((highest, pair) => pair.end.value > highest.end.value ? pair : highest);
28741
+ return `${first.start.label}:${last.end.label}`;
28742
+ }
28743
+ function renderObservationGroupsForReflection(observations, options) {
28744
+ if (parseObservationGroups(observations).length === 0) return null;
28745
+ return replaceObservationGroupTags(observations, (tag) => {
28746
+ const attributes = parseObservationGroupAttributes(tag.attributeString);
28747
+ if (!attributes.id || !attributes.range) return tag.content.trim();
28748
+ const kind = options?.includeReflectionKind && attributes.kind === "reflection" ? "\n_kind: reflection_" : "";
28749
+ return `## Group \`${attributes.id}\`${kind}\n_range: \`${attributes.range}\`_\n\n${tag.content.trim()}`;
28750
+ }).replace(/\n{3,}/g, "\n\n").trim();
28751
+ }
28752
+ function getCanonicalGroupId(sectionHeading, fallbackIndex) {
28753
+ return sectionHeading.match(/`([^`]+)`/)?.[1]?.trim() || `derived-group-${fallbackIndex + 1}`;
28754
+ }
28755
+ function getContentLines(content) {
28756
+ return content.split("\n").map((line) => line.trim()).filter(Boolean);
28757
+ }
28758
+ function deriveObservationGroupProvenance(content, groups) {
28759
+ const sections = parseReflectionObservationGroupSections(content);
28760
+ if (sections.length === 0 || groups.length === 0) return [];
28761
+ return sections.map((section, index) => {
28762
+ const canonicalGroupId = getCanonicalGroupId(section.heading, index);
28763
+ const identifiedGroup = groups.find((group) => group.id === canonicalGroupId);
28764
+ const bodyLines = new Set(getContentLines(section.body));
28765
+ const matchingGroups = groups.filter((group) => getContentLines(group.content).some((line) => bodyLines.has(line)));
28766
+ const fallbackGroup = groups[Math.min(index, groups.length - 1)];
28767
+ const identifiedLines = new Set(getContentLines(identifiedGroup?.content ?? ""));
28768
+ const contributingGroups = identifiedGroup ? matchingGroups.filter((group) => group.id !== identifiedGroup.id && getContentLines(group.content).some((line) => bodyLines.has(line) && !identifiedLines.has(line))) : [];
28769
+ return {
28770
+ id: canonicalGroupId,
28771
+ range: combineObservationGroupRanges(identifiedGroup ? groups.filter((group) => group === identifiedGroup || contributingGroups.includes(group)) : matchingGroups.length > 0 ? matchingGroups : fallbackGroup ? [fallbackGroup] : []),
28772
+ kind: "reflection",
28773
+ content: section.body
28774
+ };
28775
+ });
28776
+ }
28777
+ function reconcileObservationGroupsFromReflection(content, sourceObservations) {
28778
+ const sourceGroups = parseObservationGroups(sourceObservations);
28779
+ if (sourceGroups.length === 0) return null;
28780
+ const normalizedContent = content.trim();
28781
+ if (!normalizedContent) return "";
28782
+ const derivedGroups = deriveObservationGroupProvenance(normalizedContent, sourceGroups);
28783
+ if (derivedGroups.length > 0) return derivedGroups.map((group) => wrapInObservationGroup(group.content, group.range, group.id, void 0, group.kind)).join("\n\n");
28784
+ return wrapInObservationGroup(normalizedContent, combineObservationGroupRanges(sourceGroups), generateAnchorId(), void 0, "reflection");
28785
+ }
28786
+ const hasher = e();
28787
+ /** Thrown when storage returns history records without applying the requested filters. */
28788
+ var HistoryFiltersIgnoredError = class extends Error {};
28789
+ const HISTORY_FILTERS_IGNORED = "Observation paging is unavailable: the storage adapter returned observation history without applying the history filters (group, record, and generation). If you use Convex, redeploy the Mastra server functions; otherwise upgrade the storage adapter together with @mastra/memory. Use mode=\"messages\" with a message ID from the search hit's source range instead.";
28790
+ function rawText(record) {
28791
+ return [record.activeObservations, ...getBufferedChunks(record).map((chunk) => chunk.observations)].join("\n");
28792
+ }
28793
+ async function buildTimeline(record, threadId) {
28794
+ let text = rawText(record);
28795
+ if (record.threadId === null) {
28796
+ const obscuredId = (await hasher).h32ToString(threadId);
28797
+ text = [...text.matchAll(/<thread id="([^"]+)">([\s\S]*?)<\/thread>/g)].filter((match) => match[1] === threadId || match[1] === obscuredId).map((match) => match[2]).join("\n");
28798
+ }
28799
+ const groups = parseObservationGroups(text).filter((group) => group.kind !== "reflection");
28800
+ const indexById = /* @__PURE__ */ new Map();
28801
+ return {
28802
+ record,
28803
+ groups: groups.filter((group) => {
28804
+ if (indexById.has(group.id)) return false;
28805
+ indexById.set(group.id, indexById.size);
28806
+ return true;
28807
+ }),
28808
+ indexById
28809
+ };
28810
+ }
28811
+ async function findGroupTimeline(om, threadId, resourceId, groupId, recordId) {
28812
+ const resolve = async (options) => {
28813
+ const [record] = await om.getHistory(threadId, resourceId, 1, {
28814
+ ...options,
28815
+ groupId
28816
+ });
28817
+ if (!record || record.threadId !== null && record.threadId !== threadId) return null;
28818
+ if (!rawText(record).includes(`<observation-group id="${groupId}"`)) throw new HistoryFiltersIgnoredError();
28819
+ const timeline = await buildTimeline(record, threadId);
28820
+ return timeline.indexById.has(groupId) ? timeline : null;
28821
+ };
28822
+ return recordId !== void 0 && await resolve({ recordId }) || await resolve({ sortDirection: "ASC" });
28823
+ }
28824
+ /** Group IDs shown to the agent carry the record holding the group, so paging can read it by ID. */
28825
+ function groupCursor(groupId, recordId) {
28826
+ return recordId ? `${groupId}@${recordId}` : groupId;
28827
+ }
28828
+ function parseGroupCursor(cursor) {
28829
+ const at = cursor.indexOf("@");
28830
+ return at === -1 ? { groupId: cursor } : {
28831
+ groupId: cursor.slice(0, at),
28832
+ recordId: cursor.slice(at + 1) || void 0
28833
+ };
28834
+ }
28835
+ function pagingCall(groupId, direction, threadId, recordId) {
28836
+ return `recall(${JSON.stringify({
28837
+ mode: "observations",
28838
+ threadId,
28839
+ groupId: groupCursor(groupId, recordId),
28840
+ direction
28841
+ })})`;
28842
+ }
28843
+ /** Messages after the last observed range are raw history only; say so instead of implying the thread ends. */
28844
+ async function newerMessagesNote(group, countNewerMessages, threadId) {
28845
+ const endpoints = group?.range.split(",").at(-1)?.split(":");
28846
+ const cursor = endpoints?.length === 2 ? endpoints[1] : void 0;
28847
+ if (!cursor || !countNewerMessages) return null;
28848
+ const count = await countNewerMessages(cursor);
28849
+ if (count <= 0) return null;
28850
+ const call = `recall(${JSON.stringify({
28851
+ mode: "messages",
28852
+ threadId,
28853
+ cursor
28854
+ })})`;
28855
+ return count === 1 ? `1 newer message has not been observed yet; read it with ${call}` : `${count} newer messages have not been observed yet; read them with ${call}`;
28856
+ }
28857
+ async function pageObservationGroups(options) {
28858
+ try {
28859
+ return await pageGroups(options);
28860
+ } catch (error) {
28861
+ if (error instanceof HistoryFiltersIgnoredError) return {
28862
+ results: HISTORY_FILTERS_IGNORED,
28863
+ count: 0
28864
+ };
28865
+ throw error;
28866
+ }
28867
+ }
28868
+ async function pageGroups({ om, threadId, resourceId, groupId, recordId, direction: requestedDirection, limit, threadTitle, includeThreadId = true, countNewerMessages }) {
28869
+ const home = await findGroupTimeline(om, threadId, resourceId, groupId, recordId);
28870
+ if (!home) return {
28871
+ results: `No original observation group ${JSON.stringify(groupId)} was found in the active or buffered observations of thread ${JSON.stringify(threadId)} across its retained history. Check the threadId and groupId from the search hit, or use mode="messages" with a message ID from its source range.`,
28872
+ count: 0
28873
+ };
28874
+ const direction = requestedDirection ?? "after";
28875
+ let current = home;
28876
+ let position = home.indexById.get(groupId) - (requestedDirection === void 0 ? 1 : 0);
28877
+ const seen = new Set((direction === "after" ? home.groups.slice(0, position + 1) : home.groups.slice(position)).map((group) => group.id));
28878
+ const entries = [];
28879
+ while (entries.length <= limit) {
28880
+ position += direction === "after" ? 1 : -1;
28881
+ const group = current.groups[position];
28882
+ if (group) {
28883
+ if (!seen.has(group.id)) {
28884
+ seen.add(group.id);
28885
+ entries.push({
28886
+ group,
28887
+ record: current.record
28888
+ });
28889
+ }
28890
+ continue;
28891
+ }
28892
+ const generation = current.record.generationCount;
28893
+ const [record] = await om.getHistory(threadId, resourceId, 1, direction === "after" ? {
28894
+ afterGeneration: generation,
28895
+ sortDirection: "ASC"
28896
+ } : {
28897
+ beforeGeneration: generation,
28898
+ sortDirection: "DESC"
28899
+ });
28900
+ if (!record) break;
28901
+ if ((direction === "after" ? record.generationCount <= generation : record.generationCount >= generation) || record.threadId !== null && record.threadId !== threadId) throw new HistoryFiltersIgnoredError();
28902
+ current = await buildTimeline(record, threadId);
28903
+ position = direction === "after" ? -1 : current.groups.length;
28904
+ }
28905
+ const hasMore = entries.length > limit;
28906
+ const page = entries.slice(0, limit);
28907
+ if (direction === "before") page.reverse();
28908
+ const pagingThreadId = includeThreadId ? threadId : void 0;
28909
+ if (!page.length) {
28910
+ const note = direction === "after" ? await newerMessagesNote(home.groups[home.indexById.get(groupId)], countNewerMessages, pagingThreadId) : null;
28911
+ return {
28912
+ results: `No ${direction === "before" ? "earlier" : "later"} original observation groups in this thread's retained history.${note ? ` ${note}.` : ""}`,
28913
+ count: 0,
28914
+ hasMore: false
28915
+ };
28916
+ }
28917
+ const now = /* @__PURE__ */ new Date();
28918
+ const text = page.map(({ group, record }) => [
28919
+ `## Group \`${group.id}\``,
28920
+ `_range: \`${group.range}\`_`,
28921
+ addRelativeTimeToObservations(group.content, now, record.observedTimezone ?? void 0)
28922
+ ].join("\n"));
28923
+ const hasEarlier = direction === "before" ? hasMore : requestedDirection !== void 0 || home.indexById.get(groupId) > 0 || home.record.generationCount > 0 && (await pageGroups({
28924
+ om,
28925
+ threadId,
28926
+ resourceId,
28927
+ groupId,
28928
+ recordId: home.record.id,
28929
+ direction: "before",
28930
+ limit: 1
28931
+ })).count > 0;
28932
+ const hasLater = direction === "before" || hasMore;
28933
+ const newer = hasLater ? null : await newerMessagesNote(page.at(-1).group, countNewerMessages, pagingThreadId);
28934
+ text.unshift(hasEarlier ? `— Browse earlier: ${pagingCall(page[0].group.id, "before", pagingThreadId, page[0].record.id)} —` : "— Start of retained observation history for this thread. —");
28935
+ text.push(hasLater ? `— Browse later: ${pagingCall(page.at(-1).group.id, "after", pagingThreadId, page.at(-1).record.id)} —` : newer ? `— End of observation history for this thread. ${newer} —` : "— End of retained observation history for this thread. —");
28936
+ text.unshift(`### Observation page\nThread: ${threadTitle || "(untitled)"}\nShowing ${page.length} groups ${requestedDirection === void 0 ? "starting at" : `strictly ${direction}`} \`${groupId}\` (oldest first).`);
28937
+ return {
28938
+ results: text.join("\n\n"),
28939
+ count: page.length,
28940
+ hasMore
28941
+ };
28942
+ }
28943
+ function searchContextKey(match) {
28944
+ return JSON.stringify([match.threadId, match.groupId]);
28945
+ }
28946
+ /** Read only real recall results, not quoted tool output in user or assistant text. */
28947
+ function getVisibleSearchExcerpts(messages) {
28948
+ const excerpts = /* @__PURE__ */ new Map();
28949
+ for (const message of messages) {
28950
+ if (message.role !== "assistant") continue;
28951
+ for (const part of message.content.parts) {
28952
+ if (part.type !== "tool-invocation") continue;
28953
+ const invocation = part.toolInvocation;
28954
+ if (invocation.toolName !== "recall" || invocation.state !== "result" || invocation.args?.mode !== "search") continue;
28955
+ const resolved = resolveToolResultValue(part, invocation.result);
28956
+ let value = resolved.value;
28957
+ if (resolved.usingStoredModelOutput) {
28958
+ if (!value || typeof value !== "object" || !("type" in value) || value.type !== "json" || !("value" in value)) continue;
28959
+ value = value.value;
28960
+ }
28961
+ if (!value || typeof value !== "object" || !("results" in value) || typeof value.results !== "string") continue;
28962
+ for (const section of value.results.split(/^### (?:Current thread memory|Memory from another thread)\s*$/m)) {
28963
+ const threadId = /^- thread: (\S+)/m.exec(section)?.[1];
28964
+ const groupId = (/^- observation group: ([^\n]+)$/m.exec(section)?.[1])?.split("@")[0];
28965
+ const excerpt = /\n```text\n([\s\S]*?)\n```/.exec(section)?.[1];
28966
+ if (!threadId || !groupId || !excerpt) continue;
28967
+ const key = searchContextKey({
28968
+ threadId,
28969
+ groupId
28970
+ });
28971
+ const previous = excerpts.get(key) ?? [];
28972
+ previous.push(excerpt);
28973
+ excerpts.set(key, previous);
28974
+ }
28975
+ }
28976
+ }
28977
+ return excerpts;
28978
+ }
28979
+ function sourceRangeOverlapsContext({ match, messages }) {
28980
+ if (!match.groupId || !match.range) return false;
28981
+ const endpoints = /^([^:,]+):([^:,]+)$/.exec(match.range);
28982
+ if (!endpoints) return false;
28983
+ const visible = new Set(messages.filter((message) => message.threadId === match.threadId).map((message) => message.id));
28984
+ return visible.has(endpoints[1]) && visible.has(endpoints[2]);
28985
+ }
28266
28986
  function getMessageParts(msg) {
28267
28987
  if (typeof msg.content === "string") return [];
28268
28988
  if (Array.isArray(msg.content)) return msg.content;
@@ -28389,7 +29109,7 @@ async function listThreadsForResource({ memory, resourceId, currentThreadId, pag
28389
29109
  };
28390
29110
  }
28391
29111
  const SEARCH_NOT_CONFIGURED_MESSAGE = "Search is not configured. Enable it with `retrieval: { vector: true }` and configure a vector store and embedder on your Memory instance.";
28392
- async function searchMessagesForResource({ memory, resourceId, currentThreadId, query, topK = 10, maxTokens = DEFAULT_MAX_RESULT_TOKENS, before, after, threadScope }) {
29112
+ async function searchMessagesForResource({ memory, om, resourceId, currentThreadId, query, topK = 10, maxTokens = DEFAULT_MAX_RESULT_TOKENS, before, after, threadScope, includeThreadId = true, currentMessages = [] }) {
28393
29113
  if (!memory.searchMessages) return {
28394
29114
  results: SEARCH_NOT_CONFIGURED_MESSAGE,
28395
29115
  count: 0
@@ -28400,15 +29120,16 @@ async function searchMessagesForResource({ memory, resourceId, currentThreadId,
28400
29120
  const searchTopK = Math.min(MAX_TOPK, effectiveTopK);
28401
29121
  const beforeDate = before ? new Date(before) : void 0;
28402
29122
  const afterDate = after ? new Date(after) : void 0;
29123
+ const filter = {
29124
+ ...threadScope ? { threadId: threadScope } : {},
29125
+ ...afterDate ? { observedAfter: afterDate } : {},
29126
+ ...beforeDate ? { observedBefore: beforeDate } : {}
29127
+ };
28403
29128
  const { results } = await memory.searchMessages({
28404
29129
  query,
28405
29130
  resourceId,
28406
29131
  topK: searchTopK,
28407
- filter: {
28408
- ...threadScope ? { threadId: threadScope } : {},
28409
- ...afterDate ? { observedAfter: afterDate } : {},
28410
- ...beforeDate ? { observedBefore: beforeDate } : {}
28411
- }
29132
+ filter
28412
29133
  });
28413
29134
  if (results.length === 0) return {
28414
29135
  results: "No matching messages found.",
@@ -28420,47 +29141,169 @@ async function searchMessagesForResource({ memory, resourceId, currentThreadId,
28420
29141
  const thread = await memory.getThreadById({ threadId: id });
28421
29142
  if (thread) threadMap.set(id, thread);
28422
29143
  }));
28423
- const filteredMatches = results.filter((match) => {
29144
+ const matchesFilter = (match) => {
28424
29145
  if (threadScope && match.threadId !== threadScope) return false;
28425
29146
  if (beforeDate && match.observedAt && match.observedAt >= beforeDate) return false;
28426
29147
  if (afterDate && match.observedAt && match.observedAt <= afterDate) return false;
28427
29148
  return true;
28428
- });
29149
+ };
29150
+ const filteredMatches = results.filter(matchesFilter);
28429
29151
  if (filteredMatches.length === 0) return {
28430
29152
  results: "No matching messages found.",
28431
29153
  count: 0
28432
29154
  };
28433
29155
  const limitedMatches = filteredMatches.slice(0, clampedTopK);
28434
- const { text: limited } = truncateByTokens(limitedMatches.map((match) => {
29156
+ const contentBudget = Math.max(0, Math.floor(maxTokens));
29157
+ const perGroupBudget = Math.floor(contentBudget / limitedMatches.length);
29158
+ const now = /* @__PURE__ */ new Date();
29159
+ const terms = excerptQueryTerms(query);
29160
+ const setExcerpt = (entry, budget) => {
29161
+ const selected = selectSearchExcerpt(entry.body, terms, budget);
29162
+ entry.excerpt = selected.text;
29163
+ entry.excerptContent = selected.content;
29164
+ };
29165
+ const [currentRecord] = om && currentThreadId ? await om.getHistory(currentThreadId, resourceId, 1) : [];
29166
+ const timeZone = resolveTimeZone(currentRecord?.observedTimezone ?? void 0);
29167
+ const prepareEntry = (match, index, budget) => {
29168
+ const rawBody = (match.text || "").trim() || "_Observation text unavailable._";
29169
+ const entry = {
29170
+ match,
29171
+ index,
29172
+ body: match.groupId ? addRelativeTimeToObservations(rawBody, now, timeZone) : rawBody,
29173
+ excerpt: "",
29174
+ excerptContent: "",
29175
+ suppression: ""
29176
+ };
29177
+ setExcerpt(entry, budget);
29178
+ return entry;
29179
+ };
29180
+ const ordered = limitedMatches.map((match, index) => prepareEntry(match, index, perGroupBudget));
29181
+ let remaining = contentBudget - ordered.reduce((sum, entry) => sum + estimateTokenCount(entry.excerpt), 0);
29182
+ for (const entry of ordered) {
29183
+ if (remaining <= 0) break;
29184
+ if (entry.excerpt === entry.body) continue;
29185
+ const previousTokens = estimateTokenCount(entry.excerpt);
29186
+ setExcerpt(entry, previousTokens + remaining);
29187
+ remaining -= estimateTokenCount(entry.excerpt) - previousTokens;
29188
+ }
29189
+ const visibleExcerpts = getVisibleSearchExcerpts(currentMessages);
29190
+ const contextGroupIds = /* @__PURE__ */ new Set();
29191
+ for (const observations of [currentRecord?.activeObservations ?? "", ...getBufferedChunks(currentRecord).map((chunk) => chunk.observations)]) for (const group of parseObservationGroups(observations)) contextGroupIds.add(group.id);
29192
+ const suppressCoveredEntry = (entry) => {
29193
+ if (!entry.match.groupId) return;
29194
+ if (entry.excerptContent && visibleExcerpts.get(searchContextKey(entry.match))?.some((text) => text.includes(entry.excerptContent))) entry.suppression = "Excerpt already in current context.";
29195
+ else if (contextGroupIds.has(entry.match.groupId)) entry.suppression = "Group already in current context.";
29196
+ else if (sourceRangeOverlapsContext({
29197
+ match: entry.match,
29198
+ messages: currentMessages
29199
+ })) entry.suppression = "Source range overlaps current context.";
29200
+ if (entry.suppression) {
29201
+ entry.excerpt = "";
29202
+ entry.excerptContent = "";
29203
+ }
29204
+ };
29205
+ ordered.forEach(suppressCoveredEntry);
29206
+ let freshCount = ordered.filter((entry) => !entry.suppression).length;
29207
+ if (freshCount < ordered.length && contentBudget > 0) {
29208
+ const backfillBudget = Math.floor(contentBudget / clampedTopK);
29209
+ for (const entry of ordered) {
29210
+ if (entry.suppression) continue;
29211
+ setExcerpt(entry, backfillBudget);
29212
+ suppressCoveredEntry(entry);
29213
+ }
29214
+ freshCount = ordered.filter((entry) => !entry.suppression).length;
29215
+ const candidateLimit = Math.max(searchTopK, Math.min(100, clampedTopK * 5));
29216
+ const candidateKey = (match) => match.groupId ? searchContextKey(match) : JSON.stringify([
29217
+ match.threadId,
29218
+ match.range,
29219
+ match.text
29220
+ ]);
29221
+ const seen = new Set(ordered.map((entry) => candidateKey(entry.match)));
29222
+ const addCandidates = async (candidates) => {
29223
+ for (const match of candidates.slice(0, candidateLimit)) {
29224
+ if (freshCount >= clampedTopK || seen.size >= candidateLimit) break;
29225
+ if (!matchesFilter(match)) continue;
29226
+ const key = candidateKey(match);
29227
+ if (seen.has(key)) continue;
29228
+ seen.add(key);
29229
+ if (!threadMap.has(match.threadId) && memory.getThreadById) {
29230
+ const thread = await memory.getThreadById({ threadId: match.threadId });
29231
+ if (thread) threadMap.set(match.threadId, thread);
29232
+ }
29233
+ const entry = prepareEntry(match, ordered.length, backfillBudget);
29234
+ suppressCoveredEntry(entry);
29235
+ ordered.push(entry);
29236
+ if (!entry.suppression) freshCount++;
29237
+ }
29238
+ };
29239
+ await addCandidates(filteredMatches.slice(limitedMatches.length));
29240
+ if (freshCount < clampedTopK && seen.size < candidateLimit && searchTopK < candidateLimit) await addCandidates((await memory.searchMessages({
29241
+ query,
29242
+ resourceId,
29243
+ topK: candidateLimit,
29244
+ filter
29245
+ })).results);
29246
+ }
29247
+ remaining = contentBudget - ordered.reduce((sum, entry) => sum + estimateTokenCount(entry.excerpt), 0);
29248
+ for (const entry of ordered) {
29249
+ if (remaining <= 0) break;
29250
+ if (entry.suppression || entry.excerpt === entry.body) continue;
29251
+ const previousTokens = estimateTokenCount(entry.excerpt);
29252
+ setExcerpt(entry, previousTokens + remaining);
29253
+ remaining -= estimateTokenCount(entry.excerpt) - previousTokens;
29254
+ }
29255
+ ordered.sort((a, b) => {
29256
+ const at = a.match.observedAt?.getTime() ?? Number.POSITIVE_INFINITY;
29257
+ const bt = b.match.observedAt?.getTime() ?? Number.POSITIVE_INFINITY;
29258
+ if (at !== bt) return at < bt ? -1 : 1;
29259
+ if (a.match.threadId !== b.match.threadId) return a.match.threadId.localeCompare(b.match.threadId);
29260
+ return a.index - b.index;
29261
+ });
29262
+ const sections = [];
29263
+ const compactCount = ordered.length - freshCount;
29264
+ if (compactCount > 0) sections.push(`Showing ${freshCount} excerpts and ${compactCount} already-in-context references.` + (freshCount < clampedTopK ? " Backfill is bounded; fewer excerpts do not mean history is exhausted." : ""));
29265
+ for (let i = 0; i < ordered.length; i++) {
29266
+ const { match, body, excerpt, suppression } = ordered[i];
28435
29267
  const thread = threadMap.get(match.threadId);
28436
29268
  const title = thread?.title || "(untitled)";
28437
29269
  const isCurrentThread = match.threadId === currentThreadId;
28438
- const generationLabel = isCurrentThread ? "Current thread memory" : "Older memory from another thread";
28439
- const generationDetail = isCurrentThread ? "This result came from the current thread." : "This result came from an older memory generation in another thread.";
29270
+ const generationLabel = isCurrentThread ? "Current thread memory" : "Memory from another thread";
29271
+ const generationDetail = isCurrentThread ? "This result came from the current thread." : "This result came from another thread.";
28440
29272
  const threadLine = `- thread: ${match.threadId}${thread ? ` (${title})` : ""}`;
29273
+ const observedLine = match.observedAt ? `- observed: ${formatTimestamp(match.observedAt)} (${formatRelativeTime(match.observedAt, now, timeZone)})` : void 0;
28441
29274
  const sourceLine = match.range ? `- source: raw messages from ID ${match.range.split(":")[0] ?? "(unknown)"} through ID ${match.range.split(":")[1] ?? "(unknown)"}` : "- source: raw message range unavailable";
28442
- const updatedLine = thread ? `- thread updated: ${formatTimestamp(thread.updatedAt)}` : void 0;
28443
- const groupLine = match.groupId ? `- observation group: ${match.groupId}` : void 0;
29275
+ const groupLine = match.groupId ? `- observation group: ${groupCursor(match.groupId, match.recordId)}` : void 0;
28444
29276
  const scoreLine = `- score: ${match.score.toFixed(2)}`;
28445
- const body = (match.text || "").trim() || "_Observation text unavailable._";
28446
- return [
29277
+ const prev = i > 0 ? ordered[i - 1].match : void 0;
29278
+ if (om && prev?.groupId && match.groupId && prev.threadId === match.threadId) sections.push(`— Observation groups may be hidden between these results; continue with ${pagingCall(prev.groupId, "after", includeThreadId ? prev.threadId : void 0, prev.recordId)} —`);
29279
+ if (suppression) {
29280
+ sections.push(`${groupLine}\n thread: ${match.threadId}; ${suppression}`);
29281
+ continue;
29282
+ }
29283
+ sections.push([
28447
29284
  `### ${generationLabel}`,
28448
29285
  "",
28449
29286
  generationDetail,
28450
29287
  threadLine,
29288
+ observedLine,
28451
29289
  sourceLine,
28452
- updatedLine,
28453
29290
  groupLine,
28454
29291
  scoreLine,
28455
29292
  "",
28456
29293
  "```text",
28457
- body,
28458
- "```"
28459
- ].filter(Boolean).join("\n");
28460
- }).join("\n\n"), maxTokens);
29294
+ excerpt,
29295
+ "```",
29296
+ excerpt !== body ? "[Excerpt truncated]" : void 0
29297
+ ].filter(Boolean).join("\n"));
29298
+ }
29299
+ if (ordered.some((entry) => !entry.suppression && entry.excerpt !== entry.body)) sections.push([
29300
+ `All ${ordered.length} selected matching groups are shown; some excerpts are truncated.`,
29301
+ om ? "To read a full group, use mode=\"observations\" with its threadId and groupId, omitting direction." : void 0,
29302
+ "Use mode=\"messages\" with a source-range cursor for original messages."
29303
+ ].filter(Boolean).join(" "));
28461
29304
  return {
28462
- results: limited,
28463
- count: limitedMatches.length
29305
+ results: sections.join("\n\n"),
29306
+ count: ordered.length
28464
29307
  };
28465
29308
  }
28466
29309
  const LOW_DETAIL_PART_TOKENS = 30;
@@ -28511,6 +29354,54 @@ function chunkTextByTokens(text, maxTokens, charOffset = 0) {
28511
29354
  truncated: nextCharOffset < text.length
28512
29355
  };
28513
29356
  }
29357
+ const EXCERPT_STOP_WORDS = new Set("the and for with that this from have has had what when where which while who whom why how many much did does was were are you your our their them they there been being some also just than then those these its into about any all can could would should not but user agent asked said mention mentioned total".split(" "));
29358
+ function excerptQueryTerms(query) {
29359
+ const words = query.toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [];
29360
+ return [...new Set(words.filter((word) => word.length >= 3 && !EXCERPT_STOP_WORDS.has(word)))];
29361
+ }
29362
+ const EXCERPT_OMISSION_MARKER = "[earlier lines omitted]";
29363
+ /**
29364
+ * Fit an observation to a token budget. When it does not fit, start at the observation
29365
+ * line matching the most query terms (keeping that line's date header) instead of the head,
29366
+ * so repeated searches can reach text a head-only excerpt would never show.
29367
+ * `content` is the slice of the body that is shown, used to recognize excerpts already in context.
29368
+ */
29369
+ function selectSearchExcerpt(body, terms, maxTokens) {
29370
+ const head = chunkTextByTokens(body, maxTokens);
29371
+ if (!head.truncated || terms.length === 0) return {
29372
+ text: head.text,
29373
+ content: head.text
29374
+ };
29375
+ const starts = [0, ...[...body.matchAll(/(?<=\s)(?:Date: |\* )/g)].map((match) => match.index)];
29376
+ let best;
29377
+ for (let i = 0; i < starts.length; i++) {
29378
+ const start = starts[i];
29379
+ const end = starts[i + 1] ?? body.length;
29380
+ const line = body.slice(start, end).toLowerCase();
29381
+ const score = terms.filter((term) => line.includes(term)).length;
29382
+ if (score > (best?.score ?? 0)) best = {
29383
+ start,
29384
+ end,
29385
+ score
29386
+ };
29387
+ }
29388
+ if (!best || best.end <= head.text.length) return {
29389
+ text: head.text,
29390
+ content: head.text
29391
+ };
29392
+ const dateStart = body.lastIndexOf("Date: ", best.start);
29393
+ const dateHeader = dateStart >= 0 && dateStart < best.start ? body.slice(dateStart, starts.find((start) => start > dateStart) ?? best.start).trim() : "";
29394
+ const prefix = `${dateHeader ? `${dateHeader} ` : ""}${EXCERPT_OMISSION_MARKER} `;
29395
+ const window = chunkTextByTokens(body, maxTokens - estimateTokenCount(prefix), best.start);
29396
+ if (!window.text) return {
29397
+ text: head.text,
29398
+ content: head.text
29399
+ };
29400
+ return {
29401
+ text: prefix + window.text,
29402
+ content: window.text
29403
+ };
29404
+ }
28514
29405
  function lowDetailPartLimit(type) {
28515
29406
  if (type === "text") return AUTO_EXPAND_TEXT_TOKENS;
28516
29407
  if (type === "tool-result" || type === "tool-call") return AUTO_EXPAND_TOOL_TOKENS;
@@ -29125,11 +30016,13 @@ const recallTool = (_memoryConfig, options) => {
29125
30016
  const retrievalScope = options?.retrievalScope ?? "thread";
29126
30017
  const isResourceScope = retrievalScope === "resource";
29127
30018
  const searchEnabled = options?.searchEnabled ?? true;
29128
- const description = isResourceScope ? `Browse conversation history. Use mode="threads" to list all threads for the current user. Use mode="messages" (default) to browse messages in the current thread or pass threadId to browse another thread in the active resource. When mode="messages" has no cursor or threadId, it defaults to the current thread and says so at the top of the result. If you pass only a cursor, it must belong to the current thread.${searchEnabled ? " Use mode=\"search\" to find messages by content across all threads." : ""}` : `Browse conversation history in the current thread. Use mode="messages" (default) to page through messages near a cursor.${searchEnabled ? " Use mode=\"search\" to find messages by content in this thread." : ""} Use mode="threads" to get the current thread's ID and title.`;
30019
+ const getOMEngine = options?.getOMEngine;
30020
+ const description = isResourceScope ? `Browse conversation history. Use mode="threads" to list all threads for the current user. Use mode="messages" (default) to browse messages in the current thread or pass threadId to browse another thread in the active resource. When mode="messages" has no cursor or threadId, it defaults to the current thread and says so at the top of the result. If you pass only a cursor, it must belong to the current thread.${searchEnabled ? " Use mode=\"search\" to find messages by content across all threads; results come back oldest-first with markers where observation groups were skipped. Use mode=\"observations\" with a groupId from a search result to page chronologically through the observation groups around it." : ""}` : `Browse conversation history in the current thread. Use mode="messages" (default) to page through messages near a cursor.${searchEnabled ? " Use mode=\"search\" to find messages by content in this thread; results come back oldest-first with markers where observation groups were skipped. Use mode=\"observations\" with a groupId from a search result to page chronologically through the observation groups around it." : ""} Use mode="threads" to get the current thread's ID and title.`;
29129
30021
  const modeEnum = searchEnabled ? [
29130
30022
  "messages",
29131
30023
  "threads",
29132
- "search"
30024
+ "search",
30025
+ "observations"
29133
30026
  ] : ["messages", "threads"];
29134
30027
  return (0, _mastra_core_tools.createTool)({
29135
30028
  id: "recall",
@@ -29142,7 +30035,7 @@ const recallTool = (_memoryConfig, options) => {
29142
30035
  mode: {
29143
30036
  type: "string",
29144
30037
  enum: modeEnum,
29145
- description: `What to retrieve. "messages" (default) pages through message history. "threads" lists all threads for the current user.${searchEnabled ? " \"search\" finds messages by semantic similarity across all threads." : ""}`
30038
+ description: `What to retrieve. "messages" (default) pages through message history. "threads" lists all threads for the current user.${searchEnabled ? " \"search\" finds messages by semantic similarity across all threads. \"observations\" pages observation groups chronologically around a groupId from a search result." : ""}`
29146
30039
  },
29147
30040
  threadId: {
29148
30041
  type: "string",
@@ -29151,22 +30044,44 @@ const recallTool = (_memoryConfig, options) => {
29151
30044
  },
29152
30045
  before: {
29153
30046
  type: "string",
29154
- description: "For mode=\"threads\": only show threads created before this date. ISO 8601 or natural date string (e.g. \"2026-03-15\", \"2026-03-10T00:00:00Z\")."
30047
+ description: "Only show results before this date. Applies to mode=\"threads\" (thread creation date) and mode=\"search\" (observation date). ISO 8601 or natural date string (e.g. \"2026-03-15\", \"2026-03-10T00:00:00Z\")."
29155
30048
  },
29156
30049
  after: {
29157
30050
  type: "string",
29158
- description: "For mode=\"threads\": only show threads created after this date. ISO 8601 or natural date string (e.g. \"2026-03-01\", \"2026-03-10T00:00:00Z\")."
30051
+ description: "Only show results after this date. Applies to mode=\"threads\" (thread creation date) and mode=\"search\" (observation date). ISO 8601 or natural date string (e.g. \"2026-03-01\", \"2026-03-10T00:00:00Z\")."
29159
30052
  }
29160
30053
  } : { mode: {
29161
30054
  type: "string",
29162
30055
  enum: modeEnum,
29163
- description: `What to retrieve. "messages" (default) pages through message history. "threads" returns info about the current thread.${searchEnabled ? " \"search\" finds messages by semantic similarity in this thread." : ""}`
30056
+ description: `What to retrieve. "messages" (default) pages through message history. "threads" returns info about the current thread.${searchEnabled ? " \"search\" finds messages by semantic similarity in this thread. \"observations\" pages observation groups chronologically around a groupId from a search result." : ""}`
29164
30057
  } },
29165
- ...searchEnabled ? { query: {
29166
- type: "string",
29167
- minLength: 1,
29168
- description: "Search query for mode=\"search\". Finds messages semantically similar to this text."
29169
- } } : {},
30058
+ ...searchEnabled ? {
30059
+ ...!isResourceScope ? {
30060
+ before: {
30061
+ type: "string",
30062
+ description: "For mode=\"search\": only show observations dated before this ISO 8601 date."
30063
+ },
30064
+ after: {
30065
+ type: "string",
30066
+ description: "For mode=\"search\": only show observations dated after this ISO 8601 date."
30067
+ }
30068
+ } : {},
30069
+ query: {
30070
+ type: "string",
30071
+ minLength: 1,
30072
+ description: "Search query for mode=\"search\". Finds messages semantically similar to this text."
30073
+ },
30074
+ groupId: {
30075
+ type: "string",
30076
+ minLength: 1,
30077
+ description: "Observation group ID to page around, for mode=\"observations\". Copy it exactly from the \"observation group:\" line of a search result or a continuation call."
30078
+ },
30079
+ direction: {
30080
+ type: "string",
30081
+ enum: ["before", "after"],
30082
+ description: "For mode=\"observations\": page strictly before or after the groupId. Omit to read the full anchor group and following groups."
30083
+ }
30084
+ } : {},
29170
30085
  cursor: {
29171
30086
  type: "string",
29172
30087
  minLength: 1,
@@ -29187,7 +30102,7 @@ const recallTool = (_memoryConfig, options) => {
29187
30102
  type: "integer",
29188
30103
  minimum: 1,
29189
30104
  maximum: 20,
29190
- description: "Maximum number of items to return per page. Defaults to 20."
30105
+ description: "Maximum items per page: defaults to 20 for messages, 5 for observations. For search, target number of excerpts (default 10), plus compact references for evidence already in context; bounded backfill may return fewer excerpts."
29191
30106
  },
29192
30107
  detail: {
29193
30108
  type: "string",
@@ -29228,13 +30143,59 @@ const recallTool = (_memoryConfig, options) => {
29228
30143
  }
29229
30144
  },
29230
30145
  execute: async (inputData, context) => {
29231
- const { mode, query, cursor, threadId: explicitThreadId, anchor, page, limit, detail, partType, toolName, partIndex, charOffset, before, after, viewAttachment } = inputData;
30146
+ const { mode, query, groupId, direction, cursor, threadId: explicitThreadId, anchor, page, limit, detail, partType, toolName, partIndex, charOffset, before, after, viewAttachment } = inputData;
29232
30147
  const memory = context?.memory;
29233
30148
  const currentThreadId = context?.agent?.threadId;
29234
30149
  const resourceId = context?.agent?.resourceId;
29235
30150
  const resolvedExplicitThreadId = explicitThreadId === "current" ? currentThreadId : explicitThreadId;
29236
30151
  if (!memory) throw new Error("Memory instance is required for recall");
29237
30152
  if (explicitThreadId === "current" && !currentThreadId) throw new Error("Could not resolve current thread.");
30153
+ if (mode === "observations") {
30154
+ if (!searchEnabled) return {
30155
+ results: SEARCH_NOT_CONFIGURED_MESSAGE,
30156
+ count: 0
30157
+ };
30158
+ if (!groupId) throw new Error("groupId is required for mode=\"observations\"");
30159
+ if (!resourceId) throw new Error("Resource ID is required for recall");
30160
+ const om = await getOMEngine?.() ?? null;
30161
+ if (!om) return {
30162
+ results: "Observation group paging requires observational memory and a storage adapter supporting observation history search.",
30163
+ count: 0
30164
+ };
30165
+ const pagingThreadId = !isResourceScope ? currentThreadId : resolvedExplicitThreadId || currentThreadId;
30166
+ if (!pagingThreadId) throw new Error("Could not resolve current thread.");
30167
+ const thread = await memory.getThreadById?.({ threadId: pagingThreadId });
30168
+ if (!thread || thread.resourceId !== resourceId) throw new Error("Thread not found");
30169
+ return pageObservationGroups({
30170
+ om,
30171
+ threadId: pagingThreadId,
30172
+ resourceId,
30173
+ ...parseGroupCursor(groupId),
30174
+ direction,
30175
+ limit: Math.min(Math.max(limit ?? 5, 1), 20),
30176
+ threadTitle: thread.title,
30177
+ includeThreadId: isResourceScope,
30178
+ countNewerMessages: async (cursor) => {
30179
+ const end = (await (await memory.getMemoryStore()).listMessagesById({ messageIds: [cursor] })).messages.find((m) => m.id === cursor);
30180
+ if (!end || end.threadId !== pagingThreadId) return 0;
30181
+ const { total } = await memory.recall({
30182
+ threadId: pagingThreadId,
30183
+ resourceId,
30184
+ page: 0,
30185
+ perPage: 1,
30186
+ orderBy: {
30187
+ field: "createdAt",
30188
+ direction: "ASC"
30189
+ },
30190
+ filter: { dateRange: {
30191
+ start: end.createdAt,
30192
+ startExclusive: true
30193
+ } }
30194
+ });
30195
+ return total;
30196
+ }
30197
+ });
30198
+ }
29238
30199
  if (mode === "search") {
29239
30200
  if (!searchEnabled) return {
29240
30201
  results: SEARCH_NOT_CONFIGURED_MESSAGE,
@@ -29244,13 +30205,16 @@ const recallTool = (_memoryConfig, options) => {
29244
30205
  if (!resourceId) throw new Error("Resource ID is required for recall");
29245
30206
  return searchMessagesForResource({
29246
30207
  memory,
30208
+ om: await getOMEngine?.() ?? null,
29247
30209
  resourceId,
29248
30210
  currentThreadId: currentThreadId || void 0,
30211
+ currentMessages: context?.agent?.getMessages?.(),
29249
30212
  query,
29250
30213
  topK: limit ?? 10,
29251
30214
  before,
29252
30215
  after,
29253
- threadScope: !isResourceScope ? currentThreadId || void 0 : resolvedExplicitThreadId || void 0
30216
+ threadScope: !isResourceScope ? currentThreadId || void 0 : resolvedExplicitThreadId || void 0,
30217
+ includeThreadId: isResourceScope
29254
30218
  });
29255
30219
  }
29256
30220
  if (mode === "threads") {
@@ -29967,475 +30931,6 @@ function createThreadUpdateMarker(params) {
29967
30931
  }
29968
30932
  };
29969
30933
  }
29970
- /**
29971
- * Find the index of the last completed observation boundary (end marker) in a message's parts.
29972
- * Returns -1 if no completed observation is found.
29973
- */
29974
- function findLastCompletedObservationBoundary(message) {
29975
- const parts = message.content?.parts;
29976
- if (!parts || !Array.isArray(parts)) return -1;
29977
- for (let i = parts.length - 1; i >= 0; i--) {
29978
- const part = parts[i];
29979
- if (part?.type === "data-om-observation-end" && part.data?.operationType !== "reflection") return i;
29980
- }
29981
- return -1;
29982
- }
29983
- /**
29984
- * Get unobserved parts from a message.
29985
- * If the message has a completed observation (start + end), only return parts after the end.
29986
- * If observation is in progress (start without end), include parts before the start.
29987
- * Otherwise, return all parts.
29988
- */
29989
- function getUnobservedParts(message) {
29990
- const parts = message.content?.parts;
29991
- if (!parts || !Array.isArray(parts)) return [];
29992
- const endMarkerIndex = findLastCompletedObservationBoundary(message);
29993
- if (endMarkerIndex === -1) return parts.filter((p) => {
29994
- return p?.type !== "data-om-observation-start";
29995
- });
29996
- return parts.slice(endMarkerIndex + 1).filter((p) => {
29997
- return !p?.type?.startsWith("data-om-observation-");
29998
- });
29999
- }
30000
- /**
30001
- * Compute a cursor pointing at the latest message by createdAt.
30002
- * Used to derive a stable observation boundary for replay pruning.
30003
- */
30004
- function getLastObservedMessageCursor(messages) {
30005
- let latest;
30006
- for (const msg of messages) {
30007
- if (!msg?.id || !msg.createdAt) continue;
30008
- if (!latest || new Date(msg.createdAt).getTime() > new Date(latest.createdAt).getTime()) latest = msg;
30009
- }
30010
- return latest ? {
30011
- createdAt: new Date(latest.createdAt).toISOString(),
30012
- id: latest.id
30013
- } : void 0;
30014
- }
30015
- /**
30016
- * Check if a message is at or before a cursor (by createdAt then id).
30017
- */
30018
- function isMessageAtOrBeforeCursor(msg, cursor) {
30019
- if (!msg.createdAt) return false;
30020
- const msgIso = new Date(msg.createdAt).toISOString();
30021
- if (msgIso < cursor.createdAt) return true;
30022
- if (msgIso === cursor.createdAt && msg.id === cursor.id) return true;
30023
- return false;
30024
- }
30025
- /**
30026
- * Return the toolCallId of a tool-invocation part in the given state, or undefined.
30027
- */
30028
- function getToolInvocationId(part, state) {
30029
- if (part?.type !== "tool-invocation") return void 0;
30030
- if (part.toolInvocation.state !== state) return void 0;
30031
- return part.toolInvocation.toolCallId || void 0;
30032
- }
30033
- function collectPendingToolResultIds(allMessages, observedIds) {
30034
- const pendingToolResultIds = /* @__PURE__ */ new Set();
30035
- for (const msg of allMessages) {
30036
- if (!msg?.id) continue;
30037
- if (observedIds?.has(msg.id)) continue;
30038
- const parts = msg.content?.parts;
30039
- if (!parts || !Array.isArray(parts)) continue;
30040
- for (const part of parts) {
30041
- const id = getToolInvocationId(part, "result");
30042
- if (id) pendingToolResultIds.add(id);
30043
- }
30044
- }
30045
- return pendingToolResultIds;
30046
- }
30047
- function hasPendingToolResultPair(message, pendingToolResultIds) {
30048
- if (pendingToolResultIds.size === 0) return false;
30049
- const parts = message.content?.parts;
30050
- if (!parts || !Array.isArray(parts)) return false;
30051
- return parts.some((part) => {
30052
- const id = getToolInvocationId(part, "call");
30053
- return id !== void 0 && pendingToolResultIds.has(id);
30054
- });
30055
- }
30056
- function collectPendingToolResultIdsAfterMarker(allMessages, markerMessageIndex, markerMessage) {
30057
- const pendingToolResultIds = /* @__PURE__ */ new Set();
30058
- const unobservedMarkerParts = getUnobservedParts(markerMessage);
30059
- if (unobservedMarkerParts.length > 0) collectPendingToolResultIds([{
30060
- ...markerMessage,
30061
- content: {
30062
- ...markerMessage.content,
30063
- parts: unobservedMarkerParts
30064
- }
30065
- }], void 0).forEach((id) => pendingToolResultIds.add(id));
30066
- collectPendingToolResultIds(allMessages.slice(markerMessageIndex + 1), void 0).forEach((id) => pendingToolResultIds.add(id));
30067
- return pendingToolResultIds;
30068
- }
30069
- function getUnobservedPartsPreservingToolCallPairs(message) {
30070
- const parts = message.content?.parts;
30071
- if (!parts || !Array.isArray(parts)) return [];
30072
- const endMarkerIndex = findLastCompletedObservationBoundary(message);
30073
- if (endMarkerIndex === -1) return getUnobservedParts(message);
30074
- const unobservedParts = getUnobservedParts(message);
30075
- const unobservedResultIds = /* @__PURE__ */ new Set();
30076
- for (const part of unobservedParts) {
30077
- const id = getToolInvocationId(part, "result");
30078
- if (id) unobservedResultIds.add(id);
30079
- }
30080
- if (unobservedResultIds.size === 0) return unobservedParts;
30081
- const preservedCalls = parts.slice(0, endMarkerIndex).filter((part) => {
30082
- const id = getToolInvocationId(part, "call");
30083
- return id !== void 0 && unobservedResultIds.has(id);
30084
- });
30085
- return preservedCalls.length > 0 ? [...preservedCalls, ...unobservedParts] : unobservedParts;
30086
- }
30087
- /**
30088
- * Get the messages Observational Memory is allowed to work with.
30089
- *
30090
- * Messages supplied through the `context` option and the synthetic `om-continuation`
30091
- * message are prompt-only input. Core's persistence contract already treats context as
30092
- * never-persist, while the continuation remains memory-sourced so it can stay in the live
30093
- * actor prompt.
30094
- *
30095
- * OM builds its windows from `get.all.db()`, which includes both categories, and then seals
30096
- * and persists candidates directly. Excluding them here keeps OM's observation, buffering,
30097
- * persistence, and token accounting consistent with their ephemeral contract.
30098
- */
30099
- function getObservableMessages(messageList) {
30100
- const contextMessageIds = messageList.makeMessageSourceChecker().context;
30101
- return messageList.get.all.db().filter((message) => {
30102
- return message.id !== "om-continuation" && !contextMessageIds.has(message.id);
30103
- });
30104
- }
30105
- /**
30106
- * Safely extract buffered observation chunks from a record.
30107
- * Handles both array and JSON-string formats, returning empty array if malformed.
30108
- */
30109
- /**
30110
- * Filter out already-observed messages from the in-memory context.
30111
- * Uses marker-boundary pruning (preferred) or record-based fallback.
30112
- *
30113
- * The `fallbackCursor` is optional — callers that need it should resolve it
30114
- * from thread metadata before calling this function.
30115
- */
30116
- function filterObservedMessages(opts) {
30117
- const { messageList, record } = opts;
30118
- const allMessages = getObservableMessages(messageList);
30119
- const useMarkerBoundaryPruning = opts.useMarkerBoundaryPruning ?? true;
30120
- const preserveMessageIds = opts.preserveMessageIds ?? /* @__PURE__ */ new Set();
30121
- const observedIds = new Set(Array.isArray(record?.observedMessageIds) ? record.observedMessageIds : []);
30122
- let markerMessageIndex = -1;
30123
- let markerMessage = null;
30124
- for (let i = allMessages.length - 1; i >= 0; i--) {
30125
- const msg = allMessages[i];
30126
- if (!msg) continue;
30127
- if (findLastCompletedObservationBoundary(msg) !== -1) {
30128
- markerMessageIndex = i;
30129
- markerMessage = msg;
30130
- break;
30131
- }
30132
- }
30133
- if (useMarkerBoundaryPruning && markerMessage && markerMessageIndex !== -1) {
30134
- const pendingToolResultIds = collectPendingToolResultIdsAfterMarker(allMessages, markerMessageIndex, markerMessage);
30135
- const messagesToRemove = [];
30136
- for (let i = 0; i < markerMessageIndex; i++) {
30137
- const msg = allMessages[i];
30138
- if (msg?.id && msg.id !== "om-continuation" && !preserveMessageIds.has(msg.id) && !hasPendingToolResultPair(msg, pendingToolResultIds)) messagesToRemove.push(msg.id);
30139
- }
30140
- if (messagesToRemove.length > 0) messageList.removeByIds(messagesToRemove);
30141
- const unobserved = getUnobservedPartsPreservingToolCallPairs(markerMessage);
30142
- if (unobserved.length === 0) {
30143
- if (markerMessage.id) messageList.removeByIds([markerMessage.id]);
30144
- } else if (unobserved.length < (markerMessage.content?.parts?.length ?? 0)) markerMessage.content.parts = unobserved;
30145
- } else if (record) {
30146
- const pendingToolResultIds = collectPendingToolResultIds(allMessages, observedIds);
30147
- const derivedCursor = opts.fallbackCursor ?? getLastObservedMessageCursor(allMessages.filter((msg) => !!msg?.id && observedIds.has(msg.id) && !!msg.createdAt));
30148
- const lastObservedAt = record.lastObservedAt;
30149
- const messagesToRemove = [];
30150
- for (const msg of allMessages) {
30151
- if (!msg?.id || msg.id === "om-continuation" || preserveMessageIds.has(msg.id)) continue;
30152
- const hasPendingPair = hasPendingToolResultPair(msg, pendingToolResultIds);
30153
- if (observedIds.has(msg.id)) {
30154
- if (hasPendingPair) continue;
30155
- messagesToRemove.push(msg.id);
30156
- continue;
30157
- }
30158
- if (derivedCursor && isMessageAtOrBeforeCursor(msg, derivedCursor)) {
30159
- if (hasPendingPair) continue;
30160
- messagesToRemove.push(msg.id);
30161
- continue;
30162
- }
30163
- if (lastObservedAt && msg.createdAt) {
30164
- if (new Date(msg.createdAt) <= lastObservedAt) {
30165
- if (hasPendingPair) continue;
30166
- messagesToRemove.push(msg.id);
30167
- }
30168
- }
30169
- }
30170
- if (messagesToRemove.length > 0) messageList.removeByIds(messagesToRemove);
30171
- }
30172
- }
30173
- function getBufferedChunks(record) {
30174
- if (!record?.bufferedObservationChunks) return [];
30175
- if (Array.isArray(record.bufferedObservationChunks)) return record.bufferedObservationChunks;
30176
- if (typeof record.bufferedObservationChunks === "string") try {
30177
- const parsed = JSON.parse(record.bufferedObservationChunks);
30178
- return Array.isArray(parsed) ? parsed : [];
30179
- } catch {
30180
- return [];
30181
- }
30182
- return [];
30183
- }
30184
- /**
30185
- * Sort threads by their oldest unobserved message.
30186
- * Returns thread IDs in order from oldest to most recent.
30187
- */
30188
- /**
30189
- * Combine active and buffered observations for the buffering observer context.
30190
- */
30191
- function combineObservationsForBuffering(activeObservations, bufferedObservations) {
30192
- if (!activeObservations && !bufferedObservations) return void 0;
30193
- if (!activeObservations) return bufferedObservations;
30194
- if (!bufferedObservations) return activeObservations;
30195
- return `${activeObservations}\n\n--- BUFFERED (pending activation) ---\n\n${bufferedObservations}`;
30196
- }
30197
- function sortThreadsByOldestMessage(messagesByThread) {
30198
- return Array.from(messagesByThread.entries()).map(([threadId, messages]) => ({
30199
- threadId,
30200
- oldestTimestamp: Math.min(...messages.map((m) => m.createdAt ? new Date(m.createdAt).getTime() : Date.now()))
30201
- })).sort((a, b) => a.oldestTimestamp - b.oldestTimestamp).map((t) => t.threadId);
30202
- }
30203
- /**
30204
- * Strip any thread tags that the Observer might have added.
30205
- * Thread attribution is handled externally by the system, not by the Observer.
30206
- */
30207
- function stripThreadTags(observations) {
30208
- return observations.replace(/<\/?thread\b[^>]{0,1024}>/gi, "").trim();
30209
- }
30210
- const OBSERVATION_GROUP_OPEN = "<observation-group";
30211
- const OBSERVATION_GROUP_CLOSE = "</observation-group>";
30212
- const ATTRIBUTE_PATTERN = /([\w][\w-]*)="([^"]*)"/g;
30213
- const REFLECTION_GROUP_SPLIT_PATTERN = /^##\s+Group\s+/m;
30214
- function parseObservationGroupAttributes(attributeString) {
30215
- const attributes = {};
30216
- for (const match of attributeString.matchAll(ATTRIBUTE_PATTERN)) {
30217
- const [, key, value] = match;
30218
- if (key && value !== void 0) attributes[key] = value;
30219
- }
30220
- return attributes;
30221
- }
30222
- function isLineStart(observations, index) {
30223
- return index === 0 || observations[index - 1] === "\n";
30224
- }
30225
- function scanObservationGroupTags(observations) {
30226
- const tags = [];
30227
- const incompleteOpenings = [];
30228
- let cursor = 0;
30229
- let closeFloor = 0;
30230
- let nestedFloor = -1;
30231
- while (cursor < observations.length) {
30232
- const start = observations.indexOf(OBSERVATION_GROUP_OPEN, cursor);
30233
- if (start === -1) break;
30234
- const attributesStart = start + 18;
30235
- if (!/\s/.test(observations[attributesStart] ?? "")) {
30236
- cursor = attributesStart;
30237
- continue;
30238
- }
30239
- const openEnd = observations.indexOf(">", attributesStart);
30240
- if (openEnd === -1) break;
30241
- const closeStart = observations.indexOf(OBSERVATION_GROUP_CLOSE, Math.max(openEnd + 1, closeFloor));
30242
- if (closeStart === -1) {
30243
- if (isLineStart(observations, start)) incompleteOpenings.push({
30244
- start,
30245
- end: openEnd + 1
30246
- });
30247
- closeFloor = observations.length;
30248
- cursor = openEnd + 1;
30249
- continue;
30250
- }
30251
- if (nestedFloor <= openEnd) {
30252
- const lineOpen = observations.indexOf(`\n${OBSERVATION_GROUP_OPEN}`, openEnd);
30253
- nestedFloor = lineOpen === -1 ? observations.length : lineOpen + 1;
30254
- }
30255
- const nestedStart = nestedFloor;
30256
- if (nestedStart < closeStart) {
30257
- incompleteOpenings.push({
30258
- start,
30259
- end: openEnd + 1
30260
- });
30261
- closeFloor = closeStart;
30262
- cursor = nestedStart;
30263
- continue;
30264
- }
30265
- tags.push({
30266
- start,
30267
- end: closeStart + 20,
30268
- attributeString: observations.slice(attributesStart, openEnd),
30269
- content: observations.slice(openEnd + 1, closeStart)
30270
- });
30271
- closeFloor = closeStart;
30272
- cursor = closeStart + 20;
30273
- }
30274
- return {
30275
- tags,
30276
- incompleteOpenings
30277
- };
30278
- }
30279
- function replaceObservationGroupTags(observations, replace) {
30280
- const { tags, incompleteOpenings } = scanObservationGroupTags(observations);
30281
- const parts = [];
30282
- let cursor = 0;
30283
- let openingIndex = 0;
30284
- const skipIncompleteOpeningsBefore = (limit) => {
30285
- while (openingIndex < incompleteOpenings.length && incompleteOpenings[openingIndex].start < limit) {
30286
- const opening = incompleteOpenings[openingIndex];
30287
- openingIndex++;
30288
- if (opening.start < cursor) continue;
30289
- parts.push(observations.slice(cursor, opening.start));
30290
- cursor = opening.end;
30291
- }
30292
- };
30293
- for (const tag of tags) {
30294
- skipIncompleteOpeningsBefore(tag.start);
30295
- parts.push(observations.slice(cursor, tag.start), replace(tag));
30296
- cursor = tag.end;
30297
- }
30298
- skipIncompleteOpeningsBefore(Number.POSITIVE_INFINITY);
30299
- parts.push(observations.slice(cursor));
30300
- return parts.join("");
30301
- }
30302
- function parseReflectionObservationGroupSections(content) {
30303
- const normalizedContent = content.trim();
30304
- if (!normalizedContent || !REFLECTION_GROUP_SPLIT_PATTERN.test(normalizedContent)) return [];
30305
- return normalizedContent.split(REFLECTION_GROUP_SPLIT_PATTERN).map((section) => section.trim()).filter(Boolean).map((section) => {
30306
- const newlineIndex = section.indexOf("\n");
30307
- return {
30308
- heading: (newlineIndex >= 0 ? section.slice(0, newlineIndex) : section).trim(),
30309
- body: stripReflectionGroupMetadata((newlineIndex >= 0 ? section.slice(newlineIndex + 1) : "").trim())
30310
- };
30311
- });
30312
- }
30313
- function stripReflectionGroupMetadata(body) {
30314
- return body.replace(/^_range:\s*`[^`]*`_\s*\n?/m, "").trim();
30315
- }
30316
- function generateAnchorId() {
30317
- return Buffer.from(globalThis.crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(8))).toString("hex");
30318
- }
30319
- function wrapInObservationGroup(observations, range, id = generateAnchorId(), _sourceGroupIds, kind) {
30320
- const content = observations.trim();
30321
- return `<observation-group id="${id}" range="${range}"${kind ? ` kind="${kind}"` : ""}>\n${content}\n</observation-group>`;
30322
- }
30323
- function parseObservationGroups(observations) {
30324
- if (!observations) return [];
30325
- const groups = [];
30326
- for (const tag of scanObservationGroupTags(observations).tags) {
30327
- const attributes = parseObservationGroupAttributes(tag.attributeString);
30328
- const id = attributes.id;
30329
- const range = attributes.range;
30330
- if (!id || !range) continue;
30331
- groups.push({
30332
- id,
30333
- range,
30334
- kind: attributes.kind,
30335
- content: tag.content.trim()
30336
- });
30337
- }
30338
- return groups;
30339
- }
30340
- function stripObservationGroups(observations) {
30341
- if (!observations) return observations;
30342
- return replaceObservationGroupTags(observations, (tag) => tag.content.trim()).replace(/\n{3,}/g, "\n\n").trim();
30343
- }
30344
- function getRangeSegments(range) {
30345
- return range.split(",").map((segment) => segment.trim()).filter(Boolean);
30346
- }
30347
- function getRangeEndpoints(segment) {
30348
- const parts = segment.split(":").map((part) => part.trim());
30349
- if (parts.length !== 2 || !parts[0] || !parts[1]) return null;
30350
- return {
30351
- start: parts[0],
30352
- end: parts[1]
30353
- };
30354
- }
30355
- /**
30356
- * `buildMessageRange` produces opaque UUID endpoints, so those segments cannot be ordered by value.
30357
- * Spanning from the first start to the last end keeps reflected metadata bounded — the full segment
30358
- * list is deliberately not preserved (#14791). Segments that are not `start:end` pairs are left as
30359
- * they are rather than being joined into a fabricated span.
30360
- */
30361
- function spanRangeSegments(segments) {
30362
- const endpoints = segments.map(getRangeEndpoints);
30363
- if (endpoints.some((endpoint) => endpoint === null)) return segments.join(",");
30364
- const first = endpoints[0];
30365
- const last = endpoints.at(-1);
30366
- return `${first.start}:${last.end}`;
30367
- }
30368
- function combineObservationGroupRanges(groups) {
30369
- const segments = Array.from(new Set(groups.flatMap((group) => getRangeSegments(group.range))));
30370
- if (segments.length === 0) return "";
30371
- const numericEndpoints = [];
30372
- if (!segments.every((segment) => {
30373
- const endpoints = getRangeEndpoints(segment);
30374
- if (!endpoints || !/^\d+$/.test(endpoints.start) || !/^\d+$/.test(endpoints.end)) return false;
30375
- const start = {
30376
- label: endpoints.start,
30377
- value: Number(endpoints.start)
30378
- };
30379
- const end = {
30380
- label: endpoints.end,
30381
- value: Number(endpoints.end)
30382
- };
30383
- if (!Number.isSafeInteger(start.value) || !Number.isSafeInteger(end.value)) return false;
30384
- numericEndpoints.push(start.value <= end.value ? {
30385
- start,
30386
- end
30387
- } : {
30388
- start: end,
30389
- end: start
30390
- });
30391
- return true;
30392
- })) return spanRangeSegments(segments);
30393
- const first = numericEndpoints.reduce((lowest, pair) => pair.start.value < lowest.start.value ? pair : lowest);
30394
- const last = numericEndpoints.reduce((highest, pair) => pair.end.value > highest.end.value ? pair : highest);
30395
- return `${first.start.label}:${last.end.label}`;
30396
- }
30397
- function renderObservationGroupsForReflection(observations) {
30398
- if (parseObservationGroups(observations).length === 0) return null;
30399
- return replaceObservationGroupTags(observations, (tag) => {
30400
- const attributes = parseObservationGroupAttributes(tag.attributeString);
30401
- if (!attributes.id || !attributes.range) return tag.content.trim();
30402
- return `## Group \`${attributes.id}\`\n_range: \`${attributes.range}\`_\n\n${tag.content.trim()}`;
30403
- }).replace(/\n{3,}/g, "\n\n").trim();
30404
- }
30405
- function getCanonicalGroupId(sectionHeading, fallbackIndex) {
30406
- return sectionHeading.match(/`([^`]+)`/)?.[1]?.trim() || `derived-group-${fallbackIndex + 1}`;
30407
- }
30408
- function getContentLines(content) {
30409
- return content.split("\n").map((line) => line.trim()).filter(Boolean);
30410
- }
30411
- function deriveObservationGroupProvenance(content, groups) {
30412
- const sections = parseReflectionObservationGroupSections(content);
30413
- if (sections.length === 0 || groups.length === 0) return [];
30414
- return sections.map((section, index) => {
30415
- const canonicalGroupId = getCanonicalGroupId(section.heading, index);
30416
- const identifiedGroup = groups.find((group) => group.id === canonicalGroupId);
30417
- const bodyLines = new Set(getContentLines(section.body));
30418
- const matchingGroups = groups.filter((group) => getContentLines(group.content).some((line) => bodyLines.has(line)));
30419
- const fallbackGroup = groups[Math.min(index, groups.length - 1)];
30420
- const identifiedLines = new Set(getContentLines(identifiedGroup?.content ?? ""));
30421
- const contributingGroups = identifiedGroup ? matchingGroups.filter((group) => group.id !== identifiedGroup.id && getContentLines(group.content).some((line) => bodyLines.has(line) && !identifiedLines.has(line))) : [];
30422
- return {
30423
- id: canonicalGroupId,
30424
- range: combineObservationGroupRanges(identifiedGroup ? groups.filter((group) => group === identifiedGroup || contributingGroups.includes(group)) : matchingGroups.length > 0 ? matchingGroups : fallbackGroup ? [fallbackGroup] : []),
30425
- kind: "reflection",
30426
- content: section.body
30427
- };
30428
- });
30429
- }
30430
- function reconcileObservationGroupsFromReflection(content, sourceObservations) {
30431
- const sourceGroups = parseObservationGroups(sourceObservations);
30432
- if (sourceGroups.length === 0) return null;
30433
- const normalizedContent = content.trim();
30434
- if (!normalizedContent) return "";
30435
- const derivedGroups = deriveObservationGroupProvenance(normalizedContent, sourceGroups);
30436
- if (derivedGroups.length > 0) return derivedGroups.map((group) => wrapInObservationGroup(group.content, group.range, group.id, void 0, group.kind)).join("\n\n");
30437
- return wrapInObservationGroup(normalizedContent, combineObservationGroupRanges(sourceGroups), generateAnchorId(), void 0, "reflection");
30438
- }
30439
30934
  function isRecord$1(value) {
30440
30935
  return !!value && typeof value === "object" && !Array.isArray(value);
30441
30936
  }
@@ -30774,7 +31269,7 @@ var ObservationStrategy = class ObservationStrategy {
30774
31269
  }
30775
31270
  return `${existingObservations}${lastObservedAt ? ObservationStrategy.createMessageBoundary(lastObservedAt) : "\n\n"}${newThreadSection}`;
30776
31271
  }
30777
- async indexObservationGroups(observations, threadId, resourceId, observedAt) {
31272
+ async indexObservationGroups(observations, threadId, resourceId, observedAt, recordId) {
30778
31273
  if (!resourceId || !this.deps.onIndexObservations) return;
30779
31274
  const groups = parseObservationGroups(observations);
30780
31275
  if (groups.length === 0) return;
@@ -30784,7 +31279,8 @@ var ObservationStrategy = class ObservationStrategy {
30784
31279
  range: group.range,
30785
31280
  threadId,
30786
31281
  resourceId,
30787
- observedAt
31282
+ observedAt,
31283
+ recordId
30788
31284
  }), {
30789
31285
  label: "index-observations",
30790
31286
  abortSignal: this.opts.abortSignal
@@ -31058,7 +31554,7 @@ var SyncObservationStrategy = class extends ObservationStrategy {
31058
31554
  lastObservedAt: processed.lastObservedAt,
31059
31555
  observedMessageIds: processed.observedMessageIds
31060
31556
  });
31061
- await this.indexObservationGroups(processed.observations, threadId, resourceId, processed.lastObservedAt);
31557
+ await this.indexObservationGroups(processed.observations, threadId, resourceId, processed.lastObservedAt, record.id);
31062
31558
  }
31063
31559
  async emitEndMarkers(cycleId, processed) {
31064
31560
  const actualTokensObserved = await this.tokenCounter.countMessagesAsync(this.opts.messages);
@@ -31230,7 +31726,7 @@ var AsyncBufferObservationStrategy = class extends ObservationStrategy {
31230
31726
  label: "persist-buffered-observations",
31231
31727
  abortSignal: this.opts.abortSignal
31232
31728
  });
31233
- await this.indexObservationGroups(processed.observations, threadId, resourceId, processed.lastObservedAt);
31729
+ await this.indexObservationGroups(processed.observations, threadId, resourceId, processed.lastObservedAt, record.id);
31234
31730
  const candidateTitle = processed.threadTitle?.trim();
31235
31731
  const hasValidThreadTitle = !!candidateTitle && candidateTitle.length >= 3;
31236
31732
  if (hasValidThreadTitle || processed.extractedValues) {
@@ -31633,7 +32129,7 @@ var ResourceScopedObservationStrategy = class extends ObservationStrategy {
31633
32129
  lastObservedAt: processed.lastObservedAt,
31634
32130
  observedMessageIds: processed.observedMessageIds
31635
32131
  });
31636
- if (resourceId) await Promise.all(this.observationResults.map(({ threadId, threadMessages, result }) => this.indexObservationGroups(result.observations, threadId, resourceId, this.getMaxMessageTimestamp(threadMessages))));
32132
+ if (resourceId) await Promise.all(this.observationResults.map(({ threadId, threadMessages, result }) => this.indexObservationGroups(result.observations, threadId, resourceId, this.getMaxMessageTimestamp(threadMessages), record.id)));
31637
32133
  }
31638
32134
  async emitEndMarkers(cycleId, processed) {
31639
32135
  for (const obsResult of this.observationResults) {
@@ -34753,9 +35249,9 @@ var ObservationalMemory = class ObservationalMemory {
34753
35249
  * the message for the current thread.
34754
35250
  */
34755
35251
  formatObservationsForContext(observations, currentTask, suggestedResponse, extractedValues, unobservedContextBlocks, currentDate, retrieval = false, timeZone) {
34756
- let optimized = retrieval ? renderObservationGroupsForReflection(observations) ?? optimizeObservationsForContext(observations) : optimizeObservationsForContext(observations);
35252
+ let optimized = retrieval ? renderObservationGroupsForReflection(observations, { includeReflectionKind: true }) ?? optimizeObservationsForContext(observations) : optimizeObservationsForContext(observations);
34757
35253
  if (currentDate) optimized = addRelativeTimeToObservations(optimized, currentDate, timeZone);
34758
- const messages = [`${getObservationContextPrompt(this.scope)}\n\n${OBSERVATION_CONTEXT_INSTRUCTIONS}${retrieval ? `\n\n${getRetrievalInstructions(this.retrievalScope, this.retrievalInstructions, this.retrievalSearch)}` : ""}`];
35254
+ const messages = [`${getObservationContextPrompt(this.scope)}\n\n${OBSERVATION_CONTEXT_INSTRUCTIONS}${retrieval ? `\n\n${getRetrievalInstructions(this.retrievalScope, this.retrievalInstructions, this.retrievalSearch, this.storage.supportsObservationalMemoryHistorySearch === true)}` : ""}`];
34759
35255
  if (unobservedContextBlocks) messages.push(`The following content is from OTHER conversations different from the current conversation, they're here for reference, but they're not necessarily your focus:\nSTART_OTHER_CONVERSATIONS_BLOCK\n${unobservedContextBlocks}\nEND_OTHER_CONVERSATIONS_BLOCK`);
34760
35256
  const observationChunks = this.splitObservationContextChunks(optimized);
34761
35257
  if (observationChunks.length > 0) messages.push("<observations>", ...observationChunks);
@@ -35252,7 +35748,8 @@ ${formattedMessages}
35252
35748
  *
35253
35749
  * Loads thread metadata (currentTask, suggestedResponse), formats observations
35254
35750
  * with context prompts and instructions, and returns the fully-formed string.
35255
- * Returns undefined if no observations exist.
35751
+ * Returns recall guidance even without observations when retrieval is enabled;
35752
+ * otherwise returns undefined when no observations exist.
35256
35753
  *
35257
35754
  * This is the public entry point for context formatting — used by both
35258
35755
  * Memory.getContext() (standalone) and the processor (via injectObservationsIntoMessages).
@@ -35276,9 +35773,9 @@ ${formattedMessages}
35276
35773
  */
35277
35774
  async buildContextSystemMessages(opts) {
35278
35775
  const { threadId, resourceId, unobservedContextBlocks } = opts;
35279
- const record = opts.record ?? await this.getOrCreateRecord(threadId, resourceId);
35280
- if (!record.activeObservations) {
35281
- if (this.retrieval && this.retrievalScope === "resource") return [getRetrievalInstructions(this.retrievalScope, this.retrievalInstructions, this.retrievalSearch)];
35776
+ const record = opts.record === void 0 ? await this.getOrCreateRecord(threadId, resourceId) : opts.record;
35777
+ if (!record?.activeObservations) {
35778
+ if (this.retrieval) return [getRetrievalInstructions(this.retrievalScope, this.retrievalInstructions, this.retrievalSearch, this.storage.supportsObservationalMemoryHistorySearch === true)];
35282
35779
  return;
35283
35780
  }
35284
35781
  const omMetadata = (0, _mastra_core_memory.getThreadOMMetadata)((await this.storage.getThreadById({ threadId }))?.metadata);
@@ -36514,12 +37011,12 @@ var ObservationalMemoryProcessor = class {
36514
37011
  resourceId,
36515
37012
  runState
36516
37013
  });
36517
- const systemMessages = ctx.omRecord ? await this.engine.buildContextSystemMessages({
37014
+ const systemMessages = await this.engine.buildContextSystemMessages({
36518
37015
  threadId,
36519
37016
  resourceId,
36520
37017
  record: ctx.omRecord,
36521
37018
  unobservedContextBlocks: ctx.otherThreadsContext
36522
- }) : void 0;
37019
+ });
36523
37020
  injectObservationContextMessages({
36524
37021
  messageList,
36525
37022
  systemMessages,
@@ -38141,6 +38638,7 @@ Notes:
38141
38638
  threadId: r.metadata.thread_id,
38142
38639
  score: r.score,
38143
38640
  groupId,
38641
+ recordId: typeof r.metadata.record_id === "string" ? r.metadata.record_id : void 0,
38144
38642
  range: typeof r.metadata.range === "string" ? r.metadata.range : void 0,
38145
38643
  text: typeof r.metadata.text === "string" ? r.metadata.text : void 0,
38146
38644
  observedAt: typeof r.metadata.observed_at === "string" || r.metadata.observed_at instanceof Date ? new Date(r.metadata.observed_at) : void 0
@@ -38158,7 +38656,7 @@ Notes:
38158
38656
  /**
38159
38657
  * Index a single observation group into the observation vector store.
38160
38658
  */
38161
- async indexObservation({ text, groupId, range, threadId, resourceId, observedAt }) {
38659
+ async indexObservation({ text, groupId, range, threadId, resourceId, observedAt, recordId }) {
38162
38660
  if (!this.vector || !this.embedder) return;
38163
38661
  const embedResult = await this.embedMessageContent(text);
38164
38662
  if (embedResult.embeddings.length === 0 || embedResult.dimension === void 0) return;
@@ -38186,6 +38684,7 @@ Notes:
38186
38684
  thread_id: threadId,
38187
38685
  resource_id: resourceId,
38188
38686
  observed_at: observedAt?.toISOString(),
38687
+ ...recordId ? { record_id: recordId } : {},
38189
38688
  text: chunk
38190
38689
  }))
38191
38690
  });
@@ -38362,7 +38861,11 @@ Notes:
38362
38861
  const omConfig = normalizeObservationalMemoryConfig(mergedConfig.observationalMemory);
38363
38862
  if (omConfig?.retrieval) tools.recall = recallTool(mergedConfig, {
38364
38863
  retrievalScope: typeof omConfig.retrieval === "object" ? omConfig.retrieval.scope ?? "resource" : "resource",
38365
- searchEnabled: this.hasRetrievalSearch(omConfig.retrieval)
38864
+ searchEnabled: this.hasRetrievalSearch(omConfig.retrieval),
38865
+ getOMEngine: async () => {
38866
+ const om = await this.omEngine;
38867
+ return om?.getStorage().supportsObservationalMemoryHistorySearch ? om : null;
38868
+ }
38366
38869
  });
38367
38870
  if (omConfig?.experimental_subconscious instanceof Subconscious && omConfig.experimental_subconscious.resolved.tools) {
38368
38871
  Object.assign(tools, createKnowledgeTools(this));
@@ -49830,4 +50333,4 @@ const agentBuilderWorkflows = {
49830
50333
  //#endregion
49831
50334
  exports.agentBuilderWorkflows = agentBuilderWorkflows;
49832
50335
 
49833
- //# sourceMappingURL=dist-CxDKuckB.cjs.map
50336
+ //# sourceMappingURL=dist-BACW0Que.cjs.map