@meridiona/meridian-darwin-arm64 1.57.0 → 1.58.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/VERSION CHANGED
@@ -1 +1 @@
1
- 1.57.0
1
+ 1.58.1
package/bin/meridian CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meridiona/meridian-darwin-arm64",
3
- "version": "1.57.0",
3
+ "version": "1.58.1",
4
4
  "description": "Prebuilt Meridian app for macOS arm64 (daemon binary + dashboard + Python services). Installed via @meridiona/meridian.",
5
5
  "homepage": "https://github.com/Meridiona/meridian",
6
6
  "repository": {
@@ -127,10 +127,32 @@ while launchctl print "${GUI_TARGET}/${LABEL}" >/dev/null 2>&1; do
127
127
  done
128
128
 
129
129
  echo "→ bootstrap ${LABEL}"
130
+ # `meridian stop` runs `launchctl disable` to clear the KeepAlive intent, which
131
+ # persists in launchd's per-user override DB. bootstrap REFUSES a disabled label
132
+ # with EIO (errno 5), so the override must be cleared FIRST — otherwise a plain
133
+ # reinstall (install-dev.sh) can't revive a service that was `meridian stop`-ped.
130
134
  launchctl enable "${GUI_TARGET}/${LABEL}" 2>/dev/null || true
131
- launchctl bootstrap "${GUI_TARGET}" "${PLIST_DEST}"
132
- launchctl enable "${GUI_TARGET}/${LABEL}"
133
- launchctl kickstart -k "${GUI_TARGET}/${LABEL}"
135
+ # bootstrap is genuinely flaky: it EIOs when the prior domain entry hasn't fully
136
+ # cleared even after the bootout-wait above. Do NOT let one transient failure
137
+ # abort the whole install under `set -e` (that's what left screenpipe down after
138
+ # a stop). Retry, re-enabling each round, and treat "already loaded" as success.
139
+ _bs_try=0
140
+ until launchctl bootstrap "${GUI_TARGET}" "${PLIST_DEST}" 2>/dev/null; do
141
+ if launchctl print "${GUI_TARGET}/${LABEL}" >/dev/null 2>&1; then
142
+ break # already in the domain — bootstrap only "failed" because it's present
143
+ fi
144
+ _bs_try=$(( _bs_try + 1 ))
145
+ if [[ "${_bs_try}" -ge 5 ]]; then
146
+ echo "⚠ bootstrap ${LABEL} failed after ${_bs_try} attempts — see launchctl print" >&2
147
+ break
148
+ fi
149
+ launchctl enable "${GUI_TARGET}/${LABEL}" 2>/dev/null || true
150
+ sleep 1
151
+ done
152
+ # Always finish with enable + kickstart, even if bootstrap was a no-op above, so a
153
+ # disabled-but-loaded service ends up enabled AND running.
154
+ launchctl enable "${GUI_TARGET}/${LABEL}" 2>/dev/null || true
155
+ launchctl kickstart -k "${GUI_TARGET}/${LABEL}" 2>/dev/null || true
134
156
 
135
157
  echo
136
158
  echo "✓ screenpipe installed and started"
@@ -66,6 +66,20 @@ _CONTEXT_WINDOW = 5
66
66
  _MAX_TOKENS = 1024
67
67
  _TEMPERATURE = 0.0 # greedy decoding — deterministic classification
68
68
 
69
+ # Candidate-set policy. When the dev has CONFIRMED a daily plan, restrict the
70
+ # classifier's candidate tickets to exactly those planned tickets instead of
71
+ # offering every open ticket (the historical "boost-never-filter" behaviour).
72
+ # Rationale: a focused candidate set sharpens precision on the day's declared
73
+ # work; off-plan work then intentionally falls through to `untracked` — a
74
+ # deliberate holding state — rather than being mis-linked onto an unrelated open
75
+ # ticket. NOTE: until a recall-recovery stage exists, `untracked` sessions do
76
+ # not produce PM worklogs, so off-plan work is not written back while this is on.
77
+ # "1" (default) → plan-only filtering whenever a plan is confirmed
78
+ # "0" → legacy boost-never-filter (plan tickets floated up, all kept)
79
+ # Read once at import — flipping it requires an MLX-server restart. Only ever
80
+ # active on days with a confirmed, non-empty plan; unplanned days are unaffected.
81
+ _PLAN_ONLY_CANDIDATES = os.environ.get("CLASSIFY_PLAN_ONLY_CANDIDATES", "1") == "1"
82
+
69
83
  # The eval-tuned default classifier model. It lives in the llm_selector catalog
70
84
  # (_MODELS) as "qwen3.5-9b-optiq"; llm_selector keeps it on machines where it
71
85
  # fits and degrades only when Metal headroom can't accommodate it. The catalog
@@ -579,7 +593,8 @@ def _fetch_session(
579
593
  row = con.execute(
580
594
  "SELECT id, app_name, started_at, ended_at, duration_s, session_text,"
581
595
  " session_text_source, window_titles, category, confidence,"
582
- " session_summary, claude_session_uuid,"
596
+ " session_summary, coding_agent_session_uuid,"
597
+ " segment_started_at, sealed_at, summary_source,"
583
598
  " min_frame_id, max_frame_id, frame_count"
584
599
  " FROM app_sessions WHERE id = ?",
585
600
  (session_id,),
@@ -684,17 +699,46 @@ def _fetch_pm_tasks(
684
699
  rows = con.execute(base_cols).fetchall()
685
700
  tasks = [dict(r) for r in rows]
686
701
 
687
- # Today's-focus boost: tag the tickets the dev declared for the day and float
688
- # them to the top of the candidate list, in their declared order. This is a
689
- # BOOST, never a filter — every other candidate still follows, so recall is
690
- # untouched. A focus key that isn't in `tasks` (e.g. excluded by curation)
691
- # simply has no effect; we never resurrect a filtered-out ticket.
702
+ # Candidate-set policy (see _PLAN_ONLY_CANDIDATES). `focus_keys` are the
703
+ # tickets the dev CONFIRMED for this session's day (empty when no plan).
692
704
  focus = focus_keys or []
693
- if focus:
694
- order = {key: i for i, key in enumerate(focus)}
695
- for t in tasks:
696
- t["is_today_focus"] = t["task_key"] in order
697
- tasks.sort(key=lambda t: (0, order[t["task_key"]]) if t.get("is_today_focus") else (1, 0))
705
+ if not focus:
706
+ # No confirmed plan offer every candidate. Unchanged behaviour for
707
+ # users who don't use the plan, or days that aren't confirmed yet.
708
+ # `is_today_focus` is left unset (falsy) on every ticket.
709
+ return tasks
710
+
711
+ order = {key: i for i, key in enumerate(focus)}
712
+
713
+ if _PLAN_ONLY_CANDIDATES:
714
+ # Plan-only: the candidate set IS the confirmed plan, in declared order.
715
+ # Off-plan work then has no candidate to match, so the model returns
716
+ # `untracked` (the intended holding state) instead of being shoehorned
717
+ # onto an unrelated ticket.
718
+ in_plan = [t for t in tasks if t["task_key"] in order]
719
+ # GUARD: never return an empty candidate set. If the confirmed plan's
720
+ # tickets are all absent from the live pool (curation-excluded, closed,
721
+ # or not yet synced), fall back to the full set — an empty list would
722
+ # force EVERY session that day to `untracked`.
723
+ if not in_plan:
724
+ log.warning(
725
+ "plan-only candidates: confirmed plan has no live candidate "
726
+ "tickets (focus=%s) — falling back to full candidate set",
727
+ focus,
728
+ )
729
+ return tasks
730
+ for t in in_plan:
731
+ t["is_today_focus"] = True
732
+ in_plan.sort(key=lambda t: order[t["task_key"]])
733
+ return in_plan
734
+
735
+ # Legacy boost-never-filter: tag the declared tickets and float them to the
736
+ # top in declared order, but keep every other candidate so recall is
737
+ # untouched. A focus key not in `tasks` (e.g. excluded by curation) simply
738
+ # has no effect — we never resurrect a filtered-out ticket.
739
+ for t in tasks:
740
+ t["is_today_focus"] = t["task_key"] in order
741
+ tasks.sort(key=lambda t: (0, order[t["task_key"]]) if t.get("is_today_focus") else (1, 0))
698
742
  return tasks
699
743
 
700
744
 
@@ -786,7 +830,7 @@ def _classify_one(
786
830
  # session_text and a concise, high-quality prose summary in
787
831
  # session_summary. Classify on the summary, not the multi-MB transcript:
788
832
  # cheaper, faster, and it's already the distilled "what was done".
789
- if session_raw.get("claude_session_uuid") and (session_raw.get("session_summary") or "").strip():
833
+ if session_raw.get("coding_agent_session_uuid") and (session_raw.get("session_summary") or "").strip():
790
834
  session_text = session_raw["session_summary"]
791
835
 
792
836
  # db_fetch is the SOLE source of "what the model was given" — recorded
@@ -797,7 +841,20 @@ def _classify_one(
797
841
  # (today's-focus keys float to the front in _fetch_pm_tasks).
798
842
  candidate_keys = [t["task_key"] for t in pm_tasks]
799
843
  recent_task_keys = [r.get("task_key") for r in recent if r.get("task_key")]
844
+ # Session identity + the app_sessions row metadata, so a trace is
845
+ # self-contained — you know WHICH session and its key fields (app, window
846
+ # titles, time span) without opening meridian.db.
847
+ db_span.set_attribute("session_id", session_id)
800
848
  db_span.set_attribute("app_name", str(session_raw.get("app_name") or ""))
849
+ db_span.set_attribute("started_at", str(session_raw.get("started_at") or ""))
850
+ db_span.set_attribute("ended_at", str(session_raw.get("ended_at") or ""))
851
+ try:
852
+ _wts = json.loads(session_raw.get("window_titles") or "[]")
853
+ _wt_names = [str(w.get("window_name", "")) for w in _wts if w.get("window_name")]
854
+ except (TypeError, ValueError):
855
+ _wt_names = []
856
+ db_span.set_attribute("window_titles", " | ".join(_wt_names) if _wt_names else "-")
857
+ db_span.set_attribute("window_title_count", len(_wt_names))
801
858
  db_span.set_attribute("duration_s", float(session_raw.get("duration_s") or 0.0))
802
859
  db_span.set_attribute("text_source", str(session_raw.get("session_text_source") or ""))
803
860
  db_span.set_attribute("session_text_chars", len(session_text))
@@ -812,11 +869,36 @@ def _classify_one(
812
869
  db_span.set_attribute("min_frame_id", _min_fid)
813
870
  db_span.set_attribute("max_frame_id", _max_fid)
814
871
  db_span.set_attribute("frame_count", int(session_raw.get("frame_count") or 0))
872
+ # Coding-agent provenance: which agent conversation + segment this row came
873
+ # from, when the indexer sealed it, and who wrote the summary the model is
874
+ # classifying on. Only present on coding-agent rows (Claude Code / Codex /
875
+ # …); guard on coding_agent_session_uuid so screen-capture sessions stay clean.
876
+ _ca_uuid = session_raw.get("coding_agent_session_uuid")
877
+ if _ca_uuid:
878
+ db_span.set_attribute("coding_agent_session_uuid", str(_ca_uuid))
879
+ db_span.set_attribute("segment_started_at", str(session_raw.get("segment_started_at") or ""))
880
+ db_span.set_attribute("sealed_at", str(session_raw.get("sealed_at") or ""))
881
+ db_span.set_attribute("summary_source", str(session_raw.get("summary_source") or ""))
815
882
  db_span.set_attribute("pm_tasks_count", len(pm_tasks))
816
883
  db_span.set_attribute("today_focus_count", len(focus_keys))
817
884
  db_span.set_attribute("recent_sessions_count", len(recent))
818
885
  db_span.set_attribute("candidate_task_keys", ", ".join(candidate_keys) if candidate_keys else "-")
819
886
  db_span.set_attribute("today_focus_keys", ", ".join(focus_keys) if focus_keys else "-")
887
+ # Which candidate-set policy actually applied for this session, so a trace
888
+ # explains the candidate list without re-deriving it:
889
+ # all → no confirmed plan; every open ticket offered
890
+ # plan_only → narrowed to the confirmed plan
891
+ # plan_fallback_all → plan confirmed but its tickets weren't live → fell back
892
+ # boost → legacy boost-never-filter (flag off)
893
+ if not focus_keys:
894
+ candidate_mode = "all"
895
+ elif not _PLAN_ONLY_CANDIDATES:
896
+ candidate_mode = "boost"
897
+ elif pm_tasks and all(t.get("is_today_focus") for t in pm_tasks):
898
+ candidate_mode = "plan_only"
899
+ else:
900
+ candidate_mode = "plan_fallback_all"
901
+ db_span.set_attribute("candidate_mode", candidate_mode)
820
902
  db_span.set_attribute("recent_task_keys", ", ".join(recent_task_keys) if recent_task_keys else "-")
821
903
 
822
904
  session = {
@@ -1266,6 +1348,14 @@ def _classify_one_logged_inner(
1266
1348
  }
1267
1349
  run_log.write(json.dumps(record, default=str) + "\n")
1268
1350
  run_log.flush()
1351
+ # Promote app_name onto the classify_session span (the one row-per-session
1352
+ # span the dashboards query), so app name is a filterable column there — not
1353
+ # just on the child db_fetch span. session_raw is the DB row; current span
1354
+ # here is classify_session (db_fetch's child span has already closed).
1355
+ if session_raw:
1356
+ _cs = trace.get_current_span()
1357
+ if _cs.is_recording():
1358
+ _cs.set_attribute("app_name", str(session_raw.get("app_name") or ""))
1269
1359
  _annotate_classification_span(result)
1270
1360
  return result
1271
1361
 
@@ -17,6 +17,19 @@
17
17
  "customMultiSelectValue": [],
18
18
  "escapeSingleQuotes": true
19
19
  },
20
+ {
21
+ "type": "textbox",
22
+ "name": "app_name",
23
+ "label": "App name",
24
+ "query_data": null,
25
+ "value": "",
26
+ "options": [],
27
+ "multiSelect": false,
28
+ "hideOnDashboard": false,
29
+ "selectAllValueForMultiSelect": "custom",
30
+ "customMultiSelectValue": [],
31
+ "escapeSingleQuotes": true
32
+ },
20
33
  {
21
34
  "type": "custom",
22
35
  "name": "session_type",
@@ -141,10 +154,10 @@
141
154
  "queryType": "sql",
142
155
  "queries": [
143
156
  {
144
- "query": "SELECT to_char(to_timestamp_micros(_timestamp + 19800000000),'%Y-%m-%d %H:%M:%S') as \"Time\", session_id as \"Session\", task_key as \"Task\", session_type as \"Type\", category as \"Category\", confidence as \"Confidence\", round(CAST(elapsed_s AS DOUBLE),2) as \"Time taken (s)\", method as \"Method\", is_error as \"Error\", trace_id as \"trace_id\", encode(concat('trace_id=''', trace_id, ''''),'base64') as \"trace_filter\" FROM \"default\" WHERE operation_name='classify_session' AND ('$session_id'='' OR session_id='$session_id') AND ('$session_type'='' OR session_type='$session_type') AND ('$errors_only'='' OR is_error='$errors_only') ORDER BY _timestamp DESC",
157
+ "query": "SELECT to_char(to_timestamp_micros(_timestamp + 19800000000),'%Y-%m-%d %H:%M:%S') as \"Time\", session_id as \"Session\", app_name as \"App\", task_key as \"Task\", session_type as \"Type\", category as \"Category\", confidence as \"Confidence\", round(CAST(elapsed_s AS DOUBLE),2) as \"Time taken (s)\", method as \"Method\", is_error as \"Error\", trace_id as \"trace_id\", encode(concat('trace_id=''', trace_id, ''''),'base64') as \"trace_filter\" FROM \"default\" WHERE operation_name='classify_session' AND ('$session_id'='' OR session_id='$session_id') AND ('$session_type'='' OR session_type='$session_type') AND ('$errors_only'='' OR is_error='$errors_only') AND ('$app_name'='' OR app_name LIKE '%$app_name%') ORDER BY _timestamp DESC",
145
158
  "vrlFunctionQuery": "",
146
159
  "customQuery": true,
147
- "fields": {"stream": "default", "stream_type": "traces", "x": [{"label": "Time", "alias": "Time", "column": "_timestamp", "color": null, "isDerived": false, "havingConditions": []}, {"label": "Session", "alias": "Session", "column": "session_id", "color": null, "isDerived": false, "havingConditions": []}, {"label": "Task", "alias": "Task", "column": "task_key", "color": null, "isDerived": false, "havingConditions": []}, {"label": "Type", "alias": "Type", "column": "session_type", "color": null, "isDerived": false, "havingConditions": []}, {"label": "Category", "alias": "Category", "column": "category", "color": null, "isDerived": false, "havingConditions": []}, {"label": "Confidence", "alias": "Confidence", "column": "confidence", "color": null, "isDerived": false, "havingConditions": []}, {"label": "Time taken (s)", "alias": "Time taken (s)", "column": "elapsed_s", "color": null, "isDerived": false, "havingConditions": []}, {"label": "Method", "alias": "Method", "column": "method", "color": null, "isDerived": false, "havingConditions": []}, {"label": "Error", "alias": "Error", "column": "is_error", "color": null, "isDerived": false, "havingConditions": []}, {"label": "Trace ID", "alias": "trace_id", "column": "trace_id", "color": null, "isDerived": false, "havingConditions": []}, {"label": "trace_filter", "alias": "trace_filter", "column": "trace_filter", "color": null, "isDerived": false, "havingConditions": []}], "y": [], "z": [], "breakdown": [], "filter": {"filterType": "group", "logicalOperator": "AND", "conditions": []}},
160
+ "fields": {"stream": "default", "stream_type": "traces", "x": [{"label": "Time", "alias": "Time", "column": "_timestamp", "color": null, "isDerived": false, "havingConditions": []}, {"label": "Session", "alias": "Session", "column": "session_id", "color": null, "isDerived": false, "havingConditions": []}, {"label": "App", "alias": "App", "column": "app_name", "color": null, "isDerived": false, "havingConditions": []}, {"label": "Task", "alias": "Task", "column": "task_key", "color": null, "isDerived": false, "havingConditions": []}, {"label": "Type", "alias": "Type", "column": "session_type", "color": null, "isDerived": false, "havingConditions": []}, {"label": "Category", "alias": "Category", "column": "category", "color": null, "isDerived": false, "havingConditions": []}, {"label": "Confidence", "alias": "Confidence", "column": "confidence", "color": null, "isDerived": false, "havingConditions": []}, {"label": "Time taken (s)", "alias": "Time taken (s)", "column": "elapsed_s", "color": null, "isDerived": false, "havingConditions": []}, {"label": "Method", "alias": "Method", "column": "method", "color": null, "isDerived": false, "havingConditions": []}, {"label": "Error", "alias": "Error", "column": "is_error", "color": null, "isDerived": false, "havingConditions": []}, {"label": "Trace ID", "alias": "trace_id", "column": "trace_id", "color": null, "isDerived": false, "havingConditions": []}, {"label": "trace_filter", "alias": "trace_filter", "column": "trace_filter", "color": null, "isDerived": false, "havingConditions": []}], "y": [], "z": [], "breakdown": [], "filter": {"filterType": "group", "logicalOperator": "AND", "conditions": []}},
148
161
  "config": {"promql_legend": "", "layer_type": "scatter", "weight_fixed": 1}
149
162
  }
150
163
  ],
@@ -159,10 +172,10 @@
159
172
  "queryType": "sql",
160
173
  "queries": [
161
174
  {
162
- "query": "SELECT to_char(to_timestamp_micros(_timestamp + 19800000000),'%Y-%m-%d %H:%M:%S') as \"Time\", session_id as \"Session\", task_key as \"Task\", session_type as \"Type\", method as \"Method\", trace_id as \"trace_id\", encode(concat('trace_id=''', trace_id, ''''),'base64') as \"trace_filter\" FROM \"default\" WHERE operation_name='classify_session' AND is_error='true' ORDER BY _timestamp DESC",
175
+ "query": "SELECT to_char(to_timestamp_micros(_timestamp + 19800000000),'%Y-%m-%d %H:%M:%S') as \"Time\", session_id as \"Session\", app_name as \"App\", task_key as \"Task\", session_type as \"Type\", method as \"Method\", trace_id as \"trace_id\", encode(concat('trace_id=''', trace_id, ''''),'base64') as \"trace_filter\" FROM \"default\" WHERE operation_name='classify_session' AND is_error='true' AND ('$app_name'='' OR app_name LIKE '%$app_name%') ORDER BY _timestamp DESC",
163
176
  "vrlFunctionQuery": "",
164
177
  "customQuery": true,
165
- "fields": {"stream": "default", "stream_type": "traces", "x": [{"label": "Time", "alias": "Time", "column": "_timestamp", "color": null, "isDerived": false, "havingConditions": []}, {"label": "Session", "alias": "Session", "column": "session_id", "color": null, "isDerived": false, "havingConditions": []}, {"label": "Task", "alias": "Task", "column": "task_key", "color": null, "isDerived": false, "havingConditions": []}, {"label": "Type", "alias": "Type", "column": "session_type", "color": null, "isDerived": false, "havingConditions": []}, {"label": "Method", "alias": "Method", "column": "method", "color": null, "isDerived": false, "havingConditions": []}, {"label": "Trace ID", "alias": "trace_id", "column": "trace_id", "color": null, "isDerived": false, "havingConditions": []}, {"label": "trace_filter", "alias": "trace_filter", "column": "trace_filter", "color": null, "isDerived": false, "havingConditions": []}], "y": [], "z": [], "breakdown": [], "filter": {"filterType": "group", "logicalOperator": "AND", "conditions": []}},
178
+ "fields": {"stream": "default", "stream_type": "traces", "x": [{"label": "Time", "alias": "Time", "column": "_timestamp", "color": null, "isDerived": false, "havingConditions": []}, {"label": "Session", "alias": "Session", "column": "session_id", "color": null, "isDerived": false, "havingConditions": []}, {"label": "App", "alias": "App", "column": "app_name", "color": null, "isDerived": false, "havingConditions": []}, {"label": "Task", "alias": "Task", "column": "task_key", "color": null, "isDerived": false, "havingConditions": []}, {"label": "Type", "alias": "Type", "column": "session_type", "color": null, "isDerived": false, "havingConditions": []}, {"label": "Method", "alias": "Method", "column": "method", "color": null, "isDerived": false, "havingConditions": []}, {"label": "Trace ID", "alias": "trace_id", "column": "trace_id", "color": null, "isDerived": false, "havingConditions": []}, {"label": "trace_filter", "alias": "trace_filter", "column": "trace_filter", "color": null, "isDerived": false, "havingConditions": []}], "y": [], "z": [], "breakdown": [], "filter": {"filterType": "group", "logicalOperator": "AND", "conditions": []}},
166
179
  "config": {"promql_legend": "", "layer_type": "scatter", "weight_fixed": 1}
167
180
  }
168
181
  ],
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "meridian-agents"
7
- version = "1.57.0"
7
+ version = "1.58.1"
8
8
  description = "Meridian agents — MLX classifier server and Jira worklog synthesis for meridian.db"
9
9
  requires-python = ">=3.11"
10
10
  authors = [{ name = "Meridiona" }]
@@ -51,7 +51,7 @@ def _reconstruct_prompt(db_path: str, session_id: int) -> str | None:
51
51
  recent = _fetch_recent_sessions(con, session_id)
52
52
  pm_tasks = _fetch_pm_tasks(con)
53
53
  session_text = raw.get("session_text") or ""
54
- if raw.get("claude_session_uuid") and (raw.get("session_summary") or "").strip():
54
+ if raw.get("coding_agent_session_uuid") and (raw.get("session_summary") or "").strip():
55
55
  session_text = raw["session_summary"]
56
56
  session = {
57
57
  "id": session_id,
@@ -13,7 +13,7 @@ what meridian actually produced in app_sessions. Reports three layers:
13
13
  Join key is the screenpipe frame_id: labeled blocks carry a frame_range; app_sessions carry
14
14
  min_frame_id/max_frame_id. Each app_session is assigned to exactly one labeled block by its
15
15
  midpoint frame, so fragments are never double-counted. Only screen-derived sessions
16
- (claude_session_uuid IS NULL) participate — coding-agent rows are a separate ingest path.
16
+ (coding_agent_session_uuid IS NULL) participate — coding-agent rows are a separate ingest path.
17
17
 
18
18
  This is the measurement harness for the real-session eval (KAN-141). Re-run it after every
19
19
  ETL fix to quantify the delta against a fixed ground-truth label set.
@@ -77,7 +77,7 @@ def _load_sessions(db_path: Path, date: str) -> list[dict]:
77
77
  task_confidence, task_method
78
78
  FROM app_sessions
79
79
  WHERE substr(started_at, 1, 10) = ?
80
- AND claude_session_uuid IS NULL
80
+ AND coding_agent_session_uuid IS NULL
81
81
  ORDER BY min_frame_id
82
82
  """,
83
83
  (date,),
package/ui.tar.gz CHANGED
Binary file