fix(mcp): pass the retain strategy to the worker - #3296
Conversation
The async retain tool built the strategy into the content dict and then called submit_async_retain without it. That method only writes a strategy into the task payload when it is passed as the keyword argument, so the worker never saw one: apply_strategy() did not run and every strategy override — mission, entity labels, chunk size, free-form setting — fell back to the bank configuration, silently. Pass it the way sync_retain right below already does. The REST endpoint is unaffected; api_retain groups items by strategy and forwards it.
|
Ping on this one: it's a one-line fix for the MCP retain path dropping the strategy before it reaches the worker. CI never ran on the PR — I assume it needs workflow approval for a first-time contributor. No rush. |
ebarkhordar
left a comment
There was a problem hiding this comment.
The call-site enumeration holds. Parsing every submit_async_retain call in the repo gives four production callers, and api/http.py plus engine/memory_engine.py already pass strategy, so these two registrations were the last gaps.
One coverage gap on the new test. _make_mcp_server defaults to include_bank_id=True (tests/test_mcp_tools.py:1026) while MCPToolsConfig.include_bank_id_param defaults to False (mcp_tools.py:94), so test_retain_passes_strategy_to_the_worker pins the bank_id registration and leaves the else branch, the one a default config registers, unpinned.
Measured at ca4ad99 in a clean python:3.12-slim container (pip install -e ".[test]"):
| mutation | -k strategy |
whole file |
|---|---|---|
| none | 1 passed | 201 passed |
drop the strategy= line in the bank_id branch |
1 failed | |
drop the strategy= line in the else branch |
1 passed | 201 passed |
So half the fix can be reverted and the 201 tests stay green. That first row is also the signal your CI has not been able to give you yet: the file is green at your head. TestSyncRetain one class down already parameterizes both registrations (include_bank_id=True at :1459, False at :1466), so the same shape closes it:
async def test_retain_passes_strategy_without_bank_id_param(self, mock_memory):
mcp = _make_mcp_server(mock_memory, {"retain"}, include_bank_id=False)
await _tools(mcp)["retain"].fn(content="test", strategy="documents")
assert mock_memory.submit_async_retain.call_args.kwargs["strategy"] == "documents"strategy occurs exactly once in that test file, in your new test, so sync_retain's two registrations carry the same idiom with nothing pinning it either. That predates this PR and is yours to take or leave.
|
Thanks for the mutation run. Added Re-ran your mutations against both
|
Summary
The MCP
retaintool accepts astrategybut never delivers it to the worker, so every strategy override is silently ignored and the bank-level configuration is used instead.build_content_dictputs the strategy into the content item, and the call passes only the contents:submit_async_retainwrites the strategy into the task payload from its own keyword argument only:Nothing reads a strategy back out of the content items, so the payload has none,
apply_strategy()never runs, andretain_mission,entity_labels,retain_chunk_sizeandentities_allow_free_formall fall back to the bank config. No error, no warning.Both async
retainregistrations are affected.sync_retain, a few lines below, already does it correctly — this PR applies the same idiom:api_retain(REST) is unaffected: it groups items byitem.strategyand forwards it.Impact
Measured on a bank whose
canonical_documentstrategy restrictsrecord_typetostate,rule,procedurevia per-strategyentity_labels, importing the same three documents twice:analysis, 1decision)The illegal values are only possible because the bank-level vocabulary was in force — with the strategy's
entity_labelsapplied,build_labels_model()turns them into aLiteral[...]that structured output cannot violate.Worth noting for triage: the dry-run extract endpoint cannot reveal this, because it strips the extracted labels from its response. The defect is only visible in the stored facts, where it looks like model non-compliance rather than lost configuration.
Test
test_retain_passes_strategy_to_the_workerasserts thatsubmit_async_retainreceivesstrategyas a keyword. It fails without the change; the existingTestRetainNewParamscases keep passing.tests/test_mcp_tools.py: 201 passed../scripts/hooks/lint.shpasses.