@@ -22,6 +22,9 @@ import type { EmbeddingService, EmbeddingCallOptions } from "../store/embedding.
2222import { sanitizeText } from "../../utils/sanitize.js" ;
2323
2424const TAG = "[memory-tdai] [recall]" ;
25+ const RECALL_TRUNCATION_SUFFIX = "…(已截断;可用 tdai_memory_search 或 tdai_conversation_search 查看详情)" ;
26+ const MIN_TRUNCATED_RECALL_LINE_CHARS = 40 ;
27+ const RECALL_LINE_SEPARATOR = "\n" ;
2528
2629/**
2730 * Memory tools usage guide — injected at the end of memory context so the
@@ -127,6 +130,7 @@ async function performAutoRecallInner(params: {
127130 const searchResult = await searchMemories ( userText , pluginDataDir , cfg , logger , effectiveStrategy as "keyword" | "embedding" | "hybrid" , vectorStore , embeddingService ) ;
128131 memoryLines = searchResult . lines ;
129132 searchTiming = searchResult . timing ;
133+ memoryLines = applyRecallBudget ( memoryLines , cfg . recall , logger ) ;
130134
131135 // Extract structured RecalledMemory from formatted lines for metric reporting
132136 recalledL1Memories = memoryLines . map ( ( line ) => {
@@ -206,7 +210,7 @@ async function performAutoRecallInner(params: {
206210 let prependContext : string | undefined ;
207211 if ( memoryLines . length > 0 ) {
208212 prependContext =
209- `<relevant-memories>\n以下是当前对话召回的相关记忆,不代表当前任务进程,仅作为参考:\n\n${ memoryLines . join ( "\n" ) } \n</relevant-memories>` ;
213+ `<relevant-memories>\n以下是当前对话召回的相关记忆,不代表当前任务进程,仅作为参考:\n\n${ memoryLines . join ( RECALL_LINE_SEPARATOR ) } \n</relevant-memories>` ;
210214 }
211215
212216 // Append memory tools usage guide to the stable part so the agent knows
@@ -706,6 +710,85 @@ function formatMemoryLine(m: FormatableMemory): string {
706710 return line ;
707711}
708712
713+ function applyRecallBudget (
714+ lines : string [ ] ,
715+ recall : MemoryTdaiConfig [ "recall" ] ,
716+ logger ?: Logger ,
717+ ) : string [ ] {
718+ const maxCharsPerMemory = normalizeBudgetLimit ( recall . maxCharsPerMemory ) ;
719+ const maxTotalRecallChars = normalizeBudgetLimit ( recall . maxTotalRecallChars ) ;
720+
721+ if ( ! maxCharsPerMemory && ! maxTotalRecallChars ) {
722+ return lines ;
723+ }
724+
725+ const budgeted : string [ ] = [ ] ;
726+ let usedChars = 0 ;
727+ let truncatedCount = 0 ;
728+ let droppedCount = 0 ;
729+
730+ for ( let i = 0 ; i < lines . length ; i ++ ) {
731+ const line = lines [ i ] ;
732+ const perMemoryBounded = maxCharsPerMemory
733+ ? truncateRecallLine ( line , maxCharsPerMemory )
734+ : line ;
735+ let wasTruncated = perMemoryBounded !== line ;
736+
737+ if ( ! maxTotalRecallChars ) {
738+ budgeted . push ( perMemoryBounded ) ;
739+ if ( wasTruncated ) truncatedCount ++ ;
740+ continue ;
741+ }
742+
743+ const separatorChars = budgeted . length > 0 ? RECALL_LINE_SEPARATOR . length : 0 ;
744+ const remainingChars = maxTotalRecallChars - usedChars - separatorChars ;
745+ if ( remainingChars <= 0 ) {
746+ droppedCount += lines . length - i ;
747+ break ;
748+ }
749+
750+ if ( perMemoryBounded . length > remainingChars ) {
751+ const canFit = remainingChars >= MIN_TRUNCATED_RECALL_LINE_CHARS ;
752+ if ( canFit ) {
753+ const totalBounded = truncateRecallLine ( perMemoryBounded , remainingChars ) ;
754+ budgeted . push ( totalBounded ) ;
755+ usedChars += separatorChars + totalBounded . length ;
756+ wasTruncated ||= totalBounded !== perMemoryBounded ;
757+ if ( wasTruncated ) truncatedCount ++ ;
758+ }
759+ droppedCount += lines . length - i - ( canFit ? 1 : 0 ) ;
760+ break ;
761+ }
762+
763+ budgeted . push ( perMemoryBounded ) ;
764+ usedChars += separatorChars + perMemoryBounded . length ;
765+ if ( wasTruncated ) truncatedCount ++ ;
766+ }
767+
768+ if ( truncatedCount > 0 || droppedCount > 0 ) {
769+ logger ?. debug ?.(
770+ `${ TAG } Recall budget applied: input=${ lines . length } , output=${ budgeted . length } , ` +
771+ `truncated=${ truncatedCount } , dropped=${ droppedCount } , ` +
772+ `maxCharsPerMemory=${ recall . maxCharsPerMemory } , maxTotalRecallChars=${ recall . maxTotalRecallChars } ` ,
773+ ) ;
774+ }
775+
776+ return budgeted ;
777+ }
778+
779+ function normalizeBudgetLimit ( value : number | undefined ) : number | undefined {
780+ if ( value == null || ! Number . isFinite ( value ) || value <= 0 ) return undefined ;
781+ return Math . floor ( value ) ;
782+ }
783+
784+ function truncateRecallLine ( line : string , maxChars : number ) : string {
785+ if ( line . length <= maxChars ) return line ;
786+ if ( maxChars <= RECALL_TRUNCATION_SUFFIX . length ) {
787+ return line . slice ( 0 , maxChars ) ;
788+ }
789+ return `${ line . slice ( 0 , maxChars - RECALL_TRUNCATION_SUFFIX . length ) . trimEnd ( ) } ${ RECALL_TRUNCATION_SUFFIX } ` ;
790+ }
791+
709792/**
710793 * Format an ISO 8601 timestamp to a concise date or datetime string.
711794 * - If the time part is 00:00:00 → show date only (e.g. "2025-03-01")
0 commit comments