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/dist/cli/convoy/store.js
CHANGED
|
@@ -1,8 +1,48 @@
|
|
|
1
|
+
import { copyFileSync } from 'node:fs';
|
|
1
2
|
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
-
const SCHEMA_VERSION =
|
|
3
|
+
const SCHEMA_VERSION = 10;
|
|
4
|
+
// ── Size limits (bytes) ────────────────────────────────────────────────────────
|
|
5
|
+
const LIMIT_SPEC_YAML = 256 * 1024; // 256 KB
|
|
6
|
+
const LIMIT_OUTPUT = 1024 * 1024; // 1 MB (head 512KB + tail 512KB)
|
|
7
|
+
const LIMIT_OUTPUT_HALF = 512 * 1024; // 512 KB per half
|
|
8
|
+
const LIMIT_EVENT_DATA = 64 * 1024; // 64 KB
|
|
9
|
+
const LIMIT_SUMMARY = 4096; // 4 KB
|
|
10
|
+
export class FieldSizeLimitError extends Error {
|
|
11
|
+
constructor(field, actual, limit) {
|
|
12
|
+
super(`Field "${field}" exceeds size limit: ${actual} bytes > ${limit} bytes`);
|
|
13
|
+
this.name = 'FieldSizeLimitError';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function enforceLimit(value, field, limit) {
|
|
17
|
+
if (value == null)
|
|
18
|
+
return;
|
|
19
|
+
const size = Buffer.byteLength(value, 'utf8');
|
|
20
|
+
if (size > limit) {
|
|
21
|
+
throw new FieldSizeLimitError(field, size, limit);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function truncateOutput(value) {
|
|
25
|
+
if (value == null)
|
|
26
|
+
return null;
|
|
27
|
+
const size = Buffer.byteLength(value, 'utf8');
|
|
28
|
+
if (size <= LIMIT_OUTPUT)
|
|
29
|
+
return value;
|
|
30
|
+
// Head + tail truncation with marker
|
|
31
|
+
const head = value.slice(0, LIMIT_OUTPUT_HALF);
|
|
32
|
+
const tail = value.slice(-LIMIT_OUTPUT_HALF);
|
|
33
|
+
return head + '\n\n... [truncated: ' + size + ' bytes total, showing first/last 512KB] ...\n\n' + tail;
|
|
34
|
+
}
|
|
35
|
+
export class ConvoyArtifactLimitError extends Error {
|
|
36
|
+
constructor(convoyId) {
|
|
37
|
+
super(`Convoy ${convoyId} has reached the maximum of 50 artifacts`);
|
|
38
|
+
this.name = 'ConvoyArtifactLimitError';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
3
41
|
class ConvoyStoreImpl {
|
|
4
42
|
db;
|
|
43
|
+
dbPath;
|
|
5
44
|
constructor(dbPath) {
|
|
45
|
+
this.dbPath = dbPath;
|
|
6
46
|
this.db = new DatabaseSync(dbPath);
|
|
7
47
|
this.db.exec('PRAGMA journal_mode = WAL');
|
|
8
48
|
this.db.exec('PRAGMA synchronous = NORMAL');
|
|
@@ -13,18 +53,22 @@ class ConvoyStoreImpl {
|
|
|
13
53
|
if (version === 0) {
|
|
14
54
|
this.db.exec(`
|
|
15
55
|
CREATE TABLE IF NOT EXISTS convoy (
|
|
16
|
-
id
|
|
17
|
-
name
|
|
18
|
-
spec_hash
|
|
19
|
-
status
|
|
20
|
-
branch
|
|
21
|
-
created_at
|
|
22
|
-
started_at
|
|
23
|
-
finished_at
|
|
24
|
-
spec_yaml
|
|
25
|
-
total_tokens
|
|
26
|
-
total_cost_usd
|
|
27
|
-
|
|
56
|
+
id TEXT PRIMARY KEY,
|
|
57
|
+
name TEXT NOT NULL,
|
|
58
|
+
spec_hash TEXT NOT NULL,
|
|
59
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
60
|
+
branch TEXT,
|
|
61
|
+
created_at TEXT NOT NULL,
|
|
62
|
+
started_at TEXT,
|
|
63
|
+
finished_at TEXT,
|
|
64
|
+
spec_yaml TEXT NOT NULL,
|
|
65
|
+
total_tokens INTEGER,
|
|
66
|
+
total_cost_usd TEXT,
|
|
67
|
+
total_cost_usd_num REAL,
|
|
68
|
+
pipeline_id TEXT,
|
|
69
|
+
circuit_state TEXT,
|
|
70
|
+
review_tokens_total INTEGER,
|
|
71
|
+
review_budget INTEGER
|
|
28
72
|
);
|
|
29
73
|
|
|
30
74
|
CREATE TABLE IF NOT EXISTS pipeline (
|
|
@@ -38,7 +82,8 @@ class ConvoyStoreImpl {
|
|
|
38
82
|
started_at TEXT,
|
|
39
83
|
finished_at TEXT,
|
|
40
84
|
total_tokens INTEGER,
|
|
41
|
-
total_cost_usd TEXT
|
|
85
|
+
total_cost_usd TEXT,
|
|
86
|
+
total_cost_usd_num REAL
|
|
42
87
|
);
|
|
43
88
|
|
|
44
89
|
CREATE TABLE IF NOT EXISTS task (
|
|
@@ -64,7 +109,42 @@ class ConvoyStoreImpl {
|
|
|
64
109
|
prompt_tokens INTEGER,
|
|
65
110
|
completion_tokens INTEGER,
|
|
66
111
|
total_tokens INTEGER,
|
|
67
|
-
cost_usd TEXT
|
|
112
|
+
cost_usd TEXT,
|
|
113
|
+
cost_usd_num REAL,
|
|
114
|
+
gates TEXT,
|
|
115
|
+
on_exhausted TEXT NOT NULL DEFAULT 'dlq',
|
|
116
|
+
injected INTEGER NOT NULL DEFAULT 0,
|
|
117
|
+
provenance TEXT,
|
|
118
|
+
idempotency_key TEXT,
|
|
119
|
+
current_step INTEGER,
|
|
120
|
+
total_steps INTEGER,
|
|
121
|
+
review_level TEXT,
|
|
122
|
+
review_verdict TEXT,
|
|
123
|
+
review_tokens INTEGER,
|
|
124
|
+
review_model TEXT,
|
|
125
|
+
panel_attempts INTEGER NOT NULL DEFAULT 0,
|
|
126
|
+
dispute_id TEXT,
|
|
127
|
+
drift_score REAL,
|
|
128
|
+
drift_retried INTEGER NOT NULL DEFAULT 0,
|
|
129
|
+
outputs TEXT,
|
|
130
|
+
inputs TEXT,
|
|
131
|
+
discovered_issues TEXT
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_task_idempotency ON task(convoy_id, idempotency_key)
|
|
135
|
+
WHERE idempotency_key IS NOT NULL;
|
|
136
|
+
|
|
137
|
+
CREATE TABLE IF NOT EXISTS task_step (
|
|
138
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
139
|
+
task_id TEXT NOT NULL REFERENCES task(id),
|
|
140
|
+
step_index INTEGER NOT NULL,
|
|
141
|
+
prompt TEXT NOT NULL,
|
|
142
|
+
gates TEXT,
|
|
143
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
144
|
+
exit_code INTEGER,
|
|
145
|
+
output TEXT,
|
|
146
|
+
started_at TEXT,
|
|
147
|
+
finished_at TEXT
|
|
68
148
|
);
|
|
69
149
|
|
|
70
150
|
CREATE TABLE IF NOT EXISTS worker (
|
|
@@ -89,6 +169,57 @@ class ConvoyStoreImpl {
|
|
|
89
169
|
data TEXT,
|
|
90
170
|
created_at TEXT NOT NULL
|
|
91
171
|
);
|
|
172
|
+
|
|
173
|
+
CREATE TABLE IF NOT EXISTS dlq (
|
|
174
|
+
id TEXT PRIMARY KEY,
|
|
175
|
+
convoy_id TEXT NOT NULL REFERENCES convoy(id),
|
|
176
|
+
task_id TEXT NOT NULL REFERENCES task(id),
|
|
177
|
+
agent TEXT NOT NULL,
|
|
178
|
+
failure_type TEXT NOT NULL,
|
|
179
|
+
error_output TEXT,
|
|
180
|
+
attempts INTEGER NOT NULL,
|
|
181
|
+
tokens_spent INTEGER,
|
|
182
|
+
escalation_task_id TEXT,
|
|
183
|
+
resolved INTEGER NOT NULL DEFAULT 0,
|
|
184
|
+
resolution TEXT,
|
|
185
|
+
created_at TEXT NOT NULL,
|
|
186
|
+
resolved_at TEXT
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
CREATE TABLE IF NOT EXISTS artifact (
|
|
190
|
+
id TEXT PRIMARY KEY,
|
|
191
|
+
convoy_id TEXT NOT NULL REFERENCES convoy(id),
|
|
192
|
+
task_id TEXT NOT NULL REFERENCES task(id),
|
|
193
|
+
name TEXT NOT NULL,
|
|
194
|
+
type TEXT NOT NULL,
|
|
195
|
+
content TEXT NOT NULL CHECK (length(content) <= 1048576),
|
|
196
|
+
created_at TEXT NOT NULL,
|
|
197
|
+
UNIQUE(convoy_id, name)
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
CREATE TABLE IF NOT EXISTS agent_identity (
|
|
201
|
+
id TEXT PRIMARY KEY,
|
|
202
|
+
agent TEXT NOT NULL,
|
|
203
|
+
convoy_id TEXT NOT NULL,
|
|
204
|
+
task_id TEXT NOT NULL,
|
|
205
|
+
summary TEXT NOT NULL,
|
|
206
|
+
created_at TEXT NOT NULL,
|
|
207
|
+
retention_days INTEGER NOT NULL DEFAULT 90
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
CREATE TABLE IF NOT EXISTS scratchpad (
|
|
211
|
+
key TEXT PRIMARY KEY,
|
|
212
|
+
value TEXT NOT NULL,
|
|
213
|
+
updated_at TEXT NOT NULL
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
CREATE TABLE IF NOT EXISTS engine_lock (
|
|
217
|
+
id INTEGER PRIMARY KEY,
|
|
218
|
+
pid INTEGER NOT NULL,
|
|
219
|
+
hostname TEXT NOT NULL,
|
|
220
|
+
started_at TEXT NOT NULL,
|
|
221
|
+
last_heartbeat TEXT NOT NULL
|
|
222
|
+
);
|
|
92
223
|
`);
|
|
93
224
|
this.db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
|
|
94
225
|
version = SCHEMA_VERSION;
|
|
@@ -128,21 +259,50 @@ class ConvoyStoreImpl {
|
|
|
128
259
|
this.db.exec('PRAGMA user_version = 4');
|
|
129
260
|
version = 4;
|
|
130
261
|
}
|
|
262
|
+
if (version === 4) {
|
|
263
|
+
migrateSchema(this.db, this.dbPath, 4, 5);
|
|
264
|
+
version = 5;
|
|
265
|
+
}
|
|
266
|
+
if (version === 5) {
|
|
267
|
+
migrateSchema(this.db, this.dbPath, 5, 6);
|
|
268
|
+
version = 6;
|
|
269
|
+
}
|
|
270
|
+
if (version === 6) {
|
|
271
|
+
migrateSchema(this.db, this.dbPath, 6, 7);
|
|
272
|
+
version = 7;
|
|
273
|
+
}
|
|
274
|
+
if (version === 7) {
|
|
275
|
+
migrateSchema(this.db, this.dbPath, 7, 8);
|
|
276
|
+
version = 8;
|
|
277
|
+
}
|
|
278
|
+
if (version === 8) {
|
|
279
|
+
migrateSchema(this.db, this.dbPath, 8, 9);
|
|
280
|
+
version = 9;
|
|
281
|
+
}
|
|
282
|
+
if (version === 9) {
|
|
283
|
+
migrateSchema(this.db, this.dbPath, 9, 10);
|
|
284
|
+
version = 10;
|
|
285
|
+
}
|
|
131
286
|
}
|
|
132
287
|
insertConvoy(record) {
|
|
288
|
+
enforceLimit(record.spec_yaml, 'spec_yaml', LIMIT_SPEC_YAML);
|
|
133
289
|
this.db
|
|
134
|
-
.prepare(`INSERT INTO convoy
|
|
135
|
-
|
|
290
|
+
.prepare(`INSERT INTO convoy
|
|
291
|
+
(id, name, spec_hash, status, branch, created_at, started_at, finished_at,
|
|
292
|
+
spec_yaml, pipeline_id)
|
|
293
|
+
VALUES
|
|
294
|
+
(:id, :name, :spec_hash, :status, :branch, :created_at, NULL, NULL,
|
|
295
|
+
:spec_yaml, :pipeline_id)`)
|
|
136
296
|
.run({ ...record, pipeline_id: record.pipeline_id ?? null });
|
|
137
297
|
}
|
|
138
298
|
getConvoy(id) {
|
|
139
299
|
return this.db
|
|
140
|
-
.prepare('SELECT
|
|
300
|
+
.prepare('SELECT *, total_cost_usd_num AS total_cost_usd FROM convoy WHERE id = :id')
|
|
141
301
|
.get({ id });
|
|
142
302
|
}
|
|
143
303
|
getLatestConvoy() {
|
|
144
304
|
return this.db
|
|
145
|
-
.prepare('SELECT
|
|
305
|
+
.prepare('SELECT *, total_cost_usd_num AS total_cost_usd FROM convoy ORDER BY created_at DESC LIMIT 1')
|
|
146
306
|
.get();
|
|
147
307
|
}
|
|
148
308
|
updateConvoyStatus(id, status, extra) {
|
|
@@ -161,37 +321,101 @@ class ConvoyStoreImpl {
|
|
|
161
321
|
params.total_tokens = extra.total_tokens;
|
|
162
322
|
}
|
|
163
323
|
if (extra?.total_cost_usd !== undefined) {
|
|
164
|
-
sets.push('total_cost_usd = :
|
|
165
|
-
|
|
324
|
+
sets.push('total_cost_usd = :total_cost_usd_text');
|
|
325
|
+
sets.push('total_cost_usd_num = :total_cost_usd_num');
|
|
326
|
+
params.total_cost_usd_text = extra.total_cost_usd !== null ? String(extra.total_cost_usd) : null;
|
|
327
|
+
params.total_cost_usd_num = extra.total_cost_usd;
|
|
166
328
|
}
|
|
167
329
|
this.db.prepare(`UPDATE convoy SET ${sets.join(', ')} WHERE id = :id`).run(params);
|
|
168
330
|
}
|
|
331
|
+
updateConvoyReviewTokens(convoyId, tokens) {
|
|
332
|
+
this.db
|
|
333
|
+
.prepare(`UPDATE convoy
|
|
334
|
+
SET review_tokens_total = :tokens
|
|
335
|
+
WHERE id = :id`)
|
|
336
|
+
.run({ id: convoyId, tokens });
|
|
337
|
+
}
|
|
338
|
+
updateConvoyCircuitState(convoyId, state) {
|
|
339
|
+
this.db
|
|
340
|
+
.prepare('UPDATE convoy SET circuit_state = :state WHERE id = :id')
|
|
341
|
+
.run({ id: convoyId, state: state ?? null });
|
|
342
|
+
}
|
|
169
343
|
insertTask(record) {
|
|
170
344
|
this.db
|
|
171
345
|
.prepare(`INSERT INTO task
|
|
172
346
|
(id, convoy_id, phase, prompt, agent, adapter, model, timeout_ms, status,
|
|
173
347
|
worker_id, worktree, output, exit_code, started_at, finished_at,
|
|
174
|
-
retries, max_retries, files, depends_on
|
|
348
|
+
retries, max_retries, files, depends_on, gates,
|
|
349
|
+
on_exhausted, injected, provenance, idempotency_key,
|
|
350
|
+
outputs, inputs)
|
|
175
351
|
VALUES
|
|
176
352
|
(:id, :convoy_id, :phase, :prompt, :agent, :adapter, :model, :timeout_ms, :status,
|
|
177
353
|
NULL, NULL, NULL, NULL, NULL, NULL,
|
|
178
|
-
:retries, :max_retries, :files, :depends_on
|
|
354
|
+
:retries, :max_retries, :files, :depends_on, :gates,
|
|
355
|
+
'dlq', 0, NULL, NULL,
|
|
356
|
+
:outputs, :inputs)`)
|
|
357
|
+
.run({ ...record, outputs: record.outputs ?? null, inputs: record.inputs ?? null });
|
|
358
|
+
}
|
|
359
|
+
insertInjectedTask(record) {
|
|
360
|
+
this.db
|
|
361
|
+
.prepare(`INSERT INTO task
|
|
362
|
+
(id, convoy_id, phase, prompt, agent, adapter, model, timeout_ms, status,
|
|
363
|
+
worker_id, worktree, output, exit_code, started_at, finished_at,
|
|
364
|
+
retries, max_retries, files, depends_on, gates,
|
|
365
|
+
on_exhausted, injected, provenance, idempotency_key,
|
|
366
|
+
current_step, total_steps, review_level, review_verdict,
|
|
367
|
+
review_tokens, review_model, panel_attempts, dispute_id,
|
|
368
|
+
drift_score, drift_retried, outputs, inputs, discovered_issues)
|
|
369
|
+
VALUES
|
|
370
|
+
(:id, :convoy_id, :phase, :prompt, :agent, :adapter, :model, :timeout_ms, :status,
|
|
371
|
+
:worker_id, :worktree, :output, :exit_code, :started_at, :finished_at,
|
|
372
|
+
:retries, :max_retries, :files, :depends_on, :gates,
|
|
373
|
+
:on_exhausted, :injected, :provenance, :idempotency_key,
|
|
374
|
+
:current_step, :total_steps, :review_level, :review_verdict,
|
|
375
|
+
:review_tokens, :review_model, :panel_attempts, :dispute_id,
|
|
376
|
+
:drift_score, :drift_retried, :outputs, :inputs, :discovered_issues)`)
|
|
179
377
|
.run(record);
|
|
180
378
|
}
|
|
181
379
|
getTask(id, convoyId) {
|
|
182
380
|
return this.db
|
|
183
|
-
.prepare('SELECT
|
|
381
|
+
.prepare('SELECT *, cost_usd_num AS cost_usd FROM task WHERE id = :id AND convoy_id = :convoy_id')
|
|
184
382
|
.get({ id, convoy_id: convoyId });
|
|
185
383
|
}
|
|
186
384
|
getTasksByConvoy(convoyId) {
|
|
187
385
|
return this.db
|
|
188
|
-
.prepare('SELECT
|
|
386
|
+
.prepare('SELECT *, cost_usd_num AS cost_usd FROM task WHERE convoy_id = :convoy_id ORDER BY phase, id')
|
|
189
387
|
.all({ convoy_id: convoyId });
|
|
190
388
|
}
|
|
389
|
+
getTaskByIdempotencyKey(convoyId, key) {
|
|
390
|
+
return this.db
|
|
391
|
+
.prepare('SELECT *, cost_usd_num AS cost_usd FROM task WHERE convoy_id = :convoy_id AND idempotency_key = :key')
|
|
392
|
+
.get({ convoy_id: convoyId, key });
|
|
393
|
+
}
|
|
394
|
+
getTaskByDisputeId(disputeId) {
|
|
395
|
+
return this.db
|
|
396
|
+
.prepare('SELECT *, cost_usd_num AS cost_usd FROM task WHERE dispute_id = :dispute_id LIMIT 1')
|
|
397
|
+
.get({ dispute_id: disputeId });
|
|
398
|
+
}
|
|
399
|
+
getDisputedTasks(convoyId) {
|
|
400
|
+
if (convoyId) {
|
|
401
|
+
return this.db
|
|
402
|
+
.prepare("SELECT *, cost_usd_num AS cost_usd FROM task WHERE status = 'disputed' AND convoy_id = :convoy_id ORDER BY phase, id")
|
|
403
|
+
.all({ convoy_id: convoyId });
|
|
404
|
+
}
|
|
405
|
+
return this.db
|
|
406
|
+
.prepare("SELECT *, cost_usd_num AS cost_usd FROM task WHERE status = 'disputed' ORDER BY convoy_id, phase, id")
|
|
407
|
+
.all({});
|
|
408
|
+
}
|
|
191
409
|
updateTaskStatus(id, convoyId, status, extra) {
|
|
410
|
+
if (extra?.output !== undefined) {
|
|
411
|
+
extra = { ...extra, output: truncateOutput(extra.output) };
|
|
412
|
+
}
|
|
192
413
|
const sets = ['status = :status'];
|
|
193
414
|
const params = { id, convoy_id: convoyId, status };
|
|
194
|
-
const extraFields = [
|
|
415
|
+
const extraFields = [
|
|
416
|
+
'worker_id', 'worktree', 'output', 'exit_code', 'started_at', 'finished_at',
|
|
417
|
+
'retries', 'prompt_tokens', 'completion_tokens', 'total_tokens', 'cost_usd', 'prompt',
|
|
418
|
+
];
|
|
195
419
|
if (extra) {
|
|
196
420
|
for (const field of extraFields) {
|
|
197
421
|
if (field in extra && extra[field] !== undefined) {
|
|
@@ -199,6 +423,10 @@ class ConvoyStoreImpl {
|
|
|
199
423
|
params[field] = extra[field];
|
|
200
424
|
}
|
|
201
425
|
}
|
|
426
|
+
if ('cost_usd' in extra && extra.cost_usd !== undefined) {
|
|
427
|
+
sets.push('cost_usd_num = :cost_usd_num');
|
|
428
|
+
params.cost_usd_num = extra.cost_usd;
|
|
429
|
+
}
|
|
202
430
|
}
|
|
203
431
|
this.db
|
|
204
432
|
.prepare(`UPDATE task SET ${sets.join(', ')} WHERE id = :id AND convoy_id = :convoy_id`)
|
|
@@ -216,6 +444,65 @@ class ConvoyStoreImpl {
|
|
|
216
444
|
return deps.length === 0 || deps.every(depId => doneTaskIds.has(depId));
|
|
217
445
|
});
|
|
218
446
|
}
|
|
447
|
+
insertTaskStep(record) {
|
|
448
|
+
this.db
|
|
449
|
+
.prepare(`INSERT INTO task_step
|
|
450
|
+
(task_id, step_index, prompt, gates, status, exit_code, output, started_at, finished_at)
|
|
451
|
+
VALUES
|
|
452
|
+
(:task_id, :step_index, :prompt, :gates, :status, :exit_code, :output, :started_at, :finished_at)`)
|
|
453
|
+
.run(record);
|
|
454
|
+
const row = this.db.prepare('SELECT last_insert_rowid() AS id').get();
|
|
455
|
+
return row.id;
|
|
456
|
+
}
|
|
457
|
+
updateTaskStep(id, fields) {
|
|
458
|
+
const sets = [];
|
|
459
|
+
const params = { id };
|
|
460
|
+
const stepFields = ['status', 'exit_code', 'output', 'started_at', 'finished_at'];
|
|
461
|
+
for (const field of stepFields) {
|
|
462
|
+
if (field in fields && fields[field] !== undefined) {
|
|
463
|
+
sets.push(`${field} = :${field}`);
|
|
464
|
+
params[field] = fields[field];
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
if (sets.length === 0)
|
|
468
|
+
return;
|
|
469
|
+
this.db.prepare(`UPDATE task_step SET ${sets.join(', ')} WHERE id = :id`).run(params);
|
|
470
|
+
}
|
|
471
|
+
updateTaskReview(taskId, convoyId, fields) {
|
|
472
|
+
const sets = [];
|
|
473
|
+
const params = { id: taskId, convoy_id: convoyId };
|
|
474
|
+
const reviewFields = ['review_level', 'review_verdict', 'review_tokens', 'review_model', 'panel_attempts', 'dispute_id'];
|
|
475
|
+
for (const field of reviewFields) {
|
|
476
|
+
if (field in fields && fields[field] !== undefined) {
|
|
477
|
+
sets.push(`${field} = :${field}`);
|
|
478
|
+
params[field] = fields[field];
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (sets.length === 0)
|
|
482
|
+
return;
|
|
483
|
+
this.db.prepare(`UPDATE task SET ${sets.join(', ')} WHERE id = :id AND convoy_id = :convoy_id`).run(params);
|
|
484
|
+
}
|
|
485
|
+
updateTaskDrift(taskId, convoyId, fields) {
|
|
486
|
+
const sets = [];
|
|
487
|
+
const params = { id: taskId, convoy_id: convoyId };
|
|
488
|
+
if (fields.drift_score !== undefined) {
|
|
489
|
+
sets.push('drift_score = :drift_score');
|
|
490
|
+
params.drift_score = fields.drift_score;
|
|
491
|
+
}
|
|
492
|
+
if (fields.drift_retried !== undefined) {
|
|
493
|
+
sets.push('drift_retried = :drift_retried');
|
|
494
|
+
params.drift_retried = fields.drift_retried;
|
|
495
|
+
}
|
|
496
|
+
if (sets.length === 0)
|
|
497
|
+
return;
|
|
498
|
+
this.db.prepare(`UPDATE task SET ${sets.join(', ')} WHERE id = :id AND convoy_id = :convoy_id`).run(params);
|
|
499
|
+
}
|
|
500
|
+
updateTaskDisputeStatus(taskId, convoyId, status, disputeId) {
|
|
501
|
+
this.db
|
|
502
|
+
.prepare(`UPDATE task SET status = :status, dispute_id = :dispute_id
|
|
503
|
+
WHERE id = :id AND convoy_id = :convoy_id`)
|
|
504
|
+
.run({ id: taskId, convoy_id: convoyId, status, dispute_id: disputeId });
|
|
505
|
+
}
|
|
219
506
|
insertWorker(record) {
|
|
220
507
|
this.db
|
|
221
508
|
.prepare(`INSERT INTO worker
|
|
@@ -249,17 +536,142 @@ class ConvoyStoreImpl {
|
|
|
249
536
|
this.db.prepare(`UPDATE worker SET ${sets.join(', ')} WHERE id = :id`).run(params);
|
|
250
537
|
}
|
|
251
538
|
insertEvent(record) {
|
|
539
|
+
enforceLimit(record.data, 'event.data', LIMIT_EVENT_DATA);
|
|
252
540
|
this.db
|
|
253
541
|
.prepare(`INSERT INTO event (convoy_id, task_id, worker_id, type, data, created_at)
|
|
254
542
|
VALUES (:convoy_id, :task_id, :worker_id, :type, :data, :created_at)`)
|
|
255
543
|
.run(record);
|
|
544
|
+
const row = this.db.prepare('SELECT last_insert_rowid() AS id').get();
|
|
545
|
+
return row.id;
|
|
256
546
|
}
|
|
257
547
|
getEvents(convoyId) {
|
|
258
548
|
return this.db
|
|
259
549
|
.prepare('SELECT * FROM event WHERE convoy_id = :convoy_id ORDER BY id')
|
|
260
550
|
.all({ convoy_id: convoyId });
|
|
261
551
|
}
|
|
552
|
+
insertDlqEntry(record) {
|
|
553
|
+
this.db
|
|
554
|
+
.prepare(`INSERT INTO dlq
|
|
555
|
+
(id, convoy_id, task_id, agent, failure_type, error_output, attempts,
|
|
556
|
+
tokens_spent, escalation_task_id, resolved, resolution, created_at, resolved_at)
|
|
557
|
+
VALUES
|
|
558
|
+
(:id, :convoy_id, :task_id, :agent, :failure_type, :error_output, :attempts,
|
|
559
|
+
:tokens_spent, :escalation_task_id, :resolved, :resolution, :created_at, :resolved_at)`)
|
|
560
|
+
.run(record);
|
|
561
|
+
}
|
|
562
|
+
listDlqEntries(convoyIdFilter) {
|
|
563
|
+
if (convoyIdFilter) {
|
|
564
|
+
return this.db
|
|
565
|
+
.prepare('SELECT * FROM dlq WHERE convoy_id = :convoy_id ORDER BY created_at DESC')
|
|
566
|
+
.all({ convoy_id: convoyIdFilter });
|
|
567
|
+
}
|
|
568
|
+
return this.db
|
|
569
|
+
.prepare('SELECT * FROM dlq ORDER BY created_at DESC')
|
|
570
|
+
.all();
|
|
571
|
+
}
|
|
572
|
+
resolveDlqEntry(id, resolution) {
|
|
573
|
+
this.db
|
|
574
|
+
.prepare(`UPDATE dlq SET resolved = 1, resolution = :resolution, resolved_at = :resolved_at
|
|
575
|
+
WHERE id = :id`)
|
|
576
|
+
.run({ id, resolution, resolved_at: new Date().toISOString() });
|
|
577
|
+
}
|
|
578
|
+
insertArtifact(record) {
|
|
579
|
+
const count = this.db
|
|
580
|
+
.prepare('SELECT COUNT(*) AS cnt FROM artifact WHERE convoy_id = :convoy_id')
|
|
581
|
+
.get({ convoy_id: record.convoy_id }).cnt;
|
|
582
|
+
if (count >= 50) {
|
|
583
|
+
throw new ConvoyArtifactLimitError(record.convoy_id);
|
|
584
|
+
}
|
|
585
|
+
this.db
|
|
586
|
+
.prepare(`INSERT INTO artifact (id, convoy_id, task_id, name, type, content, created_at)
|
|
587
|
+
VALUES (:id, :convoy_id, :task_id, :name, :type, :content, :created_at)`)
|
|
588
|
+
.run(record);
|
|
589
|
+
}
|
|
590
|
+
getArtifact(convoyId, name) {
|
|
591
|
+
return this.db
|
|
592
|
+
.prepare('SELECT * FROM artifact WHERE convoy_id = :convoy_id AND name = :name')
|
|
593
|
+
.get({ convoy_id: convoyId, name });
|
|
594
|
+
}
|
|
595
|
+
getArtifactsByTask(taskId) {
|
|
596
|
+
return this.db
|
|
597
|
+
.prepare('SELECT * FROM artifact WHERE task_id = :task_id ORDER BY created_at')
|
|
598
|
+
.all({ task_id: taskId });
|
|
599
|
+
}
|
|
600
|
+
getArtifactsByConvoy(convoyId) {
|
|
601
|
+
return this.db
|
|
602
|
+
.prepare('SELECT * FROM artifact WHERE convoy_id = :convoy_id ORDER BY created_at')
|
|
603
|
+
.all({ convoy_id: convoyId });
|
|
604
|
+
}
|
|
605
|
+
deleteArtifactsOlderThan(days) {
|
|
606
|
+
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
|
|
607
|
+
const result = this.db
|
|
608
|
+
.prepare(`DELETE FROM artifact WHERE convoy_id IN (
|
|
609
|
+
SELECT id FROM convoy WHERE finished_at IS NOT NULL AND finished_at < :cutoff
|
|
610
|
+
)`)
|
|
611
|
+
.run({ cutoff });
|
|
612
|
+
return result.changes;
|
|
613
|
+
}
|
|
614
|
+
insertAgentIdentity(record) {
|
|
615
|
+
const summarySize = Buffer.byteLength(record.summary, 'utf8');
|
|
616
|
+
const truncatedSummary = summarySize > LIMIT_SUMMARY
|
|
617
|
+
? record.summary.slice(0, LIMIT_SUMMARY)
|
|
618
|
+
: record.summary;
|
|
619
|
+
this.db
|
|
620
|
+
.prepare(`INSERT INTO agent_identity
|
|
621
|
+
(id, agent, convoy_id, task_id, summary, created_at, retention_days)
|
|
622
|
+
VALUES
|
|
623
|
+
(:id, :agent, :convoy_id, :task_id, :summary, :created_at, :retention_days)`)
|
|
624
|
+
.run({ ...record, summary: truncatedSummary });
|
|
625
|
+
}
|
|
626
|
+
getAgentIdentities(agent, limit) {
|
|
627
|
+
return this.db
|
|
628
|
+
.prepare('SELECT * FROM agent_identity WHERE agent = :agent ORDER BY created_at DESC LIMIT :limit')
|
|
629
|
+
.all({ agent, limit });
|
|
630
|
+
}
|
|
631
|
+
listAgentIdentitySummary() {
|
|
632
|
+
return this.db
|
|
633
|
+
.prepare(`SELECT agent, COUNT(*) AS task_count, MAX(created_at) AS latest_date
|
|
634
|
+
FROM agent_identity GROUP BY agent ORDER BY agent`)
|
|
635
|
+
.all();
|
|
636
|
+
}
|
|
637
|
+
purgeAgentIdentities(agent) {
|
|
638
|
+
const result = this.db
|
|
639
|
+
.prepare('DELETE FROM agent_identity WHERE agent = :agent')
|
|
640
|
+
.run({ agent });
|
|
641
|
+
return result.changes;
|
|
642
|
+
}
|
|
643
|
+
deleteAgentIdentitiesOlderThan(days) {
|
|
644
|
+
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
|
|
645
|
+
const result = this.db
|
|
646
|
+
.prepare(`DELETE FROM agent_identity
|
|
647
|
+
WHERE created_at < :cutoff
|
|
648
|
+
OR (retention_days IS NOT NULL
|
|
649
|
+
AND created_at < datetime('now', '-' || retention_days || ' days'))`)
|
|
650
|
+
.run({ cutoff });
|
|
651
|
+
return result.changes;
|
|
652
|
+
}
|
|
653
|
+
getScratchpadValue(key) {
|
|
654
|
+
const row = this.db
|
|
655
|
+
.prepare('SELECT value FROM scratchpad WHERE key = :key')
|
|
656
|
+
.get({ key });
|
|
657
|
+
return row?.value ?? null;
|
|
658
|
+
}
|
|
659
|
+
setScratchpadValue(key, value) {
|
|
660
|
+
this.db
|
|
661
|
+
.prepare(`INSERT INTO scratchpad (key, value, updated_at)
|
|
662
|
+
VALUES (:key, :value, :updated_at)
|
|
663
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`)
|
|
664
|
+
.run({ key, value, updated_at: new Date().toISOString() });
|
|
665
|
+
}
|
|
666
|
+
clearScratchpad() {
|
|
667
|
+
this.db.exec('DELETE FROM scratchpad');
|
|
668
|
+
}
|
|
669
|
+
clearScratchpadOlderThan(days) {
|
|
670
|
+
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
|
|
671
|
+
this.db.prepare('DELETE FROM scratchpad WHERE updated_at < :cutoff').run({ cutoff });
|
|
672
|
+
}
|
|
262
673
|
insertPipeline(record) {
|
|
674
|
+
enforceLimit(record.spec_yaml, 'pipeline.spec_yaml', LIMIT_SPEC_YAML);
|
|
263
675
|
this.db
|
|
264
676
|
.prepare(`INSERT INTO pipeline (id, name, status, branch, spec_yaml, convoy_specs, created_at,
|
|
265
677
|
started_at, finished_at, total_tokens, total_cost_usd)
|
|
@@ -269,12 +681,12 @@ class ConvoyStoreImpl {
|
|
|
269
681
|
}
|
|
270
682
|
getPipeline(id) {
|
|
271
683
|
return this.db
|
|
272
|
-
.prepare('SELECT
|
|
684
|
+
.prepare('SELECT *, total_cost_usd_num AS total_cost_usd FROM pipeline WHERE id = :id')
|
|
273
685
|
.get({ id });
|
|
274
686
|
}
|
|
275
687
|
getLatestPipeline() {
|
|
276
688
|
return this.db
|
|
277
|
-
.prepare('SELECT
|
|
689
|
+
.prepare('SELECT *, total_cost_usd_num AS total_cost_usd FROM pipeline ORDER BY created_at DESC LIMIT 1')
|
|
278
690
|
.get();
|
|
279
691
|
}
|
|
280
692
|
updatePipelineStatus(id, status, extra) {
|
|
@@ -293,16 +705,215 @@ class ConvoyStoreImpl {
|
|
|
293
705
|
params.total_tokens = extra.total_tokens;
|
|
294
706
|
}
|
|
295
707
|
if (extra?.total_cost_usd !== undefined) {
|
|
296
|
-
sets.push('total_cost_usd = :
|
|
297
|
-
|
|
708
|
+
sets.push('total_cost_usd = :total_cost_usd_text');
|
|
709
|
+
sets.push('total_cost_usd_num = :total_cost_usd_num');
|
|
710
|
+
params.total_cost_usd_text = extra.total_cost_usd !== null ? String(extra.total_cost_usd) : null;
|
|
711
|
+
params.total_cost_usd_num = extra.total_cost_usd;
|
|
298
712
|
}
|
|
299
713
|
this.db.prepare(`UPDATE pipeline SET ${sets.join(', ')} WHERE id = :id`).run(params);
|
|
300
714
|
}
|
|
301
715
|
getConvoysByPipeline(pipelineId) {
|
|
302
716
|
return this.db
|
|
303
|
-
.prepare('SELECT
|
|
717
|
+
.prepare('SELECT *, total_cost_usd_num AS total_cost_usd FROM convoy WHERE pipeline_id = :pipeline_id ORDER BY created_at')
|
|
304
718
|
.all({ pipeline_id: pipelineId });
|
|
305
719
|
}
|
|
720
|
+
getConvoyCounts() {
|
|
721
|
+
const rows = this.db
|
|
722
|
+
.prepare('SELECT status, COUNT(*) AS cnt FROM convoy GROUP BY status')
|
|
723
|
+
.all();
|
|
724
|
+
const map = {};
|
|
725
|
+
for (const row of rows)
|
|
726
|
+
map[row.status] = row.cnt;
|
|
727
|
+
return {
|
|
728
|
+
total: rows.reduce((s, r) => s + r.cnt, 0),
|
|
729
|
+
running: map['running'] ?? 0,
|
|
730
|
+
done: map['done'] ?? 0,
|
|
731
|
+
failed: (map['failed'] ?? 0),
|
|
732
|
+
gate_failed: (map['gate-failed'] ?? 0) + (map['hook-failed'] ?? 0),
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
getConvoyDurationStats() {
|
|
736
|
+
const statsRow = this.db.prepare(`SELECT
|
|
737
|
+
AVG((julianday(finished_at) - julianday(started_at)) * 86400) AS avg_sec,
|
|
738
|
+
MAX((julianday(finished_at) - julianday(started_at)) * 86400) AS max_sec,
|
|
739
|
+
COUNT(*) AS cnt
|
|
740
|
+
FROM convoy
|
|
741
|
+
WHERE finished_at IS NOT NULL AND started_at IS NOT NULL`).get();
|
|
742
|
+
if (!statsRow || statsRow.cnt === 0)
|
|
743
|
+
return { avg_sec: null, p95_sec: null, max_sec: null };
|
|
744
|
+
const offset = Math.max(0, Math.floor(statsRow.cnt * 0.95) - 1);
|
|
745
|
+
const p95Row = this.db.prepare(`SELECT (julianday(finished_at) - julianday(started_at)) * 86400 AS duration
|
|
746
|
+
FROM convoy
|
|
747
|
+
WHERE finished_at IS NOT NULL AND started_at IS NOT NULL
|
|
748
|
+
ORDER BY duration
|
|
749
|
+
LIMIT 1 OFFSET :offset`).get({ offset });
|
|
750
|
+
return {
|
|
751
|
+
avg_sec: statsRow.avg_sec,
|
|
752
|
+
p95_sec: p95Row?.duration ?? null,
|
|
753
|
+
max_sec: statsRow.max_sec,
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
getTokenAndCostTotals() {
|
|
757
|
+
const row = this.db.prepare(`SELECT COALESCE(SUM(total_tokens), 0) AS total_tokens,
|
|
758
|
+
COALESCE(SUM(total_cost_usd_num), 0) AS total_cost_usd
|
|
759
|
+
FROM convoy`).get();
|
|
760
|
+
return { total_tokens: row.total_tokens, total_cost_usd: row.total_cost_usd };
|
|
761
|
+
}
|
|
762
|
+
getTopAgents(limit) {
|
|
763
|
+
return this.db.prepare(`SELECT agent,
|
|
764
|
+
COUNT(*) AS task_count,
|
|
765
|
+
COALESCE(SUM(total_tokens), 0) AS total_tokens
|
|
766
|
+
FROM task
|
|
767
|
+
GROUP BY agent
|
|
768
|
+
ORDER BY task_count DESC
|
|
769
|
+
LIMIT :limit`).all({ limit });
|
|
770
|
+
}
|
|
771
|
+
getTopModels(limit) {
|
|
772
|
+
return this.db.prepare(`SELECT model,
|
|
773
|
+
COUNT(*) AS task_count,
|
|
774
|
+
COALESCE(SUM(total_tokens), 0) AS total_tokens
|
|
775
|
+
FROM task
|
|
776
|
+
WHERE model IS NOT NULL
|
|
777
|
+
GROUP BY model
|
|
778
|
+
ORDER BY task_count DESC
|
|
779
|
+
LIMIT :limit`).all({ limit });
|
|
780
|
+
}
|
|
781
|
+
getDlqSummary() {
|
|
782
|
+
const countRow = this.db.prepare('SELECT COUNT(*) AS cnt FROM dlq').get();
|
|
783
|
+
const typeRows = this.db.prepare(`SELECT failure_type AS type, COUNT(*) AS count
|
|
784
|
+
FROM dlq
|
|
785
|
+
GROUP BY failure_type
|
|
786
|
+
ORDER BY count DESC`).all();
|
|
787
|
+
return { count: countRow.cnt, top_failure_types: typeRows };
|
|
788
|
+
}
|
|
789
|
+
getConvoyTaskSummary(convoyId) {
|
|
790
|
+
const row = this.db.prepare(`SELECT
|
|
791
|
+
COUNT(*) AS total,
|
|
792
|
+
SUM(CASE WHEN status = 'done' THEN 1 ELSE 0 END) AS done,
|
|
793
|
+
SUM(CASE WHEN status IN ('running', 'assigned') THEN 1 ELSE 0 END) AS running,
|
|
794
|
+
SUM(CASE WHEN status IN ('failed', 'gate-failed', 'timed-out', 'hook-failed') THEN 1 ELSE 0 END) AS failed,
|
|
795
|
+
SUM(CASE WHEN status = 'review-blocked' THEN 1 ELSE 0 END) AS review_blocked,
|
|
796
|
+
SUM(CASE WHEN status = 'disputed' THEN 1 ELSE 0 END) AS disputed,
|
|
797
|
+
SUM(CASE WHEN review_verdict IS NOT NULL THEN 1 ELSE 0 END) AS reviewed,
|
|
798
|
+
SUM(CASE WHEN panel_attempts > 0 THEN 1 ELSE 0 END) AS panel_reviewed,
|
|
799
|
+
SUM(CASE WHEN drift_score IS NOT NULL THEN 1 ELSE 0 END) AS tasks_with_drift,
|
|
800
|
+
MAX(drift_score) AS max_drift_score,
|
|
801
|
+
SUM(CASE WHEN drift_retried = 1 THEN 1 ELSE 0 END) AS drift_retried
|
|
802
|
+
FROM task
|
|
803
|
+
WHERE convoy_id = :convoy_id`).get({ convoy_id: convoyId });
|
|
804
|
+
if (!row || row.total === 0) {
|
|
805
|
+
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 };
|
|
806
|
+
}
|
|
807
|
+
return {
|
|
808
|
+
total: row.total ?? 0,
|
|
809
|
+
done: row.done ?? 0,
|
|
810
|
+
running: row.running ?? 0,
|
|
811
|
+
failed: row.failed ?? 0,
|
|
812
|
+
review_blocked: row.review_blocked ?? 0,
|
|
813
|
+
disputed: row.disputed ?? 0,
|
|
814
|
+
reviewed: row.reviewed ?? 0,
|
|
815
|
+
panel_reviewed: row.panel_reviewed ?? 0,
|
|
816
|
+
tasks_with_drift: row.tasks_with_drift ?? 0,
|
|
817
|
+
max_drift_score: row.max_drift_score ?? null,
|
|
818
|
+
drift_retried: row.drift_retried ?? 0,
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
getConvoyList(limit, offset) {
|
|
822
|
+
return this.db.prepare(`SELECT *, total_cost_usd_num AS total_cost_usd
|
|
823
|
+
FROM convoy
|
|
824
|
+
ORDER BY created_at DESC
|
|
825
|
+
LIMIT :limit OFFSET :offset`).all({ limit, offset });
|
|
826
|
+
}
|
|
827
|
+
getConvoyDetails(convoyId) {
|
|
828
|
+
const convoy = this.getConvoy(convoyId);
|
|
829
|
+
if (!convoy)
|
|
830
|
+
return null;
|
|
831
|
+
const tasks = this.getTasksByConvoy(convoyId);
|
|
832
|
+
const taskSummary = this.getConvoyTaskSummary(convoyId);
|
|
833
|
+
const dlqEntries = this.listDlqEntries(convoyId);
|
|
834
|
+
const rawEvents = this.getEvents(convoyId);
|
|
835
|
+
const artifacts = this.getArtifactsByConvoy(convoyId);
|
|
836
|
+
const limitedEvents = rawEvents.slice().reverse().slice(0, 500);
|
|
837
|
+
return {
|
|
838
|
+
convoy: {
|
|
839
|
+
id: convoy.id,
|
|
840
|
+
name: convoy.name,
|
|
841
|
+
status: convoy.status,
|
|
842
|
+
created_at: convoy.created_at,
|
|
843
|
+
finished_at: convoy.finished_at,
|
|
844
|
+
branch: convoy.branch,
|
|
845
|
+
total_tokens: convoy.total_tokens,
|
|
846
|
+
total_cost_usd: convoy.total_cost_usd,
|
|
847
|
+
},
|
|
848
|
+
taskSummary,
|
|
849
|
+
quality: {
|
|
850
|
+
reviewed_tasks: taskSummary.reviewed,
|
|
851
|
+
review_blocked_tasks: taskSummary.review_blocked,
|
|
852
|
+
disputed_tasks: taskSummary.disputed,
|
|
853
|
+
panel_reviews: taskSummary.panel_reviewed,
|
|
854
|
+
},
|
|
855
|
+
drift: {
|
|
856
|
+
tasks_with_drift: taskSummary.tasks_with_drift,
|
|
857
|
+
max_drift_score: taskSummary.max_drift_score,
|
|
858
|
+
drift_retried_tasks: taskSummary.drift_retried,
|
|
859
|
+
},
|
|
860
|
+
dlq_count: dlqEntries.length,
|
|
861
|
+
dlq_entries: dlqEntries.map(d => ({
|
|
862
|
+
id: d.id,
|
|
863
|
+
task_id: d.task_id,
|
|
864
|
+
agent: d.agent,
|
|
865
|
+
failure_type: d.failure_type,
|
|
866
|
+
attempts: d.attempts,
|
|
867
|
+
resolved: d.resolved,
|
|
868
|
+
})),
|
|
869
|
+
artifact_count: artifacts.length,
|
|
870
|
+
artifacts: artifacts.map(a => ({
|
|
871
|
+
id: a.id,
|
|
872
|
+
name: a.name,
|
|
873
|
+
type: a.type,
|
|
874
|
+
task_id: a.task_id,
|
|
875
|
+
created_at: a.created_at,
|
|
876
|
+
})),
|
|
877
|
+
has_more_events: rawEvents.length > 500,
|
|
878
|
+
events: limitedEvents.map(e => ({
|
|
879
|
+
type: e.type,
|
|
880
|
+
task_id: e.task_id,
|
|
881
|
+
data: e.data ? (() => { try {
|
|
882
|
+
return JSON.parse(e.data);
|
|
883
|
+
}
|
|
884
|
+
catch {
|
|
885
|
+
return e.data;
|
|
886
|
+
} })() : null,
|
|
887
|
+
created_at: e.created_at,
|
|
888
|
+
})),
|
|
889
|
+
tasks: tasks.map(t => ({
|
|
890
|
+
id: t.id,
|
|
891
|
+
phase: t.phase,
|
|
892
|
+
agent: t.agent,
|
|
893
|
+
model: t.model,
|
|
894
|
+
status: t.status,
|
|
895
|
+
retries: t.retries,
|
|
896
|
+
started_at: t.started_at,
|
|
897
|
+
finished_at: t.finished_at,
|
|
898
|
+
total_tokens: t.total_tokens,
|
|
899
|
+
cost_usd: t.cost_usd,
|
|
900
|
+
review_level: t.review_level,
|
|
901
|
+
review_verdict: t.review_verdict,
|
|
902
|
+
review_tokens: t.review_tokens,
|
|
903
|
+
review_model: t.review_model,
|
|
904
|
+
panel_attempts: t.panel_attempts,
|
|
905
|
+
dispute_id: t.dispute_id,
|
|
906
|
+
drift_score: t.drift_score,
|
|
907
|
+
drift_retried: t.drift_retried,
|
|
908
|
+
files: t.files ? (() => { try {
|
|
909
|
+
return JSON.parse(t.files);
|
|
910
|
+
}
|
|
911
|
+
catch {
|
|
912
|
+
return null;
|
|
913
|
+
} })() : null,
|
|
914
|
+
})),
|
|
915
|
+
};
|
|
916
|
+
}
|
|
306
917
|
withTransaction(fn) {
|
|
307
918
|
this.db.exec('BEGIN');
|
|
308
919
|
try {
|
|
@@ -319,6 +930,128 @@ class ConvoyStoreImpl {
|
|
|
319
930
|
this.db.close();
|
|
320
931
|
}
|
|
321
932
|
}
|
|
933
|
+
export function migrateSchema(db, dbPath, fromVersion, toVersion) {
|
|
934
|
+
for (let v = fromVersion; v < toVersion; v++) {
|
|
935
|
+
const backupPath = `${dbPath}.v${v}.bak`;
|
|
936
|
+
copyFileSync(dbPath, backupPath);
|
|
937
|
+
db.exec('BEGIN');
|
|
938
|
+
try {
|
|
939
|
+
if (v === 4) {
|
|
940
|
+
db.exec(`
|
|
941
|
+
ALTER TABLE task ADD COLUMN gates TEXT;
|
|
942
|
+
ALTER TABLE task ADD COLUMN on_exhausted TEXT NOT NULL DEFAULT 'dlq';
|
|
943
|
+
ALTER TABLE task ADD COLUMN injected INTEGER NOT NULL DEFAULT 0;
|
|
944
|
+
ALTER TABLE task ADD COLUMN provenance TEXT;
|
|
945
|
+
ALTER TABLE task ADD COLUMN idempotency_key TEXT;
|
|
946
|
+
CREATE UNIQUE INDEX idx_task_idempotency ON task(convoy_id, idempotency_key)
|
|
947
|
+
WHERE idempotency_key IS NOT NULL;
|
|
948
|
+
ALTER TABLE convoy ADD COLUMN circuit_state TEXT;
|
|
949
|
+
CREATE TABLE dlq (
|
|
950
|
+
id TEXT PRIMARY KEY,
|
|
951
|
+
convoy_id TEXT NOT NULL REFERENCES convoy(id),
|
|
952
|
+
task_id TEXT NOT NULL REFERENCES task(id),
|
|
953
|
+
agent TEXT NOT NULL,
|
|
954
|
+
failure_type TEXT NOT NULL,
|
|
955
|
+
error_output TEXT,
|
|
956
|
+
attempts INTEGER NOT NULL,
|
|
957
|
+
tokens_spent INTEGER,
|
|
958
|
+
escalation_task_id TEXT,
|
|
959
|
+
resolved INTEGER NOT NULL DEFAULT 0,
|
|
960
|
+
resolution TEXT,
|
|
961
|
+
created_at TEXT NOT NULL,
|
|
962
|
+
resolved_at TEXT
|
|
963
|
+
);
|
|
964
|
+
`);
|
|
965
|
+
}
|
|
966
|
+
if (v === 5) {
|
|
967
|
+
db.exec(`
|
|
968
|
+
ALTER TABLE task ADD COLUMN current_step INTEGER;
|
|
969
|
+
ALTER TABLE task ADD COLUMN total_steps INTEGER;
|
|
970
|
+
ALTER TABLE task ADD COLUMN review_level TEXT;
|
|
971
|
+
ALTER TABLE task ADD COLUMN review_verdict TEXT;
|
|
972
|
+
ALTER TABLE task ADD COLUMN review_tokens INTEGER;
|
|
973
|
+
ALTER TABLE task ADD COLUMN review_model TEXT;
|
|
974
|
+
ALTER TABLE task ADD COLUMN panel_attempts INTEGER NOT NULL DEFAULT 0;
|
|
975
|
+
ALTER TABLE task ADD COLUMN dispute_id TEXT;
|
|
976
|
+
ALTER TABLE convoy ADD COLUMN review_tokens_total INTEGER;
|
|
977
|
+
ALTER TABLE convoy ADD COLUMN review_budget INTEGER;
|
|
978
|
+
CREATE TABLE task_step (
|
|
979
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
980
|
+
task_id TEXT NOT NULL REFERENCES task(id),
|
|
981
|
+
step_index INTEGER NOT NULL,
|
|
982
|
+
prompt TEXT NOT NULL,
|
|
983
|
+
gates TEXT,
|
|
984
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
985
|
+
exit_code INTEGER,
|
|
986
|
+
output TEXT,
|
|
987
|
+
started_at TEXT,
|
|
988
|
+
finished_at TEXT
|
|
989
|
+
);
|
|
990
|
+
`);
|
|
991
|
+
}
|
|
992
|
+
if (v === 6) {
|
|
993
|
+
db.exec(`
|
|
994
|
+
ALTER TABLE task ADD COLUMN drift_score REAL;
|
|
995
|
+
ALTER TABLE task ADD COLUMN drift_retried INTEGER NOT NULL DEFAULT 0;
|
|
996
|
+
`);
|
|
997
|
+
}
|
|
998
|
+
if (v === 7) {
|
|
999
|
+
db.exec(`
|
|
1000
|
+
ALTER TABLE task ADD COLUMN outputs TEXT;
|
|
1001
|
+
ALTER TABLE task ADD COLUMN inputs TEXT;
|
|
1002
|
+
ALTER TABLE task ADD COLUMN discovered_issues TEXT;
|
|
1003
|
+
CREATE TABLE artifact (
|
|
1004
|
+
id TEXT PRIMARY KEY,
|
|
1005
|
+
convoy_id TEXT NOT NULL REFERENCES convoy(id),
|
|
1006
|
+
task_id TEXT NOT NULL REFERENCES task(id),
|
|
1007
|
+
name TEXT NOT NULL,
|
|
1008
|
+
type TEXT NOT NULL,
|
|
1009
|
+
content TEXT NOT NULL CHECK (length(content) <= 1048576),
|
|
1010
|
+
created_at TEXT NOT NULL,
|
|
1011
|
+
UNIQUE(convoy_id, name)
|
|
1012
|
+
);
|
|
1013
|
+
CREATE TABLE agent_identity (
|
|
1014
|
+
id TEXT PRIMARY KEY,
|
|
1015
|
+
agent TEXT NOT NULL,
|
|
1016
|
+
convoy_id TEXT NOT NULL,
|
|
1017
|
+
task_id TEXT NOT NULL,
|
|
1018
|
+
summary TEXT NOT NULL,
|
|
1019
|
+
created_at TEXT NOT NULL,
|
|
1020
|
+
retention_days INTEGER NOT NULL DEFAULT 90
|
|
1021
|
+
);
|
|
1022
|
+
`);
|
|
1023
|
+
}
|
|
1024
|
+
if (v === 8) {
|
|
1025
|
+
db.exec(`
|
|
1026
|
+
CREATE TABLE scratchpad (
|
|
1027
|
+
key TEXT PRIMARY KEY,
|
|
1028
|
+
value TEXT NOT NULL,
|
|
1029
|
+
updated_at TEXT NOT NULL
|
|
1030
|
+
);
|
|
1031
|
+
`);
|
|
1032
|
+
}
|
|
1033
|
+
if (v === 9) {
|
|
1034
|
+
db.exec(`
|
|
1035
|
+
ALTER TABLE convoy ADD COLUMN total_cost_usd_num REAL;
|
|
1036
|
+
ALTER TABLE task ADD COLUMN cost_usd_num REAL;
|
|
1037
|
+
ALTER TABLE pipeline ADD COLUMN total_cost_usd_num REAL;
|
|
1038
|
+
UPDATE convoy SET total_cost_usd_num = CAST(total_cost_usd AS REAL) WHERE total_cost_usd IS NOT NULL;
|
|
1039
|
+
UPDATE task SET cost_usd_num = CAST(cost_usd AS REAL) WHERE cost_usd IS NOT NULL;
|
|
1040
|
+
UPDATE pipeline SET total_cost_usd_num = CAST(total_cost_usd AS REAL) WHERE total_cost_usd IS NOT NULL;
|
|
1041
|
+
`);
|
|
1042
|
+
}
|
|
1043
|
+
db.exec('COMMIT');
|
|
1044
|
+
}
|
|
1045
|
+
catch (err) {
|
|
1046
|
+
try {
|
|
1047
|
+
db.exec('ROLLBACK');
|
|
1048
|
+
}
|
|
1049
|
+
catch { /* ignore */ }
|
|
1050
|
+
throw new Error(`Migration v${v}→v${v + 1} failed. Backup at ${backupPath}. Original error: ${err.message}`);
|
|
1051
|
+
}
|
|
1052
|
+
db.exec(`PRAGMA user_version = ${v + 1}`);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
322
1055
|
export function createConvoyStore(dbPath) {
|
|
323
1056
|
return new ConvoyStoreImpl(dbPath);
|
|
324
1057
|
}
|