@treeseed/sdk 0.10.6 → 0.10.8

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.
Files changed (50) hide show
  1. package/dist/api/auth/d1-provider.d.ts +5 -0
  2. package/dist/api/auth/d1-provider.js +3 -0
  3. package/dist/api/auth/d1-store.d.ts +5 -0
  4. package/dist/api/auth/d1-store.js +62 -0
  5. package/dist/api/auth/memory-provider.d.ts +5 -0
  6. package/dist/api/auth/memory-provider.js +41 -0
  7. package/dist/api/types.d.ts +5 -0
  8. package/dist/index.d.ts +2 -0
  9. package/dist/index.js +58 -0
  10. package/dist/market-client.d.ts +119 -0
  11. package/dist/market-client.js +79 -0
  12. package/dist/operations/repository-operations.d.ts +129 -0
  13. package/dist/operations/repository-operations.js +634 -0
  14. package/dist/operations/services/config-runtime.d.ts +7 -6
  15. package/dist/operations/services/config-runtime.js +45 -25
  16. package/dist/operations/services/deploy.d.ts +42 -0
  17. package/dist/operations/services/deploy.js +1 -1
  18. package/dist/operations/services/project-platform.d.ts +41 -1
  19. package/dist/operations/services/project-platform.js +14 -1
  20. package/dist/operations/services/railway-api.d.ts +35 -1
  21. package/dist/operations/services/railway-api.js +240 -35
  22. package/dist/operations/services/railway-deploy.d.ts +16 -234
  23. package/dist/operations/services/railway-deploy.js +177 -62
  24. package/dist/operations/services/release-candidate.js +1 -2
  25. package/dist/operations/services/runtime-tools.d.ts +14 -0
  26. package/dist/operations/services/runtime-tools.js +15 -1
  27. package/dist/operations/services/workspace-save.d.ts +24 -0
  28. package/dist/operations/services/workspace-save.js +143 -3
  29. package/dist/operations/services/workspace-tools.js +1 -1
  30. package/dist/platform/env.yaml +163 -2
  31. package/dist/platform/environment.d.ts +1 -0
  32. package/dist/platform/environment.js +9 -0
  33. package/dist/platform-operation-store.d.ts +90 -0
  34. package/dist/platform-operation-store.js +505 -0
  35. package/dist/platform-operations.d.ts +265 -0
  36. package/dist/platform-operations.js +421 -0
  37. package/dist/reconcile/bootstrap-systems.js +3 -3
  38. package/dist/reconcile/builtin-adapters.js +225 -29
  39. package/dist/reconcile/contracts.d.ts +1 -1
  40. package/dist/reconcile/desired-state.d.ts +14 -0
  41. package/dist/reconcile/desired-state.js +4 -0
  42. package/dist/reconcile/engine.d.ts +28 -0
  43. package/dist/reconcile/state.js +3 -0
  44. package/dist/reconcile/units.js +2 -0
  45. package/dist/workflow/operations.d.ts +13 -5
  46. package/dist/workflow/operations.js +69 -12
  47. package/dist/workflow-state.d.ts +2 -0
  48. package/dist/workflow-state.js +7 -2
  49. package/dist/workflow.d.ts +2 -0
  50. package/package.json +15 -2
@@ -0,0 +1,505 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { NodeSqliteD1Database } from "./db/node-sqlite.js";
3
+ const PLATFORM_OPERATION_SCHEMA_SQL = `
4
+ CREATE TABLE IF NOT EXISTS platform_operations (
5
+ id TEXT PRIMARY KEY,
6
+ namespace TEXT NOT NULL,
7
+ operation TEXT NOT NULL,
8
+ status TEXT NOT NULL,
9
+ target TEXT NOT NULL,
10
+ idempotency_key TEXT,
11
+ input_json TEXT NOT NULL DEFAULT '{}',
12
+ output_json TEXT,
13
+ error_json TEXT,
14
+ requested_by_type TEXT NOT NULL,
15
+ requested_by_id TEXT,
16
+ assigned_runner_id TEXT,
17
+ lease_expires_at TEXT,
18
+ created_at TEXT NOT NULL,
19
+ updated_at TEXT NOT NULL,
20
+ started_at TEXT,
21
+ finished_at TEXT,
22
+ cancelled_at TEXT
23
+ );
24
+
25
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_platform_operations_idempotency
26
+ ON platform_operations(namespace, operation, idempotency_key)
27
+ WHERE idempotency_key IS NOT NULL;
28
+
29
+ CREATE INDEX IF NOT EXISTS idx_platform_operations_runnable
30
+ ON platform_operations(status, created_at ASC);
31
+
32
+ CREATE TABLE IF NOT EXISTS platform_operation_events (
33
+ id TEXT PRIMARY KEY,
34
+ operation_id TEXT NOT NULL,
35
+ seq INTEGER NOT NULL,
36
+ kind TEXT NOT NULL,
37
+ data_json TEXT NOT NULL DEFAULT '{}',
38
+ created_at TEXT NOT NULL,
39
+ FOREIGN KEY (operation_id) REFERENCES platform_operations(id) ON DELETE CASCADE
40
+ );
41
+
42
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_platform_operation_events_seq
43
+ ON platform_operation_events(operation_id, seq);
44
+
45
+ CREATE TABLE IF NOT EXISTS market_operation_runners (
46
+ id TEXT PRIMARY KEY,
47
+ runner_key TEXT NOT NULL UNIQUE,
48
+ name TEXT NOT NULL,
49
+ environment TEXT NOT NULL,
50
+ status TEXT NOT NULL DEFAULT 'online',
51
+ version TEXT,
52
+ capabilities_json TEXT NOT NULL DEFAULT '[]',
53
+ active_job_count INTEGER NOT NULL DEFAULT 0,
54
+ max_concurrent_jobs INTEGER NOT NULL DEFAULT 1,
55
+ heartbeat_at TEXT,
56
+ metadata_json TEXT NOT NULL DEFAULT '{}',
57
+ created_at TEXT NOT NULL,
58
+ updated_at TEXT NOT NULL
59
+ );
60
+
61
+ CREATE TABLE IF NOT EXISTS platform_repository_claims (
62
+ id TEXT PRIMARY KEY,
63
+ repository_key TEXT NOT NULL,
64
+ runner_id TEXT NOT NULL,
65
+ workspace_path TEXT NOT NULL,
66
+ branch TEXT,
67
+ commit_sha TEXT,
68
+ claim_state TEXT NOT NULL DEFAULT 'active',
69
+ lease_expires_at TEXT,
70
+ metadata_json TEXT NOT NULL DEFAULT '{}',
71
+ created_at TEXT NOT NULL,
72
+ updated_at TEXT NOT NULL
73
+ );
74
+
75
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_platform_repository_claims_active
76
+ ON platform_repository_claims(repository_key, runner_id)
77
+ WHERE claim_state = 'active';
78
+
79
+ CREATE INDEX IF NOT EXISTS idx_platform_repository_claims_runner
80
+ ON platform_repository_claims(runner_id, claim_state);
81
+ `;
82
+ function isoNow(now) {
83
+ return now().toISOString();
84
+ }
85
+ function parseJson(value, fallback) {
86
+ if (typeof value !== "string" || !value) return fallback;
87
+ try {
88
+ return JSON.parse(value);
89
+ } catch {
90
+ return fallback;
91
+ }
92
+ }
93
+ function rowOperation(row) {
94
+ if (!row) return null;
95
+ return {
96
+ id: String(row.id),
97
+ namespace: String(row.namespace),
98
+ operation: String(row.operation),
99
+ status: String(row.status),
100
+ target: String(row.target),
101
+ idempotencyKey: row.idempotency_key == null ? null : String(row.idempotency_key),
102
+ input: parseJson(row.input_json, {}),
103
+ output: parseJson(row.output_json, null),
104
+ error: parseJson(row.error_json, null),
105
+ requestedByType: String(row.requested_by_type),
106
+ requestedById: row.requested_by_id == null ? null : String(row.requested_by_id),
107
+ assignedRunnerId: row.assigned_runner_id == null ? null : String(row.assigned_runner_id),
108
+ leaseExpiresAt: row.lease_expires_at == null ? null : String(row.lease_expires_at),
109
+ createdAt: String(row.created_at),
110
+ updatedAt: String(row.updated_at),
111
+ startedAt: row.started_at == null ? null : String(row.started_at),
112
+ finishedAt: row.finished_at == null ? null : String(row.finished_at),
113
+ cancelledAt: row.cancelled_at == null ? null : String(row.cancelled_at)
114
+ };
115
+ }
116
+ function rowEvent(row) {
117
+ if (!row) return null;
118
+ return {
119
+ id: String(row.id),
120
+ operationId: String(row.operation_id),
121
+ seq: Number(row.seq),
122
+ kind: String(row.kind),
123
+ data: parseJson(row.data_json, {}),
124
+ createdAt: String(row.created_at)
125
+ };
126
+ }
127
+ function repositoryKey(repository = {}) {
128
+ return [repository.provider ?? "git", repository.owner ?? "local", repository.name ?? "repository"].join("-").toLowerCase().replace(/[^a-z0-9.-]+/gu, "-").replace(/^-+|-+$/gu, "") || "repository";
129
+ }
130
+ function repositoryWorkspacePath(workspaceRoot, repository = {}) {
131
+ const root = String(workspaceRoot ?? "/data").replace(/\/+$/u, "") || "/data";
132
+ return `${root}/repositories/${repositoryKey(repository)}/repo`;
133
+ }
134
+ function convertQuestionPlaceholders(query) {
135
+ let index = 0;
136
+ return query.replace(/\?/gu, () => `$${++index}`);
137
+ }
138
+ function createD1RelationalAdapter(db) {
139
+ return {
140
+ provider: "d1",
141
+ async run(query, params = []) {
142
+ await db.prepare(query).bind(...params).run();
143
+ },
144
+ async first(query, params = []) {
145
+ return db.prepare(query).bind(...params).first();
146
+ },
147
+ async all(query, params = []) {
148
+ const result = await db.prepare(query).bind(...params).all();
149
+ return Array.isArray(result) ? result : result.results ?? [];
150
+ },
151
+ async exec(query) {
152
+ if (!db.exec) {
153
+ for (const statement of query.split(/;\s*/u).map((entry) => entry.trim()).filter(Boolean)) {
154
+ await this.run(statement);
155
+ }
156
+ return;
157
+ }
158
+ await db.exec(query);
159
+ }
160
+ };
161
+ }
162
+ function createSqliteRelationalAdapter(path) {
163
+ const database = new NodeSqliteD1Database(path);
164
+ return {
165
+ ...createD1RelationalAdapter(database),
166
+ provider: "sqlite",
167
+ close: () => database.close()
168
+ };
169
+ }
170
+ async function createPostgresRelationalAdapter(databaseUrl) {
171
+ const importer = new Function("specifier", "return import(specifier)");
172
+ const { Pool } = await importer("pg");
173
+ const pool = new Pool({ connectionString: databaseUrl });
174
+ return {
175
+ provider: "postgres",
176
+ async run(query, params = []) {
177
+ await pool.query(convertQuestionPlaceholders(query), params);
178
+ },
179
+ async first(query, params = []) {
180
+ const result = await pool.query(convertQuestionPlaceholders(query), params);
181
+ return result.rows[0] ?? null;
182
+ },
183
+ async all(query, params = []) {
184
+ const result = await pool.query(convertQuestionPlaceholders(query), params);
185
+ return result.rows;
186
+ },
187
+ async exec(query) {
188
+ await pool.query(query);
189
+ },
190
+ async transaction(callback) {
191
+ await pool.query("BEGIN");
192
+ try {
193
+ const result = await callback();
194
+ await pool.query("COMMIT");
195
+ return result;
196
+ } catch (error) {
197
+ await pool.query("ROLLBACK");
198
+ throw error;
199
+ }
200
+ },
201
+ close: () => pool.end()
202
+ };
203
+ }
204
+ async function createRelationalAdapterFromUrl(databaseUrl) {
205
+ const value = databaseUrl.trim();
206
+ if (/^postgres(ql)?:\/\//iu.test(value)) return createPostgresRelationalAdapter(value);
207
+ if (/^sqlite:\/\//iu.test(value)) return createSqliteRelationalAdapter(value.replace(/^sqlite:\/\//iu, ""));
208
+ return createSqliteRelationalAdapter(value);
209
+ }
210
+ async function createPlatformOperationStoreFromEnv(options = {}) {
211
+ const databaseUrl = options.databaseUrl ?? globalThis.process?.env?.TREESEED_MARKET_DATABASE_URL ?? null;
212
+ if (!databaseUrl?.trim()) throw new Error("TREESEED_MARKET_DATABASE_URL is required for direct database platform operations.");
213
+ const database = await createRelationalAdapterFromUrl(databaseUrl);
214
+ return new PlatformOperationStore({ database, initializeSchema: options.initializeSchema ?? true });
215
+ }
216
+ class PlatformOperationStore {
217
+ initialized = false;
218
+ database;
219
+ now;
220
+ initializeSchema;
221
+ constructor(options) {
222
+ this.database = options.database;
223
+ this.now = options.now ?? (() => /* @__PURE__ */ new Date());
224
+ this.initializeSchema = options.initializeSchema ?? true;
225
+ }
226
+ async close() {
227
+ await this.database.close?.();
228
+ }
229
+ async ensureInitialized() {
230
+ if (this.initialized) return;
231
+ if (this.initializeSchema) {
232
+ if (this.database.exec) await this.database.exec(PLATFORM_OPERATION_SCHEMA_SQL);
233
+ else {
234
+ for (const statement of PLATFORM_OPERATION_SCHEMA_SQL.split(/;\s*/u).map((entry) => entry.trim()).filter(Boolean)) {
235
+ await this.database.run(statement);
236
+ }
237
+ }
238
+ }
239
+ this.initialized = true;
240
+ }
241
+ async appendPlatformOperationEvent(operationId, kind, data = {}) {
242
+ await this.ensureInitialized();
243
+ const row = await this.database.first(
244
+ `SELECT COALESCE(MAX(seq), 0) + 1 AS next_seq FROM platform_operation_events WHERE operation_id = ?`,
245
+ [operationId]
246
+ );
247
+ const seq = Number(row?.next_seq ?? 1);
248
+ const timestamp = isoNow(this.now);
249
+ const id = randomUUID();
250
+ await this.database.run(
251
+ `INSERT INTO platform_operation_events (id, operation_id, seq, kind, data_json, created_at)
252
+ VALUES (?, ?, ?, ?, ?, ?)`,
253
+ [id, operationId, seq, kind, JSON.stringify(data ?? {}), timestamp]
254
+ );
255
+ return rowEvent(await this.database.first(`SELECT * FROM platform_operation_events WHERE id = ?`, [id]));
256
+ }
257
+ async register(request) {
258
+ return { ok: true, runner: await this.upsertRunner(request) };
259
+ }
260
+ async heartbeat(request) {
261
+ return { ok: true, runner: await this.upsertRunner(request) };
262
+ }
263
+ async upsertRunner(input) {
264
+ await this.ensureInitialized();
265
+ const timestamp = isoNow(this.now);
266
+ const id = input.runnerId;
267
+ await this.database.run(
268
+ `INSERT INTO market_operation_runners (
269
+ id, runner_key, name, environment, status, version, capabilities_json,
270
+ active_job_count, max_concurrent_jobs, heartbeat_at, metadata_json, created_at, updated_at
271
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
272
+ ON CONFLICT(id) DO UPDATE SET
273
+ runner_key = excluded.runner_key,
274
+ name = excluded.name,
275
+ environment = excluded.environment,
276
+ status = excluded.status,
277
+ version = excluded.version,
278
+ capabilities_json = excluded.capabilities_json,
279
+ active_job_count = excluded.active_job_count,
280
+ max_concurrent_jobs = excluded.max_concurrent_jobs,
281
+ heartbeat_at = excluded.heartbeat_at,
282
+ metadata_json = excluded.metadata_json,
283
+ updated_at = excluded.updated_at`,
284
+ [
285
+ id,
286
+ ("runnerKey" in input ? input.runnerKey : void 0) ?? id,
287
+ ("name" in input ? input.name : void 0) ?? id,
288
+ input.environment ?? "unknown",
289
+ ("status" in input ? input.status : void 0) ?? "online",
290
+ input.version ?? null,
291
+ JSON.stringify(Array.isArray(input.capabilities) ? input.capabilities : []),
292
+ Math.max(0, Number(("activeJobCount" in input ? input.activeJobCount : void 0) ?? 0) || 0),
293
+ Math.max(1, Number(input.maxConcurrentJobs ?? 1) || 1),
294
+ timestamp,
295
+ JSON.stringify(input.metadata ?? {}),
296
+ timestamp,
297
+ timestamp
298
+ ]
299
+ );
300
+ return this.database.first(`SELECT * FROM market_operation_runners WHERE id = ?`, [id]);
301
+ }
302
+ async getOperation(operationId) {
303
+ await this.ensureInitialized();
304
+ const operation = rowOperation(await this.database.first(`SELECT * FROM platform_operations WHERE id = ?`, [operationId]));
305
+ if (!operation) throw new Error(`Unknown platform operation "${operationId}".`);
306
+ return { ok: true, operation };
307
+ }
308
+ async claimJob(input) {
309
+ await this.ensureInitialized();
310
+ const runnerId = input.runnerId;
311
+ const leaseSeconds = Math.max(30, Math.min(Number(input.leaseSeconds ?? 300), 3600));
312
+ const now = isoNow(this.now);
313
+ const leaseExpiresAt = new Date(this.now().getTime() + leaseSeconds * 1e3).toISOString();
314
+ const rows = input.operationId ? await this.database.all(
315
+ `SELECT * FROM platform_operations
316
+ WHERE id = ? AND (
317
+ status = 'queued'
318
+ OR (status = 'leased' AND lease_expires_at IS NOT NULL AND lease_expires_at < ?)
319
+ )
320
+ ORDER BY created_at ASC LIMIT 1`,
321
+ [input.operationId, now]
322
+ ) : await this.database.all(
323
+ `SELECT * FROM platform_operations
324
+ WHERE status = 'queued'
325
+ OR (status = 'leased' AND lease_expires_at IS NOT NULL AND lease_expires_at < ?)
326
+ ORDER BY created_at ASC LIMIT 1`,
327
+ [now]
328
+ );
329
+ const row = rows[0];
330
+ if (!row) return { ok: true, operation: null };
331
+ await this.database.run(
332
+ `UPDATE platform_operations
333
+ SET status = 'leased',
334
+ assigned_runner_id = ?,
335
+ lease_expires_at = ?,
336
+ started_at = COALESCE(started_at, ?),
337
+ updated_at = ?
338
+ WHERE id = ?`,
339
+ [runnerId, leaseExpiresAt, now, now, row.id]
340
+ );
341
+ await this.appendPlatformOperationEvent(String(row.id), "claimed", { runnerId, leaseExpiresAt });
342
+ const operation = rowOperation(await this.database.first(`SELECT * FROM platform_operations WHERE id = ?`, [row.id]));
343
+ if (operation?.input?.repository && typeof operation.input.repository === "object" && !Array.isArray(operation.input.repository)) {
344
+ const runner = await this.database.first(`SELECT * FROM market_operation_runners WHERE id = ?`, [runnerId]);
345
+ const metadata = parseJson(runner?.metadata_json, {});
346
+ const workspaceRoot = metadata.dataDir ?? "/data";
347
+ const repository = operation.input.repository;
348
+ await this.upsertRepositoryClaim({
349
+ runnerId,
350
+ repository,
351
+ workspaceRoot,
352
+ branch: String(repository.defaultBranch ?? ""),
353
+ leaseSeconds,
354
+ metadata: { operationId: operation.id, namespace: operation.namespace, operation: operation.operation }
355
+ });
356
+ }
357
+ return { ok: true, operation };
358
+ }
359
+ async assertRunnerUpdate(operationId, runnerId) {
360
+ const operation = (await this.getOperation(operationId)).operation;
361
+ if (!runnerId) throw new Error("runnerId is required.");
362
+ if (operation.assignedRunnerId !== runnerId) throw new Error("Platform operation is assigned to a different runner.");
363
+ if (["succeeded", "failed", "cancelled"].includes(operation.status)) throw new Error(`Platform operation is already ${operation.status}.`);
364
+ return operation;
365
+ }
366
+ async appendEvent(operationId, request) {
367
+ await this.assertRunnerUpdate(operationId, request.runnerId);
368
+ return { ok: true, event: await this.appendPlatformOperationEvent(operationId, request.event?.kind ?? "event", request.event?.data ?? {}) };
369
+ }
370
+ async renewLease(operationId, request) {
371
+ await this.assertRunnerUpdate(operationId, request.runnerId);
372
+ const leaseSeconds = Math.max(30, Math.min(Number(request.leaseSeconds ?? 300), 3600));
373
+ const timestamp = isoNow(this.now);
374
+ const leaseExpiresAt = new Date(this.now().getTime() + leaseSeconds * 1e3).toISOString();
375
+ await this.database.run(
376
+ `UPDATE platform_operations SET lease_expires_at = ?, updated_at = ? WHERE id = ?`,
377
+ [leaseExpiresAt, timestamp, operationId]
378
+ );
379
+ await this.appendPlatformOperationEvent(operationId, request.event?.kind ?? "runner.lease_renewed", request.event?.data ?? { runnerId: request.runnerId, leaseExpiresAt });
380
+ await this.renewRepositoryClaimsForRunner(request.runnerId, leaseSeconds);
381
+ return this.getOperation(operationId);
382
+ }
383
+ async checkpoint(operationId, request) {
384
+ await this.assertRunnerUpdate(operationId, request.runnerId);
385
+ const timestamp = isoNow(this.now);
386
+ await this.database.run(
387
+ `UPDATE platform_operations SET status = 'running', output_json = ?, updated_at = ? WHERE id = ?`,
388
+ [JSON.stringify(request.output ?? null), timestamp, operationId]
389
+ );
390
+ await this.appendPlatformOperationEvent(operationId, request.event?.kind ?? "checkpoint", request.event?.data ?? { runnerId: request.runnerId ?? null });
391
+ return this.getOperation(operationId);
392
+ }
393
+ async complete(operationId, request) {
394
+ await this.assertRunnerUpdate(operationId, request.runnerId);
395
+ const timestamp = isoNow(this.now);
396
+ await this.database.run(
397
+ `UPDATE platform_operations
398
+ SET status = 'succeeded', output_json = ?, error_json = NULL, lease_expires_at = NULL, updated_at = ?, finished_at = ?
399
+ WHERE id = ?`,
400
+ [JSON.stringify(request.output ?? null), timestamp, timestamp, operationId]
401
+ );
402
+ await this.appendPlatformOperationEvent(operationId, request.event?.kind ?? "completed", request.event?.data ?? {});
403
+ const output = request.output && typeof request.output === "object" ? request.output : {};
404
+ await this.releaseRepositoryClaimsForRunner(request.runnerId, {
405
+ branch: output.operationBranch ?? output.branch ?? null,
406
+ commitSha: output.commitSha ?? null,
407
+ metadata: { operationId, status: "succeeded" }
408
+ });
409
+ return this.getOperation(operationId);
410
+ }
411
+ async fail(operationId, request) {
412
+ await this.assertRunnerUpdate(operationId, request.runnerId);
413
+ const timestamp = isoNow(this.now);
414
+ await this.database.run(
415
+ `UPDATE platform_operations
416
+ SET status = 'failed', error_json = ?, lease_expires_at = NULL, updated_at = ?, finished_at = ?
417
+ WHERE id = ?`,
418
+ [JSON.stringify(request.error ?? { message: "Platform operation failed." }), timestamp, timestamp, operationId]
419
+ );
420
+ await this.appendPlatformOperationEvent(operationId, request.event?.kind ?? "failed", request.event?.data ?? {});
421
+ await this.releaseRepositoryClaimsForRunner(request.runnerId, {
422
+ claimState: "released",
423
+ metadata: { operationId, status: "failed" }
424
+ });
425
+ return this.getOperation(operationId);
426
+ }
427
+ async upsertRepositoryClaim(input) {
428
+ const repositoryKeyValue = repositoryKey(input.repository);
429
+ const timestamp = isoNow(this.now);
430
+ const leaseExpiresAt = new Date(this.now().getTime() + input.leaseSeconds * 1e3).toISOString();
431
+ const existing = await this.database.first(
432
+ `SELECT * FROM platform_repository_claims WHERE repository_key = ? AND runner_id = ? AND claim_state = 'active' LIMIT 1`,
433
+ [repositoryKeyValue, input.runnerId]
434
+ );
435
+ if (existing) {
436
+ await this.database.run(
437
+ `UPDATE platform_repository_claims SET lease_expires_at = ?, metadata_json = ?, updated_at = ? WHERE id = ?`,
438
+ [leaseExpiresAt, JSON.stringify(input.metadata ?? parseJson(existing.metadata_json, {})), timestamp, existing.id]
439
+ );
440
+ return;
441
+ }
442
+ await this.database.run(
443
+ `INSERT INTO platform_repository_claims (
444
+ id, repository_key, runner_id, workspace_path, branch, commit_sha, claim_state, lease_expires_at, metadata_json, created_at, updated_at
445
+ ) VALUES (?, ?, ?, ?, ?, NULL, 'active', ?, ?, ?, ?)`,
446
+ [
447
+ randomUUID(),
448
+ repositoryKeyValue,
449
+ input.runnerId,
450
+ repositoryWorkspacePath(input.workspaceRoot, input.repository),
451
+ input.branch ?? null,
452
+ leaseExpiresAt,
453
+ JSON.stringify(input.metadata ?? {}),
454
+ timestamp,
455
+ timestamp
456
+ ]
457
+ );
458
+ }
459
+ async renewRepositoryClaimsForRunner(runnerId, leaseSeconds = 300) {
460
+ if (!runnerId) return;
461
+ const timestamp = isoNow(this.now);
462
+ const leaseExpiresAt = new Date(this.now().getTime() + leaseSeconds * 1e3).toISOString();
463
+ await this.database.run(
464
+ `UPDATE platform_repository_claims SET lease_expires_at = ?, updated_at = ? WHERE runner_id = ? AND claim_state = 'active'`,
465
+ [leaseExpiresAt, timestamp, runnerId]
466
+ );
467
+ }
468
+ async releaseRepositoryClaimsForRunner(runnerId, input = {}) {
469
+ if (!runnerId) return;
470
+ const rows = await this.database.all(
471
+ `SELECT * FROM platform_repository_claims WHERE runner_id = ? AND claim_state = 'active'`,
472
+ [runnerId]
473
+ );
474
+ const timestamp = isoNow(this.now);
475
+ for (const row of rows) {
476
+ await this.database.run(
477
+ `UPDATE platform_repository_claims
478
+ SET claim_state = ?,
479
+ branch = COALESCE(?, branch),
480
+ commit_sha = COALESCE(?, commit_sha),
481
+ lease_expires_at = NULL,
482
+ metadata_json = ?,
483
+ updated_at = ?
484
+ WHERE id = ?`,
485
+ [
486
+ input.claimState ?? "released",
487
+ input.branch ?? null,
488
+ input.commitSha ?? null,
489
+ JSON.stringify({ ...parseJson(row.metadata_json, {}), ...input.metadata ?? {} }),
490
+ timestamp,
491
+ row.id
492
+ ]
493
+ );
494
+ }
495
+ }
496
+ }
497
+ export {
498
+ PLATFORM_OPERATION_SCHEMA_SQL,
499
+ PlatformOperationStore,
500
+ createD1RelationalAdapter,
501
+ createPlatformOperationStoreFromEnv,
502
+ createPostgresRelationalAdapter,
503
+ createRelationalAdapterFromUrl,
504
+ createSqliteRelationalAdapter
505
+ };