@hasna/loops 0.4.0 → 0.4.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/CHANGELOG.md +18 -0
- package/README.md +75 -33
- package/dist/api/index.d.ts +17 -0
- package/dist/api/index.js +1290 -0
- package/dist/cli/index.js +738 -201
- package/dist/daemon/index.js +64 -15
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1005 -155
- package/dist/lib/format.d.ts +3 -1
- package/dist/lib/mode.d.ts +48 -0
- package/dist/lib/mode.js +276 -0
- package/dist/lib/route/todos-cli.d.ts +1 -0
- package/dist/lib/route/types.d.ts +10 -0
- package/dist/lib/storage/contract.d.ts +133 -0
- package/dist/lib/storage/contract.js +1 -0
- package/dist/lib/storage/index.d.ts +6 -0
- package/dist/lib/storage/index.js +4612 -0
- package/dist/lib/storage/postgres-schema.d.ts +4 -0
- package/dist/lib/storage/postgres-schema.js +328 -0
- package/dist/lib/storage/postgres.d.ts +22 -0
- package/dist/lib/storage/postgres.js +423 -0
- package/dist/lib/storage/sqlite.d.ts +84 -0
- package/dist/lib/storage/sqlite.js +4187 -0
- package/dist/lib/store.d.ts +3 -0
- package/dist/lib/store.js +27 -10
- package/dist/lib/template-kit.d.ts +10 -8
- package/dist/lib/templates.d.ts +1 -0
- package/dist/mcp/index.js +64 -15
- package/dist/runner/index.d.ts +32 -0
- package/dist/runner/index.js +2037 -0
- package/dist/sdk/index.js +33 -15
- package/dist/types.d.ts +1 -1
- package/docs/DEPLOYMENT_MODES.md +148 -0
- package/docs/USAGE.md +66 -32
- package/package.json +34 -3
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/lib/storage/postgres-schema.ts
|
|
3
|
+
import { createHash } from "crypto";
|
|
4
|
+
var POSTGRES_MIGRATION_LEDGER_TABLE = "open_loops_schema_migrations";
|
|
5
|
+
function checksumStorageSql(sql) {
|
|
6
|
+
const normalized = sql.trim().replace(/\r\n/g, `
|
|
7
|
+
`);
|
|
8
|
+
return `sha256:${createHash("sha256").update(normalized).digest("hex")}`;
|
|
9
|
+
}
|
|
10
|
+
function migration(id, sql) {
|
|
11
|
+
return Object.freeze({ id, sql: sql.trim(), checksum: checksumStorageSql(sql) });
|
|
12
|
+
}
|
|
13
|
+
var POSTGRES_STORAGE_MIGRATIONS = Object.freeze([
|
|
14
|
+
migration("0001_core_runtime", `
|
|
15
|
+
CREATE TABLE IF NOT EXISTS loops (
|
|
16
|
+
id TEXT PRIMARY KEY,
|
|
17
|
+
name TEXT NOT NULL,
|
|
18
|
+
description TEXT,
|
|
19
|
+
status TEXT NOT NULL,
|
|
20
|
+
archived_at TIMESTAMPTZ,
|
|
21
|
+
archived_from_status TEXT,
|
|
22
|
+
schedule_json JSONB NOT NULL,
|
|
23
|
+
target_json JSONB NOT NULL,
|
|
24
|
+
goal_json JSONB,
|
|
25
|
+
machine_json JSONB,
|
|
26
|
+
next_run_at TIMESTAMPTZ,
|
|
27
|
+
retry_scheduled_for TIMESTAMPTZ,
|
|
28
|
+
catch_up TEXT NOT NULL,
|
|
29
|
+
catch_up_limit INTEGER NOT NULL,
|
|
30
|
+
overlap TEXT NOT NULL,
|
|
31
|
+
max_attempts INTEGER NOT NULL,
|
|
32
|
+
retry_delay_ms INTEGER NOT NULL,
|
|
33
|
+
lease_ms INTEGER NOT NULL,
|
|
34
|
+
expires_at TIMESTAMPTZ,
|
|
35
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
36
|
+
updated_at TIMESTAMPTZ NOT NULL
|
|
37
|
+
);
|
|
38
|
+
CREATE INDEX IF NOT EXISTS idx_loops_status_next ON loops(status, next_run_at);
|
|
39
|
+
CREATE INDEX IF NOT EXISTS idx_loops_name ON loops(name);
|
|
40
|
+
|
|
41
|
+
CREATE TABLE IF NOT EXISTS loop_runs (
|
|
42
|
+
id TEXT PRIMARY KEY,
|
|
43
|
+
loop_id TEXT NOT NULL REFERENCES loops(id) ON DELETE CASCADE,
|
|
44
|
+
loop_name TEXT NOT NULL,
|
|
45
|
+
scheduled_for TIMESTAMPTZ NOT NULL,
|
|
46
|
+
attempt INTEGER NOT NULL,
|
|
47
|
+
status TEXT NOT NULL,
|
|
48
|
+
started_at TIMESTAMPTZ,
|
|
49
|
+
finished_at TIMESTAMPTZ,
|
|
50
|
+
claimed_by TEXT,
|
|
51
|
+
claim_token TEXT,
|
|
52
|
+
lease_expires_at TIMESTAMPTZ,
|
|
53
|
+
pid INTEGER,
|
|
54
|
+
pgid INTEGER,
|
|
55
|
+
process_started_at TIMESTAMPTZ,
|
|
56
|
+
exit_code INTEGER,
|
|
57
|
+
duration_ms INTEGER,
|
|
58
|
+
stdout TEXT,
|
|
59
|
+
stderr TEXT,
|
|
60
|
+
error TEXT,
|
|
61
|
+
goal_run_id TEXT,
|
|
62
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
63
|
+
updated_at TIMESTAMPTZ NOT NULL,
|
|
64
|
+
UNIQUE(loop_id, scheduled_for)
|
|
65
|
+
);
|
|
66
|
+
CREATE INDEX IF NOT EXISTS idx_runs_loop ON loop_runs(loop_id, created_at);
|
|
67
|
+
CREATE INDEX IF NOT EXISTS idx_runs_status ON loop_runs(status);
|
|
68
|
+
CREATE INDEX IF NOT EXISTS idx_runs_status_lease ON loop_runs(status, lease_expires_at);
|
|
69
|
+
CREATE INDEX IF NOT EXISTS idx_runs_scheduled ON loop_runs(scheduled_for);
|
|
70
|
+
CREATE INDEX IF NOT EXISTS idx_runs_claim_token ON loop_runs(claim_token) WHERE claim_token IS NOT NULL;
|
|
71
|
+
|
|
72
|
+
CREATE TABLE IF NOT EXISTS daemon_lease (
|
|
73
|
+
id TEXT PRIMARY KEY,
|
|
74
|
+
pid INTEGER NOT NULL,
|
|
75
|
+
hostname TEXT NOT NULL,
|
|
76
|
+
heartbeat_at TIMESTAMPTZ NOT NULL,
|
|
77
|
+
expires_at TIMESTAMPTZ NOT NULL,
|
|
78
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
79
|
+
updated_at TIMESTAMPTZ NOT NULL
|
|
80
|
+
);
|
|
81
|
+
`),
|
|
82
|
+
migration("0002_workflows_goals", `
|
|
83
|
+
CREATE TABLE IF NOT EXISTS workflow_specs (
|
|
84
|
+
id TEXT PRIMARY KEY,
|
|
85
|
+
name TEXT NOT NULL,
|
|
86
|
+
description TEXT,
|
|
87
|
+
version INTEGER NOT NULL,
|
|
88
|
+
status TEXT NOT NULL,
|
|
89
|
+
goal_json JSONB,
|
|
90
|
+
steps_json JSONB NOT NULL,
|
|
91
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
92
|
+
updated_at TIMESTAMPTZ NOT NULL
|
|
93
|
+
);
|
|
94
|
+
CREATE INDEX IF NOT EXISTS idx_workflows_status_name ON workflow_specs(status, name);
|
|
95
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_workflows_name_active ON workflow_specs(name) WHERE status = 'active';
|
|
96
|
+
|
|
97
|
+
CREATE TABLE IF NOT EXISTS workflow_runs (
|
|
98
|
+
id TEXT PRIMARY KEY,
|
|
99
|
+
workflow_id TEXT NOT NULL REFERENCES workflow_specs(id) ON DELETE CASCADE,
|
|
100
|
+
workflow_name TEXT NOT NULL,
|
|
101
|
+
loop_id TEXT REFERENCES loops(id) ON DELETE SET NULL,
|
|
102
|
+
loop_run_id TEXT REFERENCES loop_runs(id) ON DELETE SET NULL,
|
|
103
|
+
invocation_id TEXT,
|
|
104
|
+
work_item_id TEXT,
|
|
105
|
+
scheduled_for TIMESTAMPTZ,
|
|
106
|
+
idempotency_key TEXT,
|
|
107
|
+
manifest_path TEXT,
|
|
108
|
+
status TEXT NOT NULL,
|
|
109
|
+
started_at TIMESTAMPTZ,
|
|
110
|
+
finished_at TIMESTAMPTZ,
|
|
111
|
+
duration_ms INTEGER,
|
|
112
|
+
error TEXT,
|
|
113
|
+
goal_run_id TEXT,
|
|
114
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
115
|
+
updated_at TIMESTAMPTZ NOT NULL
|
|
116
|
+
);
|
|
117
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_workflow_runs_idempotency ON workflow_runs(workflow_id, idempotency_key) WHERE idempotency_key IS NOT NULL;
|
|
118
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_runs_workflow_created ON workflow_runs(workflow_id, created_at DESC);
|
|
119
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_runs_loop_run ON workflow_runs(loop_run_id);
|
|
120
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_runs_status ON workflow_runs(status);
|
|
121
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_runs_invocation ON workflow_runs(invocation_id);
|
|
122
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_runs_work_item ON workflow_runs(work_item_id);
|
|
123
|
+
|
|
124
|
+
CREATE TABLE IF NOT EXISTS workflow_invocations (
|
|
125
|
+
id TEXT PRIMARY KEY,
|
|
126
|
+
workflow_id TEXT,
|
|
127
|
+
template_id TEXT,
|
|
128
|
+
source_kind TEXT NOT NULL,
|
|
129
|
+
source_id TEXT,
|
|
130
|
+
source_dedupe_key TEXT,
|
|
131
|
+
source_json JSONB NOT NULL,
|
|
132
|
+
subject_kind TEXT NOT NULL,
|
|
133
|
+
subject_id TEXT,
|
|
134
|
+
subject_path TEXT,
|
|
135
|
+
subject_url TEXT,
|
|
136
|
+
subject_json JSONB NOT NULL,
|
|
137
|
+
intent TEXT NOT NULL,
|
|
138
|
+
scope_json JSONB,
|
|
139
|
+
output_policy_json JSONB,
|
|
140
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
141
|
+
updated_at TIMESTAMPTZ NOT NULL
|
|
142
|
+
);
|
|
143
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_invocations_source ON workflow_invocations(source_kind, source_id);
|
|
144
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_invocations_subject ON workflow_invocations(subject_kind, subject_id, subject_path);
|
|
145
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_workflow_invocations_dedupe ON workflow_invocations(source_kind, source_dedupe_key) WHERE source_dedupe_key IS NOT NULL;
|
|
146
|
+
|
|
147
|
+
CREATE TABLE IF NOT EXISTS workflow_work_items (
|
|
148
|
+
id TEXT PRIMARY KEY,
|
|
149
|
+
route_key TEXT NOT NULL,
|
|
150
|
+
idempotency_key TEXT NOT NULL,
|
|
151
|
+
invocation_id TEXT NOT NULL REFERENCES workflow_invocations(id) ON DELETE CASCADE,
|
|
152
|
+
source_type TEXT NOT NULL,
|
|
153
|
+
source_ref TEXT NOT NULL,
|
|
154
|
+
subject_ref TEXT NOT NULL,
|
|
155
|
+
project_key TEXT,
|
|
156
|
+
project_group TEXT,
|
|
157
|
+
priority INTEGER NOT NULL,
|
|
158
|
+
status TEXT NOT NULL,
|
|
159
|
+
attempts INTEGER NOT NULL,
|
|
160
|
+
next_attempt_at TIMESTAMPTZ,
|
|
161
|
+
lease_expires_at TIMESTAMPTZ,
|
|
162
|
+
workflow_id TEXT REFERENCES workflow_specs(id) ON DELETE SET NULL,
|
|
163
|
+
loop_id TEXT REFERENCES loops(id) ON DELETE SET NULL,
|
|
164
|
+
workflow_run_id TEXT REFERENCES workflow_runs(id) ON DELETE SET NULL,
|
|
165
|
+
last_reason TEXT,
|
|
166
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
167
|
+
updated_at TIMESTAMPTZ NOT NULL,
|
|
168
|
+
UNIQUE(route_key, idempotency_key)
|
|
169
|
+
);
|
|
170
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_work_items_status_next ON workflow_work_items(status, next_attempt_at, priority DESC, created_at ASC);
|
|
171
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_work_items_project ON workflow_work_items(project_key, status);
|
|
172
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_work_items_group ON workflow_work_items(project_group, status);
|
|
173
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_work_items_invocation ON workflow_work_items(invocation_id);
|
|
174
|
+
|
|
175
|
+
CREATE TABLE IF NOT EXISTS workflow_step_runs (
|
|
176
|
+
id TEXT PRIMARY KEY,
|
|
177
|
+
workflow_run_id TEXT NOT NULL REFERENCES workflow_runs(id) ON DELETE CASCADE,
|
|
178
|
+
step_id TEXT NOT NULL,
|
|
179
|
+
sequence INTEGER NOT NULL,
|
|
180
|
+
status TEXT NOT NULL,
|
|
181
|
+
started_at TIMESTAMPTZ,
|
|
182
|
+
finished_at TIMESTAMPTZ,
|
|
183
|
+
exit_code INTEGER,
|
|
184
|
+
pid INTEGER,
|
|
185
|
+
duration_ms INTEGER,
|
|
186
|
+
stdout TEXT,
|
|
187
|
+
stderr TEXT,
|
|
188
|
+
error TEXT,
|
|
189
|
+
account_profile TEXT,
|
|
190
|
+
account_tool TEXT,
|
|
191
|
+
goal_run_id TEXT,
|
|
192
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
193
|
+
updated_at TIMESTAMPTZ NOT NULL,
|
|
194
|
+
UNIQUE(workflow_run_id, step_id)
|
|
195
|
+
);
|
|
196
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_step_runs_run_sequence ON workflow_step_runs(workflow_run_id, sequence);
|
|
197
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_step_runs_status ON workflow_step_runs(status);
|
|
198
|
+
|
|
199
|
+
CREATE TABLE IF NOT EXISTS workflow_events (
|
|
200
|
+
id TEXT PRIMARY KEY,
|
|
201
|
+
workflow_run_id TEXT NOT NULL REFERENCES workflow_runs(id) ON DELETE CASCADE,
|
|
202
|
+
sequence INTEGER NOT NULL,
|
|
203
|
+
event_type TEXT NOT NULL,
|
|
204
|
+
step_id TEXT,
|
|
205
|
+
payload_json JSONB,
|
|
206
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
207
|
+
UNIQUE(workflow_run_id, sequence)
|
|
208
|
+
);
|
|
209
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_events_run_sequence ON workflow_events(workflow_run_id, sequence);
|
|
210
|
+
|
|
211
|
+
CREATE TABLE IF NOT EXISTS goals (
|
|
212
|
+
id TEXT PRIMARY KEY,
|
|
213
|
+
plan_id TEXT NOT NULL,
|
|
214
|
+
objective TEXT NOT NULL,
|
|
215
|
+
status TEXT NOT NULL,
|
|
216
|
+
token_budget INTEGER,
|
|
217
|
+
tokens_used INTEGER NOT NULL,
|
|
218
|
+
time_used_seconds INTEGER NOT NULL,
|
|
219
|
+
auto_execute TEXT NOT NULL,
|
|
220
|
+
max_tokens INTEGER,
|
|
221
|
+
source_type TEXT,
|
|
222
|
+
source_id TEXT,
|
|
223
|
+
loop_id TEXT,
|
|
224
|
+
loop_run_id TEXT,
|
|
225
|
+
workflow_id TEXT,
|
|
226
|
+
workflow_run_id TEXT,
|
|
227
|
+
workflow_step_id TEXT,
|
|
228
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
229
|
+
updated_at TIMESTAMPTZ NOT NULL
|
|
230
|
+
);
|
|
231
|
+
CREATE INDEX IF NOT EXISTS idx_goals_status_updated ON goals(status, updated_at DESC);
|
|
232
|
+
CREATE INDEX IF NOT EXISTS idx_goals_loop_run ON goals(loop_run_id);
|
|
233
|
+
CREATE INDEX IF NOT EXISTS idx_goals_workflow_run ON goals(workflow_run_id);
|
|
234
|
+
CREATE INDEX IF NOT EXISTS idx_goals_source ON goals(source_type, source_id);
|
|
235
|
+
|
|
236
|
+
CREATE TABLE IF NOT EXISTS goal_plan_nodes (
|
|
237
|
+
id TEXT PRIMARY KEY,
|
|
238
|
+
goal_id TEXT NOT NULL REFERENCES goals(id) ON DELETE CASCADE,
|
|
239
|
+
plan_id TEXT NOT NULL,
|
|
240
|
+
key TEXT NOT NULL,
|
|
241
|
+
sequence INTEGER NOT NULL,
|
|
242
|
+
priority INTEGER NOT NULL,
|
|
243
|
+
objective TEXT NOT NULL,
|
|
244
|
+
status TEXT NOT NULL,
|
|
245
|
+
ready BOOLEAN NOT NULL,
|
|
246
|
+
token_budget INTEGER,
|
|
247
|
+
tokens_used INTEGER NOT NULL,
|
|
248
|
+
time_used_seconds INTEGER NOT NULL,
|
|
249
|
+
depends_on_json JSONB NOT NULL,
|
|
250
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
251
|
+
updated_at TIMESTAMPTZ NOT NULL,
|
|
252
|
+
UNIQUE(plan_id, key)
|
|
253
|
+
);
|
|
254
|
+
CREATE INDEX IF NOT EXISTS idx_goal_plan_nodes_goal_sequence ON goal_plan_nodes(goal_id, sequence);
|
|
255
|
+
CREATE INDEX IF NOT EXISTS idx_goal_plan_nodes_status ON goal_plan_nodes(status);
|
|
256
|
+
|
|
257
|
+
CREATE TABLE IF NOT EXISTS goal_runs (
|
|
258
|
+
id TEXT PRIMARY KEY,
|
|
259
|
+
goal_id TEXT NOT NULL REFERENCES goals(id) ON DELETE CASCADE,
|
|
260
|
+
plan_id TEXT NOT NULL,
|
|
261
|
+
loop_id TEXT,
|
|
262
|
+
loop_run_id TEXT,
|
|
263
|
+
workflow_id TEXT,
|
|
264
|
+
workflow_run_id TEXT,
|
|
265
|
+
workflow_step_id TEXT,
|
|
266
|
+
turn INTEGER NOT NULL,
|
|
267
|
+
phase TEXT NOT NULL,
|
|
268
|
+
status TEXT NOT NULL,
|
|
269
|
+
node_key TEXT,
|
|
270
|
+
tokens_used INTEGER NOT NULL,
|
|
271
|
+
evidence_json JSONB,
|
|
272
|
+
raw_response_json JSONB,
|
|
273
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
274
|
+
updated_at TIMESTAMPTZ NOT NULL
|
|
275
|
+
);
|
|
276
|
+
CREATE INDEX IF NOT EXISTS idx_goal_runs_goal_created ON goal_runs(goal_id, created_at);
|
|
277
|
+
CREATE INDEX IF NOT EXISTS idx_goal_runs_loop_run ON goal_runs(loop_run_id);
|
|
278
|
+
CREATE INDEX IF NOT EXISTS idx_goal_runs_workflow_run ON goal_runs(workflow_run_id);
|
|
279
|
+
`),
|
|
280
|
+
migration("0003_remote_runners_and_audit", `
|
|
281
|
+
CREATE TABLE IF NOT EXISTS runner_machines (
|
|
282
|
+
id TEXT PRIMARY KEY,
|
|
283
|
+
hostname TEXT NOT NULL,
|
|
284
|
+
labels_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
285
|
+
capabilities_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
286
|
+
status TEXT NOT NULL,
|
|
287
|
+
last_seen_at TIMESTAMPTZ NOT NULL,
|
|
288
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
289
|
+
updated_at TIMESTAMPTZ NOT NULL
|
|
290
|
+
);
|
|
291
|
+
CREATE INDEX IF NOT EXISTS idx_runner_machines_status_seen ON runner_machines(status, last_seen_at DESC);
|
|
292
|
+
|
|
293
|
+
CREATE TABLE IF NOT EXISTS runner_leases (
|
|
294
|
+
id TEXT PRIMARY KEY,
|
|
295
|
+
runner_id TEXT NOT NULL REFERENCES runner_machines(id) ON DELETE CASCADE,
|
|
296
|
+
loop_run_id TEXT REFERENCES loop_runs(id) ON DELETE CASCADE,
|
|
297
|
+
workflow_run_id TEXT REFERENCES workflow_runs(id) ON DELETE CASCADE,
|
|
298
|
+
claim_token TEXT NOT NULL,
|
|
299
|
+
status TEXT NOT NULL,
|
|
300
|
+
heartbeat_at TIMESTAMPTZ NOT NULL,
|
|
301
|
+
expires_at TIMESTAMPTZ NOT NULL,
|
|
302
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
303
|
+
updated_at TIMESTAMPTZ NOT NULL,
|
|
304
|
+
CHECK (loop_run_id IS NOT NULL OR workflow_run_id IS NOT NULL)
|
|
305
|
+
);
|
|
306
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_runner_leases_active_loop_run ON runner_leases(loop_run_id) WHERE loop_run_id IS NOT NULL AND status = 'active';
|
|
307
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_runner_leases_active_workflow_run ON runner_leases(workflow_run_id) WHERE workflow_run_id IS NOT NULL AND status = 'active';
|
|
308
|
+
CREATE INDEX IF NOT EXISTS idx_runner_leases_runner_status ON runner_leases(runner_id, status, expires_at);
|
|
309
|
+
CREATE INDEX IF NOT EXISTS idx_runner_leases_claim_token ON runner_leases(claim_token);
|
|
310
|
+
|
|
311
|
+
CREATE TABLE IF NOT EXISTS audit_events (
|
|
312
|
+
id TEXT PRIMARY KEY,
|
|
313
|
+
actor TEXT NOT NULL,
|
|
314
|
+
action TEXT NOT NULL,
|
|
315
|
+
subject_type TEXT NOT NULL,
|
|
316
|
+
subject_id TEXT NOT NULL,
|
|
317
|
+
metadata_json JSONB,
|
|
318
|
+
created_at TIMESTAMPTZ NOT NULL
|
|
319
|
+
);
|
|
320
|
+
CREATE INDEX IF NOT EXISTS idx_audit_events_subject ON audit_events(subject_type, subject_id, created_at DESC);
|
|
321
|
+
CREATE INDEX IF NOT EXISTS idx_audit_events_action ON audit_events(action, created_at DESC);
|
|
322
|
+
`)
|
|
323
|
+
]);
|
|
324
|
+
|
|
325
|
+
// src/lib/storage/postgres.ts
|
|
326
|
+
class PostgresStorage {
|
|
327
|
+
executor;
|
|
328
|
+
backend = "postgres";
|
|
329
|
+
migrations;
|
|
330
|
+
constructor(executor, migrations = POSTGRES_STORAGE_MIGRATIONS) {
|
|
331
|
+
this.executor = executor;
|
|
332
|
+
this.migrations = migrations;
|
|
333
|
+
}
|
|
334
|
+
async listAppliedMigrations() {
|
|
335
|
+
await this.ensureLedger();
|
|
336
|
+
return this.readAppliedMigrations();
|
|
337
|
+
}
|
|
338
|
+
async migrate(opts = {}) {
|
|
339
|
+
const dryRun = opts.dryRun === true;
|
|
340
|
+
if (!dryRun)
|
|
341
|
+
await this.ensureLedger();
|
|
342
|
+
const applied = dryRun ? await this.tryReadAppliedMigrations() : await this.readAppliedMigrations();
|
|
343
|
+
const knownMigrationIds = new Set(this.migrations.map((migration2) => migration2.id));
|
|
344
|
+
for (const row of applied) {
|
|
345
|
+
if (!knownMigrationIds.has(row.id)) {
|
|
346
|
+
throw new Error(`Postgres migration ${row.id} is not recognized by this binary`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
const appliedById = new Map(applied.map((row) => [row.id, row]));
|
|
350
|
+
const plan = this.migrations.map((migration2) => ({
|
|
351
|
+
migration: migration2,
|
|
352
|
+
state: appliedById.has(migration2.id) ? "already_applied" : "pending"
|
|
353
|
+
}));
|
|
354
|
+
for (const migration2 of this.migrations) {
|
|
355
|
+
const existing = appliedById.get(migration2.id);
|
|
356
|
+
if (existing && existing.checksum !== migration2.checksum) {
|
|
357
|
+
throw new Error(`Postgres migration checksum mismatch for ${migration2.id}`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (dryRun)
|
|
361
|
+
return { backend: this.backend, dryRun, applied, plan };
|
|
362
|
+
const run = async () => {
|
|
363
|
+
for (const item of plan) {
|
|
364
|
+
if (item.state === "already_applied")
|
|
365
|
+
continue;
|
|
366
|
+
await this.executor.execute(item.migration.sql);
|
|
367
|
+
await this.executor.execute(`INSERT INTO ${POSTGRES_MIGRATION_LEDGER_TABLE} (id, checksum, applied_at) VALUES ($1, $2, NOW())`, [item.migration.id, item.migration.checksum]);
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
if (this.executor.transaction)
|
|
371
|
+
await this.executor.transaction(run);
|
|
372
|
+
else
|
|
373
|
+
await run();
|
|
374
|
+
return {
|
|
375
|
+
backend: this.backend,
|
|
376
|
+
dryRun,
|
|
377
|
+
applied: await this.readAppliedMigrations(),
|
|
378
|
+
plan
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
async close() {
|
|
382
|
+
await this.executor.close?.();
|
|
383
|
+
}
|
|
384
|
+
async ensureLedger() {
|
|
385
|
+
await this.executor.execute(`
|
|
386
|
+
CREATE TABLE IF NOT EXISTS ${POSTGRES_MIGRATION_LEDGER_TABLE} (
|
|
387
|
+
id TEXT PRIMARY KEY,
|
|
388
|
+
checksum TEXT NOT NULL,
|
|
389
|
+
applied_at TIMESTAMPTZ NOT NULL
|
|
390
|
+
);
|
|
391
|
+
`);
|
|
392
|
+
}
|
|
393
|
+
async readAppliedMigrations() {
|
|
394
|
+
const rows = await this.executor.query(`SELECT id, checksum, applied_at FROM ${POSTGRES_MIGRATION_LEDGER_TABLE} ORDER BY id ASC`);
|
|
395
|
+
return rows.map((row) => ({
|
|
396
|
+
id: row.id,
|
|
397
|
+
checksum: row.checksum,
|
|
398
|
+
appliedAt: row.applied_at instanceof Date ? row.applied_at.toISOString() : row.applied_at
|
|
399
|
+
}));
|
|
400
|
+
}
|
|
401
|
+
async tryReadAppliedMigrations() {
|
|
402
|
+
try {
|
|
403
|
+
return await this.readAppliedMigrations();
|
|
404
|
+
} catch (error) {
|
|
405
|
+
if (isMissingLedgerError(error))
|
|
406
|
+
return [];
|
|
407
|
+
throw error;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
function createPostgresStorage(executor) {
|
|
412
|
+
return new PostgresStorage(executor);
|
|
413
|
+
}
|
|
414
|
+
function isMissingLedgerError(error) {
|
|
415
|
+
if (!(error instanceof Error))
|
|
416
|
+
return false;
|
|
417
|
+
const message = error.message.toLowerCase();
|
|
418
|
+
return message.includes(POSTGRES_MIGRATION_LEDGER_TABLE.toLowerCase()) && (message.includes("does not exist") || message.includes("no such table") || message.includes("undefined_table"));
|
|
419
|
+
}
|
|
420
|
+
export {
|
|
421
|
+
createPostgresStorage,
|
|
422
|
+
PostgresStorage
|
|
423
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Store } from "../store.js";
|
|
2
|
+
import type { LoopStorageContract, LoopStorageMethodName } from "./contract.js";
|
|
3
|
+
type StoreMethod<K extends LoopStorageMethodName> = Store[K] extends (...args: infer Args) => infer Result ? {
|
|
4
|
+
args: Args;
|
|
5
|
+
result: Result;
|
|
6
|
+
} : never;
|
|
7
|
+
export declare class SqliteLoopStorage implements LoopStorageContract {
|
|
8
|
+
readonly store: Store;
|
|
9
|
+
readonly backend = "sqlite";
|
|
10
|
+
readonly supportsRemoteRunners = false;
|
|
11
|
+
constructor(store?: Store);
|
|
12
|
+
close(): Promise<void>;
|
|
13
|
+
private call;
|
|
14
|
+
createLoop(...args: StoreMethod<"createLoop">["args"]): Promise<import("../../types.js").Loop>;
|
|
15
|
+
getLoop(...args: StoreMethod<"getLoop">["args"]): Promise<import("../../types.js").Loop | undefined>;
|
|
16
|
+
findLoopByName(...args: StoreMethod<"findLoopByName">["args"]): Promise<import("../../types.js").Loop | undefined>;
|
|
17
|
+
requireLoop(...args: StoreMethod<"requireLoop">["args"]): Promise<import("../../types.js").Loop>;
|
|
18
|
+
listLoops(...args: StoreMethod<"listLoops">["args"]): Promise<import("../../types.js").Loop[]>;
|
|
19
|
+
dueLoops(...args: StoreMethod<"dueLoops">["args"]): Promise<import("../../types.js").Loop[]>;
|
|
20
|
+
updateLoop(...args: StoreMethod<"updateLoop">["args"]): Promise<import("../../types.js").Loop>;
|
|
21
|
+
renameLoop(...args: StoreMethod<"renameLoop">["args"]): Promise<import("../../types.js").Loop>;
|
|
22
|
+
archiveLoop(...args: StoreMethod<"archiveLoop">["args"]): Promise<import("../../types.js").Loop>;
|
|
23
|
+
unarchiveLoop(...args: StoreMethod<"unarchiveLoop">["args"]): Promise<import("../../types.js").Loop>;
|
|
24
|
+
deleteLoop(...args: StoreMethod<"deleteLoop">["args"]): Promise<boolean>;
|
|
25
|
+
createWorkflow(...args: StoreMethod<"createWorkflow">["args"]): Promise<import("../../types.js").WorkflowSpec>;
|
|
26
|
+
getWorkflow(...args: StoreMethod<"getWorkflow">["args"]): Promise<import("../../types.js").WorkflowSpec | undefined>;
|
|
27
|
+
listWorkflows(...args: StoreMethod<"listWorkflows">["args"]): Promise<import("../../types.js").WorkflowSpec[]>;
|
|
28
|
+
countWorkflows(...args: StoreMethod<"countWorkflows">["args"]): Promise<number>;
|
|
29
|
+
archiveWorkflow(...args: StoreMethod<"archiveWorkflow">["args"]): Promise<import("../../types.js").WorkflowSpec>;
|
|
30
|
+
createWorkflowInvocation(...args: StoreMethod<"createWorkflowInvocation">["args"]): Promise<import("../../types.js").WorkflowInvocation>;
|
|
31
|
+
getWorkflowInvocation(...args: StoreMethod<"getWorkflowInvocation">["args"]): Promise<import("../../types.js").WorkflowInvocation | undefined>;
|
|
32
|
+
listWorkflowInvocations(...args: StoreMethod<"listWorkflowInvocations">["args"]): Promise<import("../../types.js").WorkflowInvocation[]>;
|
|
33
|
+
upsertWorkflowWorkItem(...args: StoreMethod<"upsertWorkflowWorkItem">["args"]): Promise<import("../../types.js").WorkflowWorkItem>;
|
|
34
|
+
getWorkflowWorkItem(...args: StoreMethod<"getWorkflowWorkItem">["args"]): Promise<import("../../types.js").WorkflowWorkItem | undefined>;
|
|
35
|
+
listWorkflowWorkItems(...args: StoreMethod<"listWorkflowWorkItems">["args"]): Promise<import("../../types.js").WorkflowWorkItem[]>;
|
|
36
|
+
countActiveWorkflowWorkItems(...args: StoreMethod<"countActiveWorkflowWorkItems">["args"]): Promise<{
|
|
37
|
+
global: number;
|
|
38
|
+
project: number;
|
|
39
|
+
projectGroup?: number;
|
|
40
|
+
}>;
|
|
41
|
+
admitWorkflowWorkItem(...args: StoreMethod<"admitWorkflowWorkItem">["args"]): Promise<import("../../types.js").WorkflowWorkItem>;
|
|
42
|
+
createGoal(...args: StoreMethod<"createGoal">["args"]): Promise<import("../goal/types.js").Goal>;
|
|
43
|
+
getGoal(...args: StoreMethod<"getGoal">["args"]): Promise<import("../goal/types.js").Goal | undefined>;
|
|
44
|
+
listGoals(...args: StoreMethod<"listGoals">["args"]): Promise<import("../goal/types.js").Goal[]>;
|
|
45
|
+
createGoalPlanNodes(...args: StoreMethod<"createGoalPlanNodes">["args"]): Promise<import("../goal/types.js").GoalPlanNode[]>;
|
|
46
|
+
listGoalPlanNodes(...args: StoreMethod<"listGoalPlanNodes">["args"]): Promise<import("../goal/types.js").GoalPlanNode[]>;
|
|
47
|
+
updateGoalStatus(...args: StoreMethod<"updateGoalStatus">["args"]): Promise<import("../goal/types.js").Goal>;
|
|
48
|
+
updateGoalPlanNode(...args: StoreMethod<"updateGoalPlanNode">["args"]): Promise<import("../goal/types.js").GoalPlanNode>;
|
|
49
|
+
recordGoalEvent(...args: StoreMethod<"recordGoalEvent">["args"]): Promise<import("../goal/types.js").GoalRun>;
|
|
50
|
+
listGoalRuns(...args: StoreMethod<"listGoalRuns">["args"]): Promise<import("../goal/types.js").GoalRun[]>;
|
|
51
|
+
createWorkflowRun(...args: StoreMethod<"createWorkflowRun">["args"]): Promise<import("../../types.js").WorkflowRun>;
|
|
52
|
+
getWorkflowRun(...args: StoreMethod<"getWorkflowRun">["args"]): Promise<import("../../types.js").WorkflowRun | undefined>;
|
|
53
|
+
listWorkflowRuns(...args: StoreMethod<"listWorkflowRuns">["args"]): Promise<import("../../types.js").WorkflowRun[]>;
|
|
54
|
+
listWorkflowStepRuns(...args: StoreMethod<"listWorkflowStepRuns">["args"]): Promise<import("../../types.js").WorkflowStepRun[]>;
|
|
55
|
+
getWorkflowStepRun(...args: StoreMethod<"getWorkflowStepRun">["args"]): Promise<import("../../types.js").WorkflowStepRun | undefined>;
|
|
56
|
+
startWorkflowStepRun(...args: StoreMethod<"startWorkflowStepRun">["args"]): Promise<import("../../types.js").WorkflowStepRun>;
|
|
57
|
+
recoverWorkflowRun(...args: StoreMethod<"recoverWorkflowRun">["args"]): Promise<{
|
|
58
|
+
run: import("../../types.js").WorkflowRun;
|
|
59
|
+
recoveredSteps: import("../../types.js").WorkflowStepRun[];
|
|
60
|
+
}>;
|
|
61
|
+
finalizeWorkflowStepRun(...args: StoreMethod<"finalizeWorkflowStepRun">["args"]): Promise<import("../../types.js").WorkflowStepRun>;
|
|
62
|
+
finalizeWorkflowRun(...args: StoreMethod<"finalizeWorkflowRun">["args"]): Promise<import("../../types.js").WorkflowRun>;
|
|
63
|
+
appendWorkflowEvent(...args: StoreMethod<"appendWorkflowEvent">["args"]): Promise<import("../../types.js").WorkflowEvent>;
|
|
64
|
+
listWorkflowEvents(...args: StoreMethod<"listWorkflowEvents">["args"]): Promise<import("../../types.js").WorkflowEvent[]>;
|
|
65
|
+
recordRunProcess(...args: StoreMethod<"recordRunProcess">["args"]): Promise<import("../../types.js").LoopRun | undefined>;
|
|
66
|
+
createSkippedRun(...args: StoreMethod<"createSkippedRun">["args"]): Promise<import("../../types.js").LoopRun>;
|
|
67
|
+
getRun(...args: StoreMethod<"getRun">["args"]): Promise<import("../../types.js").LoopRun | undefined>;
|
|
68
|
+
getRunBySlot(...args: StoreMethod<"getRunBySlot">["args"]): Promise<import("../../types.js").LoopRun | undefined>;
|
|
69
|
+
claimRun(...args: StoreMethod<"claimRun">["args"]): Promise<import("../store.js").ClaimRunResult | undefined>;
|
|
70
|
+
finalizeRun(...args: StoreMethod<"finalizeRun">["args"]): Promise<import("../../types.js").LoopRun>;
|
|
71
|
+
heartbeatRunLease(...args: StoreMethod<"heartbeatRunLease">["args"]): Promise<import("../../types.js").LoopRun | undefined>;
|
|
72
|
+
listRuns(...args: StoreMethod<"listRuns">["args"]): Promise<import("../../types.js").LoopRun[]>;
|
|
73
|
+
recoverExpiredRunLeases(...args: StoreMethod<"recoverExpiredRunLeases">["args"]): Promise<import("../../types.js").LoopRun[]>;
|
|
74
|
+
recoverExpiredRunLeasesDetailed(...args: StoreMethod<"recoverExpiredRunLeasesDetailed">["args"]): Promise<import("../store.js").RecoverExpiredRunLeasesResult>;
|
|
75
|
+
countLoops(...args: StoreMethod<"countLoops">["args"]): Promise<number>;
|
|
76
|
+
countRuns(...args: StoreMethod<"countRuns">["args"]): Promise<number>;
|
|
77
|
+
pruneHistory(...args: StoreMethod<"pruneHistory">["args"]): Promise<import("../store.js").PruneHistorySummary>;
|
|
78
|
+
acquireDaemonLease(...args: StoreMethod<"acquireDaemonLease">["args"]): Promise<import("../store.js").DaemonLease | undefined>;
|
|
79
|
+
heartbeatDaemonLease(...args: StoreMethod<"heartbeatDaemonLease">["args"]): Promise<import("../store.js").DaemonLease | undefined>;
|
|
80
|
+
releaseDaemonLease(...args: StoreMethod<"releaseDaemonLease">["args"]): Promise<void>;
|
|
81
|
+
getDaemonLease(...args: StoreMethod<"getDaemonLease">["args"]): Promise<import("../store.js").DaemonLease | undefined>;
|
|
82
|
+
}
|
|
83
|
+
export declare function createSqliteLoopStorage(path?: string): SqliteLoopStorage;
|
|
84
|
+
export {};
|