Environment: Cortex plugin 4.13.1, Windows 11, Claude Code as MCP client (validates tool calls against the declared input schema; additionalProperties violations are rejected client-side).
Summary
The client-visible input schemas for remember and wiki_write are derived by FastMCP from the registered wrapper signatures — and those signatures have drifted from the handlers' own schemas and from the instructions your grooming planners emit. Net effect: the documented lesson-promotion loop cannot be closed over MCP, and get_grooming_health reports promotion: last_run=null, stale=true forever, even after every candidate has been processed.
The drift, file by file
1. remember — missing supersedes_id and write_class
-
Registered signature (mcp_server/tool_registry_memory.py:93-101, and the rooted variant at :65-72):
async def tool_remember(
content: str,
tags: list[str] | None = None,
directory: str | None = None,
domain: str | None = None,
source: str | None = None,
force: bool = False,
agent_topic: str | None = None,
) -> dict:
No supersedes_id, no write_class. Your own comment at tool_registry_memory.py:58-61 states that FastMCP derives the input schema from this signature — so clients never see these params.
-
Internal handler schema does declare both: mcp_server/handlers/remember_schema.py:182 (write_class, enum auto/deliberate/derived/mechanical) and :226 (supersedes_id).
-
Even if a permissive client passed them, the wrapper forwards a hand-built dict (tool_registry_memory.py:103-115) containing only content/tags/directory/domain/source/force/agent_topic — the params are silently dropped before the handler.
-
Meanwhile lesson_promotion's instructions tell the in-session LLM to call remember(content=..., supersedes_id=<memory_id>, tags=[..., 'promoted:<kind>'], write_class='deliberate', force=true) to close the loop, and curate_distill's job prompts require write_class='deliberate'. Neither call is possible.
2. wiki_write — missing memory_ids
- Registered signature (
mcp_server/tool_registry_wiki.py:58-63): path, content, mode, tags. The forwarded dict (:67-72) also omits memory_ids.
- The handler declares
memory_ids in its schema (mcp_server/handlers/wiki_write.py:132), reads it (:310), and _sync_page_and_cite (:173) writes the wiki.citations rows from it — this is the documented path by which a curate_wiki job's memory cluster becomes durable provenance. Unreachable over MCP: citations_written is always 0.
3. (minor, same family) curate_distill's job prompts instruct source='distillation', but remember_schema.py:175-181 restricts source to enum [session, tool, user, consolidation, import]. The registered signature (free string) happens to accept it — the three definitions disagree three ways.
Observed impact (live, 4.13.1)
Processed a full grooming pass by hand: all curate_wiki jobs authored, curate_distill dossiers distilled/skipped, all 3 lesson_promotion candidates resolved (one promoted to a wiki page). Afterwards get_grooming_health still reports:
"promotion": {"backlog_count": 3, "last_run_at": null, "stale": true}
because none of the loop-closing writes (wiki_write(memory_ids=...), remember(supersedes_id=...)) can be expressed through the declared schemas.
Suggested fix
- Add the missing parameters to the registered signatures and forward them:
supersedes_id: int | None = None, write_class: str | None = None on tool_remember; memory_ids: list[int] | None = None on tool_wiki_write.
- Longer term: generate the registered signatures (or at least a parity test) from the handlers'
schema["inputSchema"]["properties"], so a param added to a handler schema cannot silently miss the MCP surface again. A one-assertion test — set(handler schema properties) ⊆ set(wrapper signature params) per tool — would have caught all three of these.
Environment: Cortex plugin 4.13.1, Windows 11, Claude Code as MCP client (validates tool calls against the declared input schema;
additionalPropertiesviolations are rejected client-side).Summary
The client-visible input schemas for
rememberandwiki_writeare derived by FastMCP from the registered wrapper signatures — and those signatures have drifted from the handlers' own schemas and from the instructions your grooming planners emit. Net effect: the documented lesson-promotion loop cannot be closed over MCP, andget_grooming_healthreportspromotion: last_run=null, stale=trueforever, even after every candidate has been processed.The drift, file by file
1.
remember— missingsupersedes_idandwrite_classRegistered signature (
mcp_server/tool_registry_memory.py:93-101, and the rooted variant at:65-72):No
supersedes_id, nowrite_class. Your own comment attool_registry_memory.py:58-61states that FastMCP derives the input schema from this signature — so clients never see these params.Internal handler schema does declare both:
mcp_server/handlers/remember_schema.py:182(write_class, enum auto/deliberate/derived/mechanical) and:226(supersedes_id).Even if a permissive client passed them, the wrapper forwards a hand-built dict (
tool_registry_memory.py:103-115) containing only content/tags/directory/domain/source/force/agent_topic — the params are silently dropped before the handler.Meanwhile
lesson_promotion's instructions tell the in-session LLM to callremember(content=..., supersedes_id=<memory_id>, tags=[..., 'promoted:<kind>'], write_class='deliberate', force=true)to close the loop, andcurate_distill's job prompts requirewrite_class='deliberate'. Neither call is possible.2.
wiki_write— missingmemory_idsmcp_server/tool_registry_wiki.py:58-63):path, content, mode, tags. The forwarded dict (:67-72) also omitsmemory_ids.memory_idsin its schema (mcp_server/handlers/wiki_write.py:132), reads it (:310), and_sync_page_and_cite(:173) writes thewiki.citationsrows from it — this is the documented path by which acurate_wikijob's memory cluster becomes durable provenance. Unreachable over MCP:citations_writtenis always 0.3. (minor, same family)
curate_distill's job prompts instructsource='distillation', butremember_schema.py:175-181restrictssourceto enum[session, tool, user, consolidation, import]. The registered signature (free string) happens to accept it — the three definitions disagree three ways.Observed impact (live, 4.13.1)
Processed a full grooming pass by hand: all
curate_wikijobs authored,curate_distilldossiers distilled/skipped, all 3lesson_promotioncandidates resolved (one promoted to a wiki page). Afterwardsget_grooming_healthstill reports:because none of the loop-closing writes (
wiki_write(memory_ids=...),remember(supersedes_id=...)) can be expressed through the declared schemas.Suggested fix
supersedes_id: int | None = None,write_class: str | None = Noneontool_remember;memory_ids: list[int] | None = Noneontool_wiki_write.schema["inputSchema"]["properties"], so a param added to a handler schema cannot silently miss the MCP surface again. A one-assertion test — set(handler schema properties) ⊆ set(wrapper signature params) per tool — would have caught all three of these.