@hasna/loops 0.4.2 → 0.4.4

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.
@@ -0,0 +1,109 @@
1
+ import type { Loop, LoopRun, WorkflowSpec } from "../types.js";
2
+ import type { Store, StoreMigrationChecks } from "./store.js";
3
+ export declare const LOOPS_MIGRATION_SCHEMA = "open-loops.migration/v1";
4
+ export type LoopsMigrationResource = "workflow" | "loop" | "run" | "remote";
5
+ export type LoopsMigrationAction = "insert" | "update" | "skip" | "conflict" | "blocked";
6
+ export interface LoopsMigrationBundle {
7
+ schema: typeof LOOPS_MIGRATION_SCHEMA;
8
+ packageVersion: string;
9
+ exportedAt: string;
10
+ source: {
11
+ backend: "sqlite";
12
+ schemaVersion: number;
13
+ hostname: string;
14
+ };
15
+ checks: StoreMigrationChecks;
16
+ importable: boolean;
17
+ counts: {
18
+ workflows: number;
19
+ loops: number;
20
+ runs: number;
21
+ };
22
+ data: {
23
+ workflows: WorkflowSpec[];
24
+ loops: Loop[];
25
+ runs: LoopRun[];
26
+ };
27
+ blockers: LoopsMigrationPlanRow[];
28
+ warnings: string[];
29
+ hash: string;
30
+ }
31
+ export interface ExportLoopsMigrationOptions {
32
+ includeRuns?: boolean;
33
+ }
34
+ export interface ImportLoopsMigrationOptions {
35
+ includeRuns?: boolean;
36
+ replace?: boolean;
37
+ dryRun?: boolean;
38
+ }
39
+ export interface LoopsMigrationPlanRow {
40
+ resource: LoopsMigrationResource;
41
+ id: string;
42
+ name?: string;
43
+ action: LoopsMigrationAction;
44
+ reason?: string;
45
+ incomingHash?: string;
46
+ currentHash?: string;
47
+ }
48
+ export interface LoopsMigrationPlanSummary {
49
+ dryRun: boolean;
50
+ replace: boolean;
51
+ importable: boolean;
52
+ workflows: number;
53
+ loops: number;
54
+ runs: number;
55
+ insert: number;
56
+ update: number;
57
+ skip: number;
58
+ conflict: number;
59
+ blocked: number;
60
+ }
61
+ export interface LoopsMigrationPlan {
62
+ schema: typeof LOOPS_MIGRATION_SCHEMA;
63
+ operation: "import" | "self-hosted-push" | "self-hosted-pull" | "self-hosted-migrate";
64
+ dryRun: boolean;
65
+ replace: boolean;
66
+ importable: boolean;
67
+ summary: LoopsMigrationPlanSummary;
68
+ rows: LoopsMigrationPlanRow[];
69
+ warnings: string[];
70
+ }
71
+ export interface ApplyLoopsMigrationResult {
72
+ plan: LoopsMigrationPlan;
73
+ applied: {
74
+ workflows: number;
75
+ loops: number;
76
+ runs: number;
77
+ };
78
+ }
79
+ export interface SelfHostedPlanOptions {
80
+ operation: "self-hosted-push" | "self-hosted-pull" | "self-hosted-migrate";
81
+ apiUrl?: string;
82
+ apiToken?: string;
83
+ fetchImpl?: typeof fetch;
84
+ env?: NodeJS.ProcessEnv;
85
+ includeRuns?: boolean;
86
+ }
87
+ export interface RunnerRegistrationOptions {
88
+ apiUrl?: string;
89
+ apiToken?: string;
90
+ runnerId: string;
91
+ machineId?: string;
92
+ labels?: Record<string, string>;
93
+ capabilities?: Record<string, unknown>;
94
+ fetchImpl?: typeof fetch;
95
+ env?: NodeJS.ProcessEnv;
96
+ }
97
+ export interface RunnerRegistrationResult {
98
+ ok: boolean;
99
+ runner: Record<string, unknown>;
100
+ }
101
+ export declare function migrationHash(value: unknown): string;
102
+ export declare function exportLoopsMigrationBundle(store: Store, opts?: ExportLoopsMigrationOptions): LoopsMigrationBundle;
103
+ export declare function validateLoopsMigrationBundle(value: unknown): LoopsMigrationBundle;
104
+ export declare function buildImportMigrationPlan(store: Store, bundle: LoopsMigrationBundle, opts?: ImportLoopsMigrationOptions): LoopsMigrationPlan;
105
+ export declare function applyImportMigrationBundle(store: Store, bundle: LoopsMigrationBundle, opts?: ImportLoopsMigrationOptions): ApplyLoopsMigrationResult;
106
+ export declare function buildSelfHostedMigrationPlan(store: Store, opts: SelfHostedPlanOptions): Promise<LoopsMigrationPlan>;
107
+ export declare function registerSelfHostedRunner(opts: RunnerRegistrationOptions): Promise<RunnerRegistrationResult>;
108
+ export declare function publicMigrationBundle(bundle: LoopsMigrationBundle): Record<string, unknown>;
109
+ export declare function selfHostedControlPlaneSummary(env?: NodeJS.ProcessEnv): Record<string, unknown>;
package/dist/lib/mode.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // package.json
3
3
  var package_default = {
4
4
  name: "@hasna/loops",
5
- version: "0.4.2",
5
+ version: "0.4.4",
6
6
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
7
7
  type: "module",
8
8
  main: "dist/index.js",
@@ -76,7 +76,7 @@ var package_default = {
76
76
  prepare: "test -d dist || bun run build",
77
77
  "dev:cli": "bun run src/cli/index.ts",
78
78
  "dev:daemon": "bun run src/daemon/index.ts",
79
- prepublishOnly: "bun run build && bun run test:boundary"
79
+ prepublishOnly: "bun run build && bun run typecheck && bun test && bun run test:boundary"
80
80
  },
81
81
  keywords: [
82
82
  "loops",
@@ -11,7 +11,6 @@ export declare function runLocalCommand(command: string, args: string[], opts?:
11
11
  input?: string;
12
12
  timeoutMs?: number;
13
13
  maxBuffer?: number;
14
- env?: Record<string, string | undefined>;
15
14
  }): LocalCommandResult;
16
15
  export declare function runLocalCommandWithStdoutFile(command: string, args: string[], opts?: {
17
16
  input?: string;
@@ -47,11 +47,6 @@ export interface TodosReadyTask {
47
47
  id?: string;
48
48
  task_id?: string;
49
49
  taskId?: string;
50
- source_store_id?: string;
51
- source_repo_path?: string;
52
- source_db_path?: string;
53
- source_task_key?: string;
54
- source_selected_by_input?: boolean;
55
50
  title?: string;
56
51
  description?: string;
57
52
  body?: string;
@@ -70,16 +65,11 @@ export interface TodosReadyTask {
70
65
  task_list?: {
71
66
  id?: string;
72
67
  slug?: string;
73
- name?: string;
74
68
  };
75
69
  [key: string]: unknown;
76
70
  }
77
71
  export interface TodosDrainOptions extends TodosTaskRouteOptions {
78
72
  todosProject?: string;
79
- todosSourceRoot?: string[];
80
- todosSourceStore?: string[];
81
- todosSourceInclude?: string[];
82
- todosSourceExclude?: string[];
83
73
  todosProjectId?: string;
84
74
  taskList?: string;
85
75
  tags?: string;
@@ -3841,6 +3841,190 @@ class Store {
3841
3841
  const row = status ? this.db.query("SELECT COUNT(*) AS count FROM loop_runs WHERE status = ?").get(status) : this.db.query("SELECT COUNT(*) AS count FROM loop_runs").get();
3842
3842
  return row?.count ?? 0;
3843
3843
  }
3844
+ exportMigrationRows(opts = {}) {
3845
+ const includeRuns = opts.includeRuns ?? true;
3846
+ const workflows = this.db.query("SELECT * FROM workflow_specs ORDER BY created_at ASC, id ASC").all().map(rowToWorkflow);
3847
+ const loops = this.db.query("SELECT * FROM loops ORDER BY created_at ASC, id ASC").all().map(rowToLoop);
3848
+ const runs = includeRuns ? this.db.query("SELECT * FROM loop_runs ORDER BY created_at ASC, id ASC").all().map(rowToRun) : [];
3849
+ return { schemaVersion: SCHEMA_USER_VERSION, workflows, loops, runs, checks: this.migrationChecks() };
3850
+ }
3851
+ countTable(table) {
3852
+ const row = this.db.query(`SELECT COUNT(*) AS count FROM ${table}`).get();
3853
+ return row?.count ?? 0;
3854
+ }
3855
+ migrationChecks() {
3856
+ const now = nowIso();
3857
+ return {
3858
+ unsupportedCounts: {
3859
+ workflowInvocations: this.countTable("workflow_invocations"),
3860
+ workflowWorkItems: this.countTable("workflow_work_items"),
3861
+ workflowRuns: this.countTable("workflow_runs"),
3862
+ workflowStepRuns: this.countTable("workflow_step_runs"),
3863
+ workflowEvents: this.countTable("workflow_events"),
3864
+ goals: this.countTable("goals"),
3865
+ goalPlanNodes: this.countTable("goal_plan_nodes"),
3866
+ goalRuns: this.countTable("goal_runs")
3867
+ },
3868
+ volatileCounts: {
3869
+ daemonLeases: this.countTable("daemon_lease"),
3870
+ activeDaemonLeases: this.db.query("SELECT COUNT(*) AS count FROM daemon_lease WHERE expires_at > ?").get(now)?.count ?? 0,
3871
+ runningLoopRuns: this.db.query("SELECT COUNT(*) AS count FROM loop_runs WHERE status = 'running'").get()?.count ?? 0,
3872
+ runningWorkflowRuns: this.db.query("SELECT COUNT(*) AS count FROM workflow_runs WHERE status = 'running'").get()?.count ?? 0,
3873
+ runningWorkflowStepRuns: this.db.query("SELECT COUNT(*) AS count FROM workflow_step_runs WHERE status = 'running'").get()?.count ?? 0,
3874
+ leasedWorkflowWorkItems: this.db.query("SELECT COUNT(*) AS count FROM workflow_work_items WHERE lease_expires_at IS NOT NULL OR status IN ('admitted', 'running')").get()?.count ?? 0
3875
+ }
3876
+ };
3877
+ }
3878
+ upsertMigrationWorkflow(workflow, opts = {}) {
3879
+ const existing = this.getWorkflow(workflow.id);
3880
+ if (existing && !opts.replace)
3881
+ return existing;
3882
+ this.db.query(`INSERT INTO workflow_specs (id, name, description, version, status, goal_json, steps_json, created_at, updated_at)
3883
+ VALUES ($id, $name, $description, $version, $status, $goal, $steps, $created, $updated)
3884
+ ON CONFLICT(id) DO UPDATE SET
3885
+ name=$name,
3886
+ description=$description,
3887
+ version=$version,
3888
+ status=$status,
3889
+ goal_json=$goal,
3890
+ steps_json=$steps,
3891
+ created_at=$created,
3892
+ updated_at=$updated`).run({
3893
+ $id: workflow.id,
3894
+ $name: workflow.name,
3895
+ $description: workflow.description ?? null,
3896
+ $version: workflow.version,
3897
+ $status: workflow.status,
3898
+ $goal: workflow.goal ? JSON.stringify(workflow.goal) : null,
3899
+ $steps: JSON.stringify(workflow.steps),
3900
+ $created: workflow.createdAt,
3901
+ $updated: workflow.updatedAt
3902
+ });
3903
+ const imported = this.getWorkflow(workflow.id);
3904
+ if (!imported)
3905
+ throw new Error(`workflow not found after migration import: ${workflow.id}`);
3906
+ return imported;
3907
+ }
3908
+ upsertMigrationLoop(loop, opts = {}) {
3909
+ const existing = this.getLoop(loop.id);
3910
+ if (existing && !opts.replace)
3911
+ return existing;
3912
+ this.assertNoNestedWorkflowGoal(loop.target, loop.goal);
3913
+ this.db.query(`INSERT INTO loops (id, name, description, status, archived_at, archived_from_status, schedule_json, target_json,
3914
+ goal_json, machine_json, next_run_at, retry_scheduled_for, catch_up, catch_up_limit, overlap, max_attempts,
3915
+ retry_delay_ms, lease_ms, expires_at, created_at, updated_at)
3916
+ VALUES ($id, $name, $description, $status, $archivedAt, $archivedFromStatus, $schedule, $target,
3917
+ $goal, $machine, $nextRun, $retrySlot, $catchUp, $catchUpLimit, $overlap, $maxAttempts,
3918
+ $retryDelay, $leaseMs, $expiresAt, $created, $updated)
3919
+ ON CONFLICT(id) DO UPDATE SET
3920
+ name=$name,
3921
+ description=$description,
3922
+ status=$status,
3923
+ archived_at=$archivedAt,
3924
+ archived_from_status=$archivedFromStatus,
3925
+ schedule_json=$schedule,
3926
+ target_json=$target,
3927
+ goal_json=$goal,
3928
+ machine_json=$machine,
3929
+ next_run_at=$nextRun,
3930
+ retry_scheduled_for=$retrySlot,
3931
+ catch_up=$catchUp,
3932
+ catch_up_limit=$catchUpLimit,
3933
+ overlap=$overlap,
3934
+ max_attempts=$maxAttempts,
3935
+ retry_delay_ms=$retryDelay,
3936
+ lease_ms=$leaseMs,
3937
+ expires_at=$expiresAt,
3938
+ created_at=$created,
3939
+ updated_at=$updated`).run({
3940
+ $id: loop.id,
3941
+ $name: loop.name,
3942
+ $description: loop.description ?? null,
3943
+ $status: loop.status,
3944
+ $archivedAt: loop.archivedAt ?? null,
3945
+ $archivedFromStatus: loop.archivedFromStatus ?? null,
3946
+ $schedule: JSON.stringify(loop.schedule),
3947
+ $target: JSON.stringify(loop.target),
3948
+ $goal: loop.goal ? JSON.stringify(loop.goal) : null,
3949
+ $machine: loop.machine ? JSON.stringify(loop.machine) : null,
3950
+ $nextRun: loop.nextRunAt ?? null,
3951
+ $retrySlot: loop.retryScheduledFor ?? null,
3952
+ $catchUp: loop.catchUp,
3953
+ $catchUpLimit: loop.catchUpLimit,
3954
+ $overlap: loop.overlap,
3955
+ $maxAttempts: loop.maxAttempts,
3956
+ $retryDelay: loop.retryDelayMs,
3957
+ $leaseMs: loop.leaseMs,
3958
+ $expiresAt: loop.expiresAt ?? null,
3959
+ $created: loop.createdAt,
3960
+ $updated: loop.updatedAt
3961
+ });
3962
+ const imported = this.getLoop(loop.id);
3963
+ if (!imported)
3964
+ throw new Error(`loop not found after migration import: ${loop.id}`);
3965
+ return imported;
3966
+ }
3967
+ upsertMigrationRun(run, opts = {}) {
3968
+ if (run.status === "running")
3969
+ throw new ValidationError(`cannot import running run ${run.id}`);
3970
+ const existing = this.getRun(run.id);
3971
+ if (existing && !opts.replace)
3972
+ return existing;
3973
+ this.db.query(`INSERT INTO loop_runs (id, loop_id, loop_name, scheduled_for, attempt, status, started_at, finished_at,
3974
+ claimed_by, claim_token, lease_expires_at, pid, pgid, process_started_at, exit_code, duration_ms,
3975
+ stdout, stderr, error, goal_run_id, created_at, updated_at)
3976
+ VALUES ($id, $loopId, $loopName, $scheduledFor, $attempt, $status, $startedAt, $finishedAt,
3977
+ $claimedBy, NULL, $leaseExpiresAt, $pid, $pgid, $processStartedAt, $exitCode, $durationMs,
3978
+ $stdout, $stderr, $error, $goalRunId, $created, $updated)
3979
+ ON CONFLICT(id) DO UPDATE SET
3980
+ loop_id=$loopId,
3981
+ loop_name=$loopName,
3982
+ scheduled_for=$scheduledFor,
3983
+ attempt=$attempt,
3984
+ status=$status,
3985
+ started_at=$startedAt,
3986
+ finished_at=$finishedAt,
3987
+ claimed_by=$claimedBy,
3988
+ claim_token=NULL,
3989
+ lease_expires_at=$leaseExpiresAt,
3990
+ pid=$pid,
3991
+ pgid=$pgid,
3992
+ process_started_at=$processStartedAt,
3993
+ exit_code=$exitCode,
3994
+ duration_ms=$durationMs,
3995
+ stdout=$stdout,
3996
+ stderr=$stderr,
3997
+ error=$error,
3998
+ goal_run_id=$goalRunId,
3999
+ created_at=$created,
4000
+ updated_at=$updated`).run({
4001
+ $id: run.id,
4002
+ $loopId: run.loopId,
4003
+ $loopName: run.loopName,
4004
+ $scheduledFor: run.scheduledFor,
4005
+ $attempt: run.attempt,
4006
+ $status: run.status,
4007
+ $startedAt: run.startedAt ?? null,
4008
+ $finishedAt: run.finishedAt ?? null,
4009
+ $claimedBy: run.claimedBy ?? null,
4010
+ $leaseExpiresAt: run.leaseExpiresAt ?? null,
4011
+ $pid: run.pid ?? null,
4012
+ $pgid: run.pgid ?? null,
4013
+ $processStartedAt: run.processStartedAt ?? null,
4014
+ $exitCode: run.exitCode ?? null,
4015
+ $durationMs: run.durationMs ?? null,
4016
+ $stdout: scrubbedOrNull(run.stdout),
4017
+ $stderr: scrubbedOrNull(run.stderr),
4018
+ $error: scrubbedOrNull(run.error),
4019
+ $goalRunId: run.goalRunId ?? null,
4020
+ $created: run.createdAt,
4021
+ $updated: run.updatedAt
4022
+ });
4023
+ const imported = this.getRun(run.id);
4024
+ if (!imported)
4025
+ throw new Error(`run not found after migration import: ${run.id}`);
4026
+ return imported;
4027
+ }
3844
4028
  pruneHistory(opts) {
3845
4029
  const { maxAgeDays, keepPerLoop } = opts;
3846
4030
  if (maxAgeDays === undefined && keepPerLoop === undefined) {
@@ -3841,6 +3841,190 @@ class Store {
3841
3841
  const row = status ? this.db.query("SELECT COUNT(*) AS count FROM loop_runs WHERE status = ?").get(status) : this.db.query("SELECT COUNT(*) AS count FROM loop_runs").get();
3842
3842
  return row?.count ?? 0;
3843
3843
  }
3844
+ exportMigrationRows(opts = {}) {
3845
+ const includeRuns = opts.includeRuns ?? true;
3846
+ const workflows = this.db.query("SELECT * FROM workflow_specs ORDER BY created_at ASC, id ASC").all().map(rowToWorkflow);
3847
+ const loops = this.db.query("SELECT * FROM loops ORDER BY created_at ASC, id ASC").all().map(rowToLoop);
3848
+ const runs = includeRuns ? this.db.query("SELECT * FROM loop_runs ORDER BY created_at ASC, id ASC").all().map(rowToRun) : [];
3849
+ return { schemaVersion: SCHEMA_USER_VERSION, workflows, loops, runs, checks: this.migrationChecks() };
3850
+ }
3851
+ countTable(table) {
3852
+ const row = this.db.query(`SELECT COUNT(*) AS count FROM ${table}`).get();
3853
+ return row?.count ?? 0;
3854
+ }
3855
+ migrationChecks() {
3856
+ const now = nowIso();
3857
+ return {
3858
+ unsupportedCounts: {
3859
+ workflowInvocations: this.countTable("workflow_invocations"),
3860
+ workflowWorkItems: this.countTable("workflow_work_items"),
3861
+ workflowRuns: this.countTable("workflow_runs"),
3862
+ workflowStepRuns: this.countTable("workflow_step_runs"),
3863
+ workflowEvents: this.countTable("workflow_events"),
3864
+ goals: this.countTable("goals"),
3865
+ goalPlanNodes: this.countTable("goal_plan_nodes"),
3866
+ goalRuns: this.countTable("goal_runs")
3867
+ },
3868
+ volatileCounts: {
3869
+ daemonLeases: this.countTable("daemon_lease"),
3870
+ activeDaemonLeases: this.db.query("SELECT COUNT(*) AS count FROM daemon_lease WHERE expires_at > ?").get(now)?.count ?? 0,
3871
+ runningLoopRuns: this.db.query("SELECT COUNT(*) AS count FROM loop_runs WHERE status = 'running'").get()?.count ?? 0,
3872
+ runningWorkflowRuns: this.db.query("SELECT COUNT(*) AS count FROM workflow_runs WHERE status = 'running'").get()?.count ?? 0,
3873
+ runningWorkflowStepRuns: this.db.query("SELECT COUNT(*) AS count FROM workflow_step_runs WHERE status = 'running'").get()?.count ?? 0,
3874
+ leasedWorkflowWorkItems: this.db.query("SELECT COUNT(*) AS count FROM workflow_work_items WHERE lease_expires_at IS NOT NULL OR status IN ('admitted', 'running')").get()?.count ?? 0
3875
+ }
3876
+ };
3877
+ }
3878
+ upsertMigrationWorkflow(workflow, opts = {}) {
3879
+ const existing = this.getWorkflow(workflow.id);
3880
+ if (existing && !opts.replace)
3881
+ return existing;
3882
+ this.db.query(`INSERT INTO workflow_specs (id, name, description, version, status, goal_json, steps_json, created_at, updated_at)
3883
+ VALUES ($id, $name, $description, $version, $status, $goal, $steps, $created, $updated)
3884
+ ON CONFLICT(id) DO UPDATE SET
3885
+ name=$name,
3886
+ description=$description,
3887
+ version=$version,
3888
+ status=$status,
3889
+ goal_json=$goal,
3890
+ steps_json=$steps,
3891
+ created_at=$created,
3892
+ updated_at=$updated`).run({
3893
+ $id: workflow.id,
3894
+ $name: workflow.name,
3895
+ $description: workflow.description ?? null,
3896
+ $version: workflow.version,
3897
+ $status: workflow.status,
3898
+ $goal: workflow.goal ? JSON.stringify(workflow.goal) : null,
3899
+ $steps: JSON.stringify(workflow.steps),
3900
+ $created: workflow.createdAt,
3901
+ $updated: workflow.updatedAt
3902
+ });
3903
+ const imported = this.getWorkflow(workflow.id);
3904
+ if (!imported)
3905
+ throw new Error(`workflow not found after migration import: ${workflow.id}`);
3906
+ return imported;
3907
+ }
3908
+ upsertMigrationLoop(loop, opts = {}) {
3909
+ const existing = this.getLoop(loop.id);
3910
+ if (existing && !opts.replace)
3911
+ return existing;
3912
+ this.assertNoNestedWorkflowGoal(loop.target, loop.goal);
3913
+ this.db.query(`INSERT INTO loops (id, name, description, status, archived_at, archived_from_status, schedule_json, target_json,
3914
+ goal_json, machine_json, next_run_at, retry_scheduled_for, catch_up, catch_up_limit, overlap, max_attempts,
3915
+ retry_delay_ms, lease_ms, expires_at, created_at, updated_at)
3916
+ VALUES ($id, $name, $description, $status, $archivedAt, $archivedFromStatus, $schedule, $target,
3917
+ $goal, $machine, $nextRun, $retrySlot, $catchUp, $catchUpLimit, $overlap, $maxAttempts,
3918
+ $retryDelay, $leaseMs, $expiresAt, $created, $updated)
3919
+ ON CONFLICT(id) DO UPDATE SET
3920
+ name=$name,
3921
+ description=$description,
3922
+ status=$status,
3923
+ archived_at=$archivedAt,
3924
+ archived_from_status=$archivedFromStatus,
3925
+ schedule_json=$schedule,
3926
+ target_json=$target,
3927
+ goal_json=$goal,
3928
+ machine_json=$machine,
3929
+ next_run_at=$nextRun,
3930
+ retry_scheduled_for=$retrySlot,
3931
+ catch_up=$catchUp,
3932
+ catch_up_limit=$catchUpLimit,
3933
+ overlap=$overlap,
3934
+ max_attempts=$maxAttempts,
3935
+ retry_delay_ms=$retryDelay,
3936
+ lease_ms=$leaseMs,
3937
+ expires_at=$expiresAt,
3938
+ created_at=$created,
3939
+ updated_at=$updated`).run({
3940
+ $id: loop.id,
3941
+ $name: loop.name,
3942
+ $description: loop.description ?? null,
3943
+ $status: loop.status,
3944
+ $archivedAt: loop.archivedAt ?? null,
3945
+ $archivedFromStatus: loop.archivedFromStatus ?? null,
3946
+ $schedule: JSON.stringify(loop.schedule),
3947
+ $target: JSON.stringify(loop.target),
3948
+ $goal: loop.goal ? JSON.stringify(loop.goal) : null,
3949
+ $machine: loop.machine ? JSON.stringify(loop.machine) : null,
3950
+ $nextRun: loop.nextRunAt ?? null,
3951
+ $retrySlot: loop.retryScheduledFor ?? null,
3952
+ $catchUp: loop.catchUp,
3953
+ $catchUpLimit: loop.catchUpLimit,
3954
+ $overlap: loop.overlap,
3955
+ $maxAttempts: loop.maxAttempts,
3956
+ $retryDelay: loop.retryDelayMs,
3957
+ $leaseMs: loop.leaseMs,
3958
+ $expiresAt: loop.expiresAt ?? null,
3959
+ $created: loop.createdAt,
3960
+ $updated: loop.updatedAt
3961
+ });
3962
+ const imported = this.getLoop(loop.id);
3963
+ if (!imported)
3964
+ throw new Error(`loop not found after migration import: ${loop.id}`);
3965
+ return imported;
3966
+ }
3967
+ upsertMigrationRun(run, opts = {}) {
3968
+ if (run.status === "running")
3969
+ throw new ValidationError(`cannot import running run ${run.id}`);
3970
+ const existing = this.getRun(run.id);
3971
+ if (existing && !opts.replace)
3972
+ return existing;
3973
+ this.db.query(`INSERT INTO loop_runs (id, loop_id, loop_name, scheduled_for, attempt, status, started_at, finished_at,
3974
+ claimed_by, claim_token, lease_expires_at, pid, pgid, process_started_at, exit_code, duration_ms,
3975
+ stdout, stderr, error, goal_run_id, created_at, updated_at)
3976
+ VALUES ($id, $loopId, $loopName, $scheduledFor, $attempt, $status, $startedAt, $finishedAt,
3977
+ $claimedBy, NULL, $leaseExpiresAt, $pid, $pgid, $processStartedAt, $exitCode, $durationMs,
3978
+ $stdout, $stderr, $error, $goalRunId, $created, $updated)
3979
+ ON CONFLICT(id) DO UPDATE SET
3980
+ loop_id=$loopId,
3981
+ loop_name=$loopName,
3982
+ scheduled_for=$scheduledFor,
3983
+ attempt=$attempt,
3984
+ status=$status,
3985
+ started_at=$startedAt,
3986
+ finished_at=$finishedAt,
3987
+ claimed_by=$claimedBy,
3988
+ claim_token=NULL,
3989
+ lease_expires_at=$leaseExpiresAt,
3990
+ pid=$pid,
3991
+ pgid=$pgid,
3992
+ process_started_at=$processStartedAt,
3993
+ exit_code=$exitCode,
3994
+ duration_ms=$durationMs,
3995
+ stdout=$stdout,
3996
+ stderr=$stderr,
3997
+ error=$error,
3998
+ goal_run_id=$goalRunId,
3999
+ created_at=$created,
4000
+ updated_at=$updated`).run({
4001
+ $id: run.id,
4002
+ $loopId: run.loopId,
4003
+ $loopName: run.loopName,
4004
+ $scheduledFor: run.scheduledFor,
4005
+ $attempt: run.attempt,
4006
+ $status: run.status,
4007
+ $startedAt: run.startedAt ?? null,
4008
+ $finishedAt: run.finishedAt ?? null,
4009
+ $claimedBy: run.claimedBy ?? null,
4010
+ $leaseExpiresAt: run.leaseExpiresAt ?? null,
4011
+ $pid: run.pid ?? null,
4012
+ $pgid: run.pgid ?? null,
4013
+ $processStartedAt: run.processStartedAt ?? null,
4014
+ $exitCode: run.exitCode ?? null,
4015
+ $durationMs: run.durationMs ?? null,
4016
+ $stdout: scrubbedOrNull(run.stdout),
4017
+ $stderr: scrubbedOrNull(run.stderr),
4018
+ $error: scrubbedOrNull(run.error),
4019
+ $goalRunId: run.goalRunId ?? null,
4020
+ $created: run.createdAt,
4021
+ $updated: run.updatedAt
4022
+ });
4023
+ const imported = this.getRun(run.id);
4024
+ if (!imported)
4025
+ throw new Error(`run not found after migration import: ${run.id}`);
4026
+ return imported;
4027
+ }
3844
4028
  pruneHistory(opts) {
3845
4029
  const { maxAgeDays, keepPerLoop } = opts;
3846
4030
  if (maxAgeDays === undefined && keepPerLoop === undefined) {
@@ -77,6 +77,41 @@ export interface PruneHistorySummary {
77
77
  workflowRuns: number;
78
78
  goalRuns: number;
79
79
  }
80
+ export interface StoreMigrationRows {
81
+ schemaVersion: number;
82
+ workflows: WorkflowSpec[];
83
+ loops: Loop[];
84
+ runs: LoopRun[];
85
+ checks: StoreMigrationChecks;
86
+ }
87
+ export interface StoreMigrationUnsupportedCounts {
88
+ workflowInvocations: number;
89
+ workflowWorkItems: number;
90
+ workflowRuns: number;
91
+ workflowStepRuns: number;
92
+ workflowEvents: number;
93
+ goals: number;
94
+ goalPlanNodes: number;
95
+ goalRuns: number;
96
+ }
97
+ export interface StoreMigrationVolatileCounts {
98
+ daemonLeases: number;
99
+ activeDaemonLeases: number;
100
+ runningLoopRuns: number;
101
+ runningWorkflowRuns: number;
102
+ runningWorkflowStepRuns: number;
103
+ leasedWorkflowWorkItems: number;
104
+ }
105
+ export interface StoreMigrationChecks {
106
+ unsupportedCounts: StoreMigrationUnsupportedCounts;
107
+ volatileCounts: StoreMigrationVolatileCounts;
108
+ }
109
+ export interface StoreMigrationRowsOptions {
110
+ includeRuns?: boolean;
111
+ }
112
+ export interface StoreMigrationUpsertOptions {
113
+ replace?: boolean;
114
+ }
80
115
  export interface RecordGoalEventInput {
81
116
  goalId: string;
82
117
  turn?: number;
@@ -301,6 +336,12 @@ export declare class Store {
301
336
  includeArchived?: boolean;
302
337
  }): number;
303
338
  countRuns(status?: RunStatus): number;
339
+ exportMigrationRows(opts?: StoreMigrationRowsOptions): StoreMigrationRows;
340
+ private countTable;
341
+ private migrationChecks;
342
+ upsertMigrationWorkflow(workflow: WorkflowSpec, opts?: StoreMigrationUpsertOptions): WorkflowSpec;
343
+ upsertMigrationLoop(loop: Loop, opts?: StoreMigrationUpsertOptions): Loop;
344
+ upsertMigrationRun(run: LoopRun, opts?: StoreMigrationUpsertOptions): LoopRun;
304
345
  /**
305
346
  * Delete old terminal run history: loop runs plus their attached workflow
306
347
  * runs (step runs and events cascade), goal run events, and per-run manifest