@agentdeck/bridge 1.0.24 → 1.1.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.
Files changed (117) hide show
  1. package/dist/adapters/openclaw.d.ts +74 -4
  2. package/dist/adapters/openclaw.d.ts.map +1 -1
  3. package/dist/adapters/openclaw.js +235 -43
  4. package/dist/adapters/openclaw.js.map +1 -1
  5. package/dist/apme/adapters/openclaw-hook.d.ts +38 -10
  6. package/dist/apme/adapters/openclaw-hook.d.ts.map +1 -1
  7. package/dist/apme/adapters/openclaw-hook.js +181 -37
  8. package/dist/apme/adapters/openclaw-hook.js.map +1 -1
  9. package/dist/apme/collector.d.ts +37 -1
  10. package/dist/apme/collector.d.ts.map +1 -1
  11. package/dist/apme/collector.js +277 -35
  12. package/dist/apme/collector.js.map +1 -1
  13. package/dist/apme/pareto.d.ts +2 -1
  14. package/dist/apme/pareto.d.ts.map +1 -1
  15. package/dist/apme/pareto.js +15 -4
  16. package/dist/apme/pareto.js.map +1 -1
  17. package/dist/apme/recommend.d.ts +24 -1
  18. package/dist/apme/recommend.d.ts.map +1 -1
  19. package/dist/apme/recommend.js +43 -7
  20. package/dist/apme/recommend.js.map +1 -1
  21. package/dist/apme/store.d.ts +6 -0
  22. package/dist/apme/store.d.ts.map +1 -1
  23. package/dist/apme/store.js +244 -72
  24. package/dist/apme/store.js.map +1 -1
  25. package/dist/bridge-core.d.ts +10 -1
  26. package/dist/bridge-core.d.ts.map +1 -1
  27. package/dist/bridge-core.js +20 -5
  28. package/dist/bridge-core.js.map +1 -1
  29. package/dist/card-feed.d.ts +46 -1
  30. package/dist/card-feed.d.ts.map +1 -1
  31. package/dist/card-feed.js +166 -6
  32. package/dist/card-feed.js.map +1 -1
  33. package/dist/cli.d.ts +45 -0
  34. package/dist/cli.d.ts.map +1 -1
  35. package/dist/cli.js +388 -27
  36. package/dist/cli.js.map +1 -1
  37. package/dist/daemon-build-identity.d.ts +124 -0
  38. package/dist/daemon-build-identity.d.ts.map +1 -0
  39. package/dist/daemon-build-identity.js +310 -0
  40. package/dist/daemon-build-identity.js.map +1 -0
  41. package/dist/daemon-server.d.ts +34 -6
  42. package/dist/daemon-server.d.ts.map +1 -1
  43. package/dist/daemon-server.js +613 -108
  44. package/dist/daemon-server.js.map +1 -1
  45. package/dist/daemon-takeover.d.ts +11 -0
  46. package/dist/daemon-takeover.d.ts.map +1 -1
  47. package/dist/daemon-takeover.js +62 -17
  48. package/dist/daemon-takeover.js.map +1 -1
  49. package/dist/daemon.js +1 -1
  50. package/dist/daemon.js.map +1 -1
  51. package/dist/esp32-serial.d.ts +14 -0
  52. package/dist/esp32-serial.d.ts.map +1 -1
  53. package/dist/esp32-serial.js +19 -1
  54. package/dist/esp32-serial.js.map +1 -1
  55. package/dist/glance-frame.d.ts.map +1 -1
  56. package/dist/glance-frame.js +3 -1
  57. package/dist/glance-frame.js.map +1 -1
  58. package/dist/hook-codex-sessions.d.ts +3 -1
  59. package/dist/hook-codex-sessions.d.ts.map +1 -1
  60. package/dist/hook-codex-sessions.js +27 -7
  61. package/dist/hook-codex-sessions.js.map +1 -1
  62. package/dist/legacy-rearm-ledger.d.ts +56 -0
  63. package/dist/legacy-rearm-ledger.d.ts.map +1 -0
  64. package/dist/legacy-rearm-ledger.js +66 -0
  65. package/dist/legacy-rearm-ledger.js.map +1 -0
  66. package/dist/mdns.d.ts +33 -0
  67. package/dist/mdns.d.ts.map +1 -1
  68. package/dist/mdns.js +91 -2
  69. package/dist/mdns.js.map +1 -1
  70. package/dist/openclaw-session.d.ts.map +1 -1
  71. package/dist/openclaw-session.js +13 -0
  72. package/dist/openclaw-session.js.map +1 -1
  73. package/dist/openclaw-timeline-feed.d.ts +84 -0
  74. package/dist/openclaw-timeline-feed.d.ts.map +1 -0
  75. package/dist/openclaw-timeline-feed.js +162 -0
  76. package/dist/openclaw-timeline-feed.js.map +1 -0
  77. package/dist/openclaw-transcript-timeline.d.ts +103 -0
  78. package/dist/openclaw-transcript-timeline.d.ts.map +1 -0
  79. package/dist/openclaw-transcript-timeline.js +328 -0
  80. package/dist/openclaw-transcript-timeline.js.map +1 -0
  81. package/dist/passive-observer.d.ts +21 -1
  82. package/dist/passive-observer.d.ts.map +1 -1
  83. package/dist/passive-observer.js +37 -3
  84. package/dist/passive-observer.js.map +1 -1
  85. package/dist/relayed-usage.d.ts +68 -0
  86. package/dist/relayed-usage.d.ts.map +1 -0
  87. package/dist/relayed-usage.js +80 -0
  88. package/dist/relayed-usage.js.map +1 -0
  89. package/dist/session-aggregator.d.ts +3 -0
  90. package/dist/session-aggregator.d.ts.map +1 -1
  91. package/dist/session-aggregator.js.map +1 -1
  92. package/dist/session-registry.d.ts +1 -0
  93. package/dist/session-registry.d.ts.map +1 -1
  94. package/dist/surface-protocol.d.ts +84 -0
  95. package/dist/surface-protocol.d.ts.map +1 -0
  96. package/dist/surface-protocol.js +235 -0
  97. package/dist/surface-protocol.js.map +1 -0
  98. package/dist/timeline-store.d.ts +1 -1
  99. package/dist/timeline-store.d.ts.map +1 -1
  100. package/dist/timeline-store.js +16 -1
  101. package/dist/timeline-store.js.map +1 -1
  102. package/dist/tui/renderer.d.ts.map +1 -1
  103. package/dist/tui/renderer.js +35 -0
  104. package/dist/tui/renderer.js.map +1 -1
  105. package/dist/weather-config.d.ts +17 -0
  106. package/dist/weather-config.d.ts.map +1 -0
  107. package/dist/weather-config.js +54 -0
  108. package/dist/weather-config.js.map +1 -0
  109. package/dist/weather.d.ts +44 -4
  110. package/dist/weather.d.ts.map +1 -1
  111. package/dist/weather.js +223 -15
  112. package/dist/weather.js.map +1 -1
  113. package/dist/ws-server.d.ts.map +1 -1
  114. package/dist/ws-server.js +55 -5
  115. package/dist/ws-server.js.map +1 -1
  116. package/fm-helper/AgentDeckFMHelper.swift +51 -5
  117. package/package.json +3 -3
@@ -16,12 +16,141 @@ import { debug } from '../logger.js';
16
16
  // repo root, where the symlink doesn't exist).
17
17
  const require = createRequire(import.meta.url);
18
18
  // ─── Schema ────────────────────────────────────────────────────────────────────
19
+ const SCORECARD_DDL = `
20
+ -- Pre-aggregate metrics at the level they actually describe. New rows prefer
21
+ -- turn attribution; runs with no turn identity remain available as a legacy
22
+ -- fallback instead of being silently dropped after the schema migration.
23
+ CREATE VIEW IF NOT EXISTS v_run_metrics AS
24
+ SELECT
25
+ run_id,
26
+ MAX(CASE WHEN metric='overall' AND layer='llm_judge' THEN score END) AS overall,
27
+ MAX(CASE WHEN metric='tests_pass' AND layer='deterministic' THEN score END) AS tests_pass
28
+ FROM evals
29
+ GROUP BY run_id;
30
+
31
+ CREATE VIEW IF NOT EXISTS v_model_scorecard AS
32
+ WITH turn_eval AS (
33
+ SELECT
34
+ turn_id,
35
+ MAX(CASE WHEN metric='overall' THEN score END) AS overall,
36
+ MAX(CASE WHEN metric='tests_pass' THEN score END) AS tests_pass
37
+ FROM evals
38
+ WHERE turn_id IS NOT NULL
39
+ GROUP BY turn_id
40
+ ), turn_cost AS (
41
+ SELECT
42
+ turn_id,
43
+ CASE WHEN MIN(COALESCE(cost_known, 0))=1 THEN SUM(cost_usd) ELSE NULL END AS cost_usd,
44
+ MIN(COALESCE(cost_known, 0)) AS cost_known
45
+ FROM sample_events
46
+ WHERE turn_id IS NOT NULL AND kind='model'
47
+ GROUP BY turn_id
48
+ ), attributed_units AS (
49
+ SELECT
50
+ r.agent_type AS agent_type,
51
+ t.run_id AS run_id,
52
+ t.id AS turn_id,
53
+ t.model_id AS model_id,
54
+ COALESCE(t.provider, r.provider) AS provider,
55
+ COALESCE(
56
+ t.composite_score,
57
+ e.overall
58
+ ) AS overall,
59
+ e.tests_pass AS tests_pass,
60
+ c.cost_usd AS cost_usd,
61
+ COALESCE(c.cost_known, 0) AS cost_known
62
+ FROM turns t
63
+ JOIN runs r ON r.id=t.run_id
64
+ LEFT JOIN turn_eval e ON e.turn_id=t.id
65
+ LEFT JOIN turn_cost c ON c.turn_id=t.id
66
+ WHERE t.model_id IS NOT NULL AND t.model_id != ''
67
+ ), legacy_units AS (
68
+ SELECT
69
+ r.agent_type AS agent_type,
70
+ r.id AS run_id,
71
+ NULL AS turn_id,
72
+ COALESCE(r.model_id, 'unknown') AS model_id,
73
+ r.provider AS provider,
74
+ m.overall AS overall,
75
+ m.tests_pass AS tests_pass,
76
+ CASE WHEN r.cost_known=1 THEN r.cost_usd ELSE NULL END AS cost_usd,
77
+ COALESCE(r.cost_known, 0) AS cost_known
78
+ FROM runs r
79
+ LEFT JOIN v_run_metrics m ON m.run_id=r.id
80
+ WHERE NOT EXISTS (
81
+ SELECT 1 FROM turns t
82
+ WHERE t.run_id=r.id AND t.model_id IS NOT NULL AND t.model_id != ''
83
+ )
84
+ ), units AS (
85
+ SELECT * FROM attributed_units
86
+ UNION ALL
87
+ SELECT * FROM legacy_units
88
+ )
89
+ SELECT
90
+ agent_type,
91
+ model_id,
92
+ provider,
93
+ COUNT(DISTINCT run_id) AS runs,
94
+ AVG(overall) AS avg_overall,
95
+ AVG(tests_pass) AS avg_tests_pass,
96
+ CASE WHEN MIN(cost_known)=1 THEN SUM(cost_usd) ELSE NULL END AS total_cost,
97
+ MIN(cost_known) AS cost_known,
98
+ CASE WHEN MIN(cost_known)=1 AND AVG(overall)>0 THEN SUM(cost_usd)/AVG(overall) ELSE NULL END AS cost_per_quality
99
+ FROM units
100
+ GROUP BY agent_type, model_id, provider;
101
+
102
+ CREATE VIEW IF NOT EXISTS v_category_scorecard AS
103
+ WITH task_metrics AS (
104
+ SELECT
105
+ task_id,
106
+ MAX(CASE WHEN metric='tests_pass' AND layer='deterministic' THEN score END) AS tests_pass
107
+ FROM evals
108
+ WHERE task_id IS NOT NULL
109
+ GROUP BY task_id
110
+ )
111
+ SELECT
112
+ t.task_category AS task_category,
113
+ COALESCE(t.model_id, r.model_id, 'unknown') AS model_id,
114
+ COALESCE(t.provider, r.provider) AS provider,
115
+ COUNT(DISTINCT t.run_id) AS runs,
116
+ AVG(t.composite_score) AS avg_overall,
117
+ AVG(m.tests_pass) AS avg_tests_pass,
118
+ CASE WHEN MIN(COALESCE(t.cost_known, 0))=1 THEN SUM(t.cost_usd) ELSE NULL END AS total_cost,
119
+ MIN(COALESCE(t.cost_known, 0)) AS cost_known
120
+ FROM tasks t
121
+ JOIN runs r ON r.id=t.run_id
122
+ LEFT JOIN task_metrics m ON m.task_id=t.id
123
+ WHERE t.task_category IS NOT NULL AND t.task_category != 'unknown'
124
+ GROUP BY t.task_category, COALESCE(t.model_id, r.model_id, 'unknown'), COALESCE(t.provider, r.provider);
125
+
126
+ -- Sample-granularity scorecard: quality vs cost per (agent, provider, model,
127
+ -- category). A task that used more than one model is stored as 'mixed' rather
128
+ -- than attributed to whichever assistant message happened to arrive last.
129
+ CREATE VIEW IF NOT EXISTS v_sample_scorecard AS
130
+ SELECT
131
+ r.agent_type AS agent_type,
132
+ COALESCE(t.model_id, r.model_id, 'unknown') AS model_id,
133
+ COALESCE(t.provider, r.provider) AS provider,
134
+ t.task_category AS task_category,
135
+ COUNT(*) AS samples,
136
+ AVG(t.composite_score) AS avg_quality,
137
+ CASE WHEN MIN(COALESCE(t.cost_known, 0))=1 THEN SUM(t.cost_usd) ELSE NULL END AS total_cost,
138
+ MIN(COALESCE(t.cost_known, 0)) AS cost_known,
139
+ AVG(t.latency_ms) AS avg_latency_ms,
140
+ CASE WHEN MIN(COALESCE(t.cost_known, 0))=1 AND AVG(t.composite_score)>0
141
+ THEN SUM(t.cost_usd)/AVG(t.composite_score) ELSE NULL END AS cost_per_quality
142
+ FROM tasks t
143
+ JOIN runs r ON r.id=t.run_id
144
+ WHERE t.ended_at IS NOT NULL AND t.composite_score IS NOT NULL
145
+ GROUP BY r.agent_type, COALESCE(t.model_id, r.model_id, 'unknown'), COALESCE(t.provider, r.provider), t.task_category;
146
+ `;
19
147
  const DDL = `
20
148
  CREATE TABLE IF NOT EXISTS runs (
21
149
  id TEXT PRIMARY KEY,
22
150
  session_id TEXT NOT NULL,
23
151
  agent_type TEXT NOT NULL,
24
152
  model_id TEXT,
153
+ provider TEXT,
25
154
  project_name TEXT,
26
155
  project_path TEXT,
27
156
  task_prompt TEXT,
@@ -30,6 +159,7 @@ CREATE TABLE IF NOT EXISTS runs (
30
159
  input_tokens INTEGER,
31
160
  output_tokens INTEGER,
32
161
  cost_usd REAL,
162
+ cost_known INTEGER NOT NULL DEFAULT 0,
33
163
  exit_code INTEGER,
34
164
  git_before TEXT,
35
165
  git_after TEXT,
@@ -57,6 +187,8 @@ CREATE TABLE IF NOT EXISTS turns (
57
187
  run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
58
188
  task_id TEXT,
59
189
  turn_index INTEGER NOT NULL,
190
+ model_id TEXT,
191
+ provider TEXT,
60
192
  prompt TEXT,
61
193
  response TEXT,
62
194
  started_at INTEGER NOT NULL,
@@ -99,10 +231,12 @@ CREATE TABLE IF NOT EXISTS tasks (
99
231
  task_category TEXT,
100
232
  notes_json TEXT,
101
233
  model_id TEXT,
234
+ provider TEXT,
102
235
  model_config TEXT,
103
236
  input_tokens INTEGER,
104
237
  output_tokens INTEGER,
105
238
  cost_usd REAL,
239
+ cost_known INTEGER NOT NULL DEFAULT 0,
106
240
  latency_ms INTEGER
107
241
  );
108
242
 
@@ -123,6 +257,7 @@ CREATE TABLE IF NOT EXISTS sample_events (
123
257
  input_tokens INTEGER,
124
258
  output_tokens INTEGER,
125
259
  cost_usd REAL,
260
+ cost_known INTEGER NOT NULL DEFAULT 0,
126
261
  latency_ms INTEGER,
127
262
  tool_name TEXT,
128
263
  tool_status TEXT,
@@ -181,67 +316,7 @@ CREATE INDEX IF NOT EXISTS idx_runs_started ON runs(started_at);
181
316
  CREATE INDEX IF NOT EXISTS idx_evals_run ON evals(run_id);
182
317
  CREATE INDEX IF NOT EXISTS idx_steps_run ON steps(run_id);
183
318
 
184
- -- Pre-aggregate per-run eval metrics to avoid inflating cost_usd when
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;
319
+ ${SCORECARD_DDL}
245
320
  `;
246
321
  // ─── Default rubric v1 (seeded on first boot) ──────────────────────────────────
247
322
  const DEFAULT_RUBRIC_V1 = {
@@ -517,6 +592,8 @@ export class ApmeStore {
517
592
  return;
518
593
  const cols = this.db.prepare("PRAGMA table_info(runs)").all().map(c => c.name);
519
594
  const migrations = [
595
+ ['provider', 'ALTER TABLE runs ADD COLUMN provider TEXT'],
596
+ ['cost_known', 'ALTER TABLE runs ADD COLUMN cost_known INTEGER NOT NULL DEFAULT 0'],
520
597
  ['task_signals', 'ALTER TABLE runs ADD COLUMN task_signals TEXT'],
521
598
  ['task_category', 'ALTER TABLE runs ADD COLUMN task_category TEXT'],
522
599
  ['task_category_source', "ALTER TABLE runs ADD COLUMN task_category_source TEXT DEFAULT 'auto'"],
@@ -560,6 +637,18 @@ export class ApmeStore {
560
637
  }
561
638
  catch { /* ignore */ }
562
639
  }
640
+ if (!turnCols.includes('model_id')) {
641
+ try {
642
+ this.db.exec('ALTER TABLE turns ADD COLUMN model_id TEXT');
643
+ }
644
+ catch { /* ignore */ }
645
+ }
646
+ if (!turnCols.includes('provider')) {
647
+ try {
648
+ this.db.exec('ALTER TABLE turns ADD COLUMN provider TEXT');
649
+ }
650
+ catch { /* ignore */ }
651
+ }
563
652
  const evalCols = this.db.prepare("PRAGMA table_info(evals)").all().map(c => c.name);
564
653
  if (!evalCols.includes('task_id')) {
565
654
  try {
@@ -572,10 +661,12 @@ export class ApmeStore {
572
661
  const taskCols = this.db.prepare("PRAGMA table_info(tasks)").all().map(c => c.name);
573
662
  for (const [col, sql] of [
574
663
  ['model_id', 'ALTER TABLE tasks ADD COLUMN model_id TEXT'],
664
+ ['provider', 'ALTER TABLE tasks ADD COLUMN provider TEXT'],
575
665
  ['model_config', 'ALTER TABLE tasks ADD COLUMN model_config TEXT'],
576
666
  ['input_tokens', 'ALTER TABLE tasks ADD COLUMN input_tokens INTEGER'],
577
667
  ['output_tokens', 'ALTER TABLE tasks ADD COLUMN output_tokens INTEGER'],
578
668
  ['cost_usd', 'ALTER TABLE tasks ADD COLUMN cost_usd REAL'],
669
+ ['cost_known', 'ALTER TABLE tasks ADD COLUMN cost_known INTEGER NOT NULL DEFAULT 0'],
579
670
  ['latency_ms', 'ALTER TABLE tasks ADD COLUMN latency_ms INTEGER'],
580
671
  ]) {
581
672
  if (!taskCols.includes(col)) {
@@ -597,6 +688,12 @@ export class ApmeStore {
597
688
  // pointer to the run it continues, so one conversation shows up as N
598
689
  // disconnected components. One live session here had 127 such runs.
599
690
  const sevCols = this.db.prepare("PRAGMA table_info(sample_events)").all().map(c => c.name);
691
+ if (!sevCols.includes('cost_known')) {
692
+ try {
693
+ this.db.exec('ALTER TABLE sample_events ADD COLUMN cost_known INTEGER NOT NULL DEFAULT 0');
694
+ }
695
+ catch { /* ignore */ }
696
+ }
600
697
  if (!sevCols.includes('turn_id')) {
601
698
  try {
602
699
  this.db.exec('ALTER TABLE sample_events ADD COLUMN turn_id TEXT');
@@ -626,6 +723,21 @@ export class ApmeStore {
626
723
  }
627
724
  catch { /* ignore */ }
628
725
  }
726
+ // A positive historical value proves that a price existed. Historical
727
+ // zeroes remain unknown because they could be either a free local model or
728
+ // the old UNKNOWN_PRICE fallback — the ambiguity this column removes.
729
+ try {
730
+ this.db.exec('UPDATE runs SET cost_known=1 WHERE cost_usd > 0');
731
+ }
732
+ catch { /* ignore */ }
733
+ try {
734
+ this.db.exec('UPDATE tasks SET cost_known=1 WHERE cost_usd > 0');
735
+ }
736
+ catch { /* ignore */ }
737
+ try {
738
+ this.db.exec('UPDATE sample_events SET cost_known=1 WHERE cost_usd > 0');
739
+ }
740
+ catch { /* ignore */ }
629
741
  // ── Covering indexes for the per-run/per-task rollups ──
630
742
  // better-sqlite3 is synchronous on a single connection, so ANY slow query
631
743
  // here stalls the daemon's whole HTTP path — this is a latency budget, not
@@ -647,6 +759,16 @@ export class ApmeStore {
647
759
  }
648
760
  catch { /* ignore */ }
649
761
  }
762
+ // CREATE VIEW IF NOT EXISTS preserves stale executable SQL forever. Rebuild
763
+ // after the ALTERs above so both fresh and upgraded databases group by the
764
+ // turn/provider identity this version actually writes.
765
+ for (const view of ['v_sample_scorecard', 'v_category_scorecard', 'v_model_scorecard', 'v_run_metrics']) {
766
+ try {
767
+ this.db.exec(`DROP VIEW IF EXISTS ${view}`);
768
+ }
769
+ catch { /* ignore */ }
770
+ }
771
+ this.db.exec(SCORECARD_DDL);
650
772
  }
651
773
  seedDefaultRubric() {
652
774
  if (!this.db)
@@ -688,11 +810,12 @@ export class ApmeStore {
688
810
  insertRun(row) {
689
811
  if (!this.db)
690
812
  return;
813
+ const costKnown = row.costKnown === true || (row.costUsd != null && row.costUsd > 0);
691
814
  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,
815
+ (id, session_id, agent_type, model_id, provider, project_name, project_path, task_prompt,
816
+ started_at, ended_at, input_tokens, output_tokens, cost_usd, cost_known, exit_code,
694
817
  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);
818
+ 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
819
  }
697
820
  updateRun(id, patch) {
698
821
  if (!this.db)
@@ -701,6 +824,7 @@ export class ApmeStore {
701
824
  const values = [];
702
825
  const map = {
703
826
  modelId: 'model_id',
827
+ provider: 'provider',
704
828
  projectName: 'project_name',
705
829
  projectPath: 'project_path',
706
830
  taskPrompt: 'task_prompt',
@@ -708,6 +832,7 @@ export class ApmeStore {
708
832
  inputTokens: 'input_tokens',
709
833
  outputTokens: 'output_tokens',
710
834
  costUsd: 'cost_usd',
835
+ costKnown: 'cost_known',
711
836
  exitCode: 'exit_code',
712
837
  gitBefore: 'git_before',
713
838
  gitAfter: 'git_after',
@@ -726,7 +851,11 @@ export class ApmeStore {
726
851
  if (!col || v === undefined)
727
852
  continue;
728
853
  fields.push(`${col} = ?`);
729
- values.push(v);
854
+ values.push(k === 'costKnown' ? (v ? 1 : 0) : v);
855
+ }
856
+ if (patch.costUsd != null && patch.costUsd > 0 && patch.costKnown === undefined) {
857
+ fields.push('cost_known = ?');
858
+ values.push(1);
730
859
  }
731
860
  if (fields.length === 0)
732
861
  return;
@@ -784,6 +913,7 @@ export class ApmeStore {
784
913
  outcome: 'outcome', compositeScore: 'composite_score', efficiencyJson: 'efficiency_json',
785
914
  prompt: 'prompt', response: 'response', taskId: 'task_id',
786
915
  endSource: 'end_source',
916
+ modelId: 'model_id', provider: 'provider',
787
917
  };
788
918
  const sets = [];
789
919
  const vals = [];
@@ -886,10 +1016,12 @@ export class ApmeStore {
886
1016
  notesJson: 'notes_json',
887
1017
  boundarySignal: 'boundary_signal',
888
1018
  modelId: 'model_id',
1019
+ provider: 'provider',
889
1020
  modelConfig: 'model_config',
890
1021
  inputTokens: 'input_tokens',
891
1022
  outputTokens: 'output_tokens',
892
1023
  costUsd: 'cost_usd',
1024
+ costKnown: 'cost_known',
893
1025
  latencyMs: 'latency_ms',
894
1026
  };
895
1027
  const sets = [];
@@ -899,7 +1031,11 @@ export class ApmeStore {
899
1031
  if (!col || v === undefined)
900
1032
  continue;
901
1033
  sets.push(`${col} = ?`);
902
- vals.push(v);
1034
+ vals.push(k === 'costKnown' ? (v ? 1 : 0) : v);
1035
+ }
1036
+ if (patch.costUsd != null && patch.costUsd > 0 && patch.costKnown === undefined) {
1037
+ sets.push('cost_known = ?');
1038
+ vals.push(1);
903
1039
  }
904
1040
  if (sets.length === 0)
905
1041
  return;
@@ -1280,10 +1416,14 @@ export class ApmeStore {
1280
1416
  insertSampleEvent(row) {
1281
1417
  if (!this.db)
1282
1418
  return false;
1419
+ // A positive monetary amount is self-proving provenance even for legacy
1420
+ // callers that predate costKnown. Zero remains ambiguous and must only be
1421
+ // marked known by a pricing table / known-local producer.
1422
+ const costKnown = row.costKnown === true || (row.costUsd != null && row.costUsd > 0);
1283
1423
  const res = this.db.prepare(`INSERT OR IGNORE INTO sample_events
1284
1424
  (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 (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).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, row.latencyMs ?? null, row.toolName ?? null, row.toolStatus ?? null, row.toolError ?? null, row.payload ?? null, row.dedupKey ?? null);
1425
+ cost_usd, cost_known, latency_ms, tool_name, tool_status, tool_error, payload, dedup_key)
1426
+ 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
1427
  return res.changes > 0;
1288
1428
  }
1289
1429
  /** Update a previously-inserted event (e.g. a tool pending→resolved) by id. */
@@ -1292,7 +1432,7 @@ export class ApmeStore {
1292
1432
  return;
1293
1433
  const map = {
1294
1434
  toolStatus: 'tool_status', toolError: 'tool_error', payload: 'payload',
1295
- costUsd: 'cost_usd', latencyMs: 'latency_ms', model: 'model',
1435
+ costUsd: 'cost_usd', costKnown: 'cost_known', latencyMs: 'latency_ms', model: 'model',
1296
1436
  inputTokens: 'input_tokens', outputTokens: 'output_tokens', ts: 'ts',
1297
1437
  };
1298
1438
  const sets = [];
@@ -1302,7 +1442,11 @@ export class ApmeStore {
1302
1442
  if (!col || v === undefined)
1303
1443
  continue;
1304
1444
  sets.push(`${col} = ?`);
1305
- vals.push(v);
1445
+ vals.push(k === 'costKnown' ? (v ? 1 : 0) : v);
1446
+ }
1447
+ if (fields.costUsd != null && fields.costUsd > 0 && fields.costKnown === undefined) {
1448
+ sets.push('cost_known = ?');
1449
+ vals.push(1);
1306
1450
  }
1307
1451
  if (sets.length === 0)
1308
1452
  return;
@@ -1319,6 +1463,17 @@ export class ApmeStore {
1319
1463
  ORDER BY seq DESC LIMIT 1`).get(taskId, turnIndex, toolName);
1320
1464
  return row ? rowToSampleEvent(row) : null;
1321
1465
  }
1466
+ /** One assistant-message slot per turn. Response producers overlap, so text
1467
+ * hashes cannot be the identity: a truncated projection and the full final
1468
+ * are two payload versions of the same logical event. */
1469
+ findAssistantMessageEvent(taskId, turnId) {
1470
+ if (!this.db)
1471
+ return null;
1472
+ const row = this.db.prepare(`SELECT * FROM sample_events
1473
+ WHERE task_id = ? AND turn_id = ? AND kind = 'assistant_message'
1474
+ ORDER BY seq ASC LIMIT 1`).get(taskId, turnId);
1475
+ return row ? rowToSampleEvent(row) : null;
1476
+ }
1322
1477
  listSampleEventRows(taskId) {
1323
1478
  if (!this.db)
1324
1479
  return [];
@@ -1369,6 +1524,7 @@ export class ApmeStore {
1369
1524
  inputTokens: task.inputTokens ?? 0,
1370
1525
  outputTokens: task.outputTokens ?? 0,
1371
1526
  costUsd: task.costUsd ?? 0,
1527
+ costKnown: task.costKnown ?? false,
1372
1528
  latencyMs: task.latencyMs ?? 0,
1373
1529
  },
1374
1530
  summary: task.summary ?? null,
@@ -1382,10 +1538,15 @@ export class ApmeStore {
1382
1538
  if (!this.db)
1383
1539
  return;
1384
1540
  const row = this.db.prepare(`SELECT COALESCE(SUM(input_tokens),0) AS it, COALESCE(SUM(output_tokens),0) AS ot,
1385
- COALESCE(SUM(cost_usd),0) AS cu, COALESCE(SUM(latency_ms),0) AS lm
1541
+ CASE WHEN COUNT(*) > 0 AND MIN(COALESCE(cost_known,0))=1
1542
+ THEN SUM(cost_usd) ELSE NULL END AS cu,
1543
+ CASE WHEN COUNT(*) > 0 AND MIN(COALESCE(cost_known,0))=1
1544
+ THEN 1 ELSE 0 END AS ck,
1545
+ COALESCE(SUM(latency_ms),0) AS lm
1386
1546
  FROM sample_events WHERE task_id = ? AND kind = 'model'`).get(taskId);
1387
1547
  this.updateTask(taskId, {
1388
- inputTokens: row.it, outputTokens: row.ot, costUsd: row.cu, latencyMs: row.lm,
1548
+ inputTokens: row.it, outputTokens: row.ot, costUsd: row.cu,
1549
+ costKnown: row.ck === 1, latencyMs: row.lm,
1389
1550
  });
1390
1551
  }
1391
1552
  sampleScorecard() {
@@ -1395,10 +1556,12 @@ export class ApmeStore {
1395
1556
  return rows.map((r) => ({
1396
1557
  agentType: r.agent_type,
1397
1558
  modelId: r.model_id,
1559
+ provider: r.provider ?? null,
1398
1560
  taskCategory: r.task_category ?? null,
1399
1561
  samples: r.samples,
1400
1562
  avgQuality: r.avg_quality ?? null,
1401
1563
  totalCost: r.total_cost ?? null,
1564
+ costKnown: r.cost_known === 1,
1402
1565
  avgLatencyMs: r.avg_latency_ms ?? null,
1403
1566
  costPerQuality: r.cost_per_quality ?? null,
1404
1567
  }));
@@ -1411,10 +1574,12 @@ export class ApmeStore {
1411
1574
  return rows.map((r) => ({
1412
1575
  agentType: r.agent_type,
1413
1576
  modelId: r.model_id,
1577
+ provider: r.provider ?? null,
1414
1578
  runs: r.runs,
1415
1579
  avgOverall: r.avg_overall ?? null,
1416
1580
  avgTestsPass: r.avg_tests_pass ?? null,
1417
1581
  totalCost: r.total_cost ?? null,
1582
+ costKnown: r.cost_known === 1,
1418
1583
  costPerQuality: r.cost_per_quality ?? null,
1419
1584
  }));
1420
1585
  }
@@ -1425,10 +1590,12 @@ export class ApmeStore {
1425
1590
  return rows.map((r) => ({
1426
1591
  taskCategory: r.task_category,
1427
1592
  modelId: r.model_id,
1593
+ provider: r.provider ?? null,
1428
1594
  runs: r.runs,
1429
1595
  avgOverall: r.avg_overall ?? null,
1430
1596
  avgTestsPass: r.avg_tests_pass ?? null,
1431
1597
  totalCost: r.total_cost ?? null,
1598
+ costKnown: r.cost_known === 1,
1432
1599
  }));
1433
1600
  }
1434
1601
  }
@@ -1439,6 +1606,7 @@ function rowToRun(r) {
1439
1606
  sessionId: r.session_id,
1440
1607
  agentType: r.agent_type,
1441
1608
  modelId: r.model_id ?? null,
1609
+ provider: r.provider ?? null,
1442
1610
  projectName: r.project_name ?? null,
1443
1611
  projectPath: r.project_path ?? null,
1444
1612
  taskPrompt: r.task_prompt ?? null,
@@ -1447,6 +1615,7 @@ function rowToRun(r) {
1447
1615
  inputTokens: r.input_tokens ?? null,
1448
1616
  outputTokens: r.output_tokens ?? null,
1449
1617
  costUsd: r.cost_usd ?? null,
1618
+ costKnown: r.cost_known === 1,
1450
1619
  exitCode: r.exit_code ?? null,
1451
1620
  gitBefore: r.git_before ?? null,
1452
1621
  gitAfter: r.git_after ?? null,
@@ -1477,10 +1646,12 @@ function rowToTask(r) {
1477
1646
  taskCategory: r.task_category ?? null,
1478
1647
  notesJson: r.notes_json ?? null,
1479
1648
  modelId: r.model_id ?? null,
1649
+ provider: r.provider ?? null,
1480
1650
  modelConfig: r.model_config ?? null,
1481
1651
  inputTokens: r.input_tokens ?? null,
1482
1652
  outputTokens: r.output_tokens ?? null,
1483
1653
  costUsd: r.cost_usd ?? null,
1654
+ costKnown: r.cost_known === 1,
1484
1655
  latencyMs: r.latency_ms ?? null,
1485
1656
  };
1486
1657
  }
@@ -1519,6 +1690,7 @@ function rowToSampleEvent(r) {
1519
1690
  inputTokens: r.input_tokens ?? null,
1520
1691
  outputTokens: r.output_tokens ?? null,
1521
1692
  costUsd: r.cost_usd ?? null,
1693
+ costKnown: r.cost_known === 1,
1522
1694
  latencyMs: r.latency_ms ?? null,
1523
1695
  toolName: r.tool_name ?? null,
1524
1696
  toolStatus: r.tool_status ?? null,