Originally created by: adityaharishch
Summary
Rewrites the session task-classifier prompt and adds an end-to-end DeepEval suite that measures the failure mode that matters most: forcing non-task ("untracked") work onto a ticket. Net result on 103 real hand-labeled sessions: untracked-guard 70% → 96%, zero real tasks lost (task recall 100%).
What's in here
Prompt (SKILL.md)
- Adds a hard TASK GATE — assign a
task_key only when (1) hands-on production not viewing, (2) scope match not topic/app/repo match, (3) the ticket is a listed candidate.
- Adds a "NOT evidence of working a ticket" list (key merely visible on screen; viewing/monitoring dashboards/traces/DB/PRs/logs; different repo/product; recency alone).
Production fixes
fix(classifier): clamp confidence/category_confidence to [0,1]. Outlines' FSM constrains JSON structure/type but not numeric range, so a model-emitted -0.85 was crashing model_validate_json and silently dropping the entire verdict.
perf(mlx-server): the /classify endpoint now uses the same inference core as production classify_session (cached FSM logits-processor), cutting latency ~34s → ~20s/call. (Prefix KV-cache stays disabled on this model — its cache is non-trimmable — so ~20s is the floor for the full session_summary.)
Eval (services/tests/evals/)
- DeepEval pytest suite (
test_mlx_e2e) over 103 real goldens with deterministic BaseMetrics: UntrackedNotTask (the guard) + TaskKeyMatch + SessionTypeMatch.
- 103 hand-labeled real sessions across 3 sets: tuning (49), held-out validation (28), false-positive stress (26).
build_real_goldens.py rebuilds the byte-exact production prompt per session.
Results (DeepEval, 103 goldens)
| Metric |
Pass rate |
| UntrackedNotTask (guard) |
96.1% |
| TaskKeyMatch |
87.4% |
| SessionTypeMatch |
81.6% |
| Task recall |
100% (10/10) |
Validated on 54 unseen sessions (held-out guard 100% / 92%) → generalizes, not overfit.
Notes
data/generated/ (built goldens) is gitignored — rebuilt from the labels via build_real_goldens.py.
skills-lock.json (deepeval skill hash) was intentionally left out — unrelated to this change.
Originally posted by: Akarsh-Hegde
Bug:
routingremoved from Python response — untracked work queue will be permanently empty in the dashboard_classify_onereturn dict (lines 1567–1579) no longer includes aroutingkey. Rust'sSessionClassificationstruct has#[serde(default = "default_routing")]returning"pending"(mod.rs:132), sodb_write.rs:82writestask_routing = 'pending'for every newly classified session.The dashboard buckets untracked work exclusively on
routing === 'queue':ui/components/views/TodayView.tsx:491:else if (s.routing === 'queue') pushToBucket('_queue', bs)— never fires for new sessionsTodayView.tsx:553:queueCount = .filter(s => s.routing === 'queue').length— always 0ui/components/views/SessionsView.tsx:102andTaskBadge.tsx:52,91also key on routing valuesNote:
collect.rsalready filters ontask_session_type = 'task', nottask_routing, so worklog generation is unaffected. The breakage is dashboard-only but high-impact: the untracked session queue — where real work that didn't match a ticket surfaces for review and ticket creation — will appear empty for all sessions classified after this PR.Fix options (pick one):
"routing": "auto" if task_key else ("queue" if result.session_type == "untracked" else "skip")back to the return dict in_classify_one.TodayView.tsx,TaskBadge.tsx,SessionsView.tsxto bucket onsession_typeinstead ofrouting(longer-term cleaner).The Rust struct comment says "Routing is computed by Rust, not the server" suggesting option 2 is the intended direction — but the Rust code hasn't been updated to implement it yet.
Originally posted by: Akarsh-Hegde
Bug:
category_explanationremoved from Python response — worklog comment bullets silently dropped for all new sessionsThe Python response no longer includes
category_explanation.today/route.tsin this PR already patches the dashboard tooltip by falling back totask_reasoning— good. But the PM worklog writer (src/pm_worklog/route.rs:232) was not updated and still readscategory_explanationdirectly:Since
db_write.rsconverts the empty serde default ("") to NULL in the DB, this condition is alwaysNonefor new sessions — the category justification bullet silently disappears from every Jira/Linear/GitHub worklog comment generated after this PR, with no error or warning.Fix: Either update
src/pm_worklog/route.rsto fall back totask_reasoningthe same waytoday/route.tsdoes, or build the bullet from the classification result'sreasoningfield (which is now always present and already stored in thetask_reasoningcolumn).Also: the Rust unit test at
src/intelligence/task_linker/mod.rs:688-703(deserializes_real_mlx_server_response_with_category_fields) hardcodescategory_explanationin the fixture JSON and asserts its value. The test still passes (static fixture), but it no longer reflects the live server wire format — the production path always givescategory_explanation: "". Consider adding a test with the current wire shape to guard the fallback path.Originally posted by: Akarsh-Hegde
Bug: Apple FM hallucinated
task_keydiscards real work as overhead instead of retaining it as untracked_coerce_apple_fm_result()does not validatetask_keyagainst the candidate set. When Apple FM (not FSM-constrained) emitssession_type: "task"with a plausible-but-fabricated key, the key passes through coercion unchanged and reaches_classify_one:1516:_error_resultreturnssession_type="overhead"withconfidence=0.0— a hard discard. Real work the model recognised (but assigned to the wrong key) is thrown away instead of being retained asuntracked.The FSM path cannot produce this failure because it is structurally constrained to the candidate set. The Apple FM path is the only code path that can generate a semantically invalid
task_key, and it 's the one without the guard.Fix: Pass
valid_keysinto_coerce_apple_fm_resultand null out an out-of-set key, downgrading tountracked:Originally posted by: adityaharishch
Thanks @Akarsh-Hegde — went through all three. Verdict: #3 is a real bug and is now fixed; [#1] and [#2] are not regressions from this PR. Details below.
✅ [#3] — Apple FM hallucinated
task_keydiscarded as overhead — FIXED (20af4be)Correct and real. The Apple FM path (
model_id == APPLE_INTELLIGENCE_ID) is not FSM-constrained, so it can emitsession_type:"task"with a fabricated key, which thevalid_keysguard in_classify_onethen hard-discards as overhead (confidence 0.0).Implemented your suggested fix:
_coerce_apple_fm_resultnow takesvalid_keysand nulls an out-of-set key, downgrading the verdict tountrackedso the recognised work is retained. Threadedvalid_keysthrough_classify_apple_fm→_parse→ coercion. The FSM path is structurally immune, so no change there.❌ [#1] —
routingremoved → empty untracked queue — not a regression (declining)routingwas never a key in the Python response —git log -S'"routing"' -- services/agents/run_task_linker_mlx.pyreturns nothing across all history, and it's absent frommain's return dict too. Rust'sdefault_routing()→"pending"has therefore always been the value written for MLX-classified sessions; this PR changed nothing here.Untracked work is also not lost:
TodayView.tsx's finalelsebranch bucketstask_key==null && routing!='queue' && session_type!='overhead'sessions into_untracked(line 491). The_queuebucket (routing==='queue') is a separate, explicitly-flagged-for-review lane that the MLX path has never populated — a pre-existing TODO ("routing computed by Rust" was never wired), out of scope for a prompt-rewrite PR. Happy to track it as its own issue if you'd like the Rust-side routing logic implemented.❌ [#2] —
category_explanationremoved → worklog comment bullets dropped — misattributed impact (declining)The field removal is real, but
src/pm_worklog/route.rs:232is a lineage trace span (tracing::info_span!), not worklog comment text — it only affects the OpenObserve trace tree. And even there the loss is negligible: the category name is still emitted on theclassification_verdictspan (line 224), and the richertask_reasoningis emitted on thereasoningspan (line 228), which is always present now.The actual Jira/Linear/GitHub comment text is generated by the Python synth, which doesn't read
category_explanationat all (no references inservices/agents/pm_worklog_update/).today/route.tsalready falls backcategory_explanation || task_reasoningfor the dashboard tooltip. So no comment bullet is dropped. The removal was intentional —task_reasoningis the justification carrier going forward.Good catch on the stale unit test fixture though —
mod.rs:688still hardcodescategory_explanation; it passes (static fixture) but no longer mirrors the wire format. Noting it; not blocking this PR.Related
Tickets:
#1Tickets:
#2Tickets:
#3Ticket changed by: adityaharishch
Originally posted by: adityaharishch
🎉 This PR is included in version 1.60.0 🎉
The release is available on:
v1.60.0Your semantic-release bot 📦🚀