opencastle 0.27.1 → 0.27.2
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/dist/cli/convoy/dashboard-types.d.ts +146 -0
- package/dist/cli/convoy/dashboard-types.d.ts.map +1 -0
- package/dist/cli/convoy/dashboard-types.js +2 -0
- package/dist/cli/convoy/dashboard-types.js.map +1 -0
- package/dist/cli/convoy/engine.d.ts +0 -1
- package/dist/cli/convoy/engine.d.ts.map +1 -1
- package/dist/cli/convoy/engine.js +31 -99
- package/dist/cli/convoy/engine.js.map +1 -1
- package/dist/cli/convoy/engine.test.js +88 -1
- package/dist/cli/convoy/engine.test.js.map +1 -1
- package/dist/cli/convoy/event-schemas.d.ts +9 -0
- package/dist/cli/convoy/event-schemas.d.ts.map +1 -0
- package/dist/cli/convoy/event-schemas.js +185 -0
- package/dist/cli/convoy/event-schemas.js.map +1 -0
- package/dist/cli/convoy/events.d.ts +8 -0
- package/dist/cli/convoy/events.d.ts.map +1 -1
- package/dist/cli/convoy/events.js +117 -5
- package/dist/cli/convoy/events.js.map +1 -1
- package/dist/cli/convoy/events.test.js +173 -3
- package/dist/cli/convoy/events.test.js.map +1 -1
- package/dist/cli/convoy/log-merge.test.d.ts +2 -0
- package/dist/cli/convoy/log-merge.test.d.ts.map +1 -0
- package/dist/cli/convoy/log-merge.test.js +147 -0
- package/dist/cli/convoy/log-merge.test.js.map +1 -0
- package/dist/cli/convoy/store.d.ts +52 -2
- package/dist/cli/convoy/store.d.ts.map +1 -1
- package/dist/cli/convoy/store.js +244 -17
- package/dist/cli/convoy/store.js.map +1 -1
- package/dist/cli/convoy/store.test.js +481 -22
- package/dist/cli/convoy/store.test.js.map +1 -1
- package/dist/cli/convoy/types.d.ts +271 -3
- package/dist/cli/convoy/types.d.ts.map +1 -1
- package/dist/cli/convoy/types.js +42 -1
- package/dist/cli/convoy/types.js.map +1 -1
- package/dist/cli/log.d.ts +11 -0
- package/dist/cli/log.d.ts.map +1 -1
- package/dist/cli/log.js +114 -2
- package/dist/cli/log.js.map +1 -1
- package/package.json +5 -1
- package/src/cli/convoy/TELEMETRY.md +203 -0
- package/src/cli/convoy/dashboard-types.ts +141 -0
- package/src/cli/convoy/engine.test.ts +99 -1
- package/src/cli/convoy/engine.ts +27 -96
- package/src/cli/convoy/event-schemas.ts +195 -0
- package/src/cli/convoy/events.test.ts +207 -3
- package/src/cli/convoy/events.ts +119 -5
- package/src/cli/convoy/log-merge.test.ts +179 -0
- package/src/cli/convoy/store.test.ts +545 -22
- package/src/cli/convoy/store.ts +274 -21
- package/src/cli/convoy/types.ts +108 -3
- package/src/cli/log.ts +120 -2
- package/src/dashboard/dist/_astro/{index.DtnyD8a5.css → index.6L3_HsPT.css} +1 -1
- package/src/dashboard/dist/data/.gitkeep +0 -0
- package/src/dashboard/dist/data/convoy-list.json +1 -0
- package/src/dashboard/dist/data/overall-stats.json +24 -0
- package/src/dashboard/dist/index.html +701 -3
- package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
- package/src/dashboard/public/data/.gitkeep +0 -0
- package/src/dashboard/public/data/convoy-list.json +1 -0
- package/src/dashboard/public/data/overall-stats.json +24 -0
- package/src/dashboard/scripts/etl.test.ts +210 -0
- package/src/dashboard/scripts/etl.ts +108 -0
- package/src/dashboard/scripts/integration-test.ts +504 -0
- package/src/dashboard/src/pages/index.astro +854 -15
- package/src/dashboard/src/styles/dashboard.css +557 -1
- package/src/orchestrator/prompts/generate-convoy.prompt.md +212 -13
package/src/cli/convoy/store.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { copyFileSync } from 'node:fs'
|
|
2
2
|
import { DatabaseSync } from 'node:sqlite'
|
|
3
|
+
import type { DashboardConvoyDetail } from './dashboard-types.js'
|
|
3
4
|
import type {
|
|
4
5
|
ConvoyRecord,
|
|
5
6
|
ConvoyStatus,
|
|
@@ -16,7 +17,7 @@ import type {
|
|
|
16
17
|
TaskStepRecord,
|
|
17
18
|
} from './types.js'
|
|
18
19
|
|
|
19
|
-
const SCHEMA_VERSION =
|
|
20
|
+
const SCHEMA_VERSION = 10
|
|
20
21
|
|
|
21
22
|
// ── Size limits (bytes) ────────────────────────────────────────────────────────
|
|
22
23
|
const LIMIT_SPEC_YAML = 256 * 1024 // 256 KB
|
|
@@ -70,7 +71,7 @@ export interface ConvoyStore {
|
|
|
70
71
|
updateConvoyStatus(
|
|
71
72
|
id: string,
|
|
72
73
|
status: ConvoyStatus,
|
|
73
|
-
extra?: { started_at?: string; finished_at?: string; total_tokens?: number | null; total_cost_usd?:
|
|
74
|
+
extra?: { started_at?: string; finished_at?: string; total_tokens?: number | null; total_cost_usd?: number | null },
|
|
74
75
|
): void
|
|
75
76
|
updateConvoyReviewTokens(convoyId: string, tokens: number): void
|
|
76
77
|
updateConvoyCircuitState(convoyId: string, state: string | null): void
|
|
@@ -135,6 +136,7 @@ export interface ConvoyStore {
|
|
|
135
136
|
insertArtifact(record: ArtifactRecord): void
|
|
136
137
|
getArtifact(convoyId: string, name: string): ArtifactRecord | undefined
|
|
137
138
|
getArtifactsByTask(taskId: string): ArtifactRecord[]
|
|
139
|
+
getArtifactsByConvoy(convoyId: string): ArtifactRecord[]
|
|
138
140
|
deleteArtifactsOlderThan(days: number): number
|
|
139
141
|
insertAgentIdentity(record: AgentIdentityRecord): void
|
|
140
142
|
getAgentIdentities(agent: string, limit: number): AgentIdentityRecord[]
|
|
@@ -151,9 +153,18 @@ export interface ConvoyStore {
|
|
|
151
153
|
updatePipelineStatus(
|
|
152
154
|
id: string,
|
|
153
155
|
status: PipelineStatus,
|
|
154
|
-
extra?: { started_at?: string; finished_at?: string; total_tokens?: number | null; total_cost_usd?:
|
|
156
|
+
extra?: { started_at?: string; finished_at?: string; total_tokens?: number | null; total_cost_usd?: number | null },
|
|
155
157
|
): void
|
|
156
158
|
getConvoysByPipeline(pipelineId: string): ConvoyRecord[]
|
|
159
|
+
getConvoyCounts(): { total: number; running: number; done: number; failed: number; gate_failed: number }
|
|
160
|
+
getConvoyDurationStats(): { avg_sec: number | null; p95_sec: number | null; max_sec: number | null }
|
|
161
|
+
getTokenAndCostTotals(): { total_tokens: number; total_cost_usd: number }
|
|
162
|
+
getTopAgents(limit: number): Array<{ agent: string; task_count: number; total_tokens: number }>
|
|
163
|
+
getTopModels(limit: number): Array<{ model: string; task_count: number; total_tokens: number }>
|
|
164
|
+
getDlqSummary(): { count: number; top_failure_types: Array<{ type: string; count: number }> }
|
|
165
|
+
getConvoyTaskSummary(convoyId: string): { total: number; done: number; running: number; failed: number; review_blocked: number; disputed: number; reviewed: number; panel_reviewed: number; tasks_with_drift: number; max_drift_score: number | null; drift_retried: number }
|
|
166
|
+
getConvoyList(limit: number, offset: number): ConvoyRecord[]
|
|
167
|
+
getConvoyDetails(convoyId: string): DashboardConvoyDetail | null
|
|
157
168
|
withTransaction<T>(fn: () => T): T
|
|
158
169
|
close(): void
|
|
159
170
|
}
|
|
@@ -186,6 +197,7 @@ class ConvoyStoreImpl implements ConvoyStore {
|
|
|
186
197
|
spec_yaml TEXT NOT NULL,
|
|
187
198
|
total_tokens INTEGER,
|
|
188
199
|
total_cost_usd TEXT,
|
|
200
|
+
total_cost_usd_num REAL,
|
|
189
201
|
pipeline_id TEXT,
|
|
190
202
|
circuit_state TEXT,
|
|
191
203
|
review_tokens_total INTEGER,
|
|
@@ -203,7 +215,8 @@ class ConvoyStoreImpl implements ConvoyStore {
|
|
|
203
215
|
started_at TEXT,
|
|
204
216
|
finished_at TEXT,
|
|
205
217
|
total_tokens INTEGER,
|
|
206
|
-
total_cost_usd TEXT
|
|
218
|
+
total_cost_usd TEXT,
|
|
219
|
+
total_cost_usd_num REAL
|
|
207
220
|
);
|
|
208
221
|
|
|
209
222
|
CREATE TABLE IF NOT EXISTS task (
|
|
@@ -230,6 +243,7 @@ class ConvoyStoreImpl implements ConvoyStore {
|
|
|
230
243
|
completion_tokens INTEGER,
|
|
231
244
|
total_tokens INTEGER,
|
|
232
245
|
cost_usd TEXT,
|
|
246
|
+
cost_usd_num REAL,
|
|
233
247
|
gates TEXT,
|
|
234
248
|
on_exhausted TEXT NOT NULL DEFAULT 'dlq',
|
|
235
249
|
injected INTEGER NOT NULL DEFAULT 0,
|
|
@@ -398,6 +412,10 @@ class ConvoyStoreImpl implements ConvoyStore {
|
|
|
398
412
|
migrateSchema(this.db, this.dbPath, 8, 9)
|
|
399
413
|
version = 9
|
|
400
414
|
}
|
|
415
|
+
if (version === 9) {
|
|
416
|
+
migrateSchema(this.db, this.dbPath, 9, 10)
|
|
417
|
+
version = 10
|
|
418
|
+
}
|
|
401
419
|
}
|
|
402
420
|
|
|
403
421
|
insertConvoy(
|
|
@@ -422,20 +440,20 @@ class ConvoyStoreImpl implements ConvoyStore {
|
|
|
422
440
|
|
|
423
441
|
getConvoy(id: string): ConvoyRecord | undefined {
|
|
424
442
|
return this.db
|
|
425
|
-
.prepare('SELECT
|
|
443
|
+
.prepare('SELECT *, total_cost_usd_num AS total_cost_usd FROM convoy WHERE id = :id')
|
|
426
444
|
.get({ id }) as ConvoyRecord | undefined
|
|
427
445
|
}
|
|
428
446
|
|
|
429
447
|
getLatestConvoy(): ConvoyRecord | undefined {
|
|
430
448
|
return this.db
|
|
431
|
-
.prepare('SELECT
|
|
449
|
+
.prepare('SELECT *, total_cost_usd_num AS total_cost_usd FROM convoy ORDER BY created_at DESC LIMIT 1')
|
|
432
450
|
.get() as ConvoyRecord | undefined
|
|
433
451
|
}
|
|
434
452
|
|
|
435
453
|
updateConvoyStatus(
|
|
436
454
|
id: string,
|
|
437
455
|
status: ConvoyStatus,
|
|
438
|
-
extra?: { started_at?: string; finished_at?: string; total_tokens?: number | null; total_cost_usd?:
|
|
456
|
+
extra?: { started_at?: string; finished_at?: string; total_tokens?: number | null; total_cost_usd?: number | null },
|
|
439
457
|
): void {
|
|
440
458
|
const sets = ['status = :status']
|
|
441
459
|
const params: Record<string, string | number | null> = { id, status }
|
|
@@ -453,8 +471,10 @@ class ConvoyStoreImpl implements ConvoyStore {
|
|
|
453
471
|
params.total_tokens = extra.total_tokens
|
|
454
472
|
}
|
|
455
473
|
if (extra?.total_cost_usd !== undefined) {
|
|
456
|
-
sets.push('total_cost_usd = :
|
|
457
|
-
|
|
474
|
+
sets.push('total_cost_usd = :total_cost_usd_text')
|
|
475
|
+
sets.push('total_cost_usd_num = :total_cost_usd_num')
|
|
476
|
+
params.total_cost_usd_text = extra.total_cost_usd !== null ? String(extra.total_cost_usd) : null
|
|
477
|
+
params.total_cost_usd_num = extra.total_cost_usd
|
|
458
478
|
}
|
|
459
479
|
|
|
460
480
|
this.db.prepare(`UPDATE convoy SET ${sets.join(', ')} WHERE id = :id`).run(params)
|
|
@@ -530,36 +550,36 @@ class ConvoyStoreImpl implements ConvoyStore {
|
|
|
530
550
|
|
|
531
551
|
getTask(id: string, convoyId: string): TaskRecord | undefined {
|
|
532
552
|
return this.db
|
|
533
|
-
.prepare('SELECT
|
|
553
|
+
.prepare('SELECT *, cost_usd_num AS cost_usd FROM task WHERE id = :id AND convoy_id = :convoy_id')
|
|
534
554
|
.get({ id, convoy_id: convoyId }) as TaskRecord | undefined
|
|
535
555
|
}
|
|
536
556
|
|
|
537
557
|
getTasksByConvoy(convoyId: string): TaskRecord[] {
|
|
538
558
|
return this.db
|
|
539
|
-
.prepare('SELECT
|
|
559
|
+
.prepare('SELECT *, cost_usd_num AS cost_usd FROM task WHERE convoy_id = :convoy_id ORDER BY phase, id')
|
|
540
560
|
.all({ convoy_id: convoyId }) as unknown as TaskRecord[]
|
|
541
561
|
}
|
|
542
562
|
|
|
543
563
|
getTaskByIdempotencyKey(convoyId: string, key: string): TaskRecord | undefined {
|
|
544
564
|
return this.db
|
|
545
|
-
.prepare('SELECT
|
|
565
|
+
.prepare('SELECT *, cost_usd_num AS cost_usd FROM task WHERE convoy_id = :convoy_id AND idempotency_key = :key')
|
|
546
566
|
.get({ convoy_id: convoyId, key }) as TaskRecord | undefined
|
|
547
567
|
}
|
|
548
568
|
|
|
549
569
|
getTaskByDisputeId(disputeId: string): TaskRecord | undefined {
|
|
550
570
|
return this.db
|
|
551
|
-
.prepare('SELECT
|
|
571
|
+
.prepare('SELECT *, cost_usd_num AS cost_usd FROM task WHERE dispute_id = :dispute_id LIMIT 1')
|
|
552
572
|
.get({ dispute_id: disputeId }) as TaskRecord | undefined
|
|
553
573
|
}
|
|
554
574
|
|
|
555
575
|
getDisputedTasks(convoyId?: string): TaskRecord[] {
|
|
556
576
|
if (convoyId) {
|
|
557
577
|
return this.db
|
|
558
|
-
.prepare("SELECT
|
|
578
|
+
.prepare("SELECT *, cost_usd_num AS cost_usd FROM task WHERE status = 'disputed' AND convoy_id = :convoy_id ORDER BY phase, id")
|
|
559
579
|
.all({ convoy_id: convoyId }) as unknown as TaskRecord[]
|
|
560
580
|
}
|
|
561
581
|
return this.db
|
|
562
|
-
.prepare("SELECT
|
|
582
|
+
.prepare("SELECT *, cost_usd_num AS cost_usd FROM task WHERE status = 'disputed' ORDER BY convoy_id, phase, id")
|
|
563
583
|
.all({}) as unknown as TaskRecord[]
|
|
564
584
|
}
|
|
565
585
|
|
|
@@ -592,6 +612,10 @@ class ConvoyStoreImpl implements ConvoyStore {
|
|
|
592
612
|
params[field] = extra[field] as string | number | null
|
|
593
613
|
}
|
|
594
614
|
}
|
|
615
|
+
if ('cost_usd' in extra && extra.cost_usd !== undefined) {
|
|
616
|
+
sets.push('cost_usd_num = :cost_usd_num')
|
|
617
|
+
params.cost_usd_num = extra.cost_usd
|
|
618
|
+
}
|
|
595
619
|
}
|
|
596
620
|
|
|
597
621
|
this.db
|
|
@@ -816,6 +840,12 @@ class ConvoyStoreImpl implements ConvoyStore {
|
|
|
816
840
|
.all({ task_id: taskId }) as unknown as ArtifactRecord[]
|
|
817
841
|
}
|
|
818
842
|
|
|
843
|
+
getArtifactsByConvoy(convoyId: string): ArtifactRecord[] {
|
|
844
|
+
return this.db
|
|
845
|
+
.prepare('SELECT * FROM artifact WHERE convoy_id = :convoy_id ORDER BY created_at')
|
|
846
|
+
.all({ convoy_id: convoyId }) as unknown as ArtifactRecord[]
|
|
847
|
+
}
|
|
848
|
+
|
|
819
849
|
deleteArtifactsOlderThan(days: number): number {
|
|
820
850
|
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString()
|
|
821
851
|
const result = this.db
|
|
@@ -920,20 +950,20 @@ class ConvoyStoreImpl implements ConvoyStore {
|
|
|
920
950
|
|
|
921
951
|
getPipeline(id: string): PipelineRecord | undefined {
|
|
922
952
|
return this.db
|
|
923
|
-
.prepare('SELECT
|
|
953
|
+
.prepare('SELECT *, total_cost_usd_num AS total_cost_usd FROM pipeline WHERE id = :id')
|
|
924
954
|
.get({ id }) as PipelineRecord | undefined
|
|
925
955
|
}
|
|
926
956
|
|
|
927
957
|
getLatestPipeline(): PipelineRecord | undefined {
|
|
928
958
|
return this.db
|
|
929
|
-
.prepare('SELECT
|
|
959
|
+
.prepare('SELECT *, total_cost_usd_num AS total_cost_usd FROM pipeline ORDER BY created_at DESC LIMIT 1')
|
|
930
960
|
.get() as PipelineRecord | undefined
|
|
931
961
|
}
|
|
932
962
|
|
|
933
963
|
updatePipelineStatus(
|
|
934
964
|
id: string,
|
|
935
965
|
status: PipelineStatus,
|
|
936
|
-
extra?: { started_at?: string; finished_at?: string; total_tokens?: number | null; total_cost_usd?:
|
|
966
|
+
extra?: { started_at?: string; finished_at?: string; total_tokens?: number | null; total_cost_usd?: number | null },
|
|
937
967
|
): void {
|
|
938
968
|
const sets = ['status = :status']
|
|
939
969
|
const params: Record<string, string | number | null> = { id, status }
|
|
@@ -951,8 +981,10 @@ class ConvoyStoreImpl implements ConvoyStore {
|
|
|
951
981
|
params.total_tokens = extra.total_tokens
|
|
952
982
|
}
|
|
953
983
|
if (extra?.total_cost_usd !== undefined) {
|
|
954
|
-
sets.push('total_cost_usd = :
|
|
955
|
-
|
|
984
|
+
sets.push('total_cost_usd = :total_cost_usd_text')
|
|
985
|
+
sets.push('total_cost_usd_num = :total_cost_usd_num')
|
|
986
|
+
params.total_cost_usd_text = extra.total_cost_usd !== null ? String(extra.total_cost_usd) : null
|
|
987
|
+
params.total_cost_usd_num = extra.total_cost_usd
|
|
956
988
|
}
|
|
957
989
|
|
|
958
990
|
this.db.prepare(`UPDATE pipeline SET ${sets.join(', ')} WHERE id = :id`).run(params)
|
|
@@ -960,10 +992,221 @@ class ConvoyStoreImpl implements ConvoyStore {
|
|
|
960
992
|
|
|
961
993
|
getConvoysByPipeline(pipelineId: string): ConvoyRecord[] {
|
|
962
994
|
return this.db
|
|
963
|
-
.prepare('SELECT
|
|
995
|
+
.prepare('SELECT *, total_cost_usd_num AS total_cost_usd FROM convoy WHERE pipeline_id = :pipeline_id ORDER BY created_at')
|
|
964
996
|
.all({ pipeline_id: pipelineId }) as unknown as ConvoyRecord[]
|
|
965
997
|
}
|
|
966
998
|
|
|
999
|
+
getConvoyCounts(): { total: number; running: number; done: number; failed: number; gate_failed: number } {
|
|
1000
|
+
const rows = this.db
|
|
1001
|
+
.prepare('SELECT status, COUNT(*) AS cnt FROM convoy GROUP BY status')
|
|
1002
|
+
.all() as Array<{ status: string; cnt: number }>
|
|
1003
|
+
const map: Record<string, number> = {}
|
|
1004
|
+
for (const row of rows) map[row.status] = row.cnt
|
|
1005
|
+
return {
|
|
1006
|
+
total: rows.reduce((s, r) => s + r.cnt, 0),
|
|
1007
|
+
running: map['running'] ?? 0,
|
|
1008
|
+
done: map['done'] ?? 0,
|
|
1009
|
+
failed: (map['failed'] ?? 0),
|
|
1010
|
+
gate_failed: (map['gate-failed'] ?? 0) + (map['hook-failed'] ?? 0),
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
getConvoyDurationStats(): { avg_sec: number | null; p95_sec: number | null; max_sec: number | null } {
|
|
1015
|
+
const statsRow = this.db.prepare(
|
|
1016
|
+
`SELECT
|
|
1017
|
+
AVG((julianday(finished_at) - julianday(started_at)) * 86400) AS avg_sec,
|
|
1018
|
+
MAX((julianday(finished_at) - julianday(started_at)) * 86400) AS max_sec,
|
|
1019
|
+
COUNT(*) AS cnt
|
|
1020
|
+
FROM convoy
|
|
1021
|
+
WHERE finished_at IS NOT NULL AND started_at IS NOT NULL`,
|
|
1022
|
+
).get() as { avg_sec: number | null; max_sec: number | null; cnt: number } | undefined
|
|
1023
|
+
if (!statsRow || statsRow.cnt === 0) return { avg_sec: null, p95_sec: null, max_sec: null }
|
|
1024
|
+
const offset = Math.max(0, Math.floor(statsRow.cnt * 0.95) - 1)
|
|
1025
|
+
const p95Row = this.db.prepare(
|
|
1026
|
+
`SELECT (julianday(finished_at) - julianday(started_at)) * 86400 AS duration
|
|
1027
|
+
FROM convoy
|
|
1028
|
+
WHERE finished_at IS NOT NULL AND started_at IS NOT NULL
|
|
1029
|
+
ORDER BY duration
|
|
1030
|
+
LIMIT 1 OFFSET :offset`,
|
|
1031
|
+
).get({ offset }) as { duration: number } | undefined
|
|
1032
|
+
return {
|
|
1033
|
+
avg_sec: statsRow.avg_sec,
|
|
1034
|
+
p95_sec: p95Row?.duration ?? null,
|
|
1035
|
+
max_sec: statsRow.max_sec,
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
getTokenAndCostTotals(): { total_tokens: number; total_cost_usd: number } {
|
|
1040
|
+
const row = this.db.prepare(
|
|
1041
|
+
`SELECT COALESCE(SUM(total_tokens), 0) AS total_tokens,
|
|
1042
|
+
COALESCE(SUM(total_cost_usd_num), 0) AS total_cost_usd
|
|
1043
|
+
FROM convoy`,
|
|
1044
|
+
).get() as { total_tokens: number; total_cost_usd: number }
|
|
1045
|
+
return { total_tokens: row.total_tokens, total_cost_usd: row.total_cost_usd }
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
getTopAgents(limit: number): Array<{ agent: string; task_count: number; total_tokens: number }> {
|
|
1049
|
+
return this.db.prepare(
|
|
1050
|
+
`SELECT agent,
|
|
1051
|
+
COUNT(*) AS task_count,
|
|
1052
|
+
COALESCE(SUM(total_tokens), 0) AS total_tokens
|
|
1053
|
+
FROM task
|
|
1054
|
+
GROUP BY agent
|
|
1055
|
+
ORDER BY task_count DESC
|
|
1056
|
+
LIMIT :limit`,
|
|
1057
|
+
).all({ limit }) as Array<{ agent: string; task_count: number; total_tokens: number }>
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
getTopModels(limit: number): Array<{ model: string; task_count: number; total_tokens: number }> {
|
|
1061
|
+
return this.db.prepare(
|
|
1062
|
+
`SELECT model,
|
|
1063
|
+
COUNT(*) AS task_count,
|
|
1064
|
+
COALESCE(SUM(total_tokens), 0) AS total_tokens
|
|
1065
|
+
FROM task
|
|
1066
|
+
WHERE model IS NOT NULL
|
|
1067
|
+
GROUP BY model
|
|
1068
|
+
ORDER BY task_count DESC
|
|
1069
|
+
LIMIT :limit`,
|
|
1070
|
+
).all({ limit }) as Array<{ model: string; task_count: number; total_tokens: number }>
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
getDlqSummary(): { count: number; top_failure_types: Array<{ type: string; count: number }> } {
|
|
1074
|
+
const countRow = this.db.prepare('SELECT COUNT(*) AS cnt FROM dlq').get() as { cnt: number }
|
|
1075
|
+
const typeRows = this.db.prepare(
|
|
1076
|
+
`SELECT failure_type AS type, COUNT(*) AS count
|
|
1077
|
+
FROM dlq
|
|
1078
|
+
GROUP BY failure_type
|
|
1079
|
+
ORDER BY count DESC`,
|
|
1080
|
+
).all() as Array<{ type: string; count: number }>
|
|
1081
|
+
return { count: countRow.cnt, top_failure_types: typeRows }
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
getConvoyTaskSummary(convoyId: string): { total: number; done: number; running: number; failed: number; review_blocked: number; disputed: number; reviewed: number; panel_reviewed: number; tasks_with_drift: number; max_drift_score: number | null; drift_retried: number } {
|
|
1085
|
+
const row = this.db.prepare(
|
|
1086
|
+
`SELECT
|
|
1087
|
+
COUNT(*) AS total,
|
|
1088
|
+
SUM(CASE WHEN status = 'done' THEN 1 ELSE 0 END) AS done,
|
|
1089
|
+
SUM(CASE WHEN status IN ('running', 'assigned') THEN 1 ELSE 0 END) AS running,
|
|
1090
|
+
SUM(CASE WHEN status IN ('failed', 'gate-failed', 'timed-out', 'hook-failed') THEN 1 ELSE 0 END) AS failed,
|
|
1091
|
+
SUM(CASE WHEN status = 'review-blocked' THEN 1 ELSE 0 END) AS review_blocked,
|
|
1092
|
+
SUM(CASE WHEN status = 'disputed' THEN 1 ELSE 0 END) AS disputed,
|
|
1093
|
+
SUM(CASE WHEN review_verdict IS NOT NULL THEN 1 ELSE 0 END) AS reviewed,
|
|
1094
|
+
SUM(CASE WHEN panel_attempts > 0 THEN 1 ELSE 0 END) AS panel_reviewed,
|
|
1095
|
+
SUM(CASE WHEN drift_score IS NOT NULL THEN 1 ELSE 0 END) AS tasks_with_drift,
|
|
1096
|
+
MAX(drift_score) AS max_drift_score,
|
|
1097
|
+
SUM(CASE WHEN drift_retried = 1 THEN 1 ELSE 0 END) AS drift_retried
|
|
1098
|
+
FROM task
|
|
1099
|
+
WHERE convoy_id = :convoy_id`,
|
|
1100
|
+
).get({ convoy_id: convoyId }) as { total: number; done: number; running: number; failed: number; review_blocked: number; disputed: number; reviewed: number; panel_reviewed: number; tasks_with_drift: number; max_drift_score: number | null; drift_retried: number } | undefined
|
|
1101
|
+
if (!row || row.total === 0) {
|
|
1102
|
+
return { total: 0, done: 0, running: 0, failed: 0, review_blocked: 0, disputed: 0, reviewed: 0, panel_reviewed: 0, tasks_with_drift: 0, max_drift_score: null, drift_retried: 0 }
|
|
1103
|
+
}
|
|
1104
|
+
return {
|
|
1105
|
+
total: row.total ?? 0,
|
|
1106
|
+
done: row.done ?? 0,
|
|
1107
|
+
running: row.running ?? 0,
|
|
1108
|
+
failed: row.failed ?? 0,
|
|
1109
|
+
review_blocked: row.review_blocked ?? 0,
|
|
1110
|
+
disputed: row.disputed ?? 0,
|
|
1111
|
+
reviewed: row.reviewed ?? 0,
|
|
1112
|
+
panel_reviewed: row.panel_reviewed ?? 0,
|
|
1113
|
+
tasks_with_drift: row.tasks_with_drift ?? 0,
|
|
1114
|
+
max_drift_score: row.max_drift_score ?? null,
|
|
1115
|
+
drift_retried: row.drift_retried ?? 0,
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
getConvoyList(limit: number, offset: number): ConvoyRecord[] {
|
|
1120
|
+
return this.db.prepare(
|
|
1121
|
+
`SELECT *, total_cost_usd_num AS total_cost_usd
|
|
1122
|
+
FROM convoy
|
|
1123
|
+
ORDER BY created_at DESC
|
|
1124
|
+
LIMIT :limit OFFSET :offset`,
|
|
1125
|
+
).all({ limit, offset }) as unknown as ConvoyRecord[]
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
getConvoyDetails(convoyId: string): DashboardConvoyDetail | null {
|
|
1129
|
+
const convoy = this.getConvoy(convoyId)
|
|
1130
|
+
if (!convoy) return null
|
|
1131
|
+
|
|
1132
|
+
const tasks = this.getTasksByConvoy(convoyId)
|
|
1133
|
+
const taskSummary = this.getConvoyTaskSummary(convoyId)
|
|
1134
|
+
const dlqEntries = this.listDlqEntries(convoyId)
|
|
1135
|
+
const rawEvents = this.getEvents(convoyId)
|
|
1136
|
+
const artifacts = this.getArtifactsByConvoy(convoyId)
|
|
1137
|
+
const limitedEvents = rawEvents.slice().reverse().slice(0, 500)
|
|
1138
|
+
|
|
1139
|
+
return {
|
|
1140
|
+
convoy: {
|
|
1141
|
+
id: convoy.id,
|
|
1142
|
+
name: convoy.name,
|
|
1143
|
+
status: convoy.status,
|
|
1144
|
+
created_at: convoy.created_at,
|
|
1145
|
+
finished_at: convoy.finished_at,
|
|
1146
|
+
branch: convoy.branch,
|
|
1147
|
+
total_tokens: convoy.total_tokens,
|
|
1148
|
+
total_cost_usd: convoy.total_cost_usd,
|
|
1149
|
+
},
|
|
1150
|
+
taskSummary,
|
|
1151
|
+
quality: {
|
|
1152
|
+
reviewed_tasks: taskSummary.reviewed,
|
|
1153
|
+
review_blocked_tasks: taskSummary.review_blocked,
|
|
1154
|
+
disputed_tasks: taskSummary.disputed,
|
|
1155
|
+
panel_reviews: taskSummary.panel_reviewed,
|
|
1156
|
+
},
|
|
1157
|
+
drift: {
|
|
1158
|
+
tasks_with_drift: taskSummary.tasks_with_drift,
|
|
1159
|
+
max_drift_score: taskSummary.max_drift_score,
|
|
1160
|
+
drift_retried_tasks: taskSummary.drift_retried,
|
|
1161
|
+
},
|
|
1162
|
+
dlq_count: dlqEntries.length,
|
|
1163
|
+
dlq_entries: dlqEntries.map(d => ({
|
|
1164
|
+
id: d.id,
|
|
1165
|
+
task_id: d.task_id,
|
|
1166
|
+
agent: d.agent,
|
|
1167
|
+
failure_type: d.failure_type,
|
|
1168
|
+
attempts: d.attempts,
|
|
1169
|
+
resolved: d.resolved,
|
|
1170
|
+
})),
|
|
1171
|
+
artifact_count: artifacts.length,
|
|
1172
|
+
artifacts: artifacts.map(a => ({
|
|
1173
|
+
id: a.id,
|
|
1174
|
+
name: a.name,
|
|
1175
|
+
type: a.type,
|
|
1176
|
+
task_id: a.task_id,
|
|
1177
|
+
created_at: a.created_at,
|
|
1178
|
+
})),
|
|
1179
|
+
has_more_events: rawEvents.length > 500,
|
|
1180
|
+
events: limitedEvents.map(e => ({
|
|
1181
|
+
type: e.type,
|
|
1182
|
+
task_id: e.task_id,
|
|
1183
|
+
data: e.data ? (() => { try { return JSON.parse(e.data as string) } catch { return e.data } })() : null,
|
|
1184
|
+
created_at: e.created_at,
|
|
1185
|
+
})),
|
|
1186
|
+
tasks: tasks.map(t => ({
|
|
1187
|
+
id: t.id,
|
|
1188
|
+
phase: t.phase,
|
|
1189
|
+
agent: t.agent,
|
|
1190
|
+
model: t.model,
|
|
1191
|
+
status: t.status,
|
|
1192
|
+
retries: t.retries,
|
|
1193
|
+
started_at: t.started_at,
|
|
1194
|
+
finished_at: t.finished_at,
|
|
1195
|
+
total_tokens: t.total_tokens,
|
|
1196
|
+
cost_usd: t.cost_usd,
|
|
1197
|
+
review_level: t.review_level,
|
|
1198
|
+
review_verdict: t.review_verdict,
|
|
1199
|
+
review_tokens: t.review_tokens,
|
|
1200
|
+
review_model: t.review_model,
|
|
1201
|
+
panel_attempts: t.panel_attempts,
|
|
1202
|
+
dispute_id: t.dispute_id,
|
|
1203
|
+
drift_score: t.drift_score,
|
|
1204
|
+
drift_retried: t.drift_retried,
|
|
1205
|
+
files: t.files ? (() => { try { return JSON.parse(t.files as string) } catch { return null } })() : null,
|
|
1206
|
+
})),
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
|
|
967
1210
|
withTransaction<T>(fn: () => T): T {
|
|
968
1211
|
this.db.exec('BEGIN')
|
|
969
1212
|
try {
|
|
@@ -1081,6 +1324,16 @@ export function migrateSchema(db: DatabaseSync, dbPath: string, fromVersion: num
|
|
|
1081
1324
|
);
|
|
1082
1325
|
`)
|
|
1083
1326
|
}
|
|
1327
|
+
if (v === 9) {
|
|
1328
|
+
db.exec(`
|
|
1329
|
+
ALTER TABLE convoy ADD COLUMN total_cost_usd_num REAL;
|
|
1330
|
+
ALTER TABLE task ADD COLUMN cost_usd_num REAL;
|
|
1331
|
+
ALTER TABLE pipeline ADD COLUMN total_cost_usd_num REAL;
|
|
1332
|
+
UPDATE convoy SET total_cost_usd_num = CAST(total_cost_usd AS REAL) WHERE total_cost_usd IS NOT NULL;
|
|
1333
|
+
UPDATE task SET cost_usd_num = CAST(cost_usd AS REAL) WHERE cost_usd IS NOT NULL;
|
|
1334
|
+
UPDATE pipeline SET total_cost_usd_num = CAST(total_cost_usd AS REAL) WHERE total_cost_usd IS NOT NULL;
|
|
1335
|
+
`)
|
|
1336
|
+
}
|
|
1084
1337
|
db.exec('COMMIT')
|
|
1085
1338
|
} catch (err) {
|
|
1086
1339
|
try { db.exec('ROLLBACK') } catch { /* ignore */ }
|
package/src/cli/convoy/types.ts
CHANGED
|
@@ -29,7 +29,7 @@ export interface ConvoyRecord {
|
|
|
29
29
|
finished_at: string | null
|
|
30
30
|
spec_yaml: string
|
|
31
31
|
total_tokens: number | null
|
|
32
|
-
total_cost_usd:
|
|
32
|
+
total_cost_usd: number | null
|
|
33
33
|
pipeline_id: string | null
|
|
34
34
|
circuit_state: string | null
|
|
35
35
|
review_tokens_total: number | null
|
|
@@ -59,7 +59,7 @@ export interface TaskRecord {
|
|
|
59
59
|
prompt_tokens: number | null
|
|
60
60
|
completion_tokens: number | null
|
|
61
61
|
total_tokens: number | null
|
|
62
|
-
cost_usd:
|
|
62
|
+
cost_usd: number | null
|
|
63
63
|
gates: string | null
|
|
64
64
|
on_exhausted: 'dlq' | 'skip' | 'stop'
|
|
65
65
|
injected: number
|
|
@@ -114,7 +114,7 @@ export interface PipelineRecord {
|
|
|
114
114
|
started_at: string | null
|
|
115
115
|
finished_at: string | null
|
|
116
116
|
total_tokens: number | null
|
|
117
|
-
total_cost_usd:
|
|
117
|
+
total_cost_usd: number | null
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
export interface BuiltInGatesConfig {
|
|
@@ -259,3 +259,108 @@ export interface MCPServerConfig {
|
|
|
259
259
|
url?: string
|
|
260
260
|
config?: Record<string, unknown>
|
|
261
261
|
}
|
|
262
|
+
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
// Discriminated union covering every canonical convoy event type.
|
|
265
|
+
// Each variant constrains the `data` shape that callers may pass to emit().
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
export type ConvoyEventType =
|
|
268
|
+
| { type: 'convoy_started'; data?: { name?: string } }
|
|
269
|
+
| { type: 'convoy_finished'; data?: { status: string } }
|
|
270
|
+
| { type: 'convoy_failed'; data?: { status: string; reason?: string } }
|
|
271
|
+
| { type: 'convoy_guard'; data?: { checks?: string[]; [key: string]: unknown } }
|
|
272
|
+
| { type: 'task_started'; data?: { worker_id?: string } }
|
|
273
|
+
| { type: 'task_done'; data?: { status?: string; retries?: number; worker_id?: string } }
|
|
274
|
+
| { type: 'task_failed'; data?: { reason: string; worker_id?: string; gate?: string; hook?: string } }
|
|
275
|
+
| { type: 'task_skipped'; data?: { reason: string } }
|
|
276
|
+
| { type: 'task_retried'; data?: { previous_status: string } }
|
|
277
|
+
| { type: 'task_waiting_input'; data?: { task_id?: string; reason?: string } }
|
|
278
|
+
| { type: 'review_started'; data?: { level: string; task_id?: string; model?: string } }
|
|
279
|
+
| {
|
|
280
|
+
type: 'review_verdict'
|
|
281
|
+
data?: {
|
|
282
|
+
level: string
|
|
283
|
+
verdict: string
|
|
284
|
+
tokens: number
|
|
285
|
+
model?: string
|
|
286
|
+
feedback_length?: number
|
|
287
|
+
budget_exceeded?: boolean
|
|
288
|
+
budget_downgrade?: boolean
|
|
289
|
+
budget_skip?: boolean
|
|
290
|
+
passes?: number
|
|
291
|
+
blocks?: number
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
| { type: 'dispute_opened'; data?: { dispute_id: string; task_id: string; agent?: string; reason?: string } }
|
|
295
|
+
| { type: 'dlq_entry_created'; data?: { dlq_id: string; task_id: string; agent?: string; attempts?: number } }
|
|
296
|
+
| { type: 'drift_check_result'; data?: { score?: number; threshold?: number; passed?: boolean } }
|
|
297
|
+
| { type: 'drift_detected'; data?: { score?: number; files?: string[] } }
|
|
298
|
+
| { type: 'circuit_breaker_tripped'; data?: { agent?: string; failure_count?: number; threshold?: number } }
|
|
299
|
+
| { type: 'circuit_breaker_fallback'; data?: { original_agent?: string; fallback_agent?: string; task_id?: string } }
|
|
300
|
+
| { type: 'circuit_breaker_blocked'; data?: { agent?: string; task_id?: string } }
|
|
301
|
+
| { type: 'merge_conflict_detected'; data?: { task_id?: string; files?: string[] } }
|
|
302
|
+
| { type: 'merge_conflict_failed'; data?: { task_id?: string; error?: string } }
|
|
303
|
+
| { type: 'file_injection_received'; data?: { task_id?: string; from_task?: string; name?: string } }
|
|
304
|
+
| { type: 'artifact_limit_reached'; data?: { task_id?: string; limit?: number; current?: number } }
|
|
305
|
+
| { type: 'agent_identity_captured'; data?: { agent?: string; task_id?: string } }
|
|
306
|
+
| { type: 'agent_identity_rejected'; data?: { agent?: string; task_id?: string; reason?: string } }
|
|
307
|
+
| { type: 'weak_area_skipped'; data?: { agent?: string; weak_areas?: string[]; task_files?: string[] } }
|
|
308
|
+
| { type: 'swarm_concurrency_update'; data?: { new_concurrency?: number; reason?: string } }
|
|
309
|
+
| { type: 'post_convoy_hook_failed'; data?: { hook?: string; error?: string } }
|
|
310
|
+
| { type: 'session'; data?: { agent?: string; model?: string; task?: string; outcome?: string; duration_min?: number } }
|
|
311
|
+
| { type: 'delegation'; data?: { agent?: string; model?: string; tier?: string; mechanism?: string; outcome?: string } }
|
|
312
|
+
| {
|
|
313
|
+
type: 'secret_leak_prevented'
|
|
314
|
+
data?: { original_type?: string; patterns?: string[]; task_id?: string; findings_count?: number; context?: string }
|
|
315
|
+
}
|
|
316
|
+
| { type: 'ndjson_write_failed'; data?: { original_type?: string } }
|
|
317
|
+
| { type: 'built_in_gate_result'; data?: { gate: string; passed: boolean; output?: string; level?: string } }
|
|
318
|
+
| { type: 'watch_started'; data?: { trigger_type?: string; pid?: number } }
|
|
319
|
+
| { type: 'watch_cycle_start'; data?: { cycle_number?: number; triggered_by?: string } }
|
|
320
|
+
| { type: 'watch_cycle_end'; data?: { cycle_number?: number; status?: string } }
|
|
321
|
+
| { type: 'watch_stopped'; data?: { reason?: string } }
|
|
322
|
+
| { type: 'worker_killed'; data?: { reason?: string; worker_id?: string; task_id?: string } }
|
|
323
|
+
| { type: 'discovered_issue'; data?: { task_id?: string; title?: string; file?: string; description?: string; severity?: string } }
|
|
324
|
+
|
|
325
|
+
/** All canonical convoy event type strings. Used for runtime validation. */
|
|
326
|
+
export const KNOWN_EVENT_TYPES: Set<string> = new Set<ConvoyEventType['type']>([
|
|
327
|
+
'convoy_started',
|
|
328
|
+
'convoy_finished',
|
|
329
|
+
'convoy_failed',
|
|
330
|
+
'convoy_guard',
|
|
331
|
+
'task_started',
|
|
332
|
+
'task_done',
|
|
333
|
+
'task_failed',
|
|
334
|
+
'task_skipped',
|
|
335
|
+
'task_retried',
|
|
336
|
+
'task_waiting_input',
|
|
337
|
+
'review_started',
|
|
338
|
+
'review_verdict',
|
|
339
|
+
'dispute_opened',
|
|
340
|
+
'dlq_entry_created',
|
|
341
|
+
'drift_check_result',
|
|
342
|
+
'drift_detected',
|
|
343
|
+
'circuit_breaker_tripped',
|
|
344
|
+
'circuit_breaker_fallback',
|
|
345
|
+
'circuit_breaker_blocked',
|
|
346
|
+
'merge_conflict_detected',
|
|
347
|
+
'merge_conflict_failed',
|
|
348
|
+
'file_injection_received',
|
|
349
|
+
'artifact_limit_reached',
|
|
350
|
+
'agent_identity_captured',
|
|
351
|
+
'agent_identity_rejected',
|
|
352
|
+
'weak_area_skipped',
|
|
353
|
+
'swarm_concurrency_update',
|
|
354
|
+
'post_convoy_hook_failed',
|
|
355
|
+
'session',
|
|
356
|
+
'delegation',
|
|
357
|
+
'secret_leak_prevented',
|
|
358
|
+
'ndjson_write_failed',
|
|
359
|
+
'built_in_gate_result',
|
|
360
|
+
'watch_started',
|
|
361
|
+
'watch_cycle_start',
|
|
362
|
+
'watch_cycle_end',
|
|
363
|
+
'watch_stopped',
|
|
364
|
+
'worker_killed',
|
|
365
|
+
'discovered_issue',
|
|
366
|
+
])
|