@agentdeck/bridge 1.0.24 → 1.2.0
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/README.md +14 -6
- package/assets/fonts/NOTICE-PocketSansWorld.md +10 -0
- package/assets/fonts/OFL-PocketSansWorld.txt +93 -0
- package/assets/fonts/PocketSansWorld_12.cpfont +0 -0
- package/assets/fonts/pocket-sans-world.manifest.json +49 -0
- package/assets/learning/LICENSE-CC-BY-SA-4.0.txt +10 -0
- package/assets/learning/NOTICE.md +33 -0
- package/assets/learning/jp-n3-ko.manifest.json +27 -0
- package/assets/learning/jp-n3-ko.pdl +0 -0
- package/dist/adapters/openclaw.d.ts +74 -4
- package/dist/adapters/openclaw.d.ts.map +1 -1
- package/dist/adapters/openclaw.js +235 -43
- package/dist/adapters/openclaw.js.map +1 -1
- package/dist/agent-cli-diagnostics.d.ts +20 -0
- package/dist/agent-cli-diagnostics.d.ts.map +1 -0
- package/dist/agent-cli-diagnostics.js +100 -0
- package/dist/agent-cli-diagnostics.js.map +1 -0
- package/dist/apme/adapters/openclaw-hook.d.ts +38 -10
- package/dist/apme/adapters/openclaw-hook.d.ts.map +1 -1
- package/dist/apme/adapters/openclaw-hook.js +181 -37
- package/dist/apme/adapters/openclaw-hook.js.map +1 -1
- package/dist/apme/collector.d.ts +55 -2
- package/dist/apme/collector.d.ts.map +1 -1
- package/dist/apme/collector.js +324 -37
- package/dist/apme/collector.js.map +1 -1
- package/dist/apme/dashboard-html.d.ts.map +1 -1
- package/dist/apme/dashboard-html.js +275 -35
- package/dist/apme/dashboard-html.js.map +1 -1
- package/dist/apme/http.d.ts.map +1 -1
- package/dist/apme/http.js +33 -5
- package/dist/apme/http.js.map +1 -1
- package/dist/apme/index.d.ts.map +1 -1
- package/dist/apme/index.js +6 -2
- package/dist/apme/index.js.map +1 -1
- package/dist/apme/pareto.d.ts +2 -1
- package/dist/apme/pareto.d.ts.map +1 -1
- package/dist/apme/pareto.js +15 -4
- package/dist/apme/pareto.js.map +1 -1
- package/dist/apme/recommend.d.ts +24 -1
- package/dist/apme/recommend.d.ts.map +1 -1
- package/dist/apme/recommend.js +43 -7
- package/dist/apme/recommend.js.map +1 -1
- package/dist/apme/store.d.ts +47 -1
- package/dist/apme/store.d.ts.map +1 -1
- package/dist/apme/store.js +397 -99
- package/dist/apme/store.js.map +1 -1
- package/dist/bridge-core.d.ts +25 -1
- package/dist/bridge-core.d.ts.map +1 -1
- package/dist/bridge-core.js +50 -9
- package/dist/bridge-core.js.map +1 -1
- package/dist/card-feed.d.ts +46 -1
- package/dist/card-feed.d.ts.map +1 -1
- package/dist/card-feed.js +166 -6
- package/dist/card-feed.js.map +1 -1
- package/dist/cli.d.ts +56 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +442 -34
- package/dist/cli.js.map +1 -1
- package/dist/codex-rate-limits-live.d.ts +180 -10
- package/dist/codex-rate-limits-live.d.ts.map +1 -1
- package/dist/codex-rate-limits-live.js +475 -22
- package/dist/codex-rate-limits-live.js.map +1 -1
- package/dist/daemon-build-identity.d.ts +124 -0
- package/dist/daemon-build-identity.d.ts.map +1 -0
- package/dist/daemon-build-identity.js +310 -0
- package/dist/daemon-build-identity.js.map +1 -0
- package/dist/daemon-server.d.ts +34 -6
- package/dist/daemon-server.d.ts.map +1 -1
- package/dist/daemon-server.js +876 -109
- package/dist/daemon-server.js.map +1 -1
- package/dist/daemon-takeover.d.ts +11 -0
- package/dist/daemon-takeover.d.ts.map +1 -1
- package/dist/daemon-takeover.js +62 -17
- package/dist/daemon-takeover.js.map +1 -1
- package/dist/daemon-ws-client.d.ts +1 -0
- package/dist/daemon-ws-client.d.ts.map +1 -1
- package/dist/daemon-ws-client.js +25 -16
- package/dist/daemon-ws-client.js.map +1 -1
- package/dist/daemon.js +1 -1
- package/dist/daemon.js.map +1 -1
- package/dist/esp32-serial.d.ts +18 -0
- package/dist/esp32-serial.d.ts.map +1 -1
- package/dist/esp32-serial.js +23 -1
- package/dist/esp32-serial.js.map +1 -1
- package/dist/font-pack.d.ts +15 -0
- package/dist/font-pack.d.ts.map +1 -0
- package/dist/font-pack.js +75 -0
- package/dist/font-pack.js.map +1 -0
- package/dist/glance-frame.d.ts.map +1 -1
- package/dist/glance-frame.js +5 -1
- package/dist/glance-frame.js.map +1 -1
- package/dist/hook-codex-sessions.d.ts +3 -1
- package/dist/hook-codex-sessions.d.ts.map +1 -1
- package/dist/hook-codex-sessions.js +27 -7
- package/dist/hook-codex-sessions.js.map +1 -1
- package/dist/http-auth-gate.d.ts.map +1 -1
- package/dist/http-auth-gate.js +9 -1
- package/dist/http-auth-gate.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/learning-pack.d.ts +24 -0
- package/dist/learning-pack.d.ts.map +1 -0
- package/dist/learning-pack.js +125 -0
- package/dist/learning-pack.js.map +1 -0
- package/dist/legacy-rearm-ledger.d.ts +56 -0
- package/dist/legacy-rearm-ledger.d.ts.map +1 -0
- package/dist/legacy-rearm-ledger.js +66 -0
- package/dist/legacy-rearm-ledger.js.map +1 -0
- package/dist/mdns.d.ts +33 -0
- package/dist/mdns.d.ts.map +1 -1
- package/dist/mdns.js +92 -3
- package/dist/mdns.js.map +1 -1
- package/dist/openclaw-session.d.ts.map +1 -1
- package/dist/openclaw-session.js +13 -0
- package/dist/openclaw-session.js.map +1 -1
- package/dist/openclaw-timeline-feed.d.ts +84 -0
- package/dist/openclaw-timeline-feed.d.ts.map +1 -0
- package/dist/openclaw-timeline-feed.js +162 -0
- package/dist/openclaw-timeline-feed.js.map +1 -0
- package/dist/openclaw-transcript-timeline.d.ts +103 -0
- package/dist/openclaw-transcript-timeline.d.ts.map +1 -0
- package/dist/openclaw-transcript-timeline.js +328 -0
- package/dist/openclaw-transcript-timeline.js.map +1 -0
- package/dist/pairing-knocks.d.ts +80 -0
- package/dist/pairing-knocks.d.ts.map +1 -0
- package/dist/pairing-knocks.js +312 -0
- package/dist/pairing-knocks.js.map +1 -0
- package/dist/passive-observer.d.ts +21 -1
- package/dist/passive-observer.d.ts.map +1 -1
- package/dist/passive-observer.js +37 -3
- package/dist/passive-observer.js.map +1 -1
- package/dist/pixoo/micro-glyphs.d.ts +4 -0
- package/dist/pixoo/micro-glyphs.d.ts.map +1 -1
- package/dist/pixoo/micro-glyphs.js +29 -0
- package/dist/pixoo/micro-glyphs.js.map +1 -1
- package/dist/pixoo/pixoo-font.d.ts.map +1 -1
- package/dist/pixoo/pixoo-font.js +3 -1
- package/dist/pixoo/pixoo-font.js.map +1 -1
- package/dist/pixoo/pixoo-renderer.d.ts +1 -1
- package/dist/pixoo/pixoo-renderer.d.ts.map +1 -1
- package/dist/pixoo/pixoo-renderer.js +17 -2
- package/dist/pixoo/pixoo-renderer.js.map +1 -1
- package/dist/pty-manager.d.ts +9 -0
- package/dist/pty-manager.d.ts.map +1 -1
- package/dist/pty-manager.js +63 -4
- package/dist/pty-manager.js.map +1 -1
- package/dist/relayed-usage.d.ts +98 -0
- package/dist/relayed-usage.d.ts.map +1 -0
- package/dist/relayed-usage.js +103 -0
- package/dist/relayed-usage.js.map +1 -0
- package/dist/session-aggregator.d.ts +3 -0
- package/dist/session-aggregator.d.ts.map +1 -1
- package/dist/session-aggregator.js.map +1 -1
- package/dist/session-registry.d.ts +1 -0
- package/dist/session-registry.d.ts.map +1 -1
- package/dist/surface-protocol.d.ts +84 -0
- package/dist/surface-protocol.d.ts.map +1 -0
- package/dist/surface-protocol.js +237 -0
- package/dist/surface-protocol.js.map +1 -0
- package/dist/timeline-store.d.ts +1 -1
- package/dist/timeline-store.d.ts.map +1 -1
- package/dist/timeline-store.js +16 -1
- package/dist/timeline-store.js.map +1 -1
- package/dist/tui/dashboard.d.ts.map +1 -1
- package/dist/tui/dashboard.js +8 -1
- package/dist/tui/dashboard.js.map +1 -1
- package/dist/tui/renderer.d.ts.map +1 -1
- package/dist/tui/renderer.js +35 -0
- package/dist/tui/renderer.js.map +1 -1
- package/dist/weather-config.d.ts +17 -0
- package/dist/weather-config.d.ts.map +1 -0
- package/dist/weather-config.js +54 -0
- package/dist/weather-config.js.map +1 -0
- package/dist/weather.d.ts +44 -4
- package/dist/weather.d.ts.map +1 -1
- package/dist/weather.js +223 -15
- package/dist/weather.js.map +1 -1
- package/dist/ws-server.d.ts +18 -0
- package/dist/ws-server.d.ts.map +1 -1
- package/dist/ws-server.js +95 -7
- package/dist/ws-server.js.map +1 -1
- package/fm-helper/AgentDeckFMHelper.swift +51 -5
- package/package.json +4 -3
- package/src/timebox/sync_ble.py +35 -16
package/dist/apme/store.js
CHANGED
|
@@ -15,13 +15,143 @@ import { debug } from '../logger.js';
|
|
|
15
15
|
// workspace symlinks, regardless of the process CWD (vitest runs from the
|
|
16
16
|
// repo root, where the symlink doesn't exist).
|
|
17
17
|
const require = createRequire(import.meta.url);
|
|
18
|
+
import { TASK_ATTENTION_WINDOW_MS, TASK_ATTENTION_RED_SCORE } from '@agentdeck/shared';
|
|
18
19
|
// ─── Schema ────────────────────────────────────────────────────────────────────
|
|
20
|
+
const SCORECARD_DDL = `
|
|
21
|
+
-- Pre-aggregate metrics at the level they actually describe. New rows prefer
|
|
22
|
+
-- turn attribution; runs with no turn identity remain available as a legacy
|
|
23
|
+
-- fallback instead of being silently dropped after the schema migration.
|
|
24
|
+
CREATE VIEW IF NOT EXISTS v_run_metrics AS
|
|
25
|
+
SELECT
|
|
26
|
+
run_id,
|
|
27
|
+
MAX(CASE WHEN metric='overall' AND layer='llm_judge' THEN score END) AS overall,
|
|
28
|
+
MAX(CASE WHEN metric='tests_pass' AND layer='deterministic' THEN score END) AS tests_pass
|
|
29
|
+
FROM evals
|
|
30
|
+
GROUP BY run_id;
|
|
31
|
+
|
|
32
|
+
CREATE VIEW IF NOT EXISTS v_model_scorecard AS
|
|
33
|
+
WITH turn_eval AS (
|
|
34
|
+
SELECT
|
|
35
|
+
turn_id,
|
|
36
|
+
MAX(CASE WHEN metric='overall' THEN score END) AS overall,
|
|
37
|
+
MAX(CASE WHEN metric='tests_pass' THEN score END) AS tests_pass
|
|
38
|
+
FROM evals
|
|
39
|
+
WHERE turn_id IS NOT NULL
|
|
40
|
+
GROUP BY turn_id
|
|
41
|
+
), turn_cost AS (
|
|
42
|
+
SELECT
|
|
43
|
+
turn_id,
|
|
44
|
+
CASE WHEN MIN(COALESCE(cost_known, 0))=1 THEN SUM(cost_usd) ELSE NULL END AS cost_usd,
|
|
45
|
+
MIN(COALESCE(cost_known, 0)) AS cost_known
|
|
46
|
+
FROM sample_events
|
|
47
|
+
WHERE turn_id IS NOT NULL AND kind='model'
|
|
48
|
+
GROUP BY turn_id
|
|
49
|
+
), attributed_units AS (
|
|
50
|
+
SELECT
|
|
51
|
+
r.agent_type AS agent_type,
|
|
52
|
+
t.run_id AS run_id,
|
|
53
|
+
t.id AS turn_id,
|
|
54
|
+
t.model_id AS model_id,
|
|
55
|
+
COALESCE(t.provider, r.provider) AS provider,
|
|
56
|
+
COALESCE(
|
|
57
|
+
t.composite_score,
|
|
58
|
+
e.overall
|
|
59
|
+
) AS overall,
|
|
60
|
+
e.tests_pass AS tests_pass,
|
|
61
|
+
c.cost_usd AS cost_usd,
|
|
62
|
+
COALESCE(c.cost_known, 0) AS cost_known
|
|
63
|
+
FROM turns t
|
|
64
|
+
JOIN runs r ON r.id=t.run_id
|
|
65
|
+
LEFT JOIN turn_eval e ON e.turn_id=t.id
|
|
66
|
+
LEFT JOIN turn_cost c ON c.turn_id=t.id
|
|
67
|
+
WHERE t.model_id IS NOT NULL AND t.model_id != ''
|
|
68
|
+
), legacy_units AS (
|
|
69
|
+
SELECT
|
|
70
|
+
r.agent_type AS agent_type,
|
|
71
|
+
r.id AS run_id,
|
|
72
|
+
NULL AS turn_id,
|
|
73
|
+
COALESCE(r.model_id, 'unknown') AS model_id,
|
|
74
|
+
r.provider AS provider,
|
|
75
|
+
m.overall AS overall,
|
|
76
|
+
m.tests_pass AS tests_pass,
|
|
77
|
+
CASE WHEN r.cost_known=1 THEN r.cost_usd ELSE NULL END AS cost_usd,
|
|
78
|
+
COALESCE(r.cost_known, 0) AS cost_known
|
|
79
|
+
FROM runs r
|
|
80
|
+
LEFT JOIN v_run_metrics m ON m.run_id=r.id
|
|
81
|
+
WHERE NOT EXISTS (
|
|
82
|
+
SELECT 1 FROM turns t
|
|
83
|
+
WHERE t.run_id=r.id AND t.model_id IS NOT NULL AND t.model_id != ''
|
|
84
|
+
)
|
|
85
|
+
), units AS (
|
|
86
|
+
SELECT * FROM attributed_units
|
|
87
|
+
UNION ALL
|
|
88
|
+
SELECT * FROM legacy_units
|
|
89
|
+
)
|
|
90
|
+
SELECT
|
|
91
|
+
agent_type,
|
|
92
|
+
model_id,
|
|
93
|
+
provider,
|
|
94
|
+
COUNT(DISTINCT run_id) AS runs,
|
|
95
|
+
AVG(overall) AS avg_overall,
|
|
96
|
+
AVG(tests_pass) AS avg_tests_pass,
|
|
97
|
+
CASE WHEN MIN(cost_known)=1 THEN SUM(cost_usd) ELSE NULL END AS total_cost,
|
|
98
|
+
MIN(cost_known) AS cost_known,
|
|
99
|
+
CASE WHEN MIN(cost_known)=1 AND AVG(overall)>0 THEN SUM(cost_usd)/AVG(overall) ELSE NULL END AS cost_per_quality
|
|
100
|
+
FROM units
|
|
101
|
+
GROUP BY agent_type, model_id, provider;
|
|
102
|
+
|
|
103
|
+
CREATE VIEW IF NOT EXISTS v_category_scorecard AS
|
|
104
|
+
WITH task_metrics AS (
|
|
105
|
+
SELECT
|
|
106
|
+
task_id,
|
|
107
|
+
MAX(CASE WHEN metric='tests_pass' AND layer='deterministic' THEN score END) AS tests_pass
|
|
108
|
+
FROM evals
|
|
109
|
+
WHERE task_id IS NOT NULL
|
|
110
|
+
GROUP BY task_id
|
|
111
|
+
)
|
|
112
|
+
SELECT
|
|
113
|
+
t.task_category AS task_category,
|
|
114
|
+
COALESCE(t.model_id, r.model_id, 'unknown') AS model_id,
|
|
115
|
+
COALESCE(t.provider, r.provider) AS provider,
|
|
116
|
+
COUNT(DISTINCT t.run_id) AS runs,
|
|
117
|
+
AVG(t.composite_score) AS avg_overall,
|
|
118
|
+
AVG(m.tests_pass) AS avg_tests_pass,
|
|
119
|
+
CASE WHEN MIN(COALESCE(t.cost_known, 0))=1 THEN SUM(t.cost_usd) ELSE NULL END AS total_cost,
|
|
120
|
+
MIN(COALESCE(t.cost_known, 0)) AS cost_known
|
|
121
|
+
FROM tasks t
|
|
122
|
+
JOIN runs r ON r.id=t.run_id
|
|
123
|
+
LEFT JOIN task_metrics m ON m.task_id=t.id
|
|
124
|
+
WHERE t.task_category IS NOT NULL AND t.task_category != 'unknown'
|
|
125
|
+
GROUP BY t.task_category, COALESCE(t.model_id, r.model_id, 'unknown'), COALESCE(t.provider, r.provider);
|
|
126
|
+
|
|
127
|
+
-- Sample-granularity scorecard: quality vs cost per (agent, provider, model,
|
|
128
|
+
-- category). A task that used more than one model is stored as 'mixed' rather
|
|
129
|
+
-- than attributed to whichever assistant message happened to arrive last.
|
|
130
|
+
CREATE VIEW IF NOT EXISTS v_sample_scorecard AS
|
|
131
|
+
SELECT
|
|
132
|
+
r.agent_type AS agent_type,
|
|
133
|
+
COALESCE(t.model_id, r.model_id, 'unknown') AS model_id,
|
|
134
|
+
COALESCE(t.provider, r.provider) AS provider,
|
|
135
|
+
t.task_category AS task_category,
|
|
136
|
+
COUNT(*) AS samples,
|
|
137
|
+
AVG(t.composite_score) AS avg_quality,
|
|
138
|
+
CASE WHEN MIN(COALESCE(t.cost_known, 0))=1 THEN SUM(t.cost_usd) ELSE NULL END AS total_cost,
|
|
139
|
+
MIN(COALESCE(t.cost_known, 0)) AS cost_known,
|
|
140
|
+
AVG(t.latency_ms) AS avg_latency_ms,
|
|
141
|
+
CASE WHEN MIN(COALESCE(t.cost_known, 0))=1 AND AVG(t.composite_score)>0
|
|
142
|
+
THEN SUM(t.cost_usd)/AVG(t.composite_score) ELSE NULL END AS cost_per_quality
|
|
143
|
+
FROM tasks t
|
|
144
|
+
JOIN runs r ON r.id=t.run_id
|
|
145
|
+
WHERE t.ended_at IS NOT NULL AND t.composite_score IS NOT NULL
|
|
146
|
+
GROUP BY r.agent_type, COALESCE(t.model_id, r.model_id, 'unknown'), COALESCE(t.provider, r.provider), t.task_category;
|
|
147
|
+
`;
|
|
19
148
|
const DDL = `
|
|
20
149
|
CREATE TABLE IF NOT EXISTS runs (
|
|
21
150
|
id TEXT PRIMARY KEY,
|
|
22
151
|
session_id TEXT NOT NULL,
|
|
23
152
|
agent_type TEXT NOT NULL,
|
|
24
153
|
model_id TEXT,
|
|
154
|
+
provider TEXT,
|
|
25
155
|
project_name TEXT,
|
|
26
156
|
project_path TEXT,
|
|
27
157
|
task_prompt TEXT,
|
|
@@ -30,6 +160,7 @@ CREATE TABLE IF NOT EXISTS runs (
|
|
|
30
160
|
input_tokens INTEGER,
|
|
31
161
|
output_tokens INTEGER,
|
|
32
162
|
cost_usd REAL,
|
|
163
|
+
cost_known INTEGER NOT NULL DEFAULT 0,
|
|
33
164
|
exit_code INTEGER,
|
|
34
165
|
git_before TEXT,
|
|
35
166
|
git_after TEXT,
|
|
@@ -57,6 +188,8 @@ CREATE TABLE IF NOT EXISTS turns (
|
|
|
57
188
|
run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
|
|
58
189
|
task_id TEXT,
|
|
59
190
|
turn_index INTEGER NOT NULL,
|
|
191
|
+
model_id TEXT,
|
|
192
|
+
provider TEXT,
|
|
60
193
|
prompt TEXT,
|
|
61
194
|
response TEXT,
|
|
62
195
|
started_at INTEGER NOT NULL,
|
|
@@ -99,10 +232,12 @@ CREATE TABLE IF NOT EXISTS tasks (
|
|
|
99
232
|
task_category TEXT,
|
|
100
233
|
notes_json TEXT,
|
|
101
234
|
model_id TEXT,
|
|
235
|
+
provider TEXT,
|
|
102
236
|
model_config TEXT,
|
|
103
237
|
input_tokens INTEGER,
|
|
104
238
|
output_tokens INTEGER,
|
|
105
239
|
cost_usd REAL,
|
|
240
|
+
cost_known INTEGER NOT NULL DEFAULT 0,
|
|
106
241
|
latency_ms INTEGER
|
|
107
242
|
);
|
|
108
243
|
|
|
@@ -123,6 +258,7 @@ CREATE TABLE IF NOT EXISTS sample_events (
|
|
|
123
258
|
input_tokens INTEGER,
|
|
124
259
|
output_tokens INTEGER,
|
|
125
260
|
cost_usd REAL,
|
|
261
|
+
cost_known INTEGER NOT NULL DEFAULT 0,
|
|
126
262
|
latency_ms INTEGER,
|
|
127
263
|
tool_name TEXT,
|
|
128
264
|
tool_status TEXT,
|
|
@@ -181,67 +317,7 @@ CREATE INDEX IF NOT EXISTS idx_runs_started ON runs(started_at);
|
|
|
181
317
|
CREATE INDEX IF NOT EXISTS idx_evals_run ON evals(run_id);
|
|
182
318
|
CREATE INDEX IF NOT EXISTS idx_steps_run ON steps(run_id);
|
|
183
319
|
|
|
184
|
-
|
|
185
|
-
-- multiple eval rows exist per run (e.g. 3 deterministic + 5 judge axes).
|
|
186
|
-
CREATE VIEW IF NOT EXISTS v_run_metrics AS
|
|
187
|
-
SELECT
|
|
188
|
-
run_id,
|
|
189
|
-
MAX(CASE WHEN metric='overall' AND layer='llm_judge' THEN score END) AS overall,
|
|
190
|
-
MAX(CASE WHEN metric='tests_pass' AND layer='deterministic' THEN score END) AS tests_pass
|
|
191
|
-
FROM evals
|
|
192
|
-
GROUP BY run_id;
|
|
193
|
-
|
|
194
|
-
CREATE VIEW IF NOT EXISTS v_model_scorecard AS
|
|
195
|
-
SELECT
|
|
196
|
-
r.agent_type AS agent_type,
|
|
197
|
-
COALESCE(r.model_id, 'unknown') AS model_id,
|
|
198
|
-
COUNT(*) AS runs,
|
|
199
|
-
AVG(m.overall) AS avg_overall,
|
|
200
|
-
AVG(m.tests_pass) AS avg_tests_pass,
|
|
201
|
-
SUM(r.cost_usd) AS total_cost,
|
|
202
|
-
CASE
|
|
203
|
-
WHEN AVG(m.overall) > 0
|
|
204
|
-
THEN SUM(r.cost_usd) / AVG(m.overall)
|
|
205
|
-
ELSE NULL
|
|
206
|
-
END AS cost_per_quality
|
|
207
|
-
FROM runs r
|
|
208
|
-
LEFT JOIN v_run_metrics m ON m.run_id = r.id
|
|
209
|
-
GROUP BY r.agent_type, r.model_id;
|
|
210
|
-
|
|
211
|
-
CREATE VIEW IF NOT EXISTS v_category_scorecard AS
|
|
212
|
-
SELECT
|
|
213
|
-
r.task_category AS task_category,
|
|
214
|
-
COALESCE(r.model_id, 'unknown') AS model_id,
|
|
215
|
-
COUNT(*) AS runs,
|
|
216
|
-
AVG(m.overall) AS avg_overall,
|
|
217
|
-
AVG(m.tests_pass) AS avg_tests_pass,
|
|
218
|
-
SUM(r.cost_usd) AS total_cost
|
|
219
|
-
FROM runs r
|
|
220
|
-
LEFT JOIN v_run_metrics m ON m.run_id = r.id
|
|
221
|
-
WHERE r.task_category IS NOT NULL AND r.task_category != 'unknown'
|
|
222
|
-
GROUP BY r.task_category, r.model_id;
|
|
223
|
-
|
|
224
|
-
-- Sample-granularity scorecard: quality vs cost per (agent, model, category).
|
|
225
|
-
-- The recommender + Pareto frontier read this. Uses the task's own model_id /
|
|
226
|
-
-- cost (the sample header), falling back to the run's model when unset.
|
|
227
|
-
CREATE VIEW IF NOT EXISTS v_sample_scorecard AS
|
|
228
|
-
SELECT
|
|
229
|
-
r.agent_type AS agent_type,
|
|
230
|
-
COALESCE(t.model_id, r.model_id, 'unknown') AS model_id,
|
|
231
|
-
t.task_category AS task_category,
|
|
232
|
-
COUNT(*) AS samples,
|
|
233
|
-
AVG(t.composite_score) AS avg_quality,
|
|
234
|
-
SUM(t.cost_usd) AS total_cost,
|
|
235
|
-
AVG(t.latency_ms) AS avg_latency_ms,
|
|
236
|
-
CASE
|
|
237
|
-
WHEN AVG(t.composite_score) > 0
|
|
238
|
-
THEN SUM(t.cost_usd) / AVG(t.composite_score)
|
|
239
|
-
ELSE NULL
|
|
240
|
-
END AS cost_per_quality
|
|
241
|
-
FROM tasks t
|
|
242
|
-
JOIN runs r ON r.id = t.run_id
|
|
243
|
-
WHERE t.ended_at IS NOT NULL AND t.composite_score IS NOT NULL
|
|
244
|
-
GROUP BY r.agent_type, COALESCE(t.model_id, r.model_id, 'unknown'), t.task_category;
|
|
320
|
+
${SCORECARD_DDL}
|
|
245
321
|
`;
|
|
246
322
|
// ─── Default rubric v1 (seeded on first boot) ──────────────────────────────────
|
|
247
323
|
const DEFAULT_RUBRIC_V1 = {
|
|
@@ -455,6 +531,81 @@ Examples of well-calibrated overall scores:
|
|
|
455
531
|
notes: 'task-unit rollup (TodoWrite all-completed / /clear / session_end)',
|
|
456
532
|
},
|
|
457
533
|
};
|
|
534
|
+
/** The latest overall judge score for a task, else its composite. Inlined into
|
|
535
|
+
* several `TASK_VIEW_SQL` buckets so "judged" means one thing everywhere. */
|
|
536
|
+
const TASK_SCORE_SQL = `COALESCE((SELECT e.score FROM evals e WHERE e.task_id = t.id AND e.metric = 'overall'
|
|
537
|
+
ORDER BY e.created_at DESC LIMIT 1), t.composite_score)`;
|
|
538
|
+
/** The recency window and red-score band live in shared/src/eval-schema.ts
|
|
539
|
+
* (TASK_ATTENTION_WINDOW_MS / TASK_ATTENTION_RED_SCORE): both daemons build
|
|
540
|
+
* the same attention SQL, so the numbers are cross-daemon contracts, not
|
|
541
|
+
* store-local tuning knobs. Re-exported for existing importers. */
|
|
542
|
+
export { TASK_ATTENTION_WINDOW_MS } from '@agentdeck/shared';
|
|
543
|
+
/** The attention bucket, anchored at a caller-supplied cutoff (epoch ms) so
|
|
544
|
+
* the filter, the row flag and the badge are computed against the SAME
|
|
545
|
+
* instant within one request. Needs eyes = recent AND (reaper-closed, a
|
|
546
|
+
* closed task with an unarchived reply the judge can only partly see, or a
|
|
547
|
+
* judged score below the dashboard's red band). `cutoffMs` is always a
|
|
548
|
+
* number we computed — never user input — so inlining it is safe. */
|
|
549
|
+
function taskAttentionSql(cutoffMs) {
|
|
550
|
+
// IFNULL is load-bearing: a NULL score makes `score < 0.4` NULL, NULL OR
|
|
551
|
+
// false is NULL, and SQLite sorts NULL LAST under DESC — so without it,
|
|
552
|
+
// recent unjudged rows sank BELOW week-old rows (which get a determinate 0
|
|
553
|
+
// from the cutoff test) on the attention-first ordering.
|
|
554
|
+
return `IFNULL((t.started_at >= ${Math.floor(cutoffMs)}
|
|
555
|
+
AND (t.boundary_signal = 'orphaned'
|
|
556
|
+
OR (t.ended_at IS NOT NULL AND EXISTS (
|
|
557
|
+
SELECT 1 FROM turns tu WHERE tu.task_id = t.id AND tu.response IS NULL))
|
|
558
|
+
OR ${TASK_SCORE_SQL} < ${TASK_ATTENTION_RED_SCORE})), 0)`;
|
|
559
|
+
}
|
|
560
|
+
/** The NARROWING filters `listTaskPage` and `taskViewCounts` share — one
|
|
561
|
+
* builder so a filtered board's badges and its rows read the same WHERE. */
|
|
562
|
+
function buildTaskFilterWhere(opts) {
|
|
563
|
+
const where = [];
|
|
564
|
+
const args = [];
|
|
565
|
+
// `_empty` runs are bookkeeping shells, never work the user did.
|
|
566
|
+
where.push("COALESCE(r.task_category, '') != '_empty'");
|
|
567
|
+
if (opts.agentType) {
|
|
568
|
+
where.push('r.agent_type = ?');
|
|
569
|
+
args.push(opts.agentType);
|
|
570
|
+
}
|
|
571
|
+
if (opts.sessionId) {
|
|
572
|
+
where.push('r.session_id = ?');
|
|
573
|
+
args.push(opts.sessionId);
|
|
574
|
+
}
|
|
575
|
+
if (opts.projectName) {
|
|
576
|
+
where.push('r.project_name = ?');
|
|
577
|
+
args.push(opts.projectName);
|
|
578
|
+
}
|
|
579
|
+
if (opts.category) {
|
|
580
|
+
where.push('COALESCE(t.task_category, r.task_category) = ?');
|
|
581
|
+
args.push(opts.category);
|
|
582
|
+
}
|
|
583
|
+
if (opts.outcome) {
|
|
584
|
+
where.push('t.outcome = ?');
|
|
585
|
+
args.push(opts.outcome);
|
|
586
|
+
}
|
|
587
|
+
if (opts.q) {
|
|
588
|
+
where.push('(t.summary LIKE ? OR r.task_prompt LIKE ?)');
|
|
589
|
+
const like = `%${opts.q}%`;
|
|
590
|
+
args.push(like, like);
|
|
591
|
+
}
|
|
592
|
+
return { where, args };
|
|
593
|
+
}
|
|
594
|
+
/** ONE SQL definition per Work-board lifecycle bucket — the filter, the row's
|
|
595
|
+
* `attention` flag and the tab badges all read these same expressions, so a
|
|
596
|
+
* badge count can never disagree with the rows its tab lists.
|
|
597
|
+
*
|
|
598
|
+
* attention — see `taskAttentionSql` (recency-windowed, hence a function).
|
|
599
|
+
* inprogress — still accumulating turns.
|
|
600
|
+
* judged — carries an overall judge score or a composite.
|
|
601
|
+
* reported — closed but never scored (no judge configured / skipped).
|
|
602
|
+
* orphaned — reaper-closed; the un-windowed ledger of segmentation debt. */
|
|
603
|
+
const TASK_VIEW_SQL = {
|
|
604
|
+
inprogress: 't.ended_at IS NULL',
|
|
605
|
+
judged: `${TASK_SCORE_SQL} IS NOT NULL`,
|
|
606
|
+
reported: `(t.ended_at IS NOT NULL AND ${TASK_SCORE_SQL} IS NULL)`,
|
|
607
|
+
orphaned: `t.boundary_signal = 'orphaned'`,
|
|
608
|
+
};
|
|
458
609
|
export class ApmeStore {
|
|
459
610
|
db = null;
|
|
460
611
|
enabled = false;
|
|
@@ -517,6 +668,8 @@ export class ApmeStore {
|
|
|
517
668
|
return;
|
|
518
669
|
const cols = this.db.prepare("PRAGMA table_info(runs)").all().map(c => c.name);
|
|
519
670
|
const migrations = [
|
|
671
|
+
['provider', 'ALTER TABLE runs ADD COLUMN provider TEXT'],
|
|
672
|
+
['cost_known', 'ALTER TABLE runs ADD COLUMN cost_known INTEGER NOT NULL DEFAULT 0'],
|
|
520
673
|
['task_signals', 'ALTER TABLE runs ADD COLUMN task_signals TEXT'],
|
|
521
674
|
['task_category', 'ALTER TABLE runs ADD COLUMN task_category TEXT'],
|
|
522
675
|
['task_category_source', "ALTER TABLE runs ADD COLUMN task_category_source TEXT DEFAULT 'auto'"],
|
|
@@ -560,6 +713,18 @@ export class ApmeStore {
|
|
|
560
713
|
}
|
|
561
714
|
catch { /* ignore */ }
|
|
562
715
|
}
|
|
716
|
+
if (!turnCols.includes('model_id')) {
|
|
717
|
+
try {
|
|
718
|
+
this.db.exec('ALTER TABLE turns ADD COLUMN model_id TEXT');
|
|
719
|
+
}
|
|
720
|
+
catch { /* ignore */ }
|
|
721
|
+
}
|
|
722
|
+
if (!turnCols.includes('provider')) {
|
|
723
|
+
try {
|
|
724
|
+
this.db.exec('ALTER TABLE turns ADD COLUMN provider TEXT');
|
|
725
|
+
}
|
|
726
|
+
catch { /* ignore */ }
|
|
727
|
+
}
|
|
563
728
|
const evalCols = this.db.prepare("PRAGMA table_info(evals)").all().map(c => c.name);
|
|
564
729
|
if (!evalCols.includes('task_id')) {
|
|
565
730
|
try {
|
|
@@ -572,10 +737,12 @@ export class ApmeStore {
|
|
|
572
737
|
const taskCols = this.db.prepare("PRAGMA table_info(tasks)").all().map(c => c.name);
|
|
573
738
|
for (const [col, sql] of [
|
|
574
739
|
['model_id', 'ALTER TABLE tasks ADD COLUMN model_id TEXT'],
|
|
740
|
+
['provider', 'ALTER TABLE tasks ADD COLUMN provider TEXT'],
|
|
575
741
|
['model_config', 'ALTER TABLE tasks ADD COLUMN model_config TEXT'],
|
|
576
742
|
['input_tokens', 'ALTER TABLE tasks ADD COLUMN input_tokens INTEGER'],
|
|
577
743
|
['output_tokens', 'ALTER TABLE tasks ADD COLUMN output_tokens INTEGER'],
|
|
578
744
|
['cost_usd', 'ALTER TABLE tasks ADD COLUMN cost_usd REAL'],
|
|
745
|
+
['cost_known', 'ALTER TABLE tasks ADD COLUMN cost_known INTEGER NOT NULL DEFAULT 0'],
|
|
579
746
|
['latency_ms', 'ALTER TABLE tasks ADD COLUMN latency_ms INTEGER'],
|
|
580
747
|
]) {
|
|
581
748
|
if (!taskCols.includes(col)) {
|
|
@@ -597,6 +764,12 @@ export class ApmeStore {
|
|
|
597
764
|
// pointer to the run it continues, so one conversation shows up as N
|
|
598
765
|
// disconnected components. One live session here had 127 such runs.
|
|
599
766
|
const sevCols = this.db.prepare("PRAGMA table_info(sample_events)").all().map(c => c.name);
|
|
767
|
+
if (!sevCols.includes('cost_known')) {
|
|
768
|
+
try {
|
|
769
|
+
this.db.exec('ALTER TABLE sample_events ADD COLUMN cost_known INTEGER NOT NULL DEFAULT 0');
|
|
770
|
+
}
|
|
771
|
+
catch { /* ignore */ }
|
|
772
|
+
}
|
|
600
773
|
if (!sevCols.includes('turn_id')) {
|
|
601
774
|
try {
|
|
602
775
|
this.db.exec('ALTER TABLE sample_events ADD COLUMN turn_id TEXT');
|
|
@@ -626,6 +799,21 @@ export class ApmeStore {
|
|
|
626
799
|
}
|
|
627
800
|
catch { /* ignore */ }
|
|
628
801
|
}
|
|
802
|
+
// A positive historical value proves that a price existed. Historical
|
|
803
|
+
// zeroes remain unknown because they could be either a free local model or
|
|
804
|
+
// the old UNKNOWN_PRICE fallback — the ambiguity this column removes.
|
|
805
|
+
try {
|
|
806
|
+
this.db.exec('UPDATE runs SET cost_known=1 WHERE cost_usd > 0');
|
|
807
|
+
}
|
|
808
|
+
catch { /* ignore */ }
|
|
809
|
+
try {
|
|
810
|
+
this.db.exec('UPDATE tasks SET cost_known=1 WHERE cost_usd > 0');
|
|
811
|
+
}
|
|
812
|
+
catch { /* ignore */ }
|
|
813
|
+
try {
|
|
814
|
+
this.db.exec('UPDATE sample_events SET cost_known=1 WHERE cost_usd > 0');
|
|
815
|
+
}
|
|
816
|
+
catch { /* ignore */ }
|
|
629
817
|
// ── Covering indexes for the per-run/per-task rollups ──
|
|
630
818
|
// better-sqlite3 is synchronous on a single connection, so ANY slow query
|
|
631
819
|
// here stalls the daemon's whole HTTP path — this is a latency budget, not
|
|
@@ -647,6 +835,16 @@ export class ApmeStore {
|
|
|
647
835
|
}
|
|
648
836
|
catch { /* ignore */ }
|
|
649
837
|
}
|
|
838
|
+
// CREATE VIEW IF NOT EXISTS preserves stale executable SQL forever. Rebuild
|
|
839
|
+
// after the ALTERs above so both fresh and upgraded databases group by the
|
|
840
|
+
// turn/provider identity this version actually writes.
|
|
841
|
+
for (const view of ['v_sample_scorecard', 'v_category_scorecard', 'v_model_scorecard', 'v_run_metrics']) {
|
|
842
|
+
try {
|
|
843
|
+
this.db.exec(`DROP VIEW IF EXISTS ${view}`);
|
|
844
|
+
}
|
|
845
|
+
catch { /* ignore */ }
|
|
846
|
+
}
|
|
847
|
+
this.db.exec(SCORECARD_DDL);
|
|
650
848
|
}
|
|
651
849
|
seedDefaultRubric() {
|
|
652
850
|
if (!this.db)
|
|
@@ -688,11 +886,12 @@ export class ApmeStore {
|
|
|
688
886
|
insertRun(row) {
|
|
689
887
|
if (!this.db)
|
|
690
888
|
return;
|
|
889
|
+
const costKnown = row.costKnown === true || (row.costUsd != null && row.costUsd > 0);
|
|
691
890
|
this.db.prepare(`INSERT INTO runs
|
|
692
|
-
(id, session_id, agent_type, model_id, project_name, project_path, task_prompt,
|
|
693
|
-
started_at, ended_at, input_tokens, output_tokens, cost_usd, exit_code,
|
|
891
|
+
(id, session_id, agent_type, model_id, provider, project_name, project_path, task_prompt,
|
|
892
|
+
started_at, ended_at, input_tokens, output_tokens, cost_usd, cost_known, exit_code,
|
|
694
893
|
git_before, git_after, hw_profile)
|
|
695
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(row.id, row.sessionId, row.agentType, row.modelId ?? null, row.projectName ?? null, row.projectPath ?? null, row.taskPrompt ?? null, row.startedAt, row.endedAt ?? null, row.inputTokens ?? null, row.outputTokens ?? null, row.costUsd ?? null, row.exitCode ?? null, row.gitBefore ?? null, row.gitAfter ?? null, row.hwProfile ?? null);
|
|
894
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(row.id, row.sessionId, row.agentType, row.modelId ?? null, row.provider ?? null, row.projectName ?? null, row.projectPath ?? null, row.taskPrompt ?? null, row.startedAt, row.endedAt ?? null, row.inputTokens ?? null, row.outputTokens ?? null, row.costUsd ?? null, costKnown ? 1 : 0, row.exitCode ?? null, row.gitBefore ?? null, row.gitAfter ?? null, row.hwProfile ?? null);
|
|
696
895
|
}
|
|
697
896
|
updateRun(id, patch) {
|
|
698
897
|
if (!this.db)
|
|
@@ -701,6 +900,7 @@ export class ApmeStore {
|
|
|
701
900
|
const values = [];
|
|
702
901
|
const map = {
|
|
703
902
|
modelId: 'model_id',
|
|
903
|
+
provider: 'provider',
|
|
704
904
|
projectName: 'project_name',
|
|
705
905
|
projectPath: 'project_path',
|
|
706
906
|
taskPrompt: 'task_prompt',
|
|
@@ -708,6 +908,7 @@ export class ApmeStore {
|
|
|
708
908
|
inputTokens: 'input_tokens',
|
|
709
909
|
outputTokens: 'output_tokens',
|
|
710
910
|
costUsd: 'cost_usd',
|
|
911
|
+
costKnown: 'cost_known',
|
|
711
912
|
exitCode: 'exit_code',
|
|
712
913
|
gitBefore: 'git_before',
|
|
713
914
|
gitAfter: 'git_after',
|
|
@@ -726,7 +927,11 @@ export class ApmeStore {
|
|
|
726
927
|
if (!col || v === undefined)
|
|
727
928
|
continue;
|
|
728
929
|
fields.push(`${col} = ?`);
|
|
729
|
-
values.push(v);
|
|
930
|
+
values.push(k === 'costKnown' ? (v ? 1 : 0) : v);
|
|
931
|
+
}
|
|
932
|
+
if (patch.costUsd != null && patch.costUsd > 0 && patch.costKnown === undefined) {
|
|
933
|
+
fields.push('cost_known = ?');
|
|
934
|
+
values.push(1);
|
|
730
935
|
}
|
|
731
936
|
if (fields.length === 0)
|
|
732
937
|
return;
|
|
@@ -784,6 +989,7 @@ export class ApmeStore {
|
|
|
784
989
|
outcome: 'outcome', compositeScore: 'composite_score', efficiencyJson: 'efficiency_json',
|
|
785
990
|
prompt: 'prompt', response: 'response', taskId: 'task_id',
|
|
786
991
|
endSource: 'end_source',
|
|
992
|
+
modelId: 'model_id', provider: 'provider',
|
|
787
993
|
};
|
|
788
994
|
const sets = [];
|
|
789
995
|
const vals = [];
|
|
@@ -886,10 +1092,12 @@ export class ApmeStore {
|
|
|
886
1092
|
notesJson: 'notes_json',
|
|
887
1093
|
boundarySignal: 'boundary_signal',
|
|
888
1094
|
modelId: 'model_id',
|
|
1095
|
+
provider: 'provider',
|
|
889
1096
|
modelConfig: 'model_config',
|
|
890
1097
|
inputTokens: 'input_tokens',
|
|
891
1098
|
outputTokens: 'output_tokens',
|
|
892
1099
|
costUsd: 'cost_usd',
|
|
1100
|
+
costKnown: 'cost_known',
|
|
893
1101
|
latencyMs: 'latency_ms',
|
|
894
1102
|
};
|
|
895
1103
|
const sets = [];
|
|
@@ -899,7 +1107,11 @@ export class ApmeStore {
|
|
|
899
1107
|
if (!col || v === undefined)
|
|
900
1108
|
continue;
|
|
901
1109
|
sets.push(`${col} = ?`);
|
|
902
|
-
vals.push(v);
|
|
1110
|
+
vals.push(k === 'costKnown' ? (v ? 1 : 0) : v);
|
|
1111
|
+
}
|
|
1112
|
+
if (patch.costUsd != null && patch.costUsd > 0 && patch.costKnown === undefined) {
|
|
1113
|
+
sets.push('cost_known = ?');
|
|
1114
|
+
vals.push(1);
|
|
903
1115
|
}
|
|
904
1116
|
if (sets.length === 0)
|
|
905
1117
|
return;
|
|
@@ -945,36 +1157,18 @@ export class ApmeStore {
|
|
|
945
1157
|
return { total: 0, tasks: [] };
|
|
946
1158
|
const limit = Math.min(Math.max(opts.limit ?? 50, 1), 500);
|
|
947
1159
|
const offset = Math.max(opts.offset ?? 0, 0);
|
|
948
|
-
const where =
|
|
949
|
-
const
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
if (opts.agentType) {
|
|
953
|
-
where.push('r.agent_type = ?');
|
|
954
|
-
args.push(opts.agentType);
|
|
955
|
-
}
|
|
956
|
-
if (opts.projectName) {
|
|
957
|
-
where.push('r.project_name = ?');
|
|
958
|
-
args.push(opts.projectName);
|
|
959
|
-
}
|
|
960
|
-
if (opts.category) {
|
|
961
|
-
where.push('COALESCE(t.task_category, r.task_category) = ?');
|
|
962
|
-
args.push(opts.category);
|
|
963
|
-
}
|
|
964
|
-
if (opts.outcome) {
|
|
965
|
-
where.push('t.outcome = ?');
|
|
966
|
-
args.push(opts.outcome);
|
|
967
|
-
}
|
|
1160
|
+
const { where, args } = buildTaskFilterWhere(opts);
|
|
1161
|
+
const attentionSql = taskAttentionSql(Date.now() - TASK_ATTENTION_WINDOW_MS);
|
|
1162
|
+
if (opts.view)
|
|
1163
|
+
where.push(opts.view === 'attention' ? attentionSql : TASK_VIEW_SQL[opts.view]);
|
|
968
1164
|
if (opts.state === 'closed')
|
|
969
1165
|
where.push('t.ended_at IS NOT NULL');
|
|
970
1166
|
if (opts.state === 'open')
|
|
971
1167
|
where.push('t.ended_at IS NULL');
|
|
972
|
-
if (opts.q) {
|
|
973
|
-
where.push('(t.summary LIKE ? OR r.task_prompt LIKE ?)');
|
|
974
|
-
const like = `%${opts.q}%`;
|
|
975
|
-
args.push(like, like);
|
|
976
|
-
}
|
|
977
1168
|
const whereSql = `WHERE ${where.join(' AND ')}`;
|
|
1169
|
+
const orderSql = opts.order === 'attention'
|
|
1170
|
+
? 'ORDER BY attention DESC, t.started_at DESC'
|
|
1171
|
+
: 'ORDER BY t.started_at DESC';
|
|
978
1172
|
const total = this.db.prepare(`SELECT COUNT(*) AS n FROM tasks t JOIN runs r ON r.id = t.run_id ${whereSql}`).get(...args).n;
|
|
979
1173
|
const rows = this.db.prepare(`SELECT t.*,
|
|
980
1174
|
r.session_id, r.agent_type, r.model_id AS run_model_id, r.project_name,
|
|
@@ -983,15 +1177,78 @@ export class ApmeStore {
|
|
|
983
1177
|
(SELECT COUNT(*) FROM turns tu WHERE tu.task_id = t.id AND tu.response IS NOT NULL) AS answered_turns,
|
|
984
1178
|
(SELECT COUNT(*) FROM sample_events se WHERE se.task_id = t.id) AS event_count,
|
|
985
1179
|
(SELECT COUNT(*) FROM sample_events se WHERE se.task_id = t.id AND se.kind = 'tool') AS tool_count,
|
|
1180
|
+
(SELECT COALESCE(SUM(COALESCE(tu.files_modified, 0) + COALESCE(tu.files_created, 0)), 0)
|
|
1181
|
+
FROM turns tu WHERE tu.task_id = t.id) AS files_touched,
|
|
986
1182
|
(SELECT tu.prompt FROM turns tu WHERE tu.task_id = t.id ORDER BY tu.turn_index ASC LIMIT 1) AS first_prompt,
|
|
987
1183
|
(SELECT e.score FROM evals e WHERE e.task_id = t.id AND e.metric = 'overall' ORDER BY e.created_at DESC LIMIT 1) AS overall_score,
|
|
988
|
-
(SELECT COUNT(*) FROM evals e WHERE e.task_id = t.id) AS eval_count
|
|
1184
|
+
(SELECT COUNT(*) FROM evals e WHERE e.task_id = t.id) AS eval_count,
|
|
1185
|
+
(${attentionSql}) AS attention
|
|
989
1186
|
FROM tasks t JOIN runs r ON r.id = t.run_id
|
|
990
1187
|
${whereSql}
|
|
991
|
-
|
|
1188
|
+
${orderSql}
|
|
992
1189
|
LIMIT ? OFFSET ?`).all(...args, limit, offset);
|
|
993
1190
|
return { total, tasks: rows.map(rowToTaskListRow) };
|
|
994
1191
|
}
|
|
1192
|
+
/** 10 s TTL cache for `taskViewCounts`, keyed by the filter tuple. The Work
|
|
1193
|
+
* board polls every 15 s and refetches on every interaction; the badges
|
|
1194
|
+
* are a full-table aggregate with correlated eval subqueries, so paying it
|
|
1195
|
+
* at most once per TTL bounds the cost of leaving the dashboard open. */
|
|
1196
|
+
viewCountsCache = new Map();
|
|
1197
|
+
/** Per-lifecycle-bucket totals for the Work board's tab badges. Takes the
|
|
1198
|
+
* same NARROWING filters as `listTaskPage` (agent/project/category/
|
|
1199
|
+
* outcome/q) so a filtered board's badges count what its tabs would list.
|
|
1200
|
+
* Buckets and filters read the same SQL definitions; the TTL means a badge
|
|
1201
|
+
* may lag its rows by up to 10 s after a task moves buckets — a bounded
|
|
1202
|
+
* staleness, not a different definition. */
|
|
1203
|
+
taskViewCounts(filters = {}) {
|
|
1204
|
+
const empty = { all: 0, attention: 0, inprogress: 0, judged: 0, reported: 0, orphaned: 0 };
|
|
1205
|
+
if (!this.db)
|
|
1206
|
+
return { ...empty };
|
|
1207
|
+
const key = JSON.stringify([filters.agentType, filters.sessionId, filters.projectName, filters.category, filters.outcome, filters.q]);
|
|
1208
|
+
const cached = this.viewCountsCache.get(key);
|
|
1209
|
+
if (cached && Date.now() - cached.at < 10_000)
|
|
1210
|
+
return cached.value;
|
|
1211
|
+
const attentionSql = taskAttentionSql(Date.now() - TASK_ATTENTION_WINDOW_MS);
|
|
1212
|
+
const { where, args } = buildTaskFilterWhere(filters);
|
|
1213
|
+
const row = this.db.prepare(`SELECT COUNT(*) AS all_n,
|
|
1214
|
+
SUM(CASE WHEN ${attentionSql} THEN 1 ELSE 0 END) AS attention_n,
|
|
1215
|
+
SUM(CASE WHEN ${TASK_VIEW_SQL.inprogress} THEN 1 ELSE 0 END) AS inprogress_n,
|
|
1216
|
+
SUM(CASE WHEN ${TASK_VIEW_SQL.judged} THEN 1 ELSE 0 END) AS judged_n,
|
|
1217
|
+
SUM(CASE WHEN ${TASK_VIEW_SQL.reported} THEN 1 ELSE 0 END) AS reported_n,
|
|
1218
|
+
SUM(CASE WHEN ${TASK_VIEW_SQL.orphaned} THEN 1 ELSE 0 END) AS orphaned_n
|
|
1219
|
+
FROM tasks t JOIN runs r ON r.id = t.run_id
|
|
1220
|
+
WHERE ${where.join(' AND ')}`).get(...args);
|
|
1221
|
+
const value = {
|
|
1222
|
+
all: row.all_n ?? 0,
|
|
1223
|
+
attention: row.attention_n ?? 0,
|
|
1224
|
+
inprogress: row.inprogress_n ?? 0,
|
|
1225
|
+
judged: row.judged_n ?? 0,
|
|
1226
|
+
reported: row.reported_n ?? 0,
|
|
1227
|
+
orphaned: row.orphaned_n ?? 0,
|
|
1228
|
+
};
|
|
1229
|
+
if (this.viewCountsCache.size > 20)
|
|
1230
|
+
this.viewCountsCache.clear();
|
|
1231
|
+
this.viewCountsCache.set(key, { at: Date.now(), value });
|
|
1232
|
+
return value;
|
|
1233
|
+
}
|
|
1234
|
+
/** Per-tool call counts for a page of tasks, in one grouped query. Feeds the
|
|
1235
|
+
* shared `foldActionCounts` projection — the store returns raw counts, the
|
|
1236
|
+
* HTTP layer folds. */
|
|
1237
|
+
toolCountsForTasks(taskIds) {
|
|
1238
|
+
const out = new Map();
|
|
1239
|
+
if (!this.db || taskIds.length === 0)
|
|
1240
|
+
return out;
|
|
1241
|
+
const placeholders = taskIds.map(() => '?').join(',');
|
|
1242
|
+
const rows = this.db.prepare(`SELECT task_id, tool_name, COUNT(*) AS n FROM sample_events
|
|
1243
|
+
WHERE kind = 'tool' AND tool_name IS NOT NULL AND task_id IN (${placeholders})
|
|
1244
|
+
GROUP BY task_id, tool_name`).all(...taskIds);
|
|
1245
|
+
for (const r of rows) {
|
|
1246
|
+
const list = out.get(r.task_id) ?? [];
|
|
1247
|
+
list.push({ name: r.tool_name, count: r.n });
|
|
1248
|
+
out.set(r.task_id, list);
|
|
1249
|
+
}
|
|
1250
|
+
return out;
|
|
1251
|
+
}
|
|
995
1252
|
/** Distinct values behind the task list's filters, so the UI offers what the
|
|
996
1253
|
* data actually contains rather than a hardcoded menu. */
|
|
997
1254
|
taskFacets() {
|
|
@@ -1280,10 +1537,14 @@ export class ApmeStore {
|
|
|
1280
1537
|
insertSampleEvent(row) {
|
|
1281
1538
|
if (!this.db)
|
|
1282
1539
|
return false;
|
|
1540
|
+
// A positive monetary amount is self-proving provenance even for legacy
|
|
1541
|
+
// callers that predate costKnown. Zero remains ambiguous and must only be
|
|
1542
|
+
// marked known by a pricing table / known-local producer.
|
|
1543
|
+
const costKnown = row.costKnown === true || (row.costUsd != null && row.costUsd > 0);
|
|
1283
1544
|
const res = this.db.prepare(`INSERT OR IGNORE INTO sample_events
|
|
1284
1545
|
(task_id, run_id, turn_index, turn_id, seq, ts, kind, model, input_tokens, output_tokens,
|
|
1285
|
-
cost_usd, latency_ms, tool_name, tool_status, tool_error, payload, dedup_key)
|
|
1286
|
-
VALUES (
|
|
1546
|
+
cost_usd, cost_known, latency_ms, tool_name, tool_status, tool_error, payload, dedup_key)
|
|
1547
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(row.taskId, row.runId, row.turnIndex ?? null, row.turnId ?? null, row.seq, row.ts, row.kind, row.model ?? null, row.inputTokens ?? null, row.outputTokens ?? null, row.costUsd ?? null, costKnown ? 1 : 0, row.latencyMs ?? null, row.toolName ?? null, row.toolStatus ?? null, row.toolError ?? null, row.payload ?? null, row.dedupKey ?? null);
|
|
1287
1548
|
return res.changes > 0;
|
|
1288
1549
|
}
|
|
1289
1550
|
/** Update a previously-inserted event (e.g. a tool pending→resolved) by id. */
|
|
@@ -1292,7 +1553,7 @@ export class ApmeStore {
|
|
|
1292
1553
|
return;
|
|
1293
1554
|
const map = {
|
|
1294
1555
|
toolStatus: 'tool_status', toolError: 'tool_error', payload: 'payload',
|
|
1295
|
-
costUsd: 'cost_usd', latencyMs: 'latency_ms', model: 'model',
|
|
1556
|
+
costUsd: 'cost_usd', costKnown: 'cost_known', latencyMs: 'latency_ms', model: 'model',
|
|
1296
1557
|
inputTokens: 'input_tokens', outputTokens: 'output_tokens', ts: 'ts',
|
|
1297
1558
|
};
|
|
1298
1559
|
const sets = [];
|
|
@@ -1302,7 +1563,11 @@ export class ApmeStore {
|
|
|
1302
1563
|
if (!col || v === undefined)
|
|
1303
1564
|
continue;
|
|
1304
1565
|
sets.push(`${col} = ?`);
|
|
1305
|
-
vals.push(v);
|
|
1566
|
+
vals.push(k === 'costKnown' ? (v ? 1 : 0) : v);
|
|
1567
|
+
}
|
|
1568
|
+
if (fields.costUsd != null && fields.costUsd > 0 && fields.costKnown === undefined) {
|
|
1569
|
+
sets.push('cost_known = ?');
|
|
1570
|
+
vals.push(1);
|
|
1306
1571
|
}
|
|
1307
1572
|
if (sets.length === 0)
|
|
1308
1573
|
return;
|
|
@@ -1319,6 +1584,17 @@ export class ApmeStore {
|
|
|
1319
1584
|
ORDER BY seq DESC LIMIT 1`).get(taskId, turnIndex, toolName);
|
|
1320
1585
|
return row ? rowToSampleEvent(row) : null;
|
|
1321
1586
|
}
|
|
1587
|
+
/** One assistant-message slot per turn. Response producers overlap, so text
|
|
1588
|
+
* hashes cannot be the identity: a truncated projection and the full final
|
|
1589
|
+
* are two payload versions of the same logical event. */
|
|
1590
|
+
findAssistantMessageEvent(taskId, turnId) {
|
|
1591
|
+
if (!this.db)
|
|
1592
|
+
return null;
|
|
1593
|
+
const row = this.db.prepare(`SELECT * FROM sample_events
|
|
1594
|
+
WHERE task_id = ? AND turn_id = ? AND kind = 'assistant_message'
|
|
1595
|
+
ORDER BY seq ASC LIMIT 1`).get(taskId, turnId);
|
|
1596
|
+
return row ? rowToSampleEvent(row) : null;
|
|
1597
|
+
}
|
|
1322
1598
|
listSampleEventRows(taskId) {
|
|
1323
1599
|
if (!this.db)
|
|
1324
1600
|
return [];
|
|
@@ -1369,6 +1645,7 @@ export class ApmeStore {
|
|
|
1369
1645
|
inputTokens: task.inputTokens ?? 0,
|
|
1370
1646
|
outputTokens: task.outputTokens ?? 0,
|
|
1371
1647
|
costUsd: task.costUsd ?? 0,
|
|
1648
|
+
costKnown: task.costKnown ?? false,
|
|
1372
1649
|
latencyMs: task.latencyMs ?? 0,
|
|
1373
1650
|
},
|
|
1374
1651
|
summary: task.summary ?? null,
|
|
@@ -1382,10 +1659,15 @@ export class ApmeStore {
|
|
|
1382
1659
|
if (!this.db)
|
|
1383
1660
|
return;
|
|
1384
1661
|
const row = this.db.prepare(`SELECT COALESCE(SUM(input_tokens),0) AS it, COALESCE(SUM(output_tokens),0) AS ot,
|
|
1385
|
-
|
|
1662
|
+
CASE WHEN COUNT(*) > 0 AND MIN(COALESCE(cost_known,0))=1
|
|
1663
|
+
THEN SUM(cost_usd) ELSE NULL END AS cu,
|
|
1664
|
+
CASE WHEN COUNT(*) > 0 AND MIN(COALESCE(cost_known,0))=1
|
|
1665
|
+
THEN 1 ELSE 0 END AS ck,
|
|
1666
|
+
COALESCE(SUM(latency_ms),0) AS lm
|
|
1386
1667
|
FROM sample_events WHERE task_id = ? AND kind = 'model'`).get(taskId);
|
|
1387
1668
|
this.updateTask(taskId, {
|
|
1388
|
-
inputTokens: row.it, outputTokens: row.ot, costUsd: row.cu,
|
|
1669
|
+
inputTokens: row.it, outputTokens: row.ot, costUsd: row.cu,
|
|
1670
|
+
costKnown: row.ck === 1, latencyMs: row.lm,
|
|
1389
1671
|
});
|
|
1390
1672
|
}
|
|
1391
1673
|
sampleScorecard() {
|
|
@@ -1395,10 +1677,12 @@ export class ApmeStore {
|
|
|
1395
1677
|
return rows.map((r) => ({
|
|
1396
1678
|
agentType: r.agent_type,
|
|
1397
1679
|
modelId: r.model_id,
|
|
1680
|
+
provider: r.provider ?? null,
|
|
1398
1681
|
taskCategory: r.task_category ?? null,
|
|
1399
1682
|
samples: r.samples,
|
|
1400
1683
|
avgQuality: r.avg_quality ?? null,
|
|
1401
1684
|
totalCost: r.total_cost ?? null,
|
|
1685
|
+
costKnown: r.cost_known === 1,
|
|
1402
1686
|
avgLatencyMs: r.avg_latency_ms ?? null,
|
|
1403
1687
|
costPerQuality: r.cost_per_quality ?? null,
|
|
1404
1688
|
}));
|
|
@@ -1411,10 +1695,12 @@ export class ApmeStore {
|
|
|
1411
1695
|
return rows.map((r) => ({
|
|
1412
1696
|
agentType: r.agent_type,
|
|
1413
1697
|
modelId: r.model_id,
|
|
1698
|
+
provider: r.provider ?? null,
|
|
1414
1699
|
runs: r.runs,
|
|
1415
1700
|
avgOverall: r.avg_overall ?? null,
|
|
1416
1701
|
avgTestsPass: r.avg_tests_pass ?? null,
|
|
1417
1702
|
totalCost: r.total_cost ?? null,
|
|
1703
|
+
costKnown: r.cost_known === 1,
|
|
1418
1704
|
costPerQuality: r.cost_per_quality ?? null,
|
|
1419
1705
|
}));
|
|
1420
1706
|
}
|
|
@@ -1425,10 +1711,12 @@ export class ApmeStore {
|
|
|
1425
1711
|
return rows.map((r) => ({
|
|
1426
1712
|
taskCategory: r.task_category,
|
|
1427
1713
|
modelId: r.model_id,
|
|
1714
|
+
provider: r.provider ?? null,
|
|
1428
1715
|
runs: r.runs,
|
|
1429
1716
|
avgOverall: r.avg_overall ?? null,
|
|
1430
1717
|
avgTestsPass: r.avg_tests_pass ?? null,
|
|
1431
1718
|
totalCost: r.total_cost ?? null,
|
|
1719
|
+
costKnown: r.cost_known === 1,
|
|
1432
1720
|
}));
|
|
1433
1721
|
}
|
|
1434
1722
|
}
|
|
@@ -1439,6 +1727,7 @@ function rowToRun(r) {
|
|
|
1439
1727
|
sessionId: r.session_id,
|
|
1440
1728
|
agentType: r.agent_type,
|
|
1441
1729
|
modelId: r.model_id ?? null,
|
|
1730
|
+
provider: r.provider ?? null,
|
|
1442
1731
|
projectName: r.project_name ?? null,
|
|
1443
1732
|
projectPath: r.project_path ?? null,
|
|
1444
1733
|
taskPrompt: r.task_prompt ?? null,
|
|
@@ -1447,6 +1736,7 @@ function rowToRun(r) {
|
|
|
1447
1736
|
inputTokens: r.input_tokens ?? null,
|
|
1448
1737
|
outputTokens: r.output_tokens ?? null,
|
|
1449
1738
|
costUsd: r.cost_usd ?? null,
|
|
1739
|
+
costKnown: r.cost_known === 1,
|
|
1450
1740
|
exitCode: r.exit_code ?? null,
|
|
1451
1741
|
gitBefore: r.git_before ?? null,
|
|
1452
1742
|
gitAfter: r.git_after ?? null,
|
|
@@ -1477,10 +1767,12 @@ function rowToTask(r) {
|
|
|
1477
1767
|
taskCategory: r.task_category ?? null,
|
|
1478
1768
|
notesJson: r.notes_json ?? null,
|
|
1479
1769
|
modelId: r.model_id ?? null,
|
|
1770
|
+
provider: r.provider ?? null,
|
|
1480
1771
|
modelConfig: r.model_config ?? null,
|
|
1481
1772
|
inputTokens: r.input_tokens ?? null,
|
|
1482
1773
|
outputTokens: r.output_tokens ?? null,
|
|
1483
1774
|
costUsd: r.cost_usd ?? null,
|
|
1775
|
+
costKnown: r.cost_known === 1,
|
|
1484
1776
|
latencyMs: r.latency_ms ?? null,
|
|
1485
1777
|
};
|
|
1486
1778
|
}
|
|
@@ -1497,12 +1789,17 @@ function rowToTaskListRow(r) {
|
|
|
1497
1789
|
projectPath: r.project_path ?? null,
|
|
1498
1790
|
parentRunId: r.parent_run_id ?? null,
|
|
1499
1791
|
firstPrompt: r.first_prompt ?? r.run_prompt ?? null,
|
|
1792
|
+
// The task's OWN first turn prompt, with NO run-prompt fallback: title
|
|
1793
|
+
// derivation must never name task 2+ of a split run after task 0's intent.
|
|
1794
|
+
ownFirstPrompt: r.first_prompt ?? null,
|
|
1500
1795
|
turnCount: r.turn_count ?? 0,
|
|
1501
1796
|
answeredTurns: r.answered_turns ?? 0,
|
|
1502
1797
|
eventCount: r.event_count ?? 0,
|
|
1503
1798
|
toolCount: r.tool_count ?? 0,
|
|
1799
|
+
filesTouched: r.files_touched ?? 0,
|
|
1504
1800
|
evalCount: r.eval_count ?? 0,
|
|
1505
1801
|
overallScore: r.overall_score ?? r.composite_score ?? null,
|
|
1802
|
+
attention: r.attention === 1,
|
|
1506
1803
|
};
|
|
1507
1804
|
}
|
|
1508
1805
|
function rowToSampleEvent(r) {
|
|
@@ -1519,6 +1816,7 @@ function rowToSampleEvent(r) {
|
|
|
1519
1816
|
inputTokens: r.input_tokens ?? null,
|
|
1520
1817
|
outputTokens: r.output_tokens ?? null,
|
|
1521
1818
|
costUsd: r.cost_usd ?? null,
|
|
1819
|
+
costKnown: r.cost_known === 1,
|
|
1522
1820
|
latencyMs: r.latency_ms ?? null,
|
|
1523
1821
|
toolName: r.tool_name ?? null,
|
|
1524
1822
|
toolStatus: r.tool_status ?? null,
|