fix(plugin): make before_message_write hook a no-op by default (cache-friendly)#52
Open
YOMXXX wants to merge 1 commit into
Open
fix(plugin): make before_message_write hook a no-op by default (cache-friendly)#52YOMXXX wants to merge 1 commit into
YOMXXX wants to merge 1 commit into
Conversation
…-friendly) The before_message_write hook in index.ts unconditionally stripped <relevant-memories> tags from user messages before persisting to the session JSONL. This destroyed LLM prompt-prefix stability across sub-agent / replay boundaries: each turn's in-memory effectivePrompt contained the memories block but the same turn re-read from the JSONL did not — every cross-boundary prompt suffered a first-token cache miss. Reported in Tencent#11 by @yunhao-tech; similar direction suggested by @changxu21-spec. The hook's stated dual purpose was (1) transcript cleanliness and (2) anti-feedback-loop. (2) is already handled independently by sanitizeText() in src/core/conversation/l0-recorder.ts:254 on every L0-bound message, so the hook strip is strictly about (1) and is strictly the cause of the cache miss. Default behavior change: the hook becomes a no-op. The transcript now preserves <relevant-memories> blocks; prompt cache hits across boundaries; long agent loops no longer accumulate first-token latency. Opt-in legacy strip: set TDAI_STRIP_RELEVANT_MEMORIES_ON_WRITE=1. Env is read on every hook invocation (no constructor-time caching), so operators can flip behavior without restarting OpenClaw / Hermes. Strip logic is factored into an exported helper maybeStripRelevantMemoriesOnWrite(message) so it can be unit-tested without mocking the OpenClaw runtime. Tests: new src/__tests__/before-message-write.test.ts — 11 cases covering env-unset default, env=1 strip for string/parts content, role guard, no-tag short-circuit, and a 6-row it.each over non-literal-"1" env values. Unchanged: - src/core/hooks/auto-recall.ts still injects <relevant-memories>. - src/core/conversation/l0-recorder.ts still strips via sanitizeText on L0 capture (true anti-loop defense). - src/utils/sanitize.ts itself. Hermes plugin path and Claude Code plugin path are both unaffected (neither goes through the OpenClaw before_message_write hook). Closes Tencent#11. Signed-off-by: 李冠辰 <liguanchen@xiaomi.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary | 摘要
before_message_writehook 默认改为 no-op,恢复 LLM prompt cache 在 sub-agent / replay 边界上的命中。原剥离行为保留为TDAI_STRIP_RELEVANT_MEMORIES_ON_WRITE=1的 opt-in。Closes #11.Default
before_message_writeto no-op for<relevant-memories>stripping so the LLM prompt prefix stays stable across sub-agent / replay boundaries. Legacy strip behavior is preserved behind an opt-in env var. Closes #11.Root cause
index.ts:591的 hook 把 user message 里的<relevant-memories>块剥离再写 session JSONL。hook 注释里写它有两层用意:但 (2) 已经被
src/core/conversation/l0-recorder.ts:254上的sanitizeText()独立完成 —— 每条 L0-bound message 都会经过sanitizeText,它会剥离<relevant-memories>(以及其他几个 injected tag)。所以 hook 那次剥离严格是为 (1),也是 #11 cache miss 的唯一来源。Fix
<relevant-memories>,user message 完整写入 session JSONL → sub-agent / replay 重读 transcript 时跟 in-memoryeffectivePrompt前缀对齐 → LLM cache 命中。TDAI_STRIP_RELEVANT_MEMORIES_ON_WRITE=1一键恢复剥离。env 在每次 hook 触发时读取,运行时切换不需要重启 host。auto-recall.ts仍注入<relevant-memories>到当前轮 user messagel0-recorder.ts的sanitizeText仍在 L0 录入时剥离 memories(真正的反馈循环防御)sanitize.ts自身Theoretical cache-hit model
N 轮对话,每轮 user message 含 1-3 KB
<relevant-memories>块。Sub-agent / replay 路径:Don't-strip cost:N × (1-3 KB) 额外 tokens。N ≤ 30、memories ≤ 3 KB 时 ≤ 90 KB ≈ 25 K tokens,远小于现代 context window 上限(128 K – 1 M),也明显小于 first-token cache miss 带来的延迟与单价损失(典型 cache hit 是 ~0.1× cost)。
Refactor
把 hook callback 抽成 export 的纯函数
maybeStripRelevantMemoriesOnWrite(message),hook 注册改为一行 wrapper。让 helper 可以脱离 OpenClaw runtime 独立单测,且 hook 的语义("看 message 决定要不要替换 content")跟 helper 签名 ({ content } | null) 一一对应。Tests
新建
src/__tests__/before-message-write.test.ts(main 上还没有 src 测试,跟 #39 / #42 / #51 同套路),11 个 cases:nullenv=1+ string content + 含 memoriesenv=1+ parts content + 某 part 含 memoriesenv=1+ role=assistant + 含 memoriesenv=1+ user msg 无 memories tagnull"true"/"yes"/"0"/"1 "/""/"TRUE"(非字面值"1")null(it.each 6 rows)Compatibility
/recall拿召回内容,自己拼 prompt,不走 OpenClawbefore_message_writehook。additionalContext在 cc 的UserPromptSubmithook 里注入召回,跟 OpenClaw hook 完全独立。TDAI_STRIP_RELEVANT_MEMORIES_ON_WRITE=1恢复旧行为。Out of scope
DCO
Commit 带
Signed-off-by: 李冠辰 <liguanchen@xiaomi.com>。