Skip to content

feat(recall): cap injected memory context#71

Merged
withRiver merged 4 commits into
Tencent:mainfrom
YOMXXX:feat/recall-budget
May 26, 2026
Merged

feat(recall): cap injected memory context#71
withRiver merged 4 commits into
Tencent:mainfrom
YOMXXX:feat/recall-budget

Conversation

@YOMXXX
Copy link
Copy Markdown
Contributor

@YOMXXX YOMXXX commented May 21, 2026

Summary

  • Add recall.maxCharsPerMemory and recall.maxTotalRecallChars with defaults of 0, which do not alter existing behavior. Users can opt in by setting positive values to cap injected memory context.
  • Apply the budget after L1 search and before <relevant-memories> injection, preserving score order while truncating oversized entries and dropping overflow.
  • Document the new guards in README, README_CN, and openclaw.plugin.json.

Test Plan

  • npm test
  • npm run build
  • npm pack --dry-run --json
  • git diff --check

Notes

YOMXXX added 2 commits May 21, 2026 22:35
Signed-off-by: 李冠辰 <liguanchen@xiaomi.com>
Signed-off-by: 李冠辰 <liguanchen@xiaomi.com>
@Maxwell-Code07
Copy link
Copy Markdown
Collaborator

Maxwell-Code07 commented May 22, 2026

感谢您如此及时地解决了 #70 的问题!我们会内部评估,有结论后会尽快反馈。

yuanrengu added a commit to yuanrengu/TencentDB-Agent-Memory that referenced this pull request May 25, 2026
- Move preExtractMemories to newMessages only (after background/new split)
  to prevent extracting memories from background context that should
  only serve as conversational context for the LLM

- Remove MEDIUM-confidence hints logging (hints not wired to LLM prompt;
  keeping types as interface for follow-up PR)

- Remove src/ from package.json files field to fix Size Guard limit
  (matches pattern from Tencent#76 and Tencent#71)

- Export callLlmExtraction and passesConfidenceCheck for testability

- Add pre-extractor.test.ts covering:
  - Background messages not pre-extracted
  - HIGH-confidence dedup via mergeExtractedMemories
  - Malformed JSON triggers exactly one retry
  - Confidence filtering does not reject valid persona/instruction
Comment thread package.json
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DO NOT modify this file, which might cause some problems. We have increased the limitation of Size Guard from 512KB to 2048KB.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in 8031ec0: restored package.json to match upstream/main and removed the manifest / extension entry changes from this PR. Fresh checks are green, including Size Guard under the updated 2048KB limit.

@YOMXXX
Copy link
Copy Markdown
Contributor Author

YOMXXX commented May 25, 2026

Thanks for the review. I restored package.json to match upstream/main and removed the package manifest / extension entry changes from this PR.

Fresh local verification after the update:

  • npm test
  • npm run build
  • npm pack --dry-run ✅ package size 647.5 KB, below the updated 2048 KB Size Guard limit.

Comment thread src/config.ts Outdated
recall: {
enabled: bool(recallGroup, "enabled") ?? true,
maxResults: num(recallGroup, "maxResults") ?? 5,
maxCharsPerMemory: normalizeNonNegativeInt(num(recallGroup, "maxCharsPerMemory"), 800),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. normalizeNonNegativeInt() is unnecessary here.

All other config fields (maxResults, scoreThreshold, timeoutMs) use the simple num(...) ?? defaultValue pattern — these two fields don't need special treatment.

Defensive handling for negative/non-integer values is already done on the consumer side in auto-recall.ts's normalizeBudgetLimit() (<= 0 → treated as disabled, Math.floor() for rounding). No need to duplicate that at the parse layer.

  1. The defaults should be 0 (disabled), not 800 / 3000.

This is a new feature — the default behavior should remain backward-compatible. Existing users upgrading should not suddenly see their recall results being truncated or dropped. Those who need this guard can opt in by explicitly setting the values.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in c588558: recall budget config now follows the existing num(...) ?? 0 pattern, defaults are disabled, and normalizeNonNegativeInt() was removed.

Comment thread openclaw.plugin.json Outdated
"properties": {
"enabled": { "type": "boolean", "default": true, "description": "是否启用自动召回" },
"maxResults": { "type": "number", "default": 5, "description": "召回最大结果数" },
"maxCharsPerMemory": { "type": "number", "default": 800, "description": "单条 L1 记忆注入的最大字符数;填 0 表示不限制" },
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

default should be zero

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in c588558: plugin schema default is now 0.

Comment thread README.md Outdated
| `storeBackend` | `"sqlite"` | Storage backend: `sqlite` |
| `recall.strategy` | `"hybrid"` | Recall strategy: `keyword` / `embedding` / `hybrid` (RRF fusion, recommended) |
| `recall.maxResults` | `5` | Number of items returned per recall |
| `recall.maxCharsPerMemory` | `800` | Max characters injected for one recalled L1 memory; `0` disables this guard |
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

default should be zero

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in c588558: README default is now 0.

Comment thread README_CN.md Outdated
| `storeBackend` | `"sqlite"` | 存储后端:`sqlite` |
| `recall.strategy` | `"hybrid"` | 召回策略:`keyword` / `embedding` / `hybrid`(RRF 融合,推荐) |
| `recall.maxResults` | `5` | 每次召回条数 |
| `recall.maxCharsPerMemory` | `800` | 单条 L1 记忆注入的最大字符数;`0` 表示不限制 |
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

default should be zero

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in c588558: README_CN default is now 0.

Comment thread src/core/hooks/auto-recall.test.ts Outdated
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are no existing test files under src/core/hooks/. The budget logic in normalizeBudgetLimit / truncateRecallLine is straightforward enough that the config parsing coverage in src/config.test.ts plus manual verification should suffice for this PR. Let's remove this file for now.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in c588558: removed src/core/hooks/auto-recall.test.ts from this PR.

Comment thread src/config.test.ts Outdated
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this test file.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in c588558: removed src/config.test.ts from this PR.

Comment thread src/core/hooks/auto-recall.ts Outdated
continue;
}

const separatorChars = budgeted.length > 0 ? 1 : 0;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 1 here implicitly assumes the join separator is "\n" (line 212). Consider adding a brief comment like // "\n".length to make the coupling explicit

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in c588558: introduced RECALL_LINE_SEPARATOR and use its .length for budget accounting, matching the final join separator.

Comment thread src/core/hooks/auto-recall.ts Outdated
const totalBounded = truncateRecallLine(perMemoryBounded, remainingChars);
budgeted.push(totalBounded);
usedChars += separatorChars + totalBounded.length;
if (totalBounded !== perMemoryBounded) truncatedCount++;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

truncatedCount can be incremented twice for the same memory — once at line 734 (per-memory truncation) and again here (total-budget truncation). This makes the debug log misleading (e.g. reporting truncated=3 when only 2 distinct lines were affected). Consider tracking truncated lines with a Set or a boolean flag per iteration instead.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in c588558: truncatedCount now uses a per-iteration flag, so one emitted memory line is counted at most once even if both per-memory and total-budget truncation apply.

Comment on lines +748 to +758
if (perMemoryBounded.length > remainingChars) {
if (remainingChars >= MIN_TRUNCATED_RECALL_LINE_CHARS) {
const totalBounded = truncateRecallLine(perMemoryBounded, remainingChars);
budgeted.push(totalBounded);
usedChars += separatorChars + totalBounded.length;
if (totalBounded !== perMemoryBounded) truncatedCount++;
} else {
droppedCount++;
}
droppedCount += lines.length - i - 1;
break;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The droppedCount accumulation is split across the if/else branch and the line after it, making the logic harder to follow. Suggestion:

if (perMemoryBounded.length > remainingChars) {
  const canFit = remainingChars >= MIN_TRUNCATED_RECALL_LINE_CHARS;
  if (canFit) {
    budgeted.push(truncateRecallLine(perMemoryBounded, remainingChars));
  }
  droppedCount += lines.length - i - (canFit ? 1 : 0);
  break;
}

Single accumulation point, same semantics, easier to reason about.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in c588558: collapsed dropped-count handling to a single accumulation point in the total-budget overflow branch.

@withRiver
Copy link
Copy Markdown
Collaborator

LGTM. Thanks for your contribution!

By the way, there’s another issue #3 related to L1 recall. If you’re interested, feel free to take a look.

@withRiver withRiver merged commit 1bdcf28 into Tencent:main May 26, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants