feat(mcp): Phase 5 — write-time near-duplicate gate (update-over-create)
#755 Phase 5. create_note / create_task now BLOCK a near-duplicate instead of silently inserting: they return {"duplicate": true, "existing_id", message} pointing at the record to UPDATE. Fights store bloat and stale competing copies that semantic search (RAG) would otherwise resurface for reconciliation. A force=true override creates anyway for genuinely-distinct records. - services/dedup.py: find_duplicate_note — two signals, scoped to owner + same project + same kind: (1) normalized-title exact match (cheap, always); (2) semantic cosine ≥ 0.90 but ONLY when body ≥ 200 chars (short/title-only embeddings false-positive — the pre-pivot lesson). Project-less (orphan) records compare only to other orphans on BOTH signals (orphan_only on the semantic call) — they're not matched across every project. - Gate wired into the MCP create_note/create_task tools (the LLM write path) with force override; _INSTRUCTIONS documents the duplicate response + force. - Opt-in by design: the service helper is only called from the interactive create tools. Internal/programmatic creates (recurrence spawn, imports) go straight through services.create_note and are NOT gated — a recurring task spawning its next same-titled instance must not be blocked. - Scope v1: MCP tools only. REST/web (human CRUD, needs a UI affordance) and create_rule (not a RAG surface; _INSTRUCTIONS already steer it) are follow-ups. - tests: dedup service (title/semantic/body-gate/type-filter) + tool gate (blocks, force bypasses) for notes and tasks. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -59,6 +59,12 @@ not something you wait to be asked for:
|
||||
before you re-derive it or open a duplicate.
|
||||
- Before creating a task, search for an existing one (search content_type=
|
||||
'task') — don't open a second task for work already tracked.
|
||||
- create_note / create_task enforce this: if a title- or meaning-similar record
|
||||
already exists in the same project, the call is BLOCKED and returns
|
||||
{"duplicate": true, "existing_id": ...} instead of creating. UPDATE that
|
||||
record (update_note / update_task / add_task_log) rather than duplicating.
|
||||
Only pass force=true when it's genuinely a distinct record — a duplicate both
|
||||
bloats the store and surfaces as a stale competing copy in later searches.
|
||||
- Scope to the project in scope. When a project is active (you called
|
||||
enter_project), pass its project_id to search / list_tasks / list_notes so
|
||||
results stay inside that project. Querying with no project_id pulls in every
|
||||
|
||||
Reference in New Issue
Block a user