Summary
When Graph Memory recall returns a node (or an episodic message) whose content contains {{, assembling the system prompt fails with:
malformed prompt variable reference "{{ }}" in context "graph-memory:recall" (variable names match /^[a-z][a-z0-9_]*$/)
The failure is data-dependent: it only happens once a stored node/message containing {{...}} is recalled into context, and it fails the whole turn, not just the recall.
Environment
- Plugin:
graph-memory 1.6.0-beta.1, commit dbd72cc (docs: relaunch Graph Memory for DeepSeek Harness)
- Host: DeepSeek Harness (DSH)
--profile web, Node 25, macOS
- Store: SQLite (
~/.dsh/graph-memory/graph-memory.db)
Steps to reproduce
- Have a node in the graph whose
content (or a stored conversation message) contains {{ ... }} — in our case an EVENT node documenting a ${{ ... }} workaround:
现象: Error: ... 只要代码中含 ${{ ... }} 就报错
- Start a session whose query recalls that node (semantic recall + active-session nodes).
- The
system-prompt/assemble handler pushes the assembled text as context graph-memory:recall, and DSH's prompt interpolator throws the error above; the turn fails.
Root cause
assembleContext() in src/format/assemble.ts embeds recalled data into the prompt context with only XML escaping:
- node
name / description → escapeXml(...) (escapes & < > " only),
- node
content → inserted raw,
- edge
instruction / condition → escapeXml(...),
- episodic trace lines
[ROLE] <text> → escapeXml(...).
None of these neutralize {, so any {{...}} from memory reaches the prompt text.
On the harness side, @deepseek-ai/dsh-system-prompt treats every {{ in a contributed context as a variable group: it parses {{<name>}} and requires <name> to match ^[a-z][a-z0-9_]*$ and to be a registered variable, otherwise it throws (malformed reference / invalid name / unknown variable). So not only malformed groups like {{ }} fail — even well-formed-looking {{ foo }} from memory would fail with unknown prompt variable.
Memory content is untrusted historical data (the plugin itself labels it "untrusted reference material"), so it must be neutralized before being embedded into a prompt-templating context.
Suggested fix
Neutralize {{ / }} in all user-derived strings before embedding them into the assembled prompt, e.g.:
function neutralizePromptBraces(s: string): string {
return s.replace(/\{\{/g, "{ {").replace(/\}\}/g, "} }");
}
Apply it to node name / description / content, edge instruction / condition, and episodic message text in src/format/assemble.ts. That covers both callers: dsh.ts (DSH context graph-memory:recall) and index.ts (OpenClaw systemPromptAddition).
I verified this locally: with the patch, a node containing ${{ ... }} assembles into the context without any {{, the harness interpolation no longer throws, and the full plugin test suite (107 tests, incl. test/assemble.test.ts) passes.
Regression test suggestion
Add a unit test in test/assemble.test.ts that feeds a node whose content contains {{ ... }} and asserts the assembled XML contains no {{ group.
Summary
When Graph Memory recall returns a node (or an episodic message) whose content contains
{{, assembling the system prompt fails with:The failure is data-dependent: it only happens once a stored node/message containing
{{...}}is recalled into context, and it fails the whole turn, not just the recall.Environment
graph-memory1.6.0-beta.1, commitdbd72cc(docs: relaunch Graph Memory for DeepSeek Harness)--profile web, Node 25, macOS~/.dsh/graph-memory/graph-memory.db)Steps to reproduce
content(or a stored conversation message) contains{{ ... }}— in our case an EVENT node documenting a${{ ... }}workaround:system-prompt/assemblehandler pushes the assembled text as contextgraph-memory:recall, and DSH's prompt interpolator throws the error above; the turn fails.Root cause
assembleContext()insrc/format/assemble.tsembeds recalled data into the prompt context with only XML escaping:name/description→escapeXml(...)(escapes& < > "only),content→ inserted raw,instruction/condition→escapeXml(...),[ROLE] <text>→escapeXml(...).None of these neutralize
{, so any{{...}}from memory reaches the prompt text.On the harness side,
@deepseek-ai/dsh-system-prompttreats every{{in a contributed context as a variable group: it parses{{<name>}}and requires<name>to match^[a-z][a-z0-9_]*$and to be a registered variable, otherwise it throws (malformed reference / invalid name / unknown variable). So not only malformed groups like{{ }}fail — even well-formed-looking{{ foo }}from memory would fail withunknown prompt variable.Memory content is untrusted historical data (the plugin itself labels it "untrusted reference material"), so it must be neutralized before being embedded into a prompt-templating context.
Suggested fix
Neutralize
{{/}}in all user-derived strings before embedding them into the assembled prompt, e.g.:Apply it to node
name/description/content, edgeinstruction/condition, and episodic message text insrc/format/assemble.ts. That covers both callers:dsh.ts(DSH contextgraph-memory:recall) andindex.ts(OpenClawsystemPromptAddition).I verified this locally: with the patch, a node containing
${{ ... }}assembles into the context without any{{, the harness interpolation no longer throws, and the full plugin test suite (107 tests, incl.test/assemble.test.ts) passes.Regression test suggestion
Add a unit test in
test/assemble.test.tsthat feeds a node whose content contains{{ ... }}and asserts the assembled XML contains no{{group.