opencastle 0.27.0 → 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/bin/cli.mjs +6 -0
- package/dist/cli/agents.d.ts +3 -0
- package/dist/cli/agents.d.ts.map +1 -0
- package/dist/cli/agents.js +161 -0
- package/dist/cli/agents.js.map +1 -0
- package/dist/cli/baselines.d.ts +3 -0
- package/dist/cli/baselines.d.ts.map +1 -0
- package/dist/cli/baselines.js +128 -0
- package/dist/cli/baselines.js.map +1 -0
- 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 +67 -2
- package/dist/cli/convoy/engine.d.ts.map +1 -1
- package/dist/cli/convoy/engine.js +2036 -28
- package/dist/cli/convoy/engine.js.map +1 -1
- package/dist/cli/convoy/engine.test.js +1659 -70
- 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 +12 -1
- package/dist/cli/convoy/events.d.ts.map +1 -1
- package/dist/cli/convoy/events.js +186 -13
- package/dist/cli/convoy/events.js.map +1 -1
- package/dist/cli/convoy/events.test.js +325 -28
- package/dist/cli/convoy/events.test.js.map +1 -1
- package/dist/cli/convoy/expertise.d.ts +16 -0
- package/dist/cli/convoy/expertise.d.ts.map +1 -0
- package/dist/cli/convoy/expertise.js +121 -0
- package/dist/cli/convoy/expertise.js.map +1 -0
- package/dist/cli/convoy/expertise.test.d.ts +2 -0
- package/dist/cli/convoy/expertise.test.d.ts.map +1 -0
- package/dist/cli/convoy/expertise.test.js +96 -0
- package/dist/cli/convoy/expertise.test.js.map +1 -0
- package/dist/cli/convoy/export.test.js +1 -0
- package/dist/cli/convoy/export.test.js.map +1 -1
- package/dist/cli/convoy/formula.d.ts +19 -0
- package/dist/cli/convoy/formula.d.ts.map +1 -0
- package/dist/cli/convoy/formula.js +142 -0
- package/dist/cli/convoy/formula.js.map +1 -0
- package/dist/cli/convoy/formula.test.d.ts +2 -0
- package/dist/cli/convoy/formula.test.d.ts.map +1 -0
- package/dist/cli/convoy/formula.test.js +342 -0
- package/dist/cli/convoy/formula.test.js.map +1 -0
- package/dist/cli/convoy/gates.d.ts +128 -0
- package/dist/cli/convoy/gates.d.ts.map +1 -0
- package/dist/cli/convoy/gates.js +606 -0
- package/dist/cli/convoy/gates.js.map +1 -0
- package/dist/cli/convoy/gates.test.d.ts +2 -0
- package/dist/cli/convoy/gates.test.d.ts.map +1 -0
- package/dist/cli/convoy/gates.test.js +976 -0
- package/dist/cli/convoy/gates.test.js.map +1 -0
- package/dist/cli/convoy/health.d.ts +11 -0
- package/dist/cli/convoy/health.d.ts.map +1 -1
- package/dist/cli/convoy/health.js +54 -0
- package/dist/cli/convoy/health.js.map +1 -1
- package/dist/cli/convoy/health.test.js +56 -1
- package/dist/cli/convoy/health.test.js.map +1 -1
- package/dist/cli/convoy/issues.d.ts +8 -0
- package/dist/cli/convoy/issues.d.ts.map +1 -0
- package/dist/cli/convoy/issues.js +98 -0
- package/dist/cli/convoy/issues.js.map +1 -0
- package/dist/cli/convoy/issues.test.d.ts +2 -0
- package/dist/cli/convoy/issues.test.d.ts.map +1 -0
- package/dist/cli/convoy/issues.test.js +107 -0
- package/dist/cli/convoy/issues.test.js.map +1 -0
- package/dist/cli/convoy/knowledge.d.ts +5 -0
- package/dist/cli/convoy/knowledge.d.ts.map +1 -0
- package/dist/cli/convoy/knowledge.js +116 -0
- package/dist/cli/convoy/knowledge.js.map +1 -0
- package/dist/cli/convoy/knowledge.test.d.ts +2 -0
- package/dist/cli/convoy/knowledge.test.d.ts.map +1 -0
- package/dist/cli/convoy/knowledge.test.js +87 -0
- package/dist/cli/convoy/knowledge.test.js.map +1 -0
- package/dist/cli/convoy/lessons.d.ts +17 -0
- package/dist/cli/convoy/lessons.d.ts.map +1 -0
- package/dist/cli/convoy/lessons.js +149 -0
- package/dist/cli/convoy/lessons.js.map +1 -0
- package/dist/cli/convoy/lessons.test.d.ts +2 -0
- package/dist/cli/convoy/lessons.test.d.ts.map +1 -0
- package/dist/cli/convoy/lessons.test.js +135 -0
- package/dist/cli/convoy/lessons.test.js.map +1 -0
- package/dist/cli/convoy/lock.d.ts +13 -0
- package/dist/cli/convoy/lock.d.ts.map +1 -0
- package/dist/cli/convoy/lock.js +88 -0
- package/dist/cli/convoy/lock.js.map +1 -0
- package/dist/cli/convoy/lock.test.d.ts +2 -0
- package/dist/cli/convoy/lock.test.d.ts.map +1 -0
- package/dist/cli/convoy/lock.test.js +136 -0
- package/dist/cli/convoy/lock.test.js.map +1 -0
- 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/merge.d.ts +4 -0
- package/dist/cli/convoy/merge.d.ts.map +1 -1
- package/dist/cli/convoy/merge.js +18 -1
- package/dist/cli/convoy/merge.js.map +1 -1
- package/dist/cli/convoy/merge.test.js +6 -7
- package/dist/cli/convoy/merge.test.js.map +1 -1
- package/dist/cli/convoy/partition.d.ts +51 -0
- package/dist/cli/convoy/partition.d.ts.map +1 -0
- package/dist/cli/convoy/partition.js +186 -0
- package/dist/cli/convoy/partition.js.map +1 -0
- package/dist/cli/convoy/partition.test.d.ts +2 -0
- package/dist/cli/convoy/partition.test.d.ts.map +1 -0
- package/dist/cli/convoy/partition.test.js +315 -0
- package/dist/cli/convoy/partition.test.js.map +1 -0
- package/dist/cli/convoy/pipeline.test.js +6 -0
- package/dist/cli/convoy/pipeline.test.js.map +1 -1
- package/dist/cli/convoy/store.d.ts +99 -7
- package/dist/cli/convoy/store.d.ts.map +1 -1
- package/dist/cli/convoy/store.js +764 -31
- package/dist/cli/convoy/store.js.map +1 -1
- package/dist/cli/convoy/store.test.js +1810 -18
- package/dist/cli/convoy/store.test.js.map +1 -1
- package/dist/cli/convoy/types.d.ts +427 -5
- 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/dist/cli/run/adapters/claude.d.ts +2 -0
- package/dist/cli/run/adapters/claude.d.ts.map +1 -1
- package/dist/cli/run/adapters/claude.js +89 -49
- package/dist/cli/run/adapters/claude.js.map +1 -1
- package/dist/cli/run/adapters/claude.test.d.ts +2 -0
- package/dist/cli/run/adapters/claude.test.d.ts.map +1 -0
- package/dist/cli/run/adapters/claude.test.js +205 -0
- package/dist/cli/run/adapters/claude.test.js.map +1 -0
- package/dist/cli/run/adapters/copilot.d.ts +1 -0
- package/dist/cli/run/adapters/copilot.d.ts.map +1 -1
- package/dist/cli/run/adapters/copilot.js +84 -46
- package/dist/cli/run/adapters/copilot.js.map +1 -1
- package/dist/cli/run/adapters/copilot.test.d.ts +2 -0
- package/dist/cli/run/adapters/copilot.test.d.ts.map +1 -0
- package/dist/cli/run/adapters/copilot.test.js +195 -0
- package/dist/cli/run/adapters/copilot.test.js.map +1 -0
- package/dist/cli/run/adapters/cursor.d.ts +1 -0
- package/dist/cli/run/adapters/cursor.d.ts.map +1 -1
- package/dist/cli/run/adapters/cursor.js +83 -47
- package/dist/cli/run/adapters/cursor.js.map +1 -1
- package/dist/cli/run/adapters/cursor.test.d.ts +2 -0
- package/dist/cli/run/adapters/cursor.test.d.ts.map +1 -0
- package/dist/cli/run/adapters/cursor.test.js +129 -0
- package/dist/cli/run/adapters/cursor.test.js.map +1 -0
- package/dist/cli/run/adapters/opencode.d.ts +1 -0
- package/dist/cli/run/adapters/opencode.d.ts.map +1 -1
- package/dist/cli/run/adapters/opencode.js +81 -47
- package/dist/cli/run/adapters/opencode.js.map +1 -1
- package/dist/cli/run/adapters/opencode.test.d.ts +2 -0
- package/dist/cli/run/adapters/opencode.test.d.ts.map +1 -0
- package/dist/cli/run/adapters/opencode.test.js +119 -0
- package/dist/cli/run/adapters/opencode.test.js.map +1 -0
- package/dist/cli/run/executor.js +1 -1
- package/dist/cli/run/executor.js.map +1 -1
- package/dist/cli/run/schema.d.ts.map +1 -1
- package/dist/cli/run/schema.js +245 -4
- package/dist/cli/run/schema.js.map +1 -1
- package/dist/cli/run/schema.test.js +669 -0
- package/dist/cli/run/schema.test.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +362 -22
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/types.d.ts +85 -2
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/cli/types.js.map +1 -1
- package/dist/cli/watch.d.ts +15 -0
- package/dist/cli/watch.d.ts.map +1 -0
- package/dist/cli/watch.js +279 -0
- package/dist/cli/watch.js.map +1 -0
- package/package.json +5 -1
- package/src/cli/agents.ts +177 -0
- package/src/cli/baselines.ts +143 -0
- package/src/cli/convoy/TELEMETRY.md +203 -0
- package/src/cli/convoy/dashboard-types.ts +141 -0
- package/src/cli/convoy/engine.test.ts +1937 -70
- package/src/cli/convoy/engine.ts +2350 -40
- package/src/cli/convoy/event-schemas.ts +195 -0
- package/src/cli/convoy/events.test.ts +384 -39
- package/src/cli/convoy/events.ts +202 -16
- package/src/cli/convoy/expertise.test.ts +128 -0
- package/src/cli/convoy/expertise.ts +163 -0
- package/src/cli/convoy/export.test.ts +1 -0
- package/src/cli/convoy/formula.test.ts +405 -0
- package/src/cli/convoy/formula.ts +174 -0
- package/src/cli/convoy/gates.test.ts +1169 -0
- package/src/cli/convoy/gates.ts +774 -0
- package/src/cli/convoy/health.test.ts +64 -2
- package/src/cli/convoy/health.ts +80 -2
- package/src/cli/convoy/issues.test.ts +143 -0
- package/src/cli/convoy/issues.ts +136 -0
- package/src/cli/convoy/knowledge.test.ts +101 -0
- package/src/cli/convoy/knowledge.ts +132 -0
- package/src/cli/convoy/lessons.test.ts +188 -0
- package/src/cli/convoy/lessons.ts +164 -0
- package/src/cli/convoy/lock.test.ts +181 -0
- package/src/cli/convoy/lock.ts +103 -0
- package/src/cli/convoy/log-merge.test.ts +179 -0
- package/src/cli/convoy/merge.test.ts +6 -7
- package/src/cli/convoy/merge.ts +19 -1
- package/src/cli/convoy/partition.test.ts +423 -0
- package/src/cli/convoy/partition.ts +232 -0
- package/src/cli/convoy/pipeline.test.ts +6 -0
- package/src/cli/convoy/store.test.ts +2041 -20
- package/src/cli/convoy/store.ts +945 -46
- package/src/cli/convoy/types.ts +278 -4
- package/src/cli/log.ts +120 -2
- package/src/cli/run/adapters/claude.test.ts +234 -0
- package/src/cli/run/adapters/claude.ts +45 -5
- package/src/cli/run/adapters/copilot.test.ts +224 -0
- package/src/cli/run/adapters/copilot.ts +34 -4
- package/src/cli/run/adapters/cursor.test.ts +144 -0
- package/src/cli/run/adapters/cursor.ts +33 -2
- package/src/cli/run/adapters/opencode.test.ts +135 -0
- package/src/cli/run/adapters/opencode.ts +30 -2
- package/src/cli/run/executor.ts +1 -1
- package/src/cli/run/schema.test.ts +758 -0
- package/src/cli/run/schema.ts +300 -25
- package/src/cli/run.ts +341 -21
- package/src/cli/types.ts +86 -1
- package/src/cli/watch.ts +298 -0
- 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,4 +1,6 @@
|
|
|
1
|
+
import { copyFileSync } from 'node:fs'
|
|
1
2
|
import { DatabaseSync } from 'node:sqlite'
|
|
3
|
+
import type { DashboardConvoyDetail } from './dashboard-types.js'
|
|
2
4
|
import type {
|
|
3
5
|
ConvoyRecord,
|
|
4
6
|
ConvoyStatus,
|
|
@@ -9,36 +11,116 @@ import type {
|
|
|
9
11
|
EventRecord,
|
|
10
12
|
PipelineRecord,
|
|
11
13
|
PipelineStatus,
|
|
14
|
+
DlqRecord,
|
|
15
|
+
ArtifactRecord,
|
|
16
|
+
AgentIdentityRecord,
|
|
17
|
+
TaskStepRecord,
|
|
12
18
|
} from './types.js'
|
|
13
19
|
|
|
14
|
-
const SCHEMA_VERSION =
|
|
20
|
+
const SCHEMA_VERSION = 10
|
|
21
|
+
|
|
22
|
+
// ── Size limits (bytes) ────────────────────────────────────────────────────────
|
|
23
|
+
const LIMIT_SPEC_YAML = 256 * 1024 // 256 KB
|
|
24
|
+
const LIMIT_OUTPUT = 1024 * 1024 // 1 MB (head 512KB + tail 512KB)
|
|
25
|
+
const LIMIT_OUTPUT_HALF = 512 * 1024 // 512 KB per half
|
|
26
|
+
const LIMIT_EVENT_DATA = 64 * 1024 // 64 KB
|
|
27
|
+
const LIMIT_SUMMARY = 4096 // 4 KB
|
|
28
|
+
|
|
29
|
+
export class FieldSizeLimitError extends Error {
|
|
30
|
+
constructor(field: string, actual: number, limit: number) {
|
|
31
|
+
super(`Field "${field}" exceeds size limit: ${actual} bytes > ${limit} bytes`)
|
|
32
|
+
this.name = 'FieldSizeLimitError'
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function enforceLimit(value: string | null | undefined, field: string, limit: number): void {
|
|
37
|
+
if (value == null) return
|
|
38
|
+
const size = Buffer.byteLength(value, 'utf8')
|
|
39
|
+
if (size > limit) {
|
|
40
|
+
throw new FieldSizeLimitError(field, size, limit)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function truncateOutput(value: string | null | undefined): string | null {
|
|
45
|
+
if (value == null) return null
|
|
46
|
+
const size = Buffer.byteLength(value, 'utf8')
|
|
47
|
+
if (size <= LIMIT_OUTPUT) return value
|
|
48
|
+
// Head + tail truncation with marker
|
|
49
|
+
const head = value.slice(0, LIMIT_OUTPUT_HALF)
|
|
50
|
+
const tail = value.slice(-LIMIT_OUTPUT_HALF)
|
|
51
|
+
return head + '\n\n... [truncated: ' + size + ' bytes total, showing first/last 512KB] ...\n\n' + tail
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class ConvoyArtifactLimitError extends Error {
|
|
55
|
+
constructor(convoyId: string) {
|
|
56
|
+
super(`Convoy ${convoyId} has reached the maximum of 50 artifacts`)
|
|
57
|
+
this.name = 'ConvoyArtifactLimitError'
|
|
58
|
+
}
|
|
59
|
+
}
|
|
15
60
|
|
|
16
61
|
export interface ConvoyStore {
|
|
17
|
-
insertConvoy(
|
|
62
|
+
insertConvoy(
|
|
63
|
+
record: Omit<
|
|
64
|
+
ConvoyRecord,
|
|
65
|
+
| 'started_at' | 'finished_at' | 'total_tokens' | 'total_cost_usd'
|
|
66
|
+
| 'pipeline_id' | 'circuit_state' | 'review_tokens_total' | 'review_budget'
|
|
67
|
+
> & { pipeline_id?: string | null },
|
|
68
|
+
): void
|
|
18
69
|
getConvoy(id: string): ConvoyRecord | undefined
|
|
19
70
|
getLatestConvoy(): ConvoyRecord | undefined
|
|
20
71
|
updateConvoyStatus(
|
|
21
72
|
id: string,
|
|
22
73
|
status: ConvoyStatus,
|
|
23
|
-
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 },
|
|
24
75
|
): void
|
|
76
|
+
updateConvoyReviewTokens(convoyId: string, tokens: number): void
|
|
77
|
+
updateConvoyCircuitState(convoyId: string, state: string | null): void
|
|
25
78
|
insertTask(
|
|
26
79
|
record: Omit<
|
|
27
80
|
TaskRecord,
|
|
28
|
-
'worker_id' | 'worktree' | 'output' | 'exit_code' | 'started_at' | 'finished_at'
|
|
29
|
-
|
|
81
|
+
| 'worker_id' | 'worktree' | 'output' | 'exit_code' | 'started_at' | 'finished_at'
|
|
82
|
+
| 'prompt_tokens' | 'completion_tokens' | 'total_tokens' | 'cost_usd'
|
|
83
|
+
| 'on_exhausted' | 'injected' | 'provenance' | 'idempotency_key'
|
|
84
|
+
| 'current_step' | 'total_steps' | 'review_level' | 'review_verdict'
|
|
85
|
+
| 'review_tokens' | 'review_model' | 'panel_attempts' | 'dispute_id'
|
|
86
|
+
| 'drift_score' | 'drift_retried' | 'discovered_issues'
|
|
87
|
+
> & { outputs?: string | null; inputs?: string | null },
|
|
30
88
|
): void
|
|
89
|
+
insertInjectedTask(record: TaskRecord): void
|
|
31
90
|
getTask(id: string, convoyId: string): TaskRecord | undefined
|
|
32
91
|
getTasksByConvoy(convoyId: string): TaskRecord[]
|
|
92
|
+
getTaskByIdempotencyKey(convoyId: string, key: string): TaskRecord | undefined
|
|
93
|
+
getTaskByDisputeId(disputeId: string): TaskRecord | undefined
|
|
94
|
+
getDisputedTasks(convoyId?: string): TaskRecord[]
|
|
33
95
|
updateTaskStatus(
|
|
34
96
|
id: string,
|
|
35
97
|
convoyId: string,
|
|
36
98
|
status: ConvoyTaskStatus,
|
|
37
99
|
extra?: Partial<
|
|
38
|
-
Pick<
|
|
100
|
+
Pick<
|
|
101
|
+
TaskRecord,
|
|
102
|
+
| 'worker_id' | 'worktree' | 'output' | 'exit_code' | 'started_at' | 'finished_at'
|
|
103
|
+
| 'retries' | 'prompt_tokens' | 'completion_tokens' | 'total_tokens' | 'cost_usd' | 'prompt'
|
|
104
|
+
>
|
|
39
105
|
>,
|
|
40
106
|
): void
|
|
107
|
+
updateTaskReview(
|
|
108
|
+
taskId: string,
|
|
109
|
+
convoyId: string,
|
|
110
|
+
fields: Partial<Pick<TaskRecord, 'review_level' | 'review_verdict' | 'review_tokens' | 'review_model' | 'panel_attempts' | 'dispute_id'>>,
|
|
111
|
+
): void
|
|
112
|
+
updateTaskDrift(
|
|
113
|
+
taskId: string,
|
|
114
|
+
convoyId: string,
|
|
115
|
+
fields: Partial<Pick<TaskRecord, 'drift_score' | 'drift_retried'>>,
|
|
116
|
+
): void
|
|
117
|
+
updateTaskDisputeStatus(taskId: string, convoyId: string, status: ConvoyTaskStatus, disputeId: string): void
|
|
41
118
|
getReadyTasks(convoyId: string): TaskRecord[]
|
|
119
|
+
insertTaskStep(record: Omit<TaskStepRecord, 'id'>): number
|
|
120
|
+
updateTaskStep(
|
|
121
|
+
id: number,
|
|
122
|
+
fields: Partial<Pick<TaskStepRecord, 'status' | 'exit_code' | 'output' | 'started_at' | 'finished_at'>>,
|
|
123
|
+
): void
|
|
42
124
|
insertWorker(record: Omit<WorkerRecord, 'finished_at' | 'last_heartbeat'>): void
|
|
43
125
|
getWorker(id: string): WorkerRecord | undefined
|
|
44
126
|
updateWorkerStatus(
|
|
@@ -46,25 +128,53 @@ export interface ConvoyStore {
|
|
|
46
128
|
status: WorkerStatus,
|
|
47
129
|
extra?: Partial<Pick<WorkerRecord, 'finished_at' | 'last_heartbeat' | 'pid'>>,
|
|
48
130
|
): void
|
|
49
|
-
insertEvent(record: Omit<EventRecord, 'id'>):
|
|
131
|
+
insertEvent(record: Omit<EventRecord, 'id'>): number
|
|
50
132
|
getEvents(convoyId: string): EventRecord[]
|
|
133
|
+
insertDlqEntry(record: DlqRecord): void
|
|
134
|
+
listDlqEntries(convoyIdFilter?: string): DlqRecord[]
|
|
135
|
+
resolveDlqEntry(id: string, resolution: string): void
|
|
136
|
+
insertArtifact(record: ArtifactRecord): void
|
|
137
|
+
getArtifact(convoyId: string, name: string): ArtifactRecord | undefined
|
|
138
|
+
getArtifactsByTask(taskId: string): ArtifactRecord[]
|
|
139
|
+
getArtifactsByConvoy(convoyId: string): ArtifactRecord[]
|
|
140
|
+
deleteArtifactsOlderThan(days: number): number
|
|
141
|
+
insertAgentIdentity(record: AgentIdentityRecord): void
|
|
142
|
+
getAgentIdentities(agent: string, limit: number): AgentIdentityRecord[]
|
|
143
|
+
listAgentIdentitySummary(): Array<{ agent: string; task_count: number; latest_date: string }>
|
|
144
|
+
purgeAgentIdentities(agent: string): number
|
|
145
|
+
deleteAgentIdentitiesOlderThan(days: number): number
|
|
146
|
+
getScratchpadValue(key: string): string | null
|
|
147
|
+
setScratchpadValue(key: string, value: string): void
|
|
148
|
+
clearScratchpad(): void
|
|
149
|
+
clearScratchpadOlderThan(days: number): void
|
|
51
150
|
insertPipeline(record: Omit<PipelineRecord, 'started_at' | 'finished_at' | 'total_tokens' | 'total_cost_usd'>): void
|
|
52
151
|
getPipeline(id: string): PipelineRecord | undefined
|
|
53
152
|
getLatestPipeline(): PipelineRecord | undefined
|
|
54
153
|
updatePipelineStatus(
|
|
55
154
|
id: string,
|
|
56
155
|
status: PipelineStatus,
|
|
57
|
-
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 },
|
|
58
157
|
): void
|
|
59
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
|
|
60
168
|
withTransaction<T>(fn: () => T): T
|
|
61
169
|
close(): void
|
|
62
170
|
}
|
|
63
171
|
|
|
64
172
|
class ConvoyStoreImpl implements ConvoyStore {
|
|
65
173
|
private db: DatabaseSync
|
|
174
|
+
private dbPath: string
|
|
66
175
|
|
|
67
176
|
constructor(dbPath: string) {
|
|
177
|
+
this.dbPath = dbPath
|
|
68
178
|
this.db = new DatabaseSync(dbPath)
|
|
69
179
|
this.db.exec('PRAGMA journal_mode = WAL')
|
|
70
180
|
this.db.exec('PRAGMA synchronous = NORMAL')
|
|
@@ -76,18 +186,22 @@ class ConvoyStoreImpl implements ConvoyStore {
|
|
|
76
186
|
if (version === 0) {
|
|
77
187
|
this.db.exec(`
|
|
78
188
|
CREATE TABLE IF NOT EXISTS convoy (
|
|
79
|
-
id
|
|
80
|
-
name
|
|
81
|
-
spec_hash
|
|
82
|
-
status
|
|
83
|
-
branch
|
|
84
|
-
created_at
|
|
85
|
-
started_at
|
|
86
|
-
finished_at
|
|
87
|
-
spec_yaml
|
|
88
|
-
total_tokens
|
|
89
|
-
total_cost_usd
|
|
90
|
-
|
|
189
|
+
id TEXT PRIMARY KEY,
|
|
190
|
+
name TEXT NOT NULL,
|
|
191
|
+
spec_hash TEXT NOT NULL,
|
|
192
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
193
|
+
branch TEXT,
|
|
194
|
+
created_at TEXT NOT NULL,
|
|
195
|
+
started_at TEXT,
|
|
196
|
+
finished_at TEXT,
|
|
197
|
+
spec_yaml TEXT NOT NULL,
|
|
198
|
+
total_tokens INTEGER,
|
|
199
|
+
total_cost_usd TEXT,
|
|
200
|
+
total_cost_usd_num REAL,
|
|
201
|
+
pipeline_id TEXT,
|
|
202
|
+
circuit_state TEXT,
|
|
203
|
+
review_tokens_total INTEGER,
|
|
204
|
+
review_budget INTEGER
|
|
91
205
|
);
|
|
92
206
|
|
|
93
207
|
CREATE TABLE IF NOT EXISTS pipeline (
|
|
@@ -101,7 +215,8 @@ class ConvoyStoreImpl implements ConvoyStore {
|
|
|
101
215
|
started_at TEXT,
|
|
102
216
|
finished_at TEXT,
|
|
103
217
|
total_tokens INTEGER,
|
|
104
|
-
total_cost_usd TEXT
|
|
218
|
+
total_cost_usd TEXT,
|
|
219
|
+
total_cost_usd_num REAL
|
|
105
220
|
);
|
|
106
221
|
|
|
107
222
|
CREATE TABLE IF NOT EXISTS task (
|
|
@@ -127,7 +242,42 @@ class ConvoyStoreImpl implements ConvoyStore {
|
|
|
127
242
|
prompt_tokens INTEGER,
|
|
128
243
|
completion_tokens INTEGER,
|
|
129
244
|
total_tokens INTEGER,
|
|
130
|
-
cost_usd TEXT
|
|
245
|
+
cost_usd TEXT,
|
|
246
|
+
cost_usd_num REAL,
|
|
247
|
+
gates TEXT,
|
|
248
|
+
on_exhausted TEXT NOT NULL DEFAULT 'dlq',
|
|
249
|
+
injected INTEGER NOT NULL DEFAULT 0,
|
|
250
|
+
provenance TEXT,
|
|
251
|
+
idempotency_key TEXT,
|
|
252
|
+
current_step INTEGER,
|
|
253
|
+
total_steps INTEGER,
|
|
254
|
+
review_level TEXT,
|
|
255
|
+
review_verdict TEXT,
|
|
256
|
+
review_tokens INTEGER,
|
|
257
|
+
review_model TEXT,
|
|
258
|
+
panel_attempts INTEGER NOT NULL DEFAULT 0,
|
|
259
|
+
dispute_id TEXT,
|
|
260
|
+
drift_score REAL,
|
|
261
|
+
drift_retried INTEGER NOT NULL DEFAULT 0,
|
|
262
|
+
outputs TEXT,
|
|
263
|
+
inputs TEXT,
|
|
264
|
+
discovered_issues TEXT
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_task_idempotency ON task(convoy_id, idempotency_key)
|
|
268
|
+
WHERE idempotency_key IS NOT NULL;
|
|
269
|
+
|
|
270
|
+
CREATE TABLE IF NOT EXISTS task_step (
|
|
271
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
272
|
+
task_id TEXT NOT NULL REFERENCES task(id),
|
|
273
|
+
step_index INTEGER NOT NULL,
|
|
274
|
+
prompt TEXT NOT NULL,
|
|
275
|
+
gates TEXT,
|
|
276
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
277
|
+
exit_code INTEGER,
|
|
278
|
+
output TEXT,
|
|
279
|
+
started_at TEXT,
|
|
280
|
+
finished_at TEXT
|
|
131
281
|
);
|
|
132
282
|
|
|
133
283
|
CREATE TABLE IF NOT EXISTS worker (
|
|
@@ -152,6 +302,57 @@ class ConvoyStoreImpl implements ConvoyStore {
|
|
|
152
302
|
data TEXT,
|
|
153
303
|
created_at TEXT NOT NULL
|
|
154
304
|
);
|
|
305
|
+
|
|
306
|
+
CREATE TABLE IF NOT EXISTS dlq (
|
|
307
|
+
id TEXT PRIMARY KEY,
|
|
308
|
+
convoy_id TEXT NOT NULL REFERENCES convoy(id),
|
|
309
|
+
task_id TEXT NOT NULL REFERENCES task(id),
|
|
310
|
+
agent TEXT NOT NULL,
|
|
311
|
+
failure_type TEXT NOT NULL,
|
|
312
|
+
error_output TEXT,
|
|
313
|
+
attempts INTEGER NOT NULL,
|
|
314
|
+
tokens_spent INTEGER,
|
|
315
|
+
escalation_task_id TEXT,
|
|
316
|
+
resolved INTEGER NOT NULL DEFAULT 0,
|
|
317
|
+
resolution TEXT,
|
|
318
|
+
created_at TEXT NOT NULL,
|
|
319
|
+
resolved_at TEXT
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
CREATE TABLE IF NOT EXISTS artifact (
|
|
323
|
+
id TEXT PRIMARY KEY,
|
|
324
|
+
convoy_id TEXT NOT NULL REFERENCES convoy(id),
|
|
325
|
+
task_id TEXT NOT NULL REFERENCES task(id),
|
|
326
|
+
name TEXT NOT NULL,
|
|
327
|
+
type TEXT NOT NULL,
|
|
328
|
+
content TEXT NOT NULL CHECK (length(content) <= 1048576),
|
|
329
|
+
created_at TEXT NOT NULL,
|
|
330
|
+
UNIQUE(convoy_id, name)
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
CREATE TABLE IF NOT EXISTS agent_identity (
|
|
334
|
+
id TEXT PRIMARY KEY,
|
|
335
|
+
agent TEXT NOT NULL,
|
|
336
|
+
convoy_id TEXT NOT NULL,
|
|
337
|
+
task_id TEXT NOT NULL,
|
|
338
|
+
summary TEXT NOT NULL,
|
|
339
|
+
created_at TEXT NOT NULL,
|
|
340
|
+
retention_days INTEGER NOT NULL DEFAULT 90
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
CREATE TABLE IF NOT EXISTS scratchpad (
|
|
344
|
+
key TEXT PRIMARY KEY,
|
|
345
|
+
value TEXT NOT NULL,
|
|
346
|
+
updated_at TEXT NOT NULL
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
CREATE TABLE IF NOT EXISTS engine_lock (
|
|
350
|
+
id INTEGER PRIMARY KEY,
|
|
351
|
+
pid INTEGER NOT NULL,
|
|
352
|
+
hostname TEXT NOT NULL,
|
|
353
|
+
started_at TEXT NOT NULL,
|
|
354
|
+
last_heartbeat TEXT NOT NULL
|
|
355
|
+
);
|
|
155
356
|
`)
|
|
156
357
|
this.db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`)
|
|
157
358
|
version = SCHEMA_VERSION
|
|
@@ -191,33 +392,68 @@ class ConvoyStoreImpl implements ConvoyStore {
|
|
|
191
392
|
this.db.exec('PRAGMA user_version = 4')
|
|
192
393
|
version = 4
|
|
193
394
|
}
|
|
395
|
+
if (version === 4) {
|
|
396
|
+
migrateSchema(this.db, this.dbPath, 4, 5)
|
|
397
|
+
version = 5
|
|
398
|
+
}
|
|
399
|
+
if (version === 5) {
|
|
400
|
+
migrateSchema(this.db, this.dbPath, 5, 6)
|
|
401
|
+
version = 6
|
|
402
|
+
}
|
|
403
|
+
if (version === 6) {
|
|
404
|
+
migrateSchema(this.db, this.dbPath, 6, 7)
|
|
405
|
+
version = 7
|
|
406
|
+
}
|
|
407
|
+
if (version === 7) {
|
|
408
|
+
migrateSchema(this.db, this.dbPath, 7, 8)
|
|
409
|
+
version = 8
|
|
410
|
+
}
|
|
411
|
+
if (version === 8) {
|
|
412
|
+
migrateSchema(this.db, this.dbPath, 8, 9)
|
|
413
|
+
version = 9
|
|
414
|
+
}
|
|
415
|
+
if (version === 9) {
|
|
416
|
+
migrateSchema(this.db, this.dbPath, 9, 10)
|
|
417
|
+
version = 10
|
|
418
|
+
}
|
|
194
419
|
}
|
|
195
420
|
|
|
196
|
-
insertConvoy(
|
|
421
|
+
insertConvoy(
|
|
422
|
+
record: Omit<
|
|
423
|
+
ConvoyRecord,
|
|
424
|
+
| 'started_at' | 'finished_at' | 'total_tokens' | 'total_cost_usd'
|
|
425
|
+
| 'pipeline_id' | 'circuit_state' | 'review_tokens_total' | 'review_budget'
|
|
426
|
+
> & { pipeline_id?: string | null },
|
|
427
|
+
): void {
|
|
428
|
+
enforceLimit(record.spec_yaml, 'spec_yaml', LIMIT_SPEC_YAML)
|
|
197
429
|
this.db
|
|
198
430
|
.prepare(
|
|
199
|
-
`INSERT INTO convoy
|
|
200
|
-
|
|
431
|
+
`INSERT INTO convoy
|
|
432
|
+
(id, name, spec_hash, status, branch, created_at, started_at, finished_at,
|
|
433
|
+
spec_yaml, pipeline_id)
|
|
434
|
+
VALUES
|
|
435
|
+
(:id, :name, :spec_hash, :status, :branch, :created_at, NULL, NULL,
|
|
436
|
+
:spec_yaml, :pipeline_id)`,
|
|
201
437
|
)
|
|
202
438
|
.run({ ...record, pipeline_id: record.pipeline_id ?? null })
|
|
203
439
|
}
|
|
204
440
|
|
|
205
441
|
getConvoy(id: string): ConvoyRecord | undefined {
|
|
206
442
|
return this.db
|
|
207
|
-
.prepare('SELECT
|
|
443
|
+
.prepare('SELECT *, total_cost_usd_num AS total_cost_usd FROM convoy WHERE id = :id')
|
|
208
444
|
.get({ id }) as ConvoyRecord | undefined
|
|
209
445
|
}
|
|
210
446
|
|
|
211
447
|
getLatestConvoy(): ConvoyRecord | undefined {
|
|
212
448
|
return this.db
|
|
213
|
-
.prepare('SELECT
|
|
449
|
+
.prepare('SELECT *, total_cost_usd_num AS total_cost_usd FROM convoy ORDER BY created_at DESC LIMIT 1')
|
|
214
450
|
.get() as ConvoyRecord | undefined
|
|
215
451
|
}
|
|
216
452
|
|
|
217
453
|
updateConvoyStatus(
|
|
218
454
|
id: string,
|
|
219
455
|
status: ConvoyStatus,
|
|
220
|
-
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 },
|
|
221
457
|
): void {
|
|
222
458
|
const sets = ['status = :status']
|
|
223
459
|
const params: Record<string, string | number | null> = { id, status }
|
|
@@ -235,56 +471,139 @@ class ConvoyStoreImpl implements ConvoyStore {
|
|
|
235
471
|
params.total_tokens = extra.total_tokens
|
|
236
472
|
}
|
|
237
473
|
if (extra?.total_cost_usd !== undefined) {
|
|
238
|
-
sets.push('total_cost_usd = :
|
|
239
|
-
|
|
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
|
|
240
478
|
}
|
|
241
479
|
|
|
242
480
|
this.db.prepare(`UPDATE convoy SET ${sets.join(', ')} WHERE id = :id`).run(params)
|
|
243
481
|
}
|
|
244
482
|
|
|
483
|
+
updateConvoyReviewTokens(convoyId: string, tokens: number): void {
|
|
484
|
+
this.db
|
|
485
|
+
.prepare(
|
|
486
|
+
`UPDATE convoy
|
|
487
|
+
SET review_tokens_total = :tokens
|
|
488
|
+
WHERE id = :id`,
|
|
489
|
+
)
|
|
490
|
+
.run({ id: convoyId, tokens })
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
updateConvoyCircuitState(convoyId: string, state: string | null): void {
|
|
494
|
+
this.db
|
|
495
|
+
.prepare('UPDATE convoy SET circuit_state = :state WHERE id = :id')
|
|
496
|
+
.run({ id: convoyId, state: state ?? null })
|
|
497
|
+
}
|
|
498
|
+
|
|
245
499
|
insertTask(
|
|
246
500
|
record: Omit<
|
|
247
501
|
TaskRecord,
|
|
248
|
-
'worker_id' | 'worktree' | 'output' | 'exit_code' | 'started_at' | 'finished_at'
|
|
249
|
-
|
|
502
|
+
| 'worker_id' | 'worktree' | 'output' | 'exit_code' | 'started_at' | 'finished_at'
|
|
503
|
+
| 'prompt_tokens' | 'completion_tokens' | 'total_tokens' | 'cost_usd'
|
|
504
|
+
| 'on_exhausted' | 'injected' | 'provenance' | 'idempotency_key'
|
|
505
|
+
| 'current_step' | 'total_steps' | 'review_level' | 'review_verdict'
|
|
506
|
+
| 'review_tokens' | 'review_model' | 'panel_attempts' | 'dispute_id'
|
|
507
|
+
| 'drift_score' | 'drift_retried' | 'discovered_issues'
|
|
508
|
+
> & { outputs?: string | null; inputs?: string | null },
|
|
250
509
|
): void {
|
|
251
510
|
this.db
|
|
252
511
|
.prepare(
|
|
253
512
|
`INSERT INTO task
|
|
254
513
|
(id, convoy_id, phase, prompt, agent, adapter, model, timeout_ms, status,
|
|
255
514
|
worker_id, worktree, output, exit_code, started_at, finished_at,
|
|
256
|
-
retries, max_retries, files, depends_on
|
|
515
|
+
retries, max_retries, files, depends_on, gates,
|
|
516
|
+
on_exhausted, injected, provenance, idempotency_key,
|
|
517
|
+
outputs, inputs)
|
|
257
518
|
VALUES
|
|
258
519
|
(:id, :convoy_id, :phase, :prompt, :agent, :adapter, :model, :timeout_ms, :status,
|
|
259
520
|
NULL, NULL, NULL, NULL, NULL, NULL,
|
|
260
|
-
:retries, :max_retries, :files, :depends_on
|
|
521
|
+
:retries, :max_retries, :files, :depends_on, :gates,
|
|
522
|
+
'dlq', 0, NULL, NULL,
|
|
523
|
+
:outputs, :inputs)`,
|
|
261
524
|
)
|
|
262
|
-
.run(record)
|
|
525
|
+
.run({ ...record, outputs: record.outputs ?? null, inputs: record.inputs ?? null })
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
insertInjectedTask(record: TaskRecord): void {
|
|
529
|
+
this.db
|
|
530
|
+
.prepare(
|
|
531
|
+
`INSERT INTO task
|
|
532
|
+
(id, convoy_id, phase, prompt, agent, adapter, model, timeout_ms, status,
|
|
533
|
+
worker_id, worktree, output, exit_code, started_at, finished_at,
|
|
534
|
+
retries, max_retries, files, depends_on, gates,
|
|
535
|
+
on_exhausted, injected, provenance, idempotency_key,
|
|
536
|
+
current_step, total_steps, review_level, review_verdict,
|
|
537
|
+
review_tokens, review_model, panel_attempts, dispute_id,
|
|
538
|
+
drift_score, drift_retried, outputs, inputs, discovered_issues)
|
|
539
|
+
VALUES
|
|
540
|
+
(:id, :convoy_id, :phase, :prompt, :agent, :adapter, :model, :timeout_ms, :status,
|
|
541
|
+
:worker_id, :worktree, :output, :exit_code, :started_at, :finished_at,
|
|
542
|
+
:retries, :max_retries, :files, :depends_on, :gates,
|
|
543
|
+
:on_exhausted, :injected, :provenance, :idempotency_key,
|
|
544
|
+
:current_step, :total_steps, :review_level, :review_verdict,
|
|
545
|
+
:review_tokens, :review_model, :panel_attempts, :dispute_id,
|
|
546
|
+
:drift_score, :drift_retried, :outputs, :inputs, :discovered_issues)`,
|
|
547
|
+
)
|
|
548
|
+
.run(record as unknown as Record<string, string | number | null>)
|
|
263
549
|
}
|
|
264
550
|
|
|
265
551
|
getTask(id: string, convoyId: string): TaskRecord | undefined {
|
|
266
552
|
return this.db
|
|
267
|
-
.prepare('SELECT
|
|
553
|
+
.prepare('SELECT *, cost_usd_num AS cost_usd FROM task WHERE id = :id AND convoy_id = :convoy_id')
|
|
268
554
|
.get({ id, convoy_id: convoyId }) as TaskRecord | undefined
|
|
269
555
|
}
|
|
270
556
|
|
|
271
557
|
getTasksByConvoy(convoyId: string): TaskRecord[] {
|
|
272
558
|
return this.db
|
|
273
|
-
.prepare('SELECT
|
|
559
|
+
.prepare('SELECT *, cost_usd_num AS cost_usd FROM task WHERE convoy_id = :convoy_id ORDER BY phase, id')
|
|
274
560
|
.all({ convoy_id: convoyId }) as unknown as TaskRecord[]
|
|
275
561
|
}
|
|
276
562
|
|
|
563
|
+
getTaskByIdempotencyKey(convoyId: string, key: string): TaskRecord | undefined {
|
|
564
|
+
return this.db
|
|
565
|
+
.prepare('SELECT *, cost_usd_num AS cost_usd FROM task WHERE convoy_id = :convoy_id AND idempotency_key = :key')
|
|
566
|
+
.get({ convoy_id: convoyId, key }) as TaskRecord | undefined
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
getTaskByDisputeId(disputeId: string): TaskRecord | undefined {
|
|
570
|
+
return this.db
|
|
571
|
+
.prepare('SELECT *, cost_usd_num AS cost_usd FROM task WHERE dispute_id = :dispute_id LIMIT 1')
|
|
572
|
+
.get({ dispute_id: disputeId }) as TaskRecord | undefined
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
getDisputedTasks(convoyId?: string): TaskRecord[] {
|
|
576
|
+
if (convoyId) {
|
|
577
|
+
return this.db
|
|
578
|
+
.prepare("SELECT *, cost_usd_num AS cost_usd FROM task WHERE status = 'disputed' AND convoy_id = :convoy_id ORDER BY phase, id")
|
|
579
|
+
.all({ convoy_id: convoyId }) as unknown as TaskRecord[]
|
|
580
|
+
}
|
|
581
|
+
return this.db
|
|
582
|
+
.prepare("SELECT *, cost_usd_num AS cost_usd FROM task WHERE status = 'disputed' ORDER BY convoy_id, phase, id")
|
|
583
|
+
.all({}) as unknown as TaskRecord[]
|
|
584
|
+
}
|
|
585
|
+
|
|
277
586
|
updateTaskStatus(
|
|
278
587
|
id: string,
|
|
279
588
|
convoyId: string,
|
|
280
589
|
status: ConvoyTaskStatus,
|
|
281
590
|
extra?: Partial<
|
|
282
|
-
Pick<
|
|
591
|
+
Pick<
|
|
592
|
+
TaskRecord,
|
|
593
|
+
| 'worker_id' | 'worktree' | 'output' | 'exit_code' | 'started_at' | 'finished_at'
|
|
594
|
+
| 'retries' | 'prompt_tokens' | 'completion_tokens' | 'total_tokens' | 'cost_usd' | 'prompt'
|
|
595
|
+
>
|
|
283
596
|
>,
|
|
284
597
|
): void {
|
|
598
|
+
if (extra?.output !== undefined) {
|
|
599
|
+
extra = { ...extra, output: truncateOutput(extra.output) }
|
|
600
|
+
}
|
|
285
601
|
const sets = ['status = :status']
|
|
286
602
|
const params: Record<string, string | number | null> = { id, convoy_id: convoyId, status }
|
|
287
|
-
const extraFields = [
|
|
603
|
+
const extraFields = [
|
|
604
|
+
'worker_id', 'worktree', 'output', 'exit_code', 'started_at', 'finished_at',
|
|
605
|
+
'retries', 'prompt_tokens', 'completion_tokens', 'total_tokens', 'cost_usd', 'prompt',
|
|
606
|
+
] as const
|
|
288
607
|
|
|
289
608
|
if (extra) {
|
|
290
609
|
for (const field of extraFields) {
|
|
@@ -293,6 +612,10 @@ class ConvoyStoreImpl implements ConvoyStore {
|
|
|
293
612
|
params[field] = extra[field] as string | number | null
|
|
294
613
|
}
|
|
295
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
|
+
}
|
|
296
619
|
}
|
|
297
620
|
|
|
298
621
|
this.db
|
|
@@ -312,6 +635,88 @@ class ConvoyStoreImpl implements ConvoyStore {
|
|
|
312
635
|
})
|
|
313
636
|
}
|
|
314
637
|
|
|
638
|
+
insertTaskStep(record: Omit<TaskStepRecord, 'id'>): number {
|
|
639
|
+
this.db
|
|
640
|
+
.prepare(
|
|
641
|
+
`INSERT INTO task_step
|
|
642
|
+
(task_id, step_index, prompt, gates, status, exit_code, output, started_at, finished_at)
|
|
643
|
+
VALUES
|
|
644
|
+
(:task_id, :step_index, :prompt, :gates, :status, :exit_code, :output, :started_at, :finished_at)`,
|
|
645
|
+
)
|
|
646
|
+
.run(record)
|
|
647
|
+
const row = this.db.prepare('SELECT last_insert_rowid() AS id').get() as { id: number }
|
|
648
|
+
return row.id
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
updateTaskStep(
|
|
652
|
+
id: number,
|
|
653
|
+
fields: Partial<Pick<TaskStepRecord, 'status' | 'exit_code' | 'output' | 'started_at' | 'finished_at'>>,
|
|
654
|
+
): void {
|
|
655
|
+
const sets: string[] = []
|
|
656
|
+
const params: Record<string, string | number | null> = { id }
|
|
657
|
+
const stepFields = ['status', 'exit_code', 'output', 'started_at', 'finished_at'] as const
|
|
658
|
+
|
|
659
|
+
for (const field of stepFields) {
|
|
660
|
+
if (field in fields && fields[field] !== undefined) {
|
|
661
|
+
sets.push(`${field} = :${field}`)
|
|
662
|
+
params[field] = fields[field] as string | number | null
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (sets.length === 0) return
|
|
667
|
+
this.db.prepare(`UPDATE task_step SET ${sets.join(', ')} WHERE id = :id`).run(params)
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
updateTaskReview(
|
|
671
|
+
taskId: string,
|
|
672
|
+
convoyId: string,
|
|
673
|
+
fields: Partial<Pick<TaskRecord, 'review_level' | 'review_verdict' | 'review_tokens' | 'review_model' | 'panel_attempts' | 'dispute_id'>>,
|
|
674
|
+
): void {
|
|
675
|
+
const sets: string[] = []
|
|
676
|
+
const params: Record<string, string | number | null> = { id: taskId, convoy_id: convoyId }
|
|
677
|
+
const reviewFields = ['review_level', 'review_verdict', 'review_tokens', 'review_model', 'panel_attempts', 'dispute_id'] as const
|
|
678
|
+
|
|
679
|
+
for (const field of reviewFields) {
|
|
680
|
+
if (field in fields && fields[field] !== undefined) {
|
|
681
|
+
sets.push(`${field} = :${field}`)
|
|
682
|
+
params[field] = fields[field] as string | number | null
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (sets.length === 0) return
|
|
687
|
+
this.db.prepare(`UPDATE task SET ${sets.join(', ')} WHERE id = :id AND convoy_id = :convoy_id`).run(params)
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
updateTaskDrift(
|
|
691
|
+
taskId: string,
|
|
692
|
+
convoyId: string,
|
|
693
|
+
fields: Partial<Pick<TaskRecord, 'drift_score' | 'drift_retried'>>,
|
|
694
|
+
): void {
|
|
695
|
+
const sets: string[] = []
|
|
696
|
+
const params: Record<string, string | number | null> = { id: taskId, convoy_id: convoyId }
|
|
697
|
+
|
|
698
|
+
if (fields.drift_score !== undefined) {
|
|
699
|
+
sets.push('drift_score = :drift_score')
|
|
700
|
+
params.drift_score = fields.drift_score
|
|
701
|
+
}
|
|
702
|
+
if (fields.drift_retried !== undefined) {
|
|
703
|
+
sets.push('drift_retried = :drift_retried')
|
|
704
|
+
params.drift_retried = fields.drift_retried
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
if (sets.length === 0) return
|
|
708
|
+
this.db.prepare(`UPDATE task SET ${sets.join(', ')} WHERE id = :id AND convoy_id = :convoy_id`).run(params)
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
updateTaskDisputeStatus(taskId: string, convoyId: string, status: ConvoyTaskStatus, disputeId: string): void {
|
|
712
|
+
this.db
|
|
713
|
+
.prepare(
|
|
714
|
+
`UPDATE task SET status = :status, dispute_id = :dispute_id
|
|
715
|
+
WHERE id = :id AND convoy_id = :convoy_id`,
|
|
716
|
+
)
|
|
717
|
+
.run({ id: taskId, convoy_id: convoyId, status, dispute_id: disputeId })
|
|
718
|
+
}
|
|
719
|
+
|
|
315
720
|
insertWorker(record: Omit<WorkerRecord, 'finished_at' | 'last_heartbeat'>): void {
|
|
316
721
|
this.db
|
|
317
722
|
.prepare(
|
|
@@ -355,13 +760,16 @@ class ConvoyStoreImpl implements ConvoyStore {
|
|
|
355
760
|
this.db.prepare(`UPDATE worker SET ${sets.join(', ')} WHERE id = :id`).run(params)
|
|
356
761
|
}
|
|
357
762
|
|
|
358
|
-
insertEvent(record: Omit<EventRecord, 'id'>):
|
|
763
|
+
insertEvent(record: Omit<EventRecord, 'id'>): number {
|
|
764
|
+
enforceLimit(record.data, 'event.data', LIMIT_EVENT_DATA)
|
|
359
765
|
this.db
|
|
360
766
|
.prepare(
|
|
361
767
|
`INSERT INTO event (convoy_id, task_id, worker_id, type, data, created_at)
|
|
362
768
|
VALUES (:convoy_id, :task_id, :worker_id, :type, :data, :created_at)`,
|
|
363
769
|
)
|
|
364
770
|
.run(record)
|
|
771
|
+
const row = this.db.prepare('SELECT last_insert_rowid() AS id').get() as { id: number }
|
|
772
|
+
return row.id
|
|
365
773
|
}
|
|
366
774
|
|
|
367
775
|
getEvents(convoyId: string): EventRecord[] {
|
|
@@ -370,7 +778,166 @@ class ConvoyStoreImpl implements ConvoyStore {
|
|
|
370
778
|
.all({ convoy_id: convoyId }) as unknown as EventRecord[]
|
|
371
779
|
}
|
|
372
780
|
|
|
781
|
+
insertDlqEntry(record: DlqRecord): void {
|
|
782
|
+
this.db
|
|
783
|
+
.prepare(
|
|
784
|
+
`INSERT INTO dlq
|
|
785
|
+
(id, convoy_id, task_id, agent, failure_type, error_output, attempts,
|
|
786
|
+
tokens_spent, escalation_task_id, resolved, resolution, created_at, resolved_at)
|
|
787
|
+
VALUES
|
|
788
|
+
(:id, :convoy_id, :task_id, :agent, :failure_type, :error_output, :attempts,
|
|
789
|
+
:tokens_spent, :escalation_task_id, :resolved, :resolution, :created_at, :resolved_at)`,
|
|
790
|
+
)
|
|
791
|
+
.run(record as unknown as Record<string, string | number | null>)
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
listDlqEntries(convoyIdFilter?: string): DlqRecord[] {
|
|
795
|
+
if (convoyIdFilter) {
|
|
796
|
+
return this.db
|
|
797
|
+
.prepare('SELECT * FROM dlq WHERE convoy_id = :convoy_id ORDER BY created_at DESC')
|
|
798
|
+
.all({ convoy_id: convoyIdFilter }) as unknown as DlqRecord[]
|
|
799
|
+
}
|
|
800
|
+
return this.db
|
|
801
|
+
.prepare('SELECT * FROM dlq ORDER BY created_at DESC')
|
|
802
|
+
.all() as unknown as DlqRecord[]
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
resolveDlqEntry(id: string, resolution: string): void {
|
|
806
|
+
this.db
|
|
807
|
+
.prepare(
|
|
808
|
+
`UPDATE dlq SET resolved = 1, resolution = :resolution, resolved_at = :resolved_at
|
|
809
|
+
WHERE id = :id`,
|
|
810
|
+
)
|
|
811
|
+
.run({ id, resolution, resolved_at: new Date().toISOString() })
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
insertArtifact(record: ArtifactRecord): void {
|
|
815
|
+
const count = (
|
|
816
|
+
this.db
|
|
817
|
+
.prepare('SELECT COUNT(*) AS cnt FROM artifact WHERE convoy_id = :convoy_id')
|
|
818
|
+
.get({ convoy_id: record.convoy_id }) as { cnt: number }
|
|
819
|
+
).cnt
|
|
820
|
+
if (count >= 50) {
|
|
821
|
+
throw new ConvoyArtifactLimitError(record.convoy_id)
|
|
822
|
+
}
|
|
823
|
+
this.db
|
|
824
|
+
.prepare(
|
|
825
|
+
`INSERT INTO artifact (id, convoy_id, task_id, name, type, content, created_at)
|
|
826
|
+
VALUES (:id, :convoy_id, :task_id, :name, :type, :content, :created_at)`,
|
|
827
|
+
)
|
|
828
|
+
.run(record as unknown as Record<string, string | number | null>)
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
getArtifact(convoyId: string, name: string): ArtifactRecord | undefined {
|
|
832
|
+
return this.db
|
|
833
|
+
.prepare('SELECT * FROM artifact WHERE convoy_id = :convoy_id AND name = :name')
|
|
834
|
+
.get({ convoy_id: convoyId, name }) as ArtifactRecord | undefined
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
getArtifactsByTask(taskId: string): ArtifactRecord[] {
|
|
838
|
+
return this.db
|
|
839
|
+
.prepare('SELECT * FROM artifact WHERE task_id = :task_id ORDER BY created_at')
|
|
840
|
+
.all({ task_id: taskId }) as unknown as ArtifactRecord[]
|
|
841
|
+
}
|
|
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
|
+
|
|
849
|
+
deleteArtifactsOlderThan(days: number): number {
|
|
850
|
+
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString()
|
|
851
|
+
const result = this.db
|
|
852
|
+
.prepare(
|
|
853
|
+
`DELETE FROM artifact WHERE convoy_id IN (
|
|
854
|
+
SELECT id FROM convoy WHERE finished_at IS NOT NULL AND finished_at < :cutoff
|
|
855
|
+
)`,
|
|
856
|
+
)
|
|
857
|
+
.run({ cutoff })
|
|
858
|
+
return (result as unknown as { changes: number }).changes
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
insertAgentIdentity(record: AgentIdentityRecord): void {
|
|
862
|
+
const summarySize = Buffer.byteLength(record.summary, 'utf8')
|
|
863
|
+
const truncatedSummary = summarySize > LIMIT_SUMMARY
|
|
864
|
+
? record.summary.slice(0, LIMIT_SUMMARY)
|
|
865
|
+
: record.summary
|
|
866
|
+
this.db
|
|
867
|
+
.prepare(
|
|
868
|
+
`INSERT INTO agent_identity
|
|
869
|
+
(id, agent, convoy_id, task_id, summary, created_at, retention_days)
|
|
870
|
+
VALUES
|
|
871
|
+
(:id, :agent, :convoy_id, :task_id, :summary, :created_at, :retention_days)`,
|
|
872
|
+
)
|
|
873
|
+
.run({ ...record, summary: truncatedSummary } as unknown as Record<string, string | number | null>)
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
getAgentIdentities(agent: string, limit: number): AgentIdentityRecord[] {
|
|
877
|
+
return this.db
|
|
878
|
+
.prepare(
|
|
879
|
+
'SELECT * FROM agent_identity WHERE agent = :agent ORDER BY created_at DESC LIMIT :limit',
|
|
880
|
+
)
|
|
881
|
+
.all({ agent, limit }) as unknown as AgentIdentityRecord[]
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
listAgentIdentitySummary(): Array<{ agent: string; task_count: number; latest_date: string }> {
|
|
885
|
+
return this.db
|
|
886
|
+
.prepare(
|
|
887
|
+
`SELECT agent, COUNT(*) AS task_count, MAX(created_at) AS latest_date
|
|
888
|
+
FROM agent_identity GROUP BY agent ORDER BY agent`,
|
|
889
|
+
)
|
|
890
|
+
.all() as unknown as Array<{ agent: string; task_count: number; latest_date: string }>
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
purgeAgentIdentities(agent: string): number {
|
|
894
|
+
const result = this.db
|
|
895
|
+
.prepare('DELETE FROM agent_identity WHERE agent = :agent')
|
|
896
|
+
.run({ agent })
|
|
897
|
+
return (result as unknown as { changes: number }).changes
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
deleteAgentIdentitiesOlderThan(days: number): number {
|
|
901
|
+
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString()
|
|
902
|
+
const result = this.db
|
|
903
|
+
.prepare(
|
|
904
|
+
`DELETE FROM agent_identity
|
|
905
|
+
WHERE created_at < :cutoff
|
|
906
|
+
OR (retention_days IS NOT NULL
|
|
907
|
+
AND created_at < datetime('now', '-' || retention_days || ' days'))`,
|
|
908
|
+
)
|
|
909
|
+
.run({ cutoff })
|
|
910
|
+
return (result as unknown as { changes: number }).changes
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
getScratchpadValue(key: string): string | null {
|
|
914
|
+
const row = this.db
|
|
915
|
+
.prepare('SELECT value FROM scratchpad WHERE key = :key')
|
|
916
|
+
.get({ key }) as { value: string } | undefined
|
|
917
|
+
return row?.value ?? null
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
setScratchpadValue(key: string, value: string): void {
|
|
921
|
+
this.db
|
|
922
|
+
.prepare(
|
|
923
|
+
`INSERT INTO scratchpad (key, value, updated_at)
|
|
924
|
+
VALUES (:key, :value, :updated_at)
|
|
925
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`,
|
|
926
|
+
)
|
|
927
|
+
.run({ key, value, updated_at: new Date().toISOString() })
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
clearScratchpad(): void {
|
|
931
|
+
this.db.exec('DELETE FROM scratchpad')
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
clearScratchpadOlderThan(days: number): void {
|
|
935
|
+
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString()
|
|
936
|
+
this.db.prepare('DELETE FROM scratchpad WHERE updated_at < :cutoff').run({ cutoff })
|
|
937
|
+
}
|
|
938
|
+
|
|
373
939
|
insertPipeline(record: Omit<PipelineRecord, 'started_at' | 'finished_at' | 'total_tokens' | 'total_cost_usd'>): void {
|
|
940
|
+
enforceLimit(record.spec_yaml, 'pipeline.spec_yaml', LIMIT_SPEC_YAML)
|
|
374
941
|
this.db
|
|
375
942
|
.prepare(
|
|
376
943
|
`INSERT INTO pipeline (id, name, status, branch, spec_yaml, convoy_specs, created_at,
|
|
@@ -383,20 +950,20 @@ class ConvoyStoreImpl implements ConvoyStore {
|
|
|
383
950
|
|
|
384
951
|
getPipeline(id: string): PipelineRecord | undefined {
|
|
385
952
|
return this.db
|
|
386
|
-
.prepare('SELECT
|
|
953
|
+
.prepare('SELECT *, total_cost_usd_num AS total_cost_usd FROM pipeline WHERE id = :id')
|
|
387
954
|
.get({ id }) as PipelineRecord | undefined
|
|
388
955
|
}
|
|
389
956
|
|
|
390
957
|
getLatestPipeline(): PipelineRecord | undefined {
|
|
391
958
|
return this.db
|
|
392
|
-
.prepare('SELECT
|
|
959
|
+
.prepare('SELECT *, total_cost_usd_num AS total_cost_usd FROM pipeline ORDER BY created_at DESC LIMIT 1')
|
|
393
960
|
.get() as PipelineRecord | undefined
|
|
394
961
|
}
|
|
395
962
|
|
|
396
963
|
updatePipelineStatus(
|
|
397
964
|
id: string,
|
|
398
965
|
status: PipelineStatus,
|
|
399
|
-
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 },
|
|
400
967
|
): void {
|
|
401
968
|
const sets = ['status = :status']
|
|
402
969
|
const params: Record<string, string | number | null> = { id, status }
|
|
@@ -414,8 +981,10 @@ class ConvoyStoreImpl implements ConvoyStore {
|
|
|
414
981
|
params.total_tokens = extra.total_tokens
|
|
415
982
|
}
|
|
416
983
|
if (extra?.total_cost_usd !== undefined) {
|
|
417
|
-
sets.push('total_cost_usd = :
|
|
418
|
-
|
|
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
|
|
419
988
|
}
|
|
420
989
|
|
|
421
990
|
this.db.prepare(`UPDATE pipeline SET ${sets.join(', ')} WHERE id = :id`).run(params)
|
|
@@ -423,10 +992,221 @@ class ConvoyStoreImpl implements ConvoyStore {
|
|
|
423
992
|
|
|
424
993
|
getConvoysByPipeline(pipelineId: string): ConvoyRecord[] {
|
|
425
994
|
return this.db
|
|
426
|
-
.prepare('SELECT
|
|
995
|
+
.prepare('SELECT *, total_cost_usd_num AS total_cost_usd FROM convoy WHERE pipeline_id = :pipeline_id ORDER BY created_at')
|
|
427
996
|
.all({ pipeline_id: pipelineId }) as unknown as ConvoyRecord[]
|
|
428
997
|
}
|
|
429
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
|
+
|
|
430
1210
|
withTransaction<T>(fn: () => T): T {
|
|
431
1211
|
this.db.exec('BEGIN')
|
|
432
1212
|
try {
|
|
@@ -444,6 +1224,125 @@ class ConvoyStoreImpl implements ConvoyStore {
|
|
|
444
1224
|
}
|
|
445
1225
|
}
|
|
446
1226
|
|
|
1227
|
+
export function migrateSchema(db: DatabaseSync, dbPath: string, fromVersion: number, toVersion: number): void {
|
|
1228
|
+
for (let v = fromVersion; v < toVersion; v++) {
|
|
1229
|
+
const backupPath = `${dbPath}.v${v}.bak`
|
|
1230
|
+
copyFileSync(dbPath, backupPath)
|
|
1231
|
+
db.exec('BEGIN')
|
|
1232
|
+
try {
|
|
1233
|
+
if (v === 4) {
|
|
1234
|
+
db.exec(`
|
|
1235
|
+
ALTER TABLE task ADD COLUMN gates TEXT;
|
|
1236
|
+
ALTER TABLE task ADD COLUMN on_exhausted TEXT NOT NULL DEFAULT 'dlq';
|
|
1237
|
+
ALTER TABLE task ADD COLUMN injected INTEGER NOT NULL DEFAULT 0;
|
|
1238
|
+
ALTER TABLE task ADD COLUMN provenance TEXT;
|
|
1239
|
+
ALTER TABLE task ADD COLUMN idempotency_key TEXT;
|
|
1240
|
+
CREATE UNIQUE INDEX idx_task_idempotency ON task(convoy_id, idempotency_key)
|
|
1241
|
+
WHERE idempotency_key IS NOT NULL;
|
|
1242
|
+
ALTER TABLE convoy ADD COLUMN circuit_state TEXT;
|
|
1243
|
+
CREATE TABLE dlq (
|
|
1244
|
+
id TEXT PRIMARY KEY,
|
|
1245
|
+
convoy_id TEXT NOT NULL REFERENCES convoy(id),
|
|
1246
|
+
task_id TEXT NOT NULL REFERENCES task(id),
|
|
1247
|
+
agent TEXT NOT NULL,
|
|
1248
|
+
failure_type TEXT NOT NULL,
|
|
1249
|
+
error_output TEXT,
|
|
1250
|
+
attempts INTEGER NOT NULL,
|
|
1251
|
+
tokens_spent INTEGER,
|
|
1252
|
+
escalation_task_id TEXT,
|
|
1253
|
+
resolved INTEGER NOT NULL DEFAULT 0,
|
|
1254
|
+
resolution TEXT,
|
|
1255
|
+
created_at TEXT NOT NULL,
|
|
1256
|
+
resolved_at TEXT
|
|
1257
|
+
);
|
|
1258
|
+
`)
|
|
1259
|
+
}
|
|
1260
|
+
if (v === 5) {
|
|
1261
|
+
db.exec(`
|
|
1262
|
+
ALTER TABLE task ADD COLUMN current_step INTEGER;
|
|
1263
|
+
ALTER TABLE task ADD COLUMN total_steps INTEGER;
|
|
1264
|
+
ALTER TABLE task ADD COLUMN review_level TEXT;
|
|
1265
|
+
ALTER TABLE task ADD COLUMN review_verdict TEXT;
|
|
1266
|
+
ALTER TABLE task ADD COLUMN review_tokens INTEGER;
|
|
1267
|
+
ALTER TABLE task ADD COLUMN review_model TEXT;
|
|
1268
|
+
ALTER TABLE task ADD COLUMN panel_attempts INTEGER NOT NULL DEFAULT 0;
|
|
1269
|
+
ALTER TABLE task ADD COLUMN dispute_id TEXT;
|
|
1270
|
+
ALTER TABLE convoy ADD COLUMN review_tokens_total INTEGER;
|
|
1271
|
+
ALTER TABLE convoy ADD COLUMN review_budget INTEGER;
|
|
1272
|
+
CREATE TABLE task_step (
|
|
1273
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1274
|
+
task_id TEXT NOT NULL REFERENCES task(id),
|
|
1275
|
+
step_index INTEGER NOT NULL,
|
|
1276
|
+
prompt TEXT NOT NULL,
|
|
1277
|
+
gates TEXT,
|
|
1278
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
1279
|
+
exit_code INTEGER,
|
|
1280
|
+
output TEXT,
|
|
1281
|
+
started_at TEXT,
|
|
1282
|
+
finished_at TEXT
|
|
1283
|
+
);
|
|
1284
|
+
`)
|
|
1285
|
+
}
|
|
1286
|
+
if (v === 6) {
|
|
1287
|
+
db.exec(`
|
|
1288
|
+
ALTER TABLE task ADD COLUMN drift_score REAL;
|
|
1289
|
+
ALTER TABLE task ADD COLUMN drift_retried INTEGER NOT NULL DEFAULT 0;
|
|
1290
|
+
`)
|
|
1291
|
+
}
|
|
1292
|
+
if (v === 7) {
|
|
1293
|
+
db.exec(`
|
|
1294
|
+
ALTER TABLE task ADD COLUMN outputs TEXT;
|
|
1295
|
+
ALTER TABLE task ADD COLUMN inputs TEXT;
|
|
1296
|
+
ALTER TABLE task ADD COLUMN discovered_issues TEXT;
|
|
1297
|
+
CREATE TABLE artifact (
|
|
1298
|
+
id TEXT PRIMARY KEY,
|
|
1299
|
+
convoy_id TEXT NOT NULL REFERENCES convoy(id),
|
|
1300
|
+
task_id TEXT NOT NULL REFERENCES task(id),
|
|
1301
|
+
name TEXT NOT NULL,
|
|
1302
|
+
type TEXT NOT NULL,
|
|
1303
|
+
content TEXT NOT NULL CHECK (length(content) <= 1048576),
|
|
1304
|
+
created_at TEXT NOT NULL,
|
|
1305
|
+
UNIQUE(convoy_id, name)
|
|
1306
|
+
);
|
|
1307
|
+
CREATE TABLE agent_identity (
|
|
1308
|
+
id TEXT PRIMARY KEY,
|
|
1309
|
+
agent TEXT NOT NULL,
|
|
1310
|
+
convoy_id TEXT NOT NULL,
|
|
1311
|
+
task_id TEXT NOT NULL,
|
|
1312
|
+
summary TEXT NOT NULL,
|
|
1313
|
+
created_at TEXT NOT NULL,
|
|
1314
|
+
retention_days INTEGER NOT NULL DEFAULT 90
|
|
1315
|
+
);
|
|
1316
|
+
`)
|
|
1317
|
+
}
|
|
1318
|
+
if (v === 8) {
|
|
1319
|
+
db.exec(`
|
|
1320
|
+
CREATE TABLE scratchpad (
|
|
1321
|
+
key TEXT PRIMARY KEY,
|
|
1322
|
+
value TEXT NOT NULL,
|
|
1323
|
+
updated_at TEXT NOT NULL
|
|
1324
|
+
);
|
|
1325
|
+
`)
|
|
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
|
+
}
|
|
1337
|
+
db.exec('COMMIT')
|
|
1338
|
+
} catch (err) {
|
|
1339
|
+
try { db.exec('ROLLBACK') } catch { /* ignore */ }
|
|
1340
|
+
throw new Error(`Migration v${v}→v${v + 1} failed. Backup at ${backupPath}. Original error: ${(err as Error).message}`)
|
|
1341
|
+
}
|
|
1342
|
+
db.exec(`PRAGMA user_version = ${v + 1}`)
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
|
|
447
1346
|
export function createConvoyStore(dbPath: string): ConvoyStore {
|
|
448
1347
|
return new ConvoyStoreImpl(dbPath)
|
|
449
1348
|
}
|