opencastle 0.26.1 → 0.27.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- package/bin/cli.mjs +10 -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/engine.d.ts +68 -2
- package/dist/cli/convoy/engine.d.ts.map +1 -1
- package/dist/cli/convoy/engine.js +2102 -26
- package/dist/cli/convoy/engine.js.map +1 -1
- package/dist/cli/convoy/engine.test.js +1572 -70
- package/dist/cli/convoy/engine.test.js.map +1 -1
- package/dist/cli/convoy/events.d.ts +4 -1
- package/dist/cli/convoy/events.d.ts.map +1 -1
- package/dist/cli/convoy/events.js +74 -13
- package/dist/cli/convoy/events.js.map +1 -1
- package/dist/cli/convoy/events.test.js +154 -27
- 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/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 +47 -5
- package/dist/cli/convoy/store.d.ts.map +1 -1
- package/dist/cli/convoy/store.js +525 -19
- package/dist/cli/convoy/store.js.map +1 -1
- package/dist/cli/convoy/store.test.js +1345 -12
- package/dist/cli/convoy/store.test.js.map +1 -1
- package/dist/cli/convoy/types.d.ts +156 -2
- package/dist/cli/convoy/types.d.ts.map +1 -1
- package/dist/cli/destroy.d.ts +3 -0
- package/dist/cli/destroy.d.ts.map +1 -0
- package/dist/cli/destroy.js +69 -0
- package/dist/cli/destroy.js.map +1 -0
- package/dist/cli/destroy.test.d.ts +2 -0
- package/dist/cli/destroy.test.d.ts.map +1 -0
- package/dist/cli/destroy.test.js +116 -0
- package/dist/cli/destroy.test.js.map +1 -0
- package/dist/cli/gitignore.d.ts +9 -0
- package/dist/cli/gitignore.d.ts.map +1 -1
- package/dist/cli/gitignore.js +29 -0
- package/dist/cli/gitignore.js.map +1 -1
- package/dist/cli/plan.d.ts +3 -0
- package/dist/cli/plan.d.ts.map +1 -0
- package/dist/cli/plan.js +288 -0
- package/dist/cli/plan.js.map +1 -0
- 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 +1 -1
- package/src/cli/agents.ts +177 -0
- package/src/cli/baselines.ts +143 -0
- package/src/cli/convoy/engine.test.ts +1839 -70
- package/src/cli/convoy/engine.ts +2417 -38
- package/src/cli/convoy/events.test.ts +179 -38
- package/src/cli/convoy/events.ts +88 -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/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 +1512 -14
- package/src/cli/convoy/store.ts +676 -30
- package/src/cli/convoy/types.ts +170 -1
- package/src/cli/destroy.test.ts +141 -0
- package/src/cli/destroy.ts +88 -0
- package/src/cli/gitignore.ts +36 -0
- package/src/cli/plan.ts +316 -0
- 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/node_modules/.vite/deps/_metadata.json +6 -6
package/src/cli/convoy/store.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { copyFileSync } from 'node:fs'
|
|
1
2
|
import { DatabaseSync } from 'node:sqlite'
|
|
2
3
|
import type {
|
|
3
4
|
ConvoyRecord,
|
|
@@ -9,12 +10,61 @@ import type {
|
|
|
9
10
|
EventRecord,
|
|
10
11
|
PipelineRecord,
|
|
11
12
|
PipelineStatus,
|
|
13
|
+
DlqRecord,
|
|
14
|
+
ArtifactRecord,
|
|
15
|
+
AgentIdentityRecord,
|
|
16
|
+
TaskStepRecord,
|
|
12
17
|
} from './types.js'
|
|
13
18
|
|
|
14
|
-
const SCHEMA_VERSION =
|
|
19
|
+
const SCHEMA_VERSION = 9
|
|
20
|
+
|
|
21
|
+
// ── Size limits (bytes) ────────────────────────────────────────────────────────
|
|
22
|
+
const LIMIT_SPEC_YAML = 256 * 1024 // 256 KB
|
|
23
|
+
const LIMIT_OUTPUT = 1024 * 1024 // 1 MB (head 512KB + tail 512KB)
|
|
24
|
+
const LIMIT_OUTPUT_HALF = 512 * 1024 // 512 KB per half
|
|
25
|
+
const LIMIT_EVENT_DATA = 64 * 1024 // 64 KB
|
|
26
|
+
const LIMIT_SUMMARY = 4096 // 4 KB
|
|
27
|
+
|
|
28
|
+
export class FieldSizeLimitError extends Error {
|
|
29
|
+
constructor(field: string, actual: number, limit: number) {
|
|
30
|
+
super(`Field "${field}" exceeds size limit: ${actual} bytes > ${limit} bytes`)
|
|
31
|
+
this.name = 'FieldSizeLimitError'
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function enforceLimit(value: string | null | undefined, field: string, limit: number): void {
|
|
36
|
+
if (value == null) return
|
|
37
|
+
const size = Buffer.byteLength(value, 'utf8')
|
|
38
|
+
if (size > limit) {
|
|
39
|
+
throw new FieldSizeLimitError(field, size, limit)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function truncateOutput(value: string | null | undefined): string | null {
|
|
44
|
+
if (value == null) return null
|
|
45
|
+
const size = Buffer.byteLength(value, 'utf8')
|
|
46
|
+
if (size <= LIMIT_OUTPUT) return value
|
|
47
|
+
// Head + tail truncation with marker
|
|
48
|
+
const head = value.slice(0, LIMIT_OUTPUT_HALF)
|
|
49
|
+
const tail = value.slice(-LIMIT_OUTPUT_HALF)
|
|
50
|
+
return head + '\n\n... [truncated: ' + size + ' bytes total, showing first/last 512KB] ...\n\n' + tail
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class ConvoyArtifactLimitError extends Error {
|
|
54
|
+
constructor(convoyId: string) {
|
|
55
|
+
super(`Convoy ${convoyId} has reached the maximum of 50 artifacts`)
|
|
56
|
+
this.name = 'ConvoyArtifactLimitError'
|
|
57
|
+
}
|
|
58
|
+
}
|
|
15
59
|
|
|
16
60
|
export interface ConvoyStore {
|
|
17
|
-
insertConvoy(
|
|
61
|
+
insertConvoy(
|
|
62
|
+
record: Omit<
|
|
63
|
+
ConvoyRecord,
|
|
64
|
+
| 'started_at' | 'finished_at' | 'total_tokens' | 'total_cost_usd'
|
|
65
|
+
| 'pipeline_id' | 'circuit_state' | 'review_tokens_total' | 'review_budget'
|
|
66
|
+
> & { pipeline_id?: string | null },
|
|
67
|
+
): void
|
|
18
68
|
getConvoy(id: string): ConvoyRecord | undefined
|
|
19
69
|
getLatestConvoy(): ConvoyRecord | undefined
|
|
20
70
|
updateConvoyStatus(
|
|
@@ -22,23 +72,54 @@ export interface ConvoyStore {
|
|
|
22
72
|
status: ConvoyStatus,
|
|
23
73
|
extra?: { started_at?: string; finished_at?: string; total_tokens?: number | null; total_cost_usd?: string | null },
|
|
24
74
|
): void
|
|
75
|
+
updateConvoyReviewTokens(convoyId: string, tokens: number): void
|
|
76
|
+
updateConvoyCircuitState(convoyId: string, state: string | null): void
|
|
25
77
|
insertTask(
|
|
26
78
|
record: Omit<
|
|
27
79
|
TaskRecord,
|
|
28
|
-
'worker_id' | 'worktree' | 'output' | 'exit_code' | 'started_at' | 'finished_at'
|
|
29
|
-
|
|
80
|
+
| 'worker_id' | 'worktree' | 'output' | 'exit_code' | 'started_at' | 'finished_at'
|
|
81
|
+
| 'prompt_tokens' | 'completion_tokens' | 'total_tokens' | 'cost_usd'
|
|
82
|
+
| 'on_exhausted' | 'injected' | 'provenance' | 'idempotency_key'
|
|
83
|
+
| 'current_step' | 'total_steps' | 'review_level' | 'review_verdict'
|
|
84
|
+
| 'review_tokens' | 'review_model' | 'panel_attempts' | 'dispute_id'
|
|
85
|
+
| 'drift_score' | 'drift_retried' | 'discovered_issues'
|
|
86
|
+
> & { outputs?: string | null; inputs?: string | null },
|
|
30
87
|
): void
|
|
88
|
+
insertInjectedTask(record: TaskRecord): void
|
|
31
89
|
getTask(id: string, convoyId: string): TaskRecord | undefined
|
|
32
90
|
getTasksByConvoy(convoyId: string): TaskRecord[]
|
|
91
|
+
getTaskByIdempotencyKey(convoyId: string, key: string): TaskRecord | undefined
|
|
92
|
+
getTaskByDisputeId(disputeId: string): TaskRecord | undefined
|
|
93
|
+
getDisputedTasks(convoyId?: string): TaskRecord[]
|
|
33
94
|
updateTaskStatus(
|
|
34
95
|
id: string,
|
|
35
96
|
convoyId: string,
|
|
36
97
|
status: ConvoyTaskStatus,
|
|
37
98
|
extra?: Partial<
|
|
38
|
-
Pick<
|
|
99
|
+
Pick<
|
|
100
|
+
TaskRecord,
|
|
101
|
+
| 'worker_id' | 'worktree' | 'output' | 'exit_code' | 'started_at' | 'finished_at'
|
|
102
|
+
| 'retries' | 'prompt_tokens' | 'completion_tokens' | 'total_tokens' | 'cost_usd' | 'prompt'
|
|
103
|
+
>
|
|
39
104
|
>,
|
|
40
105
|
): void
|
|
106
|
+
updateTaskReview(
|
|
107
|
+
taskId: string,
|
|
108
|
+
convoyId: string,
|
|
109
|
+
fields: Partial<Pick<TaskRecord, 'review_level' | 'review_verdict' | 'review_tokens' | 'review_model' | 'panel_attempts' | 'dispute_id'>>,
|
|
110
|
+
): void
|
|
111
|
+
updateTaskDrift(
|
|
112
|
+
taskId: string,
|
|
113
|
+
convoyId: string,
|
|
114
|
+
fields: Partial<Pick<TaskRecord, 'drift_score' | 'drift_retried'>>,
|
|
115
|
+
): void
|
|
116
|
+
updateTaskDisputeStatus(taskId: string, convoyId: string, status: ConvoyTaskStatus, disputeId: string): void
|
|
41
117
|
getReadyTasks(convoyId: string): TaskRecord[]
|
|
118
|
+
insertTaskStep(record: Omit<TaskStepRecord, 'id'>): number
|
|
119
|
+
updateTaskStep(
|
|
120
|
+
id: number,
|
|
121
|
+
fields: Partial<Pick<TaskStepRecord, 'status' | 'exit_code' | 'output' | 'started_at' | 'finished_at'>>,
|
|
122
|
+
): void
|
|
42
123
|
insertWorker(record: Omit<WorkerRecord, 'finished_at' | 'last_heartbeat'>): void
|
|
43
124
|
getWorker(id: string): WorkerRecord | undefined
|
|
44
125
|
updateWorkerStatus(
|
|
@@ -46,8 +127,24 @@ export interface ConvoyStore {
|
|
|
46
127
|
status: WorkerStatus,
|
|
47
128
|
extra?: Partial<Pick<WorkerRecord, 'finished_at' | 'last_heartbeat' | 'pid'>>,
|
|
48
129
|
): void
|
|
49
|
-
insertEvent(record: Omit<EventRecord, 'id'>):
|
|
130
|
+
insertEvent(record: Omit<EventRecord, 'id'>): number
|
|
50
131
|
getEvents(convoyId: string): EventRecord[]
|
|
132
|
+
insertDlqEntry(record: DlqRecord): void
|
|
133
|
+
listDlqEntries(convoyIdFilter?: string): DlqRecord[]
|
|
134
|
+
resolveDlqEntry(id: string, resolution: string): void
|
|
135
|
+
insertArtifact(record: ArtifactRecord): void
|
|
136
|
+
getArtifact(convoyId: string, name: string): ArtifactRecord | undefined
|
|
137
|
+
getArtifactsByTask(taskId: string): ArtifactRecord[]
|
|
138
|
+
deleteArtifactsOlderThan(days: number): number
|
|
139
|
+
insertAgentIdentity(record: AgentIdentityRecord): void
|
|
140
|
+
getAgentIdentities(agent: string, limit: number): AgentIdentityRecord[]
|
|
141
|
+
listAgentIdentitySummary(): Array<{ agent: string; task_count: number; latest_date: string }>
|
|
142
|
+
purgeAgentIdentities(agent: string): number
|
|
143
|
+
deleteAgentIdentitiesOlderThan(days: number): number
|
|
144
|
+
getScratchpadValue(key: string): string | null
|
|
145
|
+
setScratchpadValue(key: string, value: string): void
|
|
146
|
+
clearScratchpad(): void
|
|
147
|
+
clearScratchpadOlderThan(days: number): void
|
|
51
148
|
insertPipeline(record: Omit<PipelineRecord, 'started_at' | 'finished_at' | 'total_tokens' | 'total_cost_usd'>): void
|
|
52
149
|
getPipeline(id: string): PipelineRecord | undefined
|
|
53
150
|
getLatestPipeline(): PipelineRecord | undefined
|
|
@@ -63,8 +160,10 @@ export interface ConvoyStore {
|
|
|
63
160
|
|
|
64
161
|
class ConvoyStoreImpl implements ConvoyStore {
|
|
65
162
|
private db: DatabaseSync
|
|
163
|
+
private dbPath: string
|
|
66
164
|
|
|
67
165
|
constructor(dbPath: string) {
|
|
166
|
+
this.dbPath = dbPath
|
|
68
167
|
this.db = new DatabaseSync(dbPath)
|
|
69
168
|
this.db.exec('PRAGMA journal_mode = WAL')
|
|
70
169
|
this.db.exec('PRAGMA synchronous = NORMAL')
|
|
@@ -76,18 +175,21 @@ class ConvoyStoreImpl implements ConvoyStore {
|
|
|
76
175
|
if (version === 0) {
|
|
77
176
|
this.db.exec(`
|
|
78
177
|
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
|
-
pipeline_id
|
|
178
|
+
id TEXT PRIMARY KEY,
|
|
179
|
+
name TEXT NOT NULL,
|
|
180
|
+
spec_hash TEXT NOT NULL,
|
|
181
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
182
|
+
branch TEXT,
|
|
183
|
+
created_at TEXT NOT NULL,
|
|
184
|
+
started_at TEXT,
|
|
185
|
+
finished_at TEXT,
|
|
186
|
+
spec_yaml TEXT NOT NULL,
|
|
187
|
+
total_tokens INTEGER,
|
|
188
|
+
total_cost_usd TEXT,
|
|
189
|
+
pipeline_id TEXT,
|
|
190
|
+
circuit_state TEXT,
|
|
191
|
+
review_tokens_total INTEGER,
|
|
192
|
+
review_budget INTEGER
|
|
91
193
|
);
|
|
92
194
|
|
|
93
195
|
CREATE TABLE IF NOT EXISTS pipeline (
|
|
@@ -127,7 +229,41 @@ class ConvoyStoreImpl implements ConvoyStore {
|
|
|
127
229
|
prompt_tokens INTEGER,
|
|
128
230
|
completion_tokens INTEGER,
|
|
129
231
|
total_tokens INTEGER,
|
|
130
|
-
cost_usd TEXT
|
|
232
|
+
cost_usd TEXT,
|
|
233
|
+
gates TEXT,
|
|
234
|
+
on_exhausted TEXT NOT NULL DEFAULT 'dlq',
|
|
235
|
+
injected INTEGER NOT NULL DEFAULT 0,
|
|
236
|
+
provenance TEXT,
|
|
237
|
+
idempotency_key TEXT,
|
|
238
|
+
current_step INTEGER,
|
|
239
|
+
total_steps INTEGER,
|
|
240
|
+
review_level TEXT,
|
|
241
|
+
review_verdict TEXT,
|
|
242
|
+
review_tokens INTEGER,
|
|
243
|
+
review_model TEXT,
|
|
244
|
+
panel_attempts INTEGER NOT NULL DEFAULT 0,
|
|
245
|
+
dispute_id TEXT,
|
|
246
|
+
drift_score REAL,
|
|
247
|
+
drift_retried INTEGER NOT NULL DEFAULT 0,
|
|
248
|
+
outputs TEXT,
|
|
249
|
+
inputs TEXT,
|
|
250
|
+
discovered_issues TEXT
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_task_idempotency ON task(convoy_id, idempotency_key)
|
|
254
|
+
WHERE idempotency_key IS NOT NULL;
|
|
255
|
+
|
|
256
|
+
CREATE TABLE IF NOT EXISTS task_step (
|
|
257
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
258
|
+
task_id TEXT NOT NULL REFERENCES task(id),
|
|
259
|
+
step_index INTEGER NOT NULL,
|
|
260
|
+
prompt TEXT NOT NULL,
|
|
261
|
+
gates TEXT,
|
|
262
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
263
|
+
exit_code INTEGER,
|
|
264
|
+
output TEXT,
|
|
265
|
+
started_at TEXT,
|
|
266
|
+
finished_at TEXT
|
|
131
267
|
);
|
|
132
268
|
|
|
133
269
|
CREATE TABLE IF NOT EXISTS worker (
|
|
@@ -152,6 +288,57 @@ class ConvoyStoreImpl implements ConvoyStore {
|
|
|
152
288
|
data TEXT,
|
|
153
289
|
created_at TEXT NOT NULL
|
|
154
290
|
);
|
|
291
|
+
|
|
292
|
+
CREATE TABLE IF NOT EXISTS dlq (
|
|
293
|
+
id TEXT PRIMARY KEY,
|
|
294
|
+
convoy_id TEXT NOT NULL REFERENCES convoy(id),
|
|
295
|
+
task_id TEXT NOT NULL REFERENCES task(id),
|
|
296
|
+
agent TEXT NOT NULL,
|
|
297
|
+
failure_type TEXT NOT NULL,
|
|
298
|
+
error_output TEXT,
|
|
299
|
+
attempts INTEGER NOT NULL,
|
|
300
|
+
tokens_spent INTEGER,
|
|
301
|
+
escalation_task_id TEXT,
|
|
302
|
+
resolved INTEGER NOT NULL DEFAULT 0,
|
|
303
|
+
resolution TEXT,
|
|
304
|
+
created_at TEXT NOT NULL,
|
|
305
|
+
resolved_at TEXT
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
CREATE TABLE IF NOT EXISTS artifact (
|
|
309
|
+
id TEXT PRIMARY KEY,
|
|
310
|
+
convoy_id TEXT NOT NULL REFERENCES convoy(id),
|
|
311
|
+
task_id TEXT NOT NULL REFERENCES task(id),
|
|
312
|
+
name TEXT NOT NULL,
|
|
313
|
+
type TEXT NOT NULL,
|
|
314
|
+
content TEXT NOT NULL CHECK (length(content) <= 1048576),
|
|
315
|
+
created_at TEXT NOT NULL,
|
|
316
|
+
UNIQUE(convoy_id, name)
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
CREATE TABLE IF NOT EXISTS agent_identity (
|
|
320
|
+
id TEXT PRIMARY KEY,
|
|
321
|
+
agent TEXT NOT NULL,
|
|
322
|
+
convoy_id TEXT NOT NULL,
|
|
323
|
+
task_id TEXT NOT NULL,
|
|
324
|
+
summary TEXT NOT NULL,
|
|
325
|
+
created_at TEXT NOT NULL,
|
|
326
|
+
retention_days INTEGER NOT NULL DEFAULT 90
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
CREATE TABLE IF NOT EXISTS scratchpad (
|
|
330
|
+
key TEXT PRIMARY KEY,
|
|
331
|
+
value TEXT NOT NULL,
|
|
332
|
+
updated_at TEXT NOT NULL
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
CREATE TABLE IF NOT EXISTS engine_lock (
|
|
336
|
+
id INTEGER PRIMARY KEY,
|
|
337
|
+
pid INTEGER NOT NULL,
|
|
338
|
+
hostname TEXT NOT NULL,
|
|
339
|
+
started_at TEXT NOT NULL,
|
|
340
|
+
last_heartbeat TEXT NOT NULL
|
|
341
|
+
);
|
|
155
342
|
`)
|
|
156
343
|
this.db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`)
|
|
157
344
|
version = SCHEMA_VERSION
|
|
@@ -191,13 +378,44 @@ class ConvoyStoreImpl implements ConvoyStore {
|
|
|
191
378
|
this.db.exec('PRAGMA user_version = 4')
|
|
192
379
|
version = 4
|
|
193
380
|
}
|
|
381
|
+
if (version === 4) {
|
|
382
|
+
migrateSchema(this.db, this.dbPath, 4, 5)
|
|
383
|
+
version = 5
|
|
384
|
+
}
|
|
385
|
+
if (version === 5) {
|
|
386
|
+
migrateSchema(this.db, this.dbPath, 5, 6)
|
|
387
|
+
version = 6
|
|
388
|
+
}
|
|
389
|
+
if (version === 6) {
|
|
390
|
+
migrateSchema(this.db, this.dbPath, 6, 7)
|
|
391
|
+
version = 7
|
|
392
|
+
}
|
|
393
|
+
if (version === 7) {
|
|
394
|
+
migrateSchema(this.db, this.dbPath, 7, 8)
|
|
395
|
+
version = 8
|
|
396
|
+
}
|
|
397
|
+
if (version === 8) {
|
|
398
|
+
migrateSchema(this.db, this.dbPath, 8, 9)
|
|
399
|
+
version = 9
|
|
400
|
+
}
|
|
194
401
|
}
|
|
195
402
|
|
|
196
|
-
insertConvoy(
|
|
403
|
+
insertConvoy(
|
|
404
|
+
record: Omit<
|
|
405
|
+
ConvoyRecord,
|
|
406
|
+
| 'started_at' | 'finished_at' | 'total_tokens' | 'total_cost_usd'
|
|
407
|
+
| 'pipeline_id' | 'circuit_state' | 'review_tokens_total' | 'review_budget'
|
|
408
|
+
> & { pipeline_id?: string | null },
|
|
409
|
+
): void {
|
|
410
|
+
enforceLimit(record.spec_yaml, 'spec_yaml', LIMIT_SPEC_YAML)
|
|
197
411
|
this.db
|
|
198
412
|
.prepare(
|
|
199
|
-
`INSERT INTO convoy
|
|
200
|
-
|
|
413
|
+
`INSERT INTO convoy
|
|
414
|
+
(id, name, spec_hash, status, branch, created_at, started_at, finished_at,
|
|
415
|
+
spec_yaml, pipeline_id)
|
|
416
|
+
VALUES
|
|
417
|
+
(:id, :name, :spec_hash, :status, :branch, :created_at, NULL, NULL,
|
|
418
|
+
:spec_yaml, :pipeline_id)`,
|
|
201
419
|
)
|
|
202
420
|
.run({ ...record, pipeline_id: record.pipeline_id ?? null })
|
|
203
421
|
}
|
|
@@ -242,24 +460,72 @@ class ConvoyStoreImpl implements ConvoyStore {
|
|
|
242
460
|
this.db.prepare(`UPDATE convoy SET ${sets.join(', ')} WHERE id = :id`).run(params)
|
|
243
461
|
}
|
|
244
462
|
|
|
463
|
+
updateConvoyReviewTokens(convoyId: string, tokens: number): void {
|
|
464
|
+
this.db
|
|
465
|
+
.prepare(
|
|
466
|
+
`UPDATE convoy
|
|
467
|
+
SET review_tokens_total = :tokens
|
|
468
|
+
WHERE id = :id`,
|
|
469
|
+
)
|
|
470
|
+
.run({ id: convoyId, tokens })
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
updateConvoyCircuitState(convoyId: string, state: string | null): void {
|
|
474
|
+
this.db
|
|
475
|
+
.prepare('UPDATE convoy SET circuit_state = :state WHERE id = :id')
|
|
476
|
+
.run({ id: convoyId, state: state ?? null })
|
|
477
|
+
}
|
|
478
|
+
|
|
245
479
|
insertTask(
|
|
246
480
|
record: Omit<
|
|
247
481
|
TaskRecord,
|
|
248
|
-
'worker_id' | 'worktree' | 'output' | 'exit_code' | 'started_at' | 'finished_at'
|
|
249
|
-
|
|
482
|
+
| 'worker_id' | 'worktree' | 'output' | 'exit_code' | 'started_at' | 'finished_at'
|
|
483
|
+
| 'prompt_tokens' | 'completion_tokens' | 'total_tokens' | 'cost_usd'
|
|
484
|
+
| 'on_exhausted' | 'injected' | 'provenance' | 'idempotency_key'
|
|
485
|
+
| 'current_step' | 'total_steps' | 'review_level' | 'review_verdict'
|
|
486
|
+
| 'review_tokens' | 'review_model' | 'panel_attempts' | 'dispute_id'
|
|
487
|
+
| 'drift_score' | 'drift_retried' | 'discovered_issues'
|
|
488
|
+
> & { outputs?: string | null; inputs?: string | null },
|
|
250
489
|
): void {
|
|
251
490
|
this.db
|
|
252
491
|
.prepare(
|
|
253
492
|
`INSERT INTO task
|
|
254
493
|
(id, convoy_id, phase, prompt, agent, adapter, model, timeout_ms, status,
|
|
255
494
|
worker_id, worktree, output, exit_code, started_at, finished_at,
|
|
256
|
-
retries, max_retries, files, depends_on
|
|
495
|
+
retries, max_retries, files, depends_on, gates,
|
|
496
|
+
on_exhausted, injected, provenance, idempotency_key,
|
|
497
|
+
outputs, inputs)
|
|
257
498
|
VALUES
|
|
258
499
|
(:id, :convoy_id, :phase, :prompt, :agent, :adapter, :model, :timeout_ms, :status,
|
|
259
500
|
NULL, NULL, NULL, NULL, NULL, NULL,
|
|
260
|
-
:retries, :max_retries, :files, :depends_on
|
|
501
|
+
:retries, :max_retries, :files, :depends_on, :gates,
|
|
502
|
+
'dlq', 0, NULL, NULL,
|
|
503
|
+
:outputs, :inputs)`,
|
|
261
504
|
)
|
|
262
|
-
.run(record)
|
|
505
|
+
.run({ ...record, outputs: record.outputs ?? null, inputs: record.inputs ?? null })
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
insertInjectedTask(record: TaskRecord): void {
|
|
509
|
+
this.db
|
|
510
|
+
.prepare(
|
|
511
|
+
`INSERT INTO task
|
|
512
|
+
(id, convoy_id, phase, prompt, agent, adapter, model, timeout_ms, status,
|
|
513
|
+
worker_id, worktree, output, exit_code, started_at, finished_at,
|
|
514
|
+
retries, max_retries, files, depends_on, gates,
|
|
515
|
+
on_exhausted, injected, provenance, idempotency_key,
|
|
516
|
+
current_step, total_steps, review_level, review_verdict,
|
|
517
|
+
review_tokens, review_model, panel_attempts, dispute_id,
|
|
518
|
+
drift_score, drift_retried, outputs, inputs, discovered_issues)
|
|
519
|
+
VALUES
|
|
520
|
+
(:id, :convoy_id, :phase, :prompt, :agent, :adapter, :model, :timeout_ms, :status,
|
|
521
|
+
:worker_id, :worktree, :output, :exit_code, :started_at, :finished_at,
|
|
522
|
+
:retries, :max_retries, :files, :depends_on, :gates,
|
|
523
|
+
:on_exhausted, :injected, :provenance, :idempotency_key,
|
|
524
|
+
:current_step, :total_steps, :review_level, :review_verdict,
|
|
525
|
+
:review_tokens, :review_model, :panel_attempts, :dispute_id,
|
|
526
|
+
:drift_score, :drift_retried, :outputs, :inputs, :discovered_issues)`,
|
|
527
|
+
)
|
|
528
|
+
.run(record as unknown as Record<string, string | number | null>)
|
|
263
529
|
}
|
|
264
530
|
|
|
265
531
|
getTask(id: string, convoyId: string): TaskRecord | undefined {
|
|
@@ -274,17 +540,50 @@ class ConvoyStoreImpl implements ConvoyStore {
|
|
|
274
540
|
.all({ convoy_id: convoyId }) as unknown as TaskRecord[]
|
|
275
541
|
}
|
|
276
542
|
|
|
543
|
+
getTaskByIdempotencyKey(convoyId: string, key: string): TaskRecord | undefined {
|
|
544
|
+
return this.db
|
|
545
|
+
.prepare('SELECT * FROM task WHERE convoy_id = :convoy_id AND idempotency_key = :key')
|
|
546
|
+
.get({ convoy_id: convoyId, key }) as TaskRecord | undefined
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
getTaskByDisputeId(disputeId: string): TaskRecord | undefined {
|
|
550
|
+
return this.db
|
|
551
|
+
.prepare('SELECT * FROM task WHERE dispute_id = :dispute_id LIMIT 1')
|
|
552
|
+
.get({ dispute_id: disputeId }) as TaskRecord | undefined
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
getDisputedTasks(convoyId?: string): TaskRecord[] {
|
|
556
|
+
if (convoyId) {
|
|
557
|
+
return this.db
|
|
558
|
+
.prepare("SELECT * FROM task WHERE status = 'disputed' AND convoy_id = :convoy_id ORDER BY phase, id")
|
|
559
|
+
.all({ convoy_id: convoyId }) as unknown as TaskRecord[]
|
|
560
|
+
}
|
|
561
|
+
return this.db
|
|
562
|
+
.prepare("SELECT * FROM task WHERE status = 'disputed' ORDER BY convoy_id, phase, id")
|
|
563
|
+
.all({}) as unknown as TaskRecord[]
|
|
564
|
+
}
|
|
565
|
+
|
|
277
566
|
updateTaskStatus(
|
|
278
567
|
id: string,
|
|
279
568
|
convoyId: string,
|
|
280
569
|
status: ConvoyTaskStatus,
|
|
281
570
|
extra?: Partial<
|
|
282
|
-
Pick<
|
|
571
|
+
Pick<
|
|
572
|
+
TaskRecord,
|
|
573
|
+
| 'worker_id' | 'worktree' | 'output' | 'exit_code' | 'started_at' | 'finished_at'
|
|
574
|
+
| 'retries' | 'prompt_tokens' | 'completion_tokens' | 'total_tokens' | 'cost_usd' | 'prompt'
|
|
575
|
+
>
|
|
283
576
|
>,
|
|
284
577
|
): void {
|
|
578
|
+
if (extra?.output !== undefined) {
|
|
579
|
+
extra = { ...extra, output: truncateOutput(extra.output) }
|
|
580
|
+
}
|
|
285
581
|
const sets = ['status = :status']
|
|
286
582
|
const params: Record<string, string | number | null> = { id, convoy_id: convoyId, status }
|
|
287
|
-
const extraFields = [
|
|
583
|
+
const extraFields = [
|
|
584
|
+
'worker_id', 'worktree', 'output', 'exit_code', 'started_at', 'finished_at',
|
|
585
|
+
'retries', 'prompt_tokens', 'completion_tokens', 'total_tokens', 'cost_usd', 'prompt',
|
|
586
|
+
] as const
|
|
288
587
|
|
|
289
588
|
if (extra) {
|
|
290
589
|
for (const field of extraFields) {
|
|
@@ -312,6 +611,88 @@ class ConvoyStoreImpl implements ConvoyStore {
|
|
|
312
611
|
})
|
|
313
612
|
}
|
|
314
613
|
|
|
614
|
+
insertTaskStep(record: Omit<TaskStepRecord, 'id'>): number {
|
|
615
|
+
this.db
|
|
616
|
+
.prepare(
|
|
617
|
+
`INSERT INTO task_step
|
|
618
|
+
(task_id, step_index, prompt, gates, status, exit_code, output, started_at, finished_at)
|
|
619
|
+
VALUES
|
|
620
|
+
(:task_id, :step_index, :prompt, :gates, :status, :exit_code, :output, :started_at, :finished_at)`,
|
|
621
|
+
)
|
|
622
|
+
.run(record)
|
|
623
|
+
const row = this.db.prepare('SELECT last_insert_rowid() AS id').get() as { id: number }
|
|
624
|
+
return row.id
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
updateTaskStep(
|
|
628
|
+
id: number,
|
|
629
|
+
fields: Partial<Pick<TaskStepRecord, 'status' | 'exit_code' | 'output' | 'started_at' | 'finished_at'>>,
|
|
630
|
+
): void {
|
|
631
|
+
const sets: string[] = []
|
|
632
|
+
const params: Record<string, string | number | null> = { id }
|
|
633
|
+
const stepFields = ['status', 'exit_code', 'output', 'started_at', 'finished_at'] as const
|
|
634
|
+
|
|
635
|
+
for (const field of stepFields) {
|
|
636
|
+
if (field in fields && fields[field] !== undefined) {
|
|
637
|
+
sets.push(`${field} = :${field}`)
|
|
638
|
+
params[field] = fields[field] as string | number | null
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (sets.length === 0) return
|
|
643
|
+
this.db.prepare(`UPDATE task_step SET ${sets.join(', ')} WHERE id = :id`).run(params)
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
updateTaskReview(
|
|
647
|
+
taskId: string,
|
|
648
|
+
convoyId: string,
|
|
649
|
+
fields: Partial<Pick<TaskRecord, 'review_level' | 'review_verdict' | 'review_tokens' | 'review_model' | 'panel_attempts' | 'dispute_id'>>,
|
|
650
|
+
): void {
|
|
651
|
+
const sets: string[] = []
|
|
652
|
+
const params: Record<string, string | number | null> = { id: taskId, convoy_id: convoyId }
|
|
653
|
+
const reviewFields = ['review_level', 'review_verdict', 'review_tokens', 'review_model', 'panel_attempts', 'dispute_id'] as const
|
|
654
|
+
|
|
655
|
+
for (const field of reviewFields) {
|
|
656
|
+
if (field in fields && fields[field] !== undefined) {
|
|
657
|
+
sets.push(`${field} = :${field}`)
|
|
658
|
+
params[field] = fields[field] as string | number | null
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (sets.length === 0) return
|
|
663
|
+
this.db.prepare(`UPDATE task SET ${sets.join(', ')} WHERE id = :id AND convoy_id = :convoy_id`).run(params)
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
updateTaskDrift(
|
|
667
|
+
taskId: string,
|
|
668
|
+
convoyId: string,
|
|
669
|
+
fields: Partial<Pick<TaskRecord, 'drift_score' | 'drift_retried'>>,
|
|
670
|
+
): void {
|
|
671
|
+
const sets: string[] = []
|
|
672
|
+
const params: Record<string, string | number | null> = { id: taskId, convoy_id: convoyId }
|
|
673
|
+
|
|
674
|
+
if (fields.drift_score !== undefined) {
|
|
675
|
+
sets.push('drift_score = :drift_score')
|
|
676
|
+
params.drift_score = fields.drift_score
|
|
677
|
+
}
|
|
678
|
+
if (fields.drift_retried !== undefined) {
|
|
679
|
+
sets.push('drift_retried = :drift_retried')
|
|
680
|
+
params.drift_retried = fields.drift_retried
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (sets.length === 0) return
|
|
684
|
+
this.db.prepare(`UPDATE task SET ${sets.join(', ')} WHERE id = :id AND convoy_id = :convoy_id`).run(params)
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
updateTaskDisputeStatus(taskId: string, convoyId: string, status: ConvoyTaskStatus, disputeId: string): void {
|
|
688
|
+
this.db
|
|
689
|
+
.prepare(
|
|
690
|
+
`UPDATE task SET status = :status, dispute_id = :dispute_id
|
|
691
|
+
WHERE id = :id AND convoy_id = :convoy_id`,
|
|
692
|
+
)
|
|
693
|
+
.run({ id: taskId, convoy_id: convoyId, status, dispute_id: disputeId })
|
|
694
|
+
}
|
|
695
|
+
|
|
315
696
|
insertWorker(record: Omit<WorkerRecord, 'finished_at' | 'last_heartbeat'>): void {
|
|
316
697
|
this.db
|
|
317
698
|
.prepare(
|
|
@@ -355,13 +736,16 @@ class ConvoyStoreImpl implements ConvoyStore {
|
|
|
355
736
|
this.db.prepare(`UPDATE worker SET ${sets.join(', ')} WHERE id = :id`).run(params)
|
|
356
737
|
}
|
|
357
738
|
|
|
358
|
-
insertEvent(record: Omit<EventRecord, 'id'>):
|
|
739
|
+
insertEvent(record: Omit<EventRecord, 'id'>): number {
|
|
740
|
+
enforceLimit(record.data, 'event.data', LIMIT_EVENT_DATA)
|
|
359
741
|
this.db
|
|
360
742
|
.prepare(
|
|
361
743
|
`INSERT INTO event (convoy_id, task_id, worker_id, type, data, created_at)
|
|
362
744
|
VALUES (:convoy_id, :task_id, :worker_id, :type, :data, :created_at)`,
|
|
363
745
|
)
|
|
364
746
|
.run(record)
|
|
747
|
+
const row = this.db.prepare('SELECT last_insert_rowid() AS id').get() as { id: number }
|
|
748
|
+
return row.id
|
|
365
749
|
}
|
|
366
750
|
|
|
367
751
|
getEvents(convoyId: string): EventRecord[] {
|
|
@@ -370,7 +754,160 @@ class ConvoyStoreImpl implements ConvoyStore {
|
|
|
370
754
|
.all({ convoy_id: convoyId }) as unknown as EventRecord[]
|
|
371
755
|
}
|
|
372
756
|
|
|
757
|
+
insertDlqEntry(record: DlqRecord): void {
|
|
758
|
+
this.db
|
|
759
|
+
.prepare(
|
|
760
|
+
`INSERT INTO dlq
|
|
761
|
+
(id, convoy_id, task_id, agent, failure_type, error_output, attempts,
|
|
762
|
+
tokens_spent, escalation_task_id, resolved, resolution, created_at, resolved_at)
|
|
763
|
+
VALUES
|
|
764
|
+
(:id, :convoy_id, :task_id, :agent, :failure_type, :error_output, :attempts,
|
|
765
|
+
:tokens_spent, :escalation_task_id, :resolved, :resolution, :created_at, :resolved_at)`,
|
|
766
|
+
)
|
|
767
|
+
.run(record as unknown as Record<string, string | number | null>)
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
listDlqEntries(convoyIdFilter?: string): DlqRecord[] {
|
|
771
|
+
if (convoyIdFilter) {
|
|
772
|
+
return this.db
|
|
773
|
+
.prepare('SELECT * FROM dlq WHERE convoy_id = :convoy_id ORDER BY created_at DESC')
|
|
774
|
+
.all({ convoy_id: convoyIdFilter }) as unknown as DlqRecord[]
|
|
775
|
+
}
|
|
776
|
+
return this.db
|
|
777
|
+
.prepare('SELECT * FROM dlq ORDER BY created_at DESC')
|
|
778
|
+
.all() as unknown as DlqRecord[]
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
resolveDlqEntry(id: string, resolution: string): void {
|
|
782
|
+
this.db
|
|
783
|
+
.prepare(
|
|
784
|
+
`UPDATE dlq SET resolved = 1, resolution = :resolution, resolved_at = :resolved_at
|
|
785
|
+
WHERE id = :id`,
|
|
786
|
+
)
|
|
787
|
+
.run({ id, resolution, resolved_at: new Date().toISOString() })
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
insertArtifact(record: ArtifactRecord): void {
|
|
791
|
+
const count = (
|
|
792
|
+
this.db
|
|
793
|
+
.prepare('SELECT COUNT(*) AS cnt FROM artifact WHERE convoy_id = :convoy_id')
|
|
794
|
+
.get({ convoy_id: record.convoy_id }) as { cnt: number }
|
|
795
|
+
).cnt
|
|
796
|
+
if (count >= 50) {
|
|
797
|
+
throw new ConvoyArtifactLimitError(record.convoy_id)
|
|
798
|
+
}
|
|
799
|
+
this.db
|
|
800
|
+
.prepare(
|
|
801
|
+
`INSERT INTO artifact (id, convoy_id, task_id, name, type, content, created_at)
|
|
802
|
+
VALUES (:id, :convoy_id, :task_id, :name, :type, :content, :created_at)`,
|
|
803
|
+
)
|
|
804
|
+
.run(record as unknown as Record<string, string | number | null>)
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
getArtifact(convoyId: string, name: string): ArtifactRecord | undefined {
|
|
808
|
+
return this.db
|
|
809
|
+
.prepare('SELECT * FROM artifact WHERE convoy_id = :convoy_id AND name = :name')
|
|
810
|
+
.get({ convoy_id: convoyId, name }) as ArtifactRecord | undefined
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
getArtifactsByTask(taskId: string): ArtifactRecord[] {
|
|
814
|
+
return this.db
|
|
815
|
+
.prepare('SELECT * FROM artifact WHERE task_id = :task_id ORDER BY created_at')
|
|
816
|
+
.all({ task_id: taskId }) as unknown as ArtifactRecord[]
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
deleteArtifactsOlderThan(days: number): number {
|
|
820
|
+
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString()
|
|
821
|
+
const result = this.db
|
|
822
|
+
.prepare(
|
|
823
|
+
`DELETE FROM artifact WHERE convoy_id IN (
|
|
824
|
+
SELECT id FROM convoy WHERE finished_at IS NOT NULL AND finished_at < :cutoff
|
|
825
|
+
)`,
|
|
826
|
+
)
|
|
827
|
+
.run({ cutoff })
|
|
828
|
+
return (result as unknown as { changes: number }).changes
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
insertAgentIdentity(record: AgentIdentityRecord): void {
|
|
832
|
+
const summarySize = Buffer.byteLength(record.summary, 'utf8')
|
|
833
|
+
const truncatedSummary = summarySize > LIMIT_SUMMARY
|
|
834
|
+
? record.summary.slice(0, LIMIT_SUMMARY)
|
|
835
|
+
: record.summary
|
|
836
|
+
this.db
|
|
837
|
+
.prepare(
|
|
838
|
+
`INSERT INTO agent_identity
|
|
839
|
+
(id, agent, convoy_id, task_id, summary, created_at, retention_days)
|
|
840
|
+
VALUES
|
|
841
|
+
(:id, :agent, :convoy_id, :task_id, :summary, :created_at, :retention_days)`,
|
|
842
|
+
)
|
|
843
|
+
.run({ ...record, summary: truncatedSummary } as unknown as Record<string, string | number | null>)
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
getAgentIdentities(agent: string, limit: number): AgentIdentityRecord[] {
|
|
847
|
+
return this.db
|
|
848
|
+
.prepare(
|
|
849
|
+
'SELECT * FROM agent_identity WHERE agent = :agent ORDER BY created_at DESC LIMIT :limit',
|
|
850
|
+
)
|
|
851
|
+
.all({ agent, limit }) as unknown as AgentIdentityRecord[]
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
listAgentIdentitySummary(): Array<{ agent: string; task_count: number; latest_date: string }> {
|
|
855
|
+
return this.db
|
|
856
|
+
.prepare(
|
|
857
|
+
`SELECT agent, COUNT(*) AS task_count, MAX(created_at) AS latest_date
|
|
858
|
+
FROM agent_identity GROUP BY agent ORDER BY agent`,
|
|
859
|
+
)
|
|
860
|
+
.all() as unknown as Array<{ agent: string; task_count: number; latest_date: string }>
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
purgeAgentIdentities(agent: string): number {
|
|
864
|
+
const result = this.db
|
|
865
|
+
.prepare('DELETE FROM agent_identity WHERE agent = :agent')
|
|
866
|
+
.run({ agent })
|
|
867
|
+
return (result as unknown as { changes: number }).changes
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
deleteAgentIdentitiesOlderThan(days: number): number {
|
|
871
|
+
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString()
|
|
872
|
+
const result = this.db
|
|
873
|
+
.prepare(
|
|
874
|
+
`DELETE FROM agent_identity
|
|
875
|
+
WHERE created_at < :cutoff
|
|
876
|
+
OR (retention_days IS NOT NULL
|
|
877
|
+
AND created_at < datetime('now', '-' || retention_days || ' days'))`,
|
|
878
|
+
)
|
|
879
|
+
.run({ cutoff })
|
|
880
|
+
return (result as unknown as { changes: number }).changes
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
getScratchpadValue(key: string): string | null {
|
|
884
|
+
const row = this.db
|
|
885
|
+
.prepare('SELECT value FROM scratchpad WHERE key = :key')
|
|
886
|
+
.get({ key }) as { value: string } | undefined
|
|
887
|
+
return row?.value ?? null
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
setScratchpadValue(key: string, value: string): void {
|
|
891
|
+
this.db
|
|
892
|
+
.prepare(
|
|
893
|
+
`INSERT INTO scratchpad (key, value, updated_at)
|
|
894
|
+
VALUES (:key, :value, :updated_at)
|
|
895
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`,
|
|
896
|
+
)
|
|
897
|
+
.run({ key, value, updated_at: new Date().toISOString() })
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
clearScratchpad(): void {
|
|
901
|
+
this.db.exec('DELETE FROM scratchpad')
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
clearScratchpadOlderThan(days: number): void {
|
|
905
|
+
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString()
|
|
906
|
+
this.db.prepare('DELETE FROM scratchpad WHERE updated_at < :cutoff').run({ cutoff })
|
|
907
|
+
}
|
|
908
|
+
|
|
373
909
|
insertPipeline(record: Omit<PipelineRecord, 'started_at' | 'finished_at' | 'total_tokens' | 'total_cost_usd'>): void {
|
|
910
|
+
enforceLimit(record.spec_yaml, 'pipeline.spec_yaml', LIMIT_SPEC_YAML)
|
|
374
911
|
this.db
|
|
375
912
|
.prepare(
|
|
376
913
|
`INSERT INTO pipeline (id, name, status, branch, spec_yaml, convoy_specs, created_at,
|
|
@@ -444,6 +981,115 @@ class ConvoyStoreImpl implements ConvoyStore {
|
|
|
444
981
|
}
|
|
445
982
|
}
|
|
446
983
|
|
|
984
|
+
export function migrateSchema(db: DatabaseSync, dbPath: string, fromVersion: number, toVersion: number): void {
|
|
985
|
+
for (let v = fromVersion; v < toVersion; v++) {
|
|
986
|
+
const backupPath = `${dbPath}.v${v}.bak`
|
|
987
|
+
copyFileSync(dbPath, backupPath)
|
|
988
|
+
db.exec('BEGIN')
|
|
989
|
+
try {
|
|
990
|
+
if (v === 4) {
|
|
991
|
+
db.exec(`
|
|
992
|
+
ALTER TABLE task ADD COLUMN gates TEXT;
|
|
993
|
+
ALTER TABLE task ADD COLUMN on_exhausted TEXT NOT NULL DEFAULT 'dlq';
|
|
994
|
+
ALTER TABLE task ADD COLUMN injected INTEGER NOT NULL DEFAULT 0;
|
|
995
|
+
ALTER TABLE task ADD COLUMN provenance TEXT;
|
|
996
|
+
ALTER TABLE task ADD COLUMN idempotency_key TEXT;
|
|
997
|
+
CREATE UNIQUE INDEX idx_task_idempotency ON task(convoy_id, idempotency_key)
|
|
998
|
+
WHERE idempotency_key IS NOT NULL;
|
|
999
|
+
ALTER TABLE convoy ADD COLUMN circuit_state TEXT;
|
|
1000
|
+
CREATE TABLE dlq (
|
|
1001
|
+
id TEXT PRIMARY KEY,
|
|
1002
|
+
convoy_id TEXT NOT NULL REFERENCES convoy(id),
|
|
1003
|
+
task_id TEXT NOT NULL REFERENCES task(id),
|
|
1004
|
+
agent TEXT NOT NULL,
|
|
1005
|
+
failure_type TEXT NOT NULL,
|
|
1006
|
+
error_output TEXT,
|
|
1007
|
+
attempts INTEGER NOT NULL,
|
|
1008
|
+
tokens_spent INTEGER,
|
|
1009
|
+
escalation_task_id TEXT,
|
|
1010
|
+
resolved INTEGER NOT NULL DEFAULT 0,
|
|
1011
|
+
resolution TEXT,
|
|
1012
|
+
created_at TEXT NOT NULL,
|
|
1013
|
+
resolved_at TEXT
|
|
1014
|
+
);
|
|
1015
|
+
`)
|
|
1016
|
+
}
|
|
1017
|
+
if (v === 5) {
|
|
1018
|
+
db.exec(`
|
|
1019
|
+
ALTER TABLE task ADD COLUMN current_step INTEGER;
|
|
1020
|
+
ALTER TABLE task ADD COLUMN total_steps INTEGER;
|
|
1021
|
+
ALTER TABLE task ADD COLUMN review_level TEXT;
|
|
1022
|
+
ALTER TABLE task ADD COLUMN review_verdict TEXT;
|
|
1023
|
+
ALTER TABLE task ADD COLUMN review_tokens INTEGER;
|
|
1024
|
+
ALTER TABLE task ADD COLUMN review_model TEXT;
|
|
1025
|
+
ALTER TABLE task ADD COLUMN panel_attempts INTEGER NOT NULL DEFAULT 0;
|
|
1026
|
+
ALTER TABLE task ADD COLUMN dispute_id TEXT;
|
|
1027
|
+
ALTER TABLE convoy ADD COLUMN review_tokens_total INTEGER;
|
|
1028
|
+
ALTER TABLE convoy ADD COLUMN review_budget INTEGER;
|
|
1029
|
+
CREATE TABLE task_step (
|
|
1030
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1031
|
+
task_id TEXT NOT NULL REFERENCES task(id),
|
|
1032
|
+
step_index INTEGER NOT NULL,
|
|
1033
|
+
prompt TEXT NOT NULL,
|
|
1034
|
+
gates TEXT,
|
|
1035
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
1036
|
+
exit_code INTEGER,
|
|
1037
|
+
output TEXT,
|
|
1038
|
+
started_at TEXT,
|
|
1039
|
+
finished_at TEXT
|
|
1040
|
+
);
|
|
1041
|
+
`)
|
|
1042
|
+
}
|
|
1043
|
+
if (v === 6) {
|
|
1044
|
+
db.exec(`
|
|
1045
|
+
ALTER TABLE task ADD COLUMN drift_score REAL;
|
|
1046
|
+
ALTER TABLE task ADD COLUMN drift_retried INTEGER NOT NULL DEFAULT 0;
|
|
1047
|
+
`)
|
|
1048
|
+
}
|
|
1049
|
+
if (v === 7) {
|
|
1050
|
+
db.exec(`
|
|
1051
|
+
ALTER TABLE task ADD COLUMN outputs TEXT;
|
|
1052
|
+
ALTER TABLE task ADD COLUMN inputs TEXT;
|
|
1053
|
+
ALTER TABLE task ADD COLUMN discovered_issues TEXT;
|
|
1054
|
+
CREATE TABLE artifact (
|
|
1055
|
+
id TEXT PRIMARY KEY,
|
|
1056
|
+
convoy_id TEXT NOT NULL REFERENCES convoy(id),
|
|
1057
|
+
task_id TEXT NOT NULL REFERENCES task(id),
|
|
1058
|
+
name TEXT NOT NULL,
|
|
1059
|
+
type TEXT NOT NULL,
|
|
1060
|
+
content TEXT NOT NULL CHECK (length(content) <= 1048576),
|
|
1061
|
+
created_at TEXT NOT NULL,
|
|
1062
|
+
UNIQUE(convoy_id, name)
|
|
1063
|
+
);
|
|
1064
|
+
CREATE TABLE agent_identity (
|
|
1065
|
+
id TEXT PRIMARY KEY,
|
|
1066
|
+
agent TEXT NOT NULL,
|
|
1067
|
+
convoy_id TEXT NOT NULL,
|
|
1068
|
+
task_id TEXT NOT NULL,
|
|
1069
|
+
summary TEXT NOT NULL,
|
|
1070
|
+
created_at TEXT NOT NULL,
|
|
1071
|
+
retention_days INTEGER NOT NULL DEFAULT 90
|
|
1072
|
+
);
|
|
1073
|
+
`)
|
|
1074
|
+
}
|
|
1075
|
+
if (v === 8) {
|
|
1076
|
+
db.exec(`
|
|
1077
|
+
CREATE TABLE scratchpad (
|
|
1078
|
+
key TEXT PRIMARY KEY,
|
|
1079
|
+
value TEXT NOT NULL,
|
|
1080
|
+
updated_at TEXT NOT NULL
|
|
1081
|
+
);
|
|
1082
|
+
`)
|
|
1083
|
+
}
|
|
1084
|
+
db.exec('COMMIT')
|
|
1085
|
+
} catch (err) {
|
|
1086
|
+
try { db.exec('ROLLBACK') } catch { /* ignore */ }
|
|
1087
|
+
throw new Error(`Migration v${v}→v${v + 1} failed. Backup at ${backupPath}. Original error: ${(err as Error).message}`)
|
|
1088
|
+
}
|
|
1089
|
+
db.exec(`PRAGMA user_version = ${v + 1}`)
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
447
1093
|
export function createConvoyStore(dbPath: string): ConvoyStore {
|
|
448
1094
|
return new ConvoyStoreImpl(dbPath)
|
|
449
1095
|
}
|