im_wower
·
2026-03-21
index.ts
1export const D1_TABLES = [
2 "leader_lease",
3 "controllers",
4 "workers",
5 "tasks",
6 "task_steps",
7 "task_runs",
8 "task_checkpoints",
9 "task_logs",
10 "system_state",
11 "task_artifacts"
12] as const;
13
14export type D1TableName = (typeof D1_TABLES)[number];
15
16export const GLOBAL_LEASE_NAME = "global";
17export const AUTOMATION_STATE_KEY = "automation";
18export const DEFAULT_AUTOMATION_MODE = "running";
19export const DEFAULT_LEASE_TTL_SEC = 30;
20export const DEFAULT_LEASE_RENEW_INTERVAL_SEC = 5;
21export const DEFAULT_LEASE_RENEW_FAILURE_THRESHOLD = 2;
22
23export const AUTOMATION_MODE_VALUES = ["running", "draining", "paused"] as const;
24export const TASK_STATUS_VALUES = [
25 "queued",
26 "planning",
27 "running",
28 "paused",
29 "done",
30 "failed",
31 "canceled"
32] as const;
33export const STEP_STATUS_VALUES = ["pending", "running", "done", "failed", "timeout"] as const;
34export const STEP_KIND_VALUES = ["planner", "codex", "shell", "git", "review", "finalize"] as const;
35
36export type AutomationMode = (typeof AUTOMATION_MODE_VALUES)[number];
37export type TaskStatus = (typeof TASK_STATUS_VALUES)[number];
38export type StepStatus = (typeof STEP_STATUS_VALUES)[number];
39export type StepKind = (typeof STEP_KIND_VALUES)[number];
40
41export type JsonPrimitive = boolean | number | null | string;
42export type JsonValue = JsonPrimitive | JsonObject | JsonValue[];
43
44export interface JsonObject {
45 [key: string]: JsonValue;
46}
47
48export type D1Bindable = number | null | string;
49
50export interface DatabaseRow {
51 [column: string]: unknown;
52}
53
54export interface D1ResultMeta {
55 changed_db?: boolean;
56 changes?: number;
57 duration?: number;
58 last_row_id?: number;
59 rows_read?: number;
60 rows_written?: number;
61 served_by?: string;
62 size_after?: number;
63}
64
65export interface D1QueryResult<T = never> {
66 meta: D1ResultMeta;
67 results?: T[];
68 success: boolean;
69}
70
71export interface D1ExecResult {
72 count?: number;
73 duration?: number;
74}
75
76export interface D1PreparedStatementLike {
77 all<T = DatabaseRow>(): Promise<D1QueryResult<T>>;
78 bind(...values: D1Bindable[]): D1PreparedStatementLike;
79 first<T = DatabaseRow>(columnName?: string): Promise<T | null>;
80 raw<T = unknown[]>(options?: { columnNames?: boolean }): Promise<T[]>;
81 run(): Promise<D1QueryResult<never>>;
82}
83
84export interface D1DatabaseLike {
85 batch<T = never>(statements: D1PreparedStatementLike[]): Promise<Array<D1QueryResult<T>>>;
86 exec(query: string): Promise<D1ExecResult>;
87 prepare(query: string): D1PreparedStatementLike;
88}
89
90export interface LeaderLeaseRecord {
91 leaseName: string;
92 holderId: string;
93 holderHost: string;
94 term: number;
95 leaseExpiresAt: number;
96 renewedAt: number;
97 preferredHolderId: string | null;
98 metadataJson: string | null;
99}
100
101export interface ControllerRecord {
102 controllerId: string;
103 host: string;
104 role: string;
105 priority: number;
106 status: string;
107 version: string | null;
108 lastHeartbeatAt: number;
109 lastStartedAt: number | null;
110 metadataJson: string | null;
111}
112
113export type LeaderLeaseOperation = "acquire" | "renew";
114
115export interface ControllerHeartbeatInput {
116 controllerId: string;
117 host: string;
118 role: string;
119 priority: number;
120 status: string;
121 version?: string | null;
122 heartbeatAt?: number;
123 startedAt?: number | null;
124 metadataJson?: string | null;
125}
126
127export interface LeaderLeaseAcquireInput {
128 controllerId: string;
129 host: string;
130 ttlSec: number;
131 preferred?: boolean;
132 metadataJson?: string | null;
133 now?: number;
134}
135
136export interface LeaderLeaseAcquireResult {
137 holderId: string;
138 holderHost: string;
139 term: number;
140 leaseExpiresAt: number;
141 renewedAt: number;
142 isLeader: boolean;
143 operation: LeaderLeaseOperation;
144 lease: LeaderLeaseRecord;
145}
146
147export interface WorkerRecord {
148 workerId: string;
149 controllerId: string;
150 host: string;
151 workerType: string;
152 status: string;
153 maxParallelism: number;
154 currentLoad: number;
155 lastHeartbeatAt: number;
156 capabilitiesJson: string | null;
157 metadataJson: string | null;
158}
159
160export interface TaskRecord {
161 taskId: string;
162 repo: string;
163 taskType: string;
164 title: string;
165 goal: string;
166 source: string;
167 priority: number;
168 status: TaskStatus;
169 planningStrategy: string | null;
170 plannerProvider: string | null;
171 branchName: string | null;
172 baseRef: string | null;
173 targetHost: string | null;
174 assignedControllerId: string | null;
175 currentStepIndex: number;
176 constraintsJson: string | null;
177 acceptanceJson: string | null;
178 metadataJson: string | null;
179 resultSummary: string | null;
180 resultJson: string | null;
181 errorText: string | null;
182 createdAt: number;
183 updatedAt: number;
184 startedAt: number | null;
185 finishedAt: number | null;
186}
187
188export interface TaskStepRecord {
189 stepId: string;
190 taskId: string;
191 stepIndex: number;
192 stepName: string;
193 stepKind: StepKind;
194 status: StepStatus;
195 assignedWorkerId: string | null;
196 assignedControllerId: string | null;
197 timeoutSec: number;
198 retryLimit: number;
199 retryCount: number;
200 leaseExpiresAt: number | null;
201 inputJson: string | null;
202 outputJson: string | null;
203 summary: string | null;
204 errorText: string | null;
205 createdAt: number;
206 updatedAt: number;
207 startedAt: number | null;
208 finishedAt: number | null;
209}
210
211export interface TaskRunRecord {
212 runId: string;
213 taskId: string;
214 stepId: string;
215 workerId: string;
216 controllerId: string;
217 host: string;
218 pid: number | null;
219 status: string;
220 leaseExpiresAt: number | null;
221 heartbeatAt: number | null;
222 logDir: string;
223 stdoutPath: string | null;
224 stderrPath: string | null;
225 workerLogPath: string | null;
226 checkpointSeq: number;
227 exitCode: number | null;
228 resultJson: string | null;
229 errorText: string | null;
230 createdAt: number;
231 startedAt: number | null;
232 finishedAt: number | null;
233}
234
235export interface TaskCheckpointRecord {
236 checkpointId: string;
237 taskId: string;
238 stepId: string;
239 runId: string;
240 seq: number;
241 checkpointType: string;
242 summary: string | null;
243 contentText: string | null;
244 contentJson: string | null;
245 createdAt: number;
246}
247
248export interface TaskLogRecord {
249 logId: number;
250 taskId: string;
251 stepId: string | null;
252 runId: string;
253 seq: number;
254 stream: string;
255 level: string | null;
256 message: string;
257 createdAt: number;
258}
259
260export interface NewTaskLogRecord {
261 taskId: string;
262 stepId: string | null;
263 runId: string;
264 seq: number;
265 stream: string;
266 level: string | null;
267 message: string;
268 createdAt: number;
269}
270
271export interface SystemStateRecord {
272 stateKey: string;
273 valueJson: string;
274 updatedAt: number;
275}
276
277export interface AutomationStateRecord extends SystemStateRecord {
278 stateKey: typeof AUTOMATION_STATE_KEY;
279 mode: AutomationMode;
280}
281
282export interface TaskArtifactRecord {
283 artifactId: string;
284 taskId: string;
285 stepId: string | null;
286 runId: string | null;
287 artifactType: string;
288 path: string | null;
289 uri: string | null;
290 sizeBytes: number | null;
291 sha256: string | null;
292 metadataJson: string | null;
293 createdAt: number;
294}
295
296export interface ControlPlaneRepository {
297 appendTaskLog(record: NewTaskLogRecord): Promise<number | null>;
298 heartbeatController(input: ControllerHeartbeatInput): Promise<ControllerRecord>;
299 ensureAutomationState(mode?: AutomationMode): Promise<void>;
300 acquireLeaderLease(input: LeaderLeaseAcquireInput): Promise<LeaderLeaseAcquireResult>;
301 getAutomationState(): Promise<AutomationStateRecord | null>;
302 getCurrentLease(): Promise<LeaderLeaseRecord | null>;
303 getSystemState(stateKey: string): Promise<SystemStateRecord | null>;
304 getTask(taskId: string): Promise<TaskRecord | null>;
305 insertTask(record: TaskRecord): Promise<void>;
306 insertTaskArtifact(record: TaskArtifactRecord): Promise<void>;
307 insertTaskCheckpoint(record: TaskCheckpointRecord): Promise<void>;
308 insertTaskRun(record: TaskRunRecord): Promise<void>;
309 insertTaskStep(record: TaskStepRecord): Promise<void>;
310 insertTaskSteps(records: TaskStepRecord[]): Promise<void>;
311 listTaskSteps(taskId: string): Promise<TaskStepRecord[]>;
312 putLeaderLease(record: LeaderLeaseRecord): Promise<void>;
313 putSystemState(record: SystemStateRecord): Promise<void>;
314 setAutomationMode(mode: AutomationMode, updatedAt?: number): Promise<void>;
315 upsertController(record: ControllerRecord): Promise<void>;
316 upsertWorker(record: WorkerRecord): Promise<void>;
317}
318
319const AUTOMATION_MODE_SET = new Set<string>(AUTOMATION_MODE_VALUES);
320const TASK_STATUS_SET = new Set<string>(TASK_STATUS_VALUES);
321const STEP_STATUS_SET = new Set<string>(STEP_STATUS_VALUES);
322const STEP_KIND_SET = new Set<string>(STEP_KIND_VALUES);
323
324export function nowUnixSeconds(date: Date = new Date()): number {
325 return Math.floor(date.getTime() / 1000);
326}
327
328export function stringifyJson(value: JsonValue | null | undefined): string | null {
329 if (value == null) {
330 return null;
331 }
332
333 return JSON.stringify(value);
334}
335
336export function parseJsonText<T>(jsonText: string | null | undefined): T | null {
337 if (jsonText == null || jsonText === "") {
338 return null;
339 }
340
341 return JSON.parse(jsonText) as T;
342}
343
344export function isAutomationMode(value: unknown): value is AutomationMode {
345 return typeof value === "string" && AUTOMATION_MODE_SET.has(value);
346}
347
348export function isTaskStatus(value: unknown): value is TaskStatus {
349 return typeof value === "string" && TASK_STATUS_SET.has(value);
350}
351
352export function isStepStatus(value: unknown): value is StepStatus {
353 return typeof value === "string" && STEP_STATUS_SET.has(value);
354}
355
356export function isStepKind(value: unknown): value is StepKind {
357 return typeof value === "string" && STEP_KIND_SET.has(value);
358}
359
360export function buildAutomationStateValue(mode: AutomationMode): string {
361 return JSON.stringify({ mode });
362}
363
364export function isLeaderLeaseExpired(
365 lease: Pick<LeaderLeaseRecord, "leaseExpiresAt">,
366 now: number = nowUnixSeconds()
367): boolean {
368 return lease.leaseExpiresAt <= now;
369}
370
371export function canAcquireLeaderLease(
372 lease: LeaderLeaseRecord | null,
373 controllerId: string,
374 now: number = nowUnixSeconds()
375): boolean {
376 return lease == null || lease.holderId === controllerId || isLeaderLeaseExpired(lease, now);
377}
378
379export function getLeaderLeaseOperation(
380 lease: LeaderLeaseRecord | null,
381 controllerId: string,
382 now: number = nowUnixSeconds()
383): LeaderLeaseOperation {
384 return lease != null && lease.holderId === controllerId && !isLeaderLeaseExpired(lease, now)
385 ? "renew"
386 : "acquire";
387}
388
389export function buildControllerHeartbeatRecord(input: ControllerHeartbeatInput): ControllerRecord {
390 const heartbeatAt = input.heartbeatAt ?? nowUnixSeconds();
391
392 return {
393 controllerId: input.controllerId,
394 host: input.host,
395 role: input.role,
396 priority: input.priority,
397 status: input.status,
398 version: input.version ?? null,
399 lastHeartbeatAt: heartbeatAt,
400 lastStartedAt: input.startedAt ?? heartbeatAt,
401 metadataJson: input.metadataJson ?? null
402 };
403}
404
405export function buildLeaderLeaseRecord(
406 currentLease: LeaderLeaseRecord | null,
407 input: LeaderLeaseAcquireInput
408): LeaderLeaseRecord {
409 const now = input.now ?? nowUnixSeconds();
410 const operation = getLeaderLeaseOperation(currentLease, input.controllerId, now);
411
412 return {
413 leaseName: GLOBAL_LEASE_NAME,
414 holderId: input.controllerId,
415 holderHost: input.host,
416 term: operation === "renew" ? currentLease!.term : (currentLease?.term ?? 0) + 1,
417 leaseExpiresAt: now + input.ttlSec,
418 renewedAt: now,
419 preferredHolderId: input.preferred ? input.controllerId : currentLease?.preferredHolderId ?? null,
420 metadataJson: input.metadataJson ?? currentLease?.metadataJson ?? null
421 };
422}
423
424export function buildLeaderLeaseAcquireResult(
425 previousLease: LeaderLeaseRecord | null,
426 currentLease: LeaderLeaseRecord,
427 input: LeaderLeaseAcquireInput
428): LeaderLeaseAcquireResult {
429 const now = input.now ?? currentLease.renewedAt;
430
431 return {
432 holderId: currentLease.holderId,
433 holderHost: currentLease.holderHost,
434 term: currentLease.term,
435 leaseExpiresAt: currentLease.leaseExpiresAt,
436 renewedAt: currentLease.renewedAt,
437 isLeader: currentLease.holderId === input.controllerId,
438 operation:
439 currentLease.holderId === input.controllerId
440 ? getLeaderLeaseOperation(previousLease, input.controllerId, now)
441 : "acquire",
442 lease: currentLease
443 };
444}
445
446function toD1Bindable(value: D1Bindable | undefined): D1Bindable {
447 return value ?? null;
448}
449
450function readColumn(row: DatabaseRow, column: string): unknown {
451 if (!(column in row)) {
452 throw new TypeError(`Expected D1 row to include column "${column}".`);
453 }
454
455 return row[column];
456}
457
458function readRequiredString(row: DatabaseRow, column: string): string {
459 const value = readColumn(row, column);
460
461 if (typeof value !== "string") {
462 throw new TypeError(`Expected column "${column}" to be a string.`);
463 }
464
465 return value;
466}
467
468function readOptionalString(row: DatabaseRow, column: string): string | null {
469 const value = readColumn(row, column);
470
471 if (value == null) {
472 return null;
473 }
474
475 if (typeof value !== "string") {
476 throw new TypeError(`Expected column "${column}" to be a string or null.`);
477 }
478
479 return value;
480}
481
482function readRequiredNumber(row: DatabaseRow, column: string): number {
483 const value = readColumn(row, column);
484
485 if (typeof value !== "number") {
486 throw new TypeError(`Expected column "${column}" to be a number.`);
487 }
488
489 return value;
490}
491
492function readOptionalNumber(row: DatabaseRow, column: string): number | null {
493 const value = readColumn(row, column);
494
495 if (value == null) {
496 return null;
497 }
498
499 if (typeof value !== "number") {
500 throw new TypeError(`Expected column "${column}" to be a number or null.`);
501 }
502
503 return value;
504}
505
506function readTaskStatus(row: DatabaseRow, column: string): TaskStatus {
507 const value = readRequiredString(row, column);
508
509 if (!isTaskStatus(value)) {
510 throw new TypeError(`Unexpected task status "${value}".`);
511 }
512
513 return value;
514}
515
516function readStepStatus(row: DatabaseRow, column: string): StepStatus {
517 const value = readRequiredString(row, column);
518
519 if (!isStepStatus(value)) {
520 throw new TypeError(`Unexpected step status "${value}".`);
521 }
522
523 return value;
524}
525
526function readStepKind(row: DatabaseRow, column: string): StepKind {
527 const value = readRequiredString(row, column);
528
529 if (!isStepKind(value)) {
530 throw new TypeError(`Unexpected step kind "${value}".`);
531 }
532
533 return value;
534}
535
536export function mapLeaderLeaseRow(row: DatabaseRow): LeaderLeaseRecord {
537 return {
538 leaseName: readRequiredString(row, "lease_name"),
539 holderId: readRequiredString(row, "holder_id"),
540 holderHost: readRequiredString(row, "holder_host"),
541 term: readRequiredNumber(row, "term"),
542 leaseExpiresAt: readRequiredNumber(row, "lease_expires_at"),
543 renewedAt: readRequiredNumber(row, "renewed_at"),
544 preferredHolderId: readOptionalString(row, "preferred_holder_id"),
545 metadataJson: readOptionalString(row, "metadata_json")
546 };
547}
548
549export function mapControllerRow(row: DatabaseRow): ControllerRecord {
550 return {
551 controllerId: readRequiredString(row, "controller_id"),
552 host: readRequiredString(row, "host"),
553 role: readRequiredString(row, "role"),
554 priority: readRequiredNumber(row, "priority"),
555 status: readRequiredString(row, "status"),
556 version: readOptionalString(row, "version"),
557 lastHeartbeatAt: readRequiredNumber(row, "last_heartbeat_at"),
558 lastStartedAt: readOptionalNumber(row, "last_started_at"),
559 metadataJson: readOptionalString(row, "metadata_json")
560 };
561}
562
563export function mapWorkerRow(row: DatabaseRow): WorkerRecord {
564 return {
565 workerId: readRequiredString(row, "worker_id"),
566 controllerId: readRequiredString(row, "controller_id"),
567 host: readRequiredString(row, "host"),
568 workerType: readRequiredString(row, "worker_type"),
569 status: readRequiredString(row, "status"),
570 maxParallelism: readRequiredNumber(row, "max_parallelism"),
571 currentLoad: readRequiredNumber(row, "current_load"),
572 lastHeartbeatAt: readRequiredNumber(row, "last_heartbeat_at"),
573 capabilitiesJson: readOptionalString(row, "capabilities_json"),
574 metadataJson: readOptionalString(row, "metadata_json")
575 };
576}
577
578export function mapTaskRow(row: DatabaseRow): TaskRecord {
579 return {
580 taskId: readRequiredString(row, "task_id"),
581 repo: readRequiredString(row, "repo"),
582 taskType: readRequiredString(row, "task_type"),
583 title: readRequiredString(row, "title"),
584 goal: readRequiredString(row, "goal"),
585 source: readRequiredString(row, "source"),
586 priority: readRequiredNumber(row, "priority"),
587 status: readTaskStatus(row, "status"),
588 planningStrategy: readOptionalString(row, "planning_strategy"),
589 plannerProvider: readOptionalString(row, "planner_provider"),
590 branchName: readOptionalString(row, "branch_name"),
591 baseRef: readOptionalString(row, "base_ref"),
592 targetHost: readOptionalString(row, "target_host"),
593 assignedControllerId: readOptionalString(row, "assigned_controller_id"),
594 currentStepIndex: readRequiredNumber(row, "current_step_index"),
595 constraintsJson: readOptionalString(row, "constraints_json"),
596 acceptanceJson: readOptionalString(row, "acceptance_json"),
597 metadataJson: readOptionalString(row, "metadata_json"),
598 resultSummary: readOptionalString(row, "result_summary"),
599 resultJson: readOptionalString(row, "result_json"),
600 errorText: readOptionalString(row, "error_text"),
601 createdAt: readRequiredNumber(row, "created_at"),
602 updatedAt: readRequiredNumber(row, "updated_at"),
603 startedAt: readOptionalNumber(row, "started_at"),
604 finishedAt: readOptionalNumber(row, "finished_at")
605 };
606}
607
608export function mapTaskStepRow(row: DatabaseRow): TaskStepRecord {
609 return {
610 stepId: readRequiredString(row, "step_id"),
611 taskId: readRequiredString(row, "task_id"),
612 stepIndex: readRequiredNumber(row, "step_index"),
613 stepName: readRequiredString(row, "step_name"),
614 stepKind: readStepKind(row, "step_kind"),
615 status: readStepStatus(row, "status"),
616 assignedWorkerId: readOptionalString(row, "assigned_worker_id"),
617 assignedControllerId: readOptionalString(row, "assigned_controller_id"),
618 timeoutSec: readRequiredNumber(row, "timeout_sec"),
619 retryLimit: readRequiredNumber(row, "retry_limit"),
620 retryCount: readRequiredNumber(row, "retry_count"),
621 leaseExpiresAt: readOptionalNumber(row, "lease_expires_at"),
622 inputJson: readOptionalString(row, "input_json"),
623 outputJson: readOptionalString(row, "output_json"),
624 summary: readOptionalString(row, "summary"),
625 errorText: readOptionalString(row, "error_text"),
626 createdAt: readRequiredNumber(row, "created_at"),
627 updatedAt: readRequiredNumber(row, "updated_at"),
628 startedAt: readOptionalNumber(row, "started_at"),
629 finishedAt: readOptionalNumber(row, "finished_at")
630 };
631}
632
633export function mapTaskRunRow(row: DatabaseRow): TaskRunRecord {
634 return {
635 runId: readRequiredString(row, "run_id"),
636 taskId: readRequiredString(row, "task_id"),
637 stepId: readRequiredString(row, "step_id"),
638 workerId: readRequiredString(row, "worker_id"),
639 controllerId: readRequiredString(row, "controller_id"),
640 host: readRequiredString(row, "host"),
641 pid: readOptionalNumber(row, "pid"),
642 status: readRequiredString(row, "status"),
643 leaseExpiresAt: readOptionalNumber(row, "lease_expires_at"),
644 heartbeatAt: readOptionalNumber(row, "heartbeat_at"),
645 logDir: readRequiredString(row, "log_dir"),
646 stdoutPath: readOptionalString(row, "stdout_path"),
647 stderrPath: readOptionalString(row, "stderr_path"),
648 workerLogPath: readOptionalString(row, "worker_log_path"),
649 checkpointSeq: readRequiredNumber(row, "checkpoint_seq"),
650 exitCode: readOptionalNumber(row, "exit_code"),
651 resultJson: readOptionalString(row, "result_json"),
652 errorText: readOptionalString(row, "error_text"),
653 createdAt: readRequiredNumber(row, "created_at"),
654 startedAt: readOptionalNumber(row, "started_at"),
655 finishedAt: readOptionalNumber(row, "finished_at")
656 };
657}
658
659export function mapTaskCheckpointRow(row: DatabaseRow): TaskCheckpointRecord {
660 return {
661 checkpointId: readRequiredString(row, "checkpoint_id"),
662 taskId: readRequiredString(row, "task_id"),
663 stepId: readRequiredString(row, "step_id"),
664 runId: readRequiredString(row, "run_id"),
665 seq: readRequiredNumber(row, "seq"),
666 checkpointType: readRequiredString(row, "checkpoint_type"),
667 summary: readOptionalString(row, "summary"),
668 contentText: readOptionalString(row, "content_text"),
669 contentJson: readOptionalString(row, "content_json"),
670 createdAt: readRequiredNumber(row, "created_at")
671 };
672}
673
674export function mapTaskLogRow(row: DatabaseRow): TaskLogRecord {
675 return {
676 logId: readRequiredNumber(row, "log_id"),
677 taskId: readRequiredString(row, "task_id"),
678 stepId: readOptionalString(row, "step_id"),
679 runId: readRequiredString(row, "run_id"),
680 seq: readRequiredNumber(row, "seq"),
681 stream: readRequiredString(row, "stream"),
682 level: readOptionalString(row, "level"),
683 message: readRequiredString(row, "message"),
684 createdAt: readRequiredNumber(row, "created_at")
685 };
686}
687
688export function mapSystemStateRow(row: DatabaseRow): SystemStateRecord {
689 return {
690 stateKey: readRequiredString(row, "state_key"),
691 valueJson: readRequiredString(row, "value_json"),
692 updatedAt: readRequiredNumber(row, "updated_at")
693 };
694}
695
696export function mapAutomationStateRow(row: DatabaseRow): AutomationStateRecord {
697 const record = mapSystemStateRow(row);
698
699 if (record.stateKey !== AUTOMATION_STATE_KEY) {
700 throw new TypeError(`Expected state_key "${AUTOMATION_STATE_KEY}", received "${record.stateKey}".`);
701 }
702
703 const parsed = parseJsonText<{ mode?: unknown }>(record.valueJson);
704 const mode = parsed?.mode;
705
706 if (!isAutomationMode(mode)) {
707 throw new TypeError(`Automation state is missing a valid mode in "${record.valueJson}".`);
708 }
709
710 return {
711 ...record,
712 stateKey: AUTOMATION_STATE_KEY,
713 mode
714 };
715}
716
717export function mapTaskArtifactRow(row: DatabaseRow): TaskArtifactRecord {
718 return {
719 artifactId: readRequiredString(row, "artifact_id"),
720 taskId: readRequiredString(row, "task_id"),
721 stepId: readOptionalString(row, "step_id"),
722 runId: readOptionalString(row, "run_id"),
723 artifactType: readRequiredString(row, "artifact_type"),
724 path: readOptionalString(row, "path"),
725 uri: readOptionalString(row, "uri"),
726 sizeBytes: readOptionalNumber(row, "size_bytes"),
727 sha256: readOptionalString(row, "sha256"),
728 metadataJson: readOptionalString(row, "metadata_json"),
729 createdAt: readRequiredNumber(row, "created_at")
730 };
731}
732
733export const SELECT_CURRENT_LEASE_SQL = `
734 SELECT
735 lease_name,
736 holder_id,
737 holder_host,
738 term,
739 lease_expires_at,
740 renewed_at,
741 preferred_holder_id,
742 metadata_json
743 FROM leader_lease
744 WHERE lease_name = ?
745`;
746
747export const UPSERT_LEADER_LEASE_SQL = `
748 INSERT INTO leader_lease (
749 lease_name,
750 holder_id,
751 holder_host,
752 term,
753 lease_expires_at,
754 renewed_at,
755 preferred_holder_id,
756 metadata_json
757 )
758 VALUES (?, ?, ?, ?, ?, ?, ?, ?)
759 ON CONFLICT(lease_name) DO UPDATE SET
760 holder_id = excluded.holder_id,
761 holder_host = excluded.holder_host,
762 term = excluded.term,
763 lease_expires_at = excluded.lease_expires_at,
764 renewed_at = excluded.renewed_at,
765 preferred_holder_id = excluded.preferred_holder_id,
766 metadata_json = excluded.metadata_json
767`;
768
769export const ACQUIRE_LEADER_LEASE_SQL = `
770 INSERT INTO leader_lease (
771 lease_name,
772 holder_id,
773 holder_host,
774 term,
775 lease_expires_at,
776 renewed_at,
777 preferred_holder_id,
778 metadata_json
779 )
780 VALUES (?, ?, ?, ?, ?, ?, ?, ?)
781 ON CONFLICT(lease_name) DO UPDATE SET
782 holder_id = CASE
783 WHEN leader_lease.holder_id = excluded.holder_id OR leader_lease.lease_expires_at <= ? THEN excluded.holder_id
784 ELSE leader_lease.holder_id
785 END,
786 holder_host = CASE
787 WHEN leader_lease.holder_id = excluded.holder_id OR leader_lease.lease_expires_at <= ? THEN excluded.holder_host
788 ELSE leader_lease.holder_host
789 END,
790 term = CASE
791 WHEN leader_lease.holder_id = excluded.holder_id THEN leader_lease.term
792 WHEN leader_lease.lease_expires_at <= ? THEN leader_lease.term + 1
793 ELSE leader_lease.term
794 END,
795 lease_expires_at = CASE
796 WHEN leader_lease.holder_id = excluded.holder_id OR leader_lease.lease_expires_at <= ? THEN excluded.lease_expires_at
797 ELSE leader_lease.lease_expires_at
798 END,
799 renewed_at = CASE
800 WHEN leader_lease.holder_id = excluded.holder_id OR leader_lease.lease_expires_at <= ? THEN excluded.renewed_at
801 ELSE leader_lease.renewed_at
802 END,
803 preferred_holder_id = CASE
804 WHEN leader_lease.holder_id = excluded.holder_id OR leader_lease.lease_expires_at <= ? THEN excluded.preferred_holder_id
805 ELSE leader_lease.preferred_holder_id
806 END,
807 metadata_json = CASE
808 WHEN leader_lease.holder_id = excluded.holder_id OR leader_lease.lease_expires_at <= ? THEN excluded.metadata_json
809 ELSE leader_lease.metadata_json
810 END
811`;
812
813export const UPSERT_CONTROLLER_SQL = `
814 INSERT INTO controllers (
815 controller_id,
816 host,
817 role,
818 priority,
819 status,
820 version,
821 last_heartbeat_at,
822 last_started_at,
823 metadata_json
824 )
825 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
826 ON CONFLICT(controller_id) DO UPDATE SET
827 host = excluded.host,
828 role = excluded.role,
829 priority = excluded.priority,
830 status = excluded.status,
831 version = excluded.version,
832 last_heartbeat_at = excluded.last_heartbeat_at,
833 last_started_at = excluded.last_started_at,
834 metadata_json = excluded.metadata_json
835`;
836
837export const UPSERT_WORKER_SQL = `
838 INSERT INTO workers (
839 worker_id,
840 controller_id,
841 host,
842 worker_type,
843 status,
844 max_parallelism,
845 current_load,
846 last_heartbeat_at,
847 capabilities_json,
848 metadata_json
849 )
850 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
851 ON CONFLICT(worker_id) DO UPDATE SET
852 controller_id = excluded.controller_id,
853 host = excluded.host,
854 worker_type = excluded.worker_type,
855 status = excluded.status,
856 max_parallelism = excluded.max_parallelism,
857 current_load = excluded.current_load,
858 last_heartbeat_at = excluded.last_heartbeat_at,
859 capabilities_json = excluded.capabilities_json,
860 metadata_json = excluded.metadata_json
861`;
862
863export const UPSERT_SYSTEM_STATE_SQL = `
864 INSERT INTO system_state (
865 state_key,
866 value_json,
867 updated_at
868 )
869 VALUES (?, ?, ?)
870 ON CONFLICT(state_key) DO UPDATE SET
871 value_json = excluded.value_json,
872 updated_at = excluded.updated_at
873`;
874
875export const ENSURE_AUTOMATION_STATE_SQL = `
876 INSERT INTO system_state (
877 state_key,
878 value_json,
879 updated_at
880 )
881 VALUES (?, ?, ?)
882 ON CONFLICT(state_key) DO NOTHING
883`;
884
885export const SELECT_SYSTEM_STATE_SQL = `
886 SELECT
887 state_key,
888 value_json,
889 updated_at
890 FROM system_state
891 WHERE state_key = ?
892`;
893
894export const INSERT_TASK_SQL = `
895 INSERT INTO tasks (
896 task_id,
897 repo,
898 task_type,
899 title,
900 goal,
901 source,
902 priority,
903 status,
904 planning_strategy,
905 planner_provider,
906 branch_name,
907 base_ref,
908 target_host,
909 assigned_controller_id,
910 current_step_index,
911 constraints_json,
912 acceptance_json,
913 metadata_json,
914 result_summary,
915 result_json,
916 error_text,
917 created_at,
918 updated_at,
919 started_at,
920 finished_at
921 )
922 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
923`;
924
925export const SELECT_TASK_SQL = `
926 SELECT
927 task_id,
928 repo,
929 task_type,
930 title,
931 goal,
932 source,
933 priority,
934 status,
935 planning_strategy,
936 planner_provider,
937 branch_name,
938 base_ref,
939 target_host,
940 assigned_controller_id,
941 current_step_index,
942 constraints_json,
943 acceptance_json,
944 metadata_json,
945 result_summary,
946 result_json,
947 error_text,
948 created_at,
949 updated_at,
950 started_at,
951 finished_at
952 FROM tasks
953 WHERE task_id = ?
954`;
955
956export const INSERT_TASK_STEP_SQL = `
957 INSERT INTO task_steps (
958 step_id,
959 task_id,
960 step_index,
961 step_name,
962 step_kind,
963 status,
964 assigned_worker_id,
965 assigned_controller_id,
966 timeout_sec,
967 retry_limit,
968 retry_count,
969 lease_expires_at,
970 input_json,
971 output_json,
972 summary,
973 error_text,
974 created_at,
975 updated_at,
976 started_at,
977 finished_at
978 )
979 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
980`;
981
982export const SELECT_TASK_STEPS_SQL = `
983 SELECT
984 step_id,
985 task_id,
986 step_index,
987 step_name,
988 step_kind,
989 status,
990 assigned_worker_id,
991 assigned_controller_id,
992 timeout_sec,
993 retry_limit,
994 retry_count,
995 lease_expires_at,
996 input_json,
997 output_json,
998 summary,
999 error_text,
1000 created_at,
1001 updated_at,
1002 started_at,
1003 finished_at
1004 FROM task_steps
1005 WHERE task_id = ?
1006 ORDER BY step_index ASC
1007`;
1008
1009export const INSERT_TASK_RUN_SQL = `
1010 INSERT INTO task_runs (
1011 run_id,
1012 task_id,
1013 step_id,
1014 worker_id,
1015 controller_id,
1016 host,
1017 pid,
1018 status,
1019 lease_expires_at,
1020 heartbeat_at,
1021 log_dir,
1022 stdout_path,
1023 stderr_path,
1024 worker_log_path,
1025 checkpoint_seq,
1026 exit_code,
1027 result_json,
1028 error_text,
1029 created_at,
1030 started_at,
1031 finished_at
1032 )
1033 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1034`;
1035
1036export const INSERT_TASK_CHECKPOINT_SQL = `
1037 INSERT INTO task_checkpoints (
1038 checkpoint_id,
1039 task_id,
1040 step_id,
1041 run_id,
1042 seq,
1043 checkpoint_type,
1044 summary,
1045 content_text,
1046 content_json,
1047 created_at
1048 )
1049 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1050`;
1051
1052export const INSERT_TASK_LOG_SQL = `
1053 INSERT INTO task_logs (
1054 task_id,
1055 step_id,
1056 run_id,
1057 seq,
1058 stream,
1059 level,
1060 message,
1061 created_at
1062 )
1063 VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1064`;
1065
1066export const INSERT_TASK_ARTIFACT_SQL = `
1067 INSERT INTO task_artifacts (
1068 artifact_id,
1069 task_id,
1070 step_id,
1071 run_id,
1072 artifact_type,
1073 path,
1074 uri,
1075 size_bytes,
1076 sha256,
1077 metadata_json,
1078 created_at
1079 )
1080 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1081`;
1082
1083function leaderLeaseParams(record: LeaderLeaseRecord): D1Bindable[] {
1084 return [
1085 record.leaseName,
1086 record.holderId,
1087 record.holderHost,
1088 record.term,
1089 record.leaseExpiresAt,
1090 record.renewedAt,
1091 record.preferredHolderId,
1092 record.metadataJson
1093 ];
1094}
1095
1096function acquireLeaderLeaseParams(record: LeaderLeaseRecord, now: number): D1Bindable[] {
1097 return [...leaderLeaseParams(record), now, now, now, now, now, now, now];
1098}
1099
1100function controllerParams(record: ControllerRecord): D1Bindable[] {
1101 return [
1102 record.controllerId,
1103 record.host,
1104 record.role,
1105 record.priority,
1106 record.status,
1107 record.version,
1108 record.lastHeartbeatAt,
1109 record.lastStartedAt,
1110 record.metadataJson
1111 ];
1112}
1113
1114function workerParams(record: WorkerRecord): D1Bindable[] {
1115 return [
1116 record.workerId,
1117 record.controllerId,
1118 record.host,
1119 record.workerType,
1120 record.status,
1121 record.maxParallelism,
1122 record.currentLoad,
1123 record.lastHeartbeatAt,
1124 record.capabilitiesJson,
1125 record.metadataJson
1126 ];
1127}
1128
1129function systemStateParams(record: SystemStateRecord): D1Bindable[] {
1130 return [record.stateKey, record.valueJson, record.updatedAt];
1131}
1132
1133function taskParams(record: TaskRecord): D1Bindable[] {
1134 return [
1135 record.taskId,
1136 record.repo,
1137 record.taskType,
1138 record.title,
1139 record.goal,
1140 record.source,
1141 record.priority,
1142 record.status,
1143 record.planningStrategy,
1144 record.plannerProvider,
1145 record.branchName,
1146 record.baseRef,
1147 record.targetHost,
1148 record.assignedControllerId,
1149 record.currentStepIndex,
1150 record.constraintsJson,
1151 record.acceptanceJson,
1152 record.metadataJson,
1153 record.resultSummary,
1154 record.resultJson,
1155 record.errorText,
1156 record.createdAt,
1157 record.updatedAt,
1158 record.startedAt,
1159 record.finishedAt
1160 ];
1161}
1162
1163function taskStepParams(record: TaskStepRecord): D1Bindable[] {
1164 return [
1165 record.stepId,
1166 record.taskId,
1167 record.stepIndex,
1168 record.stepName,
1169 record.stepKind,
1170 record.status,
1171 record.assignedWorkerId,
1172 record.assignedControllerId,
1173 record.timeoutSec,
1174 record.retryLimit,
1175 record.retryCount,
1176 record.leaseExpiresAt,
1177 record.inputJson,
1178 record.outputJson,
1179 record.summary,
1180 record.errorText,
1181 record.createdAt,
1182 record.updatedAt,
1183 record.startedAt,
1184 record.finishedAt
1185 ];
1186}
1187
1188function taskRunParams(record: TaskRunRecord): D1Bindable[] {
1189 return [
1190 record.runId,
1191 record.taskId,
1192 record.stepId,
1193 record.workerId,
1194 record.controllerId,
1195 record.host,
1196 record.pid,
1197 record.status,
1198 record.leaseExpiresAt,
1199 record.heartbeatAt,
1200 record.logDir,
1201 record.stdoutPath,
1202 record.stderrPath,
1203 record.workerLogPath,
1204 record.checkpointSeq,
1205 record.exitCode,
1206 record.resultJson,
1207 record.errorText,
1208 record.createdAt,
1209 record.startedAt,
1210 record.finishedAt
1211 ];
1212}
1213
1214function taskCheckpointParams(record: TaskCheckpointRecord): D1Bindable[] {
1215 return [
1216 record.checkpointId,
1217 record.taskId,
1218 record.stepId,
1219 record.runId,
1220 record.seq,
1221 record.checkpointType,
1222 record.summary,
1223 record.contentText,
1224 record.contentJson,
1225 record.createdAt
1226 ];
1227}
1228
1229function taskLogParams(record: NewTaskLogRecord): D1Bindable[] {
1230 return [
1231 record.taskId,
1232 record.stepId,
1233 record.runId,
1234 record.seq,
1235 record.stream,
1236 record.level,
1237 record.message,
1238 record.createdAt
1239 ];
1240}
1241
1242function taskArtifactParams(record: TaskArtifactRecord): D1Bindable[] {
1243 return [
1244 record.artifactId,
1245 record.taskId,
1246 record.stepId,
1247 record.runId,
1248 record.artifactType,
1249 record.path,
1250 record.uri,
1251 record.sizeBytes,
1252 record.sha256,
1253 record.metadataJson,
1254 record.createdAt
1255 ];
1256}
1257
1258export class D1ControlPlaneRepository implements ControlPlaneRepository {
1259 private readonly db: D1DatabaseLike;
1260
1261 constructor(db: D1DatabaseLike) {
1262 this.db = db;
1263 }
1264
1265 async ensureAutomationState(mode: AutomationMode = DEFAULT_AUTOMATION_MODE): Promise<void> {
1266 await this.run(ENSURE_AUTOMATION_STATE_SQL, [
1267 AUTOMATION_STATE_KEY,
1268 buildAutomationStateValue(mode),
1269 nowUnixSeconds()
1270 ]);
1271 }
1272
1273 async heartbeatController(input: ControllerHeartbeatInput): Promise<ControllerRecord> {
1274 const record = buildControllerHeartbeatRecord(input);
1275 await this.upsertController(record);
1276 return record;
1277 }
1278
1279 async acquireLeaderLease(input: LeaderLeaseAcquireInput): Promise<LeaderLeaseAcquireResult> {
1280 const now = input.now ?? nowUnixSeconds();
1281 const normalizedInput = { ...input, now };
1282 const currentLease = await this.getCurrentLease();
1283 const desiredLease = buildLeaderLeaseRecord(currentLease, normalizedInput);
1284
1285 await this.run(ACQUIRE_LEADER_LEASE_SQL, acquireLeaderLeaseParams(desiredLease, now));
1286
1287 const updatedLease = await this.getCurrentLease();
1288
1289 if (updatedLease == null) {
1290 throw new Error("leader_lease row was not available after acquire attempt.");
1291 }
1292
1293 return buildLeaderLeaseAcquireResult(currentLease, updatedLease, normalizedInput);
1294 }
1295
1296 async getAutomationState(): Promise<AutomationStateRecord | null> {
1297 const row = await this.fetchFirst(SELECT_SYSTEM_STATE_SQL, [AUTOMATION_STATE_KEY]);
1298 return row == null ? null : mapAutomationStateRow(row);
1299 }
1300
1301 async getCurrentLease(): Promise<LeaderLeaseRecord | null> {
1302 const row = await this.fetchFirst(SELECT_CURRENT_LEASE_SQL, [GLOBAL_LEASE_NAME]);
1303 return row == null ? null : mapLeaderLeaseRow(row);
1304 }
1305
1306 async getSystemState(stateKey: string): Promise<SystemStateRecord | null> {
1307 const row = await this.fetchFirst(SELECT_SYSTEM_STATE_SQL, [stateKey]);
1308 return row == null ? null : mapSystemStateRow(row);
1309 }
1310
1311 async getTask(taskId: string): Promise<TaskRecord | null> {
1312 const row = await this.fetchFirst(SELECT_TASK_SQL, [taskId]);
1313 return row == null ? null : mapTaskRow(row);
1314 }
1315
1316 async insertTask(record: TaskRecord): Promise<void> {
1317 await this.run(INSERT_TASK_SQL, taskParams(record));
1318 }
1319
1320 async insertTaskArtifact(record: TaskArtifactRecord): Promise<void> {
1321 await this.run(INSERT_TASK_ARTIFACT_SQL, taskArtifactParams(record));
1322 }
1323
1324 async insertTaskCheckpoint(record: TaskCheckpointRecord): Promise<void> {
1325 await this.run(INSERT_TASK_CHECKPOINT_SQL, taskCheckpointParams(record));
1326 }
1327
1328 async insertTaskRun(record: TaskRunRecord): Promise<void> {
1329 await this.run(INSERT_TASK_RUN_SQL, taskRunParams(record));
1330 }
1331
1332 async insertTaskStep(record: TaskStepRecord): Promise<void> {
1333 await this.run(INSERT_TASK_STEP_SQL, taskStepParams(record));
1334 }
1335
1336 async insertTaskSteps(records: TaskStepRecord[]): Promise<void> {
1337 if (records.length === 0) {
1338 return;
1339 }
1340
1341 await this.db.batch(records.map((record) => this.bind(INSERT_TASK_STEP_SQL, taskStepParams(record))));
1342 }
1343
1344 async listTaskSteps(taskId: string): Promise<TaskStepRecord[]> {
1345 const rows = await this.fetchAll(SELECT_TASK_STEPS_SQL, [taskId]);
1346 return rows.map(mapTaskStepRow);
1347 }
1348
1349 async putLeaderLease(record: LeaderLeaseRecord): Promise<void> {
1350 if (record.leaseName !== GLOBAL_LEASE_NAME) {
1351 throw new RangeError(`leader_lease only supports lease_name="${GLOBAL_LEASE_NAME}".`);
1352 }
1353
1354 await this.run(UPSERT_LEADER_LEASE_SQL, leaderLeaseParams(record));
1355 }
1356
1357 async putSystemState(record: SystemStateRecord): Promise<void> {
1358 await this.run(UPSERT_SYSTEM_STATE_SQL, systemStateParams(record));
1359 }
1360
1361 async setAutomationMode(
1362 mode: AutomationMode,
1363 updatedAt: number = nowUnixSeconds()
1364 ): Promise<void> {
1365 await this.run(UPSERT_SYSTEM_STATE_SQL, [
1366 AUTOMATION_STATE_KEY,
1367 buildAutomationStateValue(mode),
1368 updatedAt
1369 ]);
1370 }
1371
1372 async upsertController(record: ControllerRecord): Promise<void> {
1373 await this.run(UPSERT_CONTROLLER_SQL, controllerParams(record));
1374 }
1375
1376 async upsertWorker(record: WorkerRecord): Promise<void> {
1377 await this.run(UPSERT_WORKER_SQL, workerParams(record));
1378 }
1379
1380 async appendTaskLog(record: NewTaskLogRecord): Promise<number | null> {
1381 const result = await this.run(INSERT_TASK_LOG_SQL, taskLogParams(record));
1382 return result.meta.last_row_id ?? null;
1383 }
1384
1385 private bind(query: string, params: readonly (D1Bindable | undefined)[]): D1PreparedStatementLike {
1386 return this.db.prepare(query).bind(...params.map(toD1Bindable));
1387 }
1388
1389 private async fetchAll(
1390 query: string,
1391 params: readonly (D1Bindable | undefined)[] = []
1392 ): Promise<DatabaseRow[]> {
1393 const result = await this.bind(query, params).all<DatabaseRow>();
1394 return result.results ?? [];
1395 }
1396
1397 private async fetchFirst(
1398 query: string,
1399 params: readonly (D1Bindable | undefined)[] = []
1400 ): Promise<DatabaseRow | null> {
1401 return this.bind(query, params).first<DatabaseRow>();
1402 }
1403
1404 private async run(
1405 query: string,
1406 params: readonly (D1Bindable | undefined)[] = []
1407 ): Promise<D1QueryResult<never>> {
1408 return this.bind(query, params).run();
1409 }
1410}
1411
1412export function createD1ControlPlaneRepository(db: D1DatabaseLike): ControlPlaneRepository {
1413 return new D1ControlPlaneRepository(db);
1414}