Conflict Detector ability#270
Conversation
Passive background daemon that extracts your plans and commitments from natural conversation, stores them as structured events, and cross-checks every new capture against existing ones for scheduling conflicts. Fires a targeted interrupt the moment a clash is detected — naming both commitments and explaining exactly why they conflict. background.py: - Two-phase extraction: word count gate + LLM commitment extractor - Captures type, people, date hint, time hint, duration per commitment - Natural date resolution for day names, relative phrases, specific dates - Two-phase conflict check: free same-date scan first, LLM only on hits - Adjacent-day travel detection (overnight travel vs next-morning commitments) - Conflict pair dedup so the same clash never fires twice - Daily stale commitment expiry keeps the dataset clean - Startup alert for any unalerted conflicts from previous sessions main.py: - Triggered by 20+ hotword variants - Five intents: LIST, CONFLICTS, ADD, DISMISS, CLEAR - LIST shows upcoming commitments sorted by date with conflict count - CONFLICTS shows open conflicts sorted by severity, dismiss individually or all - ADD walks through commitment, date, and optional time - DISMISS and CLEAR both guarded with run_confirmation_loop
✅ Community PR Path Check — PassedAll changed files are inside the |
🔀 Branch Merge CheckPR direction: ✅ Passed — |
✅ Ability Validation Passed |
🔍 Lint Results✅
|
Bug 1 — intent routing: 'conflict detector' was hitting the CONFLICTS handler because 'conflict' matched the broad keyword check. Tightened to require specific phrases like 'any conflicts' or 'my conflicts'. 'conflict detector' alone now correctly falls through to LIST. Bug 2 — silent drops: when LLM extraction returned empty or date_hint was missing, the message was skipped with no log. Added INFO logs so these are visible in the editor and debuggable going forward. Bug 3 — adjacent-date conflict check had two problems: - Adjacent dates were computed from today's date instead of the commitment's own date, so 'flying out Thursday' was checking May 5/7 instead of May 6/8 — completely missing Friday's call - When the new commitment is travel, the check was only looking for other travel on adjacent days. It should check ALL commitments on adjacent days since travel blocks anything nearby. Flipped the logic: travel new_c checks all adjacent items, non-travel new_c checks travel-only on adjacent days.
Duplicate commitments: 'flying out' and 'Flying out Thursday night' were both stored because 70% word overlap (2/4 words = 50%) fell below the threshold. Added a subset check — if either phrase's words are fully contained in the other, it's a duplicate regardless of overlap ratio. 'clear all conflicts' bypassed the skill: hotword list only had 'clear conflicts', not 'clear all conflicts', so the main agent was handling it. Added 'clear all conflicts', 'clear all commitments', and 'clear all my' to HOTWORDS. No conflict fired for adjacent-day travel: the LLM conflict check prompt only described same-time conflicts, so the LLM correctly said 'no conflict' when Thursday night travel and a Friday call were on different days. Updated prompt to explicitly instruct the LLM to treat overnight/evening travel the night before any other commitment as a soft conflict, and added an adjacent-day note to the prompt when the two commitments aren't on the same date.
background.py: - _conflict_pair_exists now skips dismissed conflicts so re-added commitments can be re-detected - MAX_COMMITMENTS eviction falls back to oldest active commitment when no expired items exist - Conflict alert fires a single save before speaking instead of one per conflict - Alert message ends with "Say 'conflict detector' to review" so the user knows what to do - _resolve_date handles "after the weekend" → Monday in both files main.py: - "scheduling conflict" hotword correctly routes to CONFLICTS intent - _handle_clear allows clearing when only open conflicts remain (not just active commitments) - Orphaned conflict entries show "a previous commitment" instead of silently disappearing - CONFLICTS view notes "Showing the first 5" when more than 5 conflicts exist - Dismissing a single conflict tells the user how many remain and where to go next - All handlers reload fresh data from disk rather than using a snapshot from _run() - _handle_add runs a quick same-day / adjacent-travel hint after saving manually - LIST detail view offers follow-up navigation instead of exiting immediately - _handle_dismiss and _handle_clear both reload data internally, dropping stale params
uzair401
left a comment
There was a problem hiding this comment.
Great work on this @hassan1731996 — tested it with several conflicting flows and it captures conflicts clearly and explains them well.
One thing I'd flag for the next background daemon: please keep the polling interval between 60–90 seconds rather than 15s. A tighter loop sounds responsive but in practice it doesn't add much — most real commitments come in over the course of a conversation, not in 15-second bursts — and the LLM calls stack up fast. With MAX_LLM_CALLS_PER_POLL = 3 plus the per-commitment conflict checks, this can fire 5–10+ LLM calls per minute on a normal conversation. A 60–90s cadence gives the daemon room to batch newly-arrived messages, run extraction once over the batch, and only invoke the conflict check when there's a real same-date or adjacent-day candidate — same end result, fraction of the API spend.
Bigger picture: we have several abilities now built around the same passive-capture idea — Conflict Detector, Decision Journal, Social Memory — each running its own daemon, its own poll loop, its own LLM extraction over the same message history. That's three independent LLM extractions on every message. There's real value in consolidating them into a single passive-capture daemon that polls the message history once per cycle, runs one LLM call that classifies and extracts whatever it finds (commitment, decision, person/relationship note, etc.), and then routes the structured output to the right storage bucket. One graceful poll, one inference, multiple abilities served. Happy to sketch that architecture if you want to take it on next.
|
Thanks @uzair401, really appreciate the detailed feedback! The 15s polling was something I went back and forth on - I wanted it to feel reactive but you're right, the LLM cost doesn't justify it. Will bump it to 60–90s in the next revision. The shared passive-capture daemon idea is genuinely interesting. The redundancy is already obvious looking at how Conflict Detector, Decision Journal and Social Memory all do the same extraction pass independently - it's wasteful. Once I wrap up Portfolio Monitor I'm going to take this on - consolidating all three into a single daemon feels like the right next move. |
Sure @hassan1731996, I’ll share a rough sketch with you soon. Your ideas are quite creative as well, so I’d definitely like your input too so we can build something genuinely interesting and publish it on the marketplace together. |
What is Conflict Detector?
A passive ability that silently extracts your plans and commitments from natural conversation and cross-checks every new one against existing ones for scheduling conflicts. When a clash is detected, it interrupts immediately — not a vague reminder, a specific alert naming both commitments and explaining exactly why they conflict.
Just talk naturally. Say "I'll call Marcus on Friday" today and "flying out Thursday night" tomorrow — it will catch the conflict and tell you.
Key Features
Passive Capture
Conflict Detection
Interactive Queries
Smart Maintenance
How It Works Under the Hood
background.py (daemon)
_resolve_date: pure Python natural language → ISO date_run_conflict_check: same-date scan (free) → LLM conflict check (targeted)_llm_check_conflict: returns conflicts/reason/severity JSON_expire_stale_commitments: runs once per day, marks past commitments expiredsend_interrupt_signal()+speak(), flag set before speak to prevent double-firemain.py (interactive skill)
run_confirmation_loopon all destructive operations (DISMISS single, DISMISS all, CLEAR)resume_normal_flow()in finally on all exit pathsPlatform Compliance
resume_normal_flow()called immediately inwatch_loopand infinallyin_runsend_interrupt_signalsdict__init__.pyempty