@hasna/loops 0.4.3 → 0.4.5

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.
@@ -1525,7 +1525,7 @@ class Store {
1525
1525
  }
1526
1526
  const applied = new Set(this.db.query("SELECT id FROM schema_migrations").all().map((row) => row.id));
1527
1527
  for (const migration of this.migrations()) {
1528
- if (!migration.baseline && !migration.reapply && applied.has(migration.id))
1528
+ if (!migration.baseline && applied.has(migration.id))
1529
1529
  continue;
1530
1530
  migration.apply();
1531
1531
  if (!applied.has(migration.id)) {
@@ -1578,7 +1578,6 @@ class Store {
1578
1578
  },
1579
1579
  {
1580
1580
  id: "0007_run_claim_tokens",
1581
- reapply: true,
1582
1581
  apply: () => {
1583
1582
  this.addColumnIfMissing("loop_runs", "claim_token", "TEXT");
1584
1583
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_runs_claim_token ON loop_runs(claim_token) WHERE claim_token IS NOT NULL");
@@ -3843,6 +3842,190 @@ class Store {
3843
3842
  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();
3844
3843
  return row?.count ?? 0;
3845
3844
  }
3845
+ exportMigrationRows(opts = {}) {
3846
+ const includeRuns = opts.includeRuns ?? true;
3847
+ const workflows = this.db.query("SELECT * FROM workflow_specs ORDER BY created_at ASC, id ASC").all().map(rowToWorkflow);
3848
+ const loops = this.db.query("SELECT * FROM loops ORDER BY created_at ASC, id ASC").all().map(rowToLoop);
3849
+ const runs = includeRuns ? this.db.query("SELECT * FROM loop_runs ORDER BY created_at ASC, id ASC").all().map(rowToRun) : [];
3850
+ return { schemaVersion: SCHEMA_USER_VERSION, workflows, loops, runs, checks: this.migrationChecks() };
3851
+ }
3852
+ countTable(table) {
3853
+ const row = this.db.query(`SELECT COUNT(*) AS count FROM ${table}`).get();
3854
+ return row?.count ?? 0;
3855
+ }
3856
+ migrationChecks() {
3857
+ const now = nowIso();
3858
+ return {
3859
+ unsupportedCounts: {
3860
+ workflowInvocations: this.countTable("workflow_invocations"),
3861
+ workflowWorkItems: this.countTable("workflow_work_items"),
3862
+ workflowRuns: this.countTable("workflow_runs"),
3863
+ workflowStepRuns: this.countTable("workflow_step_runs"),
3864
+ workflowEvents: this.countTable("workflow_events"),
3865
+ goals: this.countTable("goals"),
3866
+ goalPlanNodes: this.countTable("goal_plan_nodes"),
3867
+ goalRuns: this.countTable("goal_runs")
3868
+ },
3869
+ volatileCounts: {
3870
+ daemonLeases: this.countTable("daemon_lease"),
3871
+ activeDaemonLeases: this.db.query("SELECT COUNT(*) AS count FROM daemon_lease WHERE expires_at > ?").get(now)?.count ?? 0,
3872
+ runningLoopRuns: this.db.query("SELECT COUNT(*) AS count FROM loop_runs WHERE status = 'running'").get()?.count ?? 0,
3873
+ runningWorkflowRuns: this.db.query("SELECT COUNT(*) AS count FROM workflow_runs WHERE status = 'running'").get()?.count ?? 0,
3874
+ runningWorkflowStepRuns: this.db.query("SELECT COUNT(*) AS count FROM workflow_step_runs WHERE status = 'running'").get()?.count ?? 0,
3875
+ 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
3876
+ }
3877
+ };
3878
+ }
3879
+ upsertMigrationWorkflow(workflow, opts = {}) {
3880
+ const existing = this.getWorkflow(workflow.id);
3881
+ if (existing && !opts.replace)
3882
+ return existing;
3883
+ this.db.query(`INSERT INTO workflow_specs (id, name, description, version, status, goal_json, steps_json, created_at, updated_at)
3884
+ VALUES ($id, $name, $description, $version, $status, $goal, $steps, $created, $updated)
3885
+ ON CONFLICT(id) DO UPDATE SET
3886
+ name=$name,
3887
+ description=$description,
3888
+ version=$version,
3889
+ status=$status,
3890
+ goal_json=$goal,
3891
+ steps_json=$steps,
3892
+ created_at=$created,
3893
+ updated_at=$updated`).run({
3894
+ $id: workflow.id,
3895
+ $name: workflow.name,
3896
+ $description: workflow.description ?? null,
3897
+ $version: workflow.version,
3898
+ $status: workflow.status,
3899
+ $goal: workflow.goal ? JSON.stringify(workflow.goal) : null,
3900
+ $steps: JSON.stringify(workflow.steps),
3901
+ $created: workflow.createdAt,
3902
+ $updated: workflow.updatedAt
3903
+ });
3904
+ const imported = this.getWorkflow(workflow.id);
3905
+ if (!imported)
3906
+ throw new Error(`workflow not found after migration import: ${workflow.id}`);
3907
+ return imported;
3908
+ }
3909
+ upsertMigrationLoop(loop, opts = {}) {
3910
+ const existing = this.getLoop(loop.id);
3911
+ if (existing && !opts.replace)
3912
+ return existing;
3913
+ this.assertNoNestedWorkflowGoal(loop.target, loop.goal);
3914
+ this.db.query(`INSERT INTO loops (id, name, description, status, archived_at, archived_from_status, schedule_json, target_json,
3915
+ goal_json, machine_json, next_run_at, retry_scheduled_for, catch_up, catch_up_limit, overlap, max_attempts,
3916
+ retry_delay_ms, lease_ms, expires_at, created_at, updated_at)
3917
+ VALUES ($id, $name, $description, $status, $archivedAt, $archivedFromStatus, $schedule, $target,
3918
+ $goal, $machine, $nextRun, $retrySlot, $catchUp, $catchUpLimit, $overlap, $maxAttempts,
3919
+ $retryDelay, $leaseMs, $expiresAt, $created, $updated)
3920
+ ON CONFLICT(id) DO UPDATE SET
3921
+ name=$name,
3922
+ description=$description,
3923
+ status=$status,
3924
+ archived_at=$archivedAt,
3925
+ archived_from_status=$archivedFromStatus,
3926
+ schedule_json=$schedule,
3927
+ target_json=$target,
3928
+ goal_json=$goal,
3929
+ machine_json=$machine,
3930
+ next_run_at=$nextRun,
3931
+ retry_scheduled_for=$retrySlot,
3932
+ catch_up=$catchUp,
3933
+ catch_up_limit=$catchUpLimit,
3934
+ overlap=$overlap,
3935
+ max_attempts=$maxAttempts,
3936
+ retry_delay_ms=$retryDelay,
3937
+ lease_ms=$leaseMs,
3938
+ expires_at=$expiresAt,
3939
+ created_at=$created,
3940
+ updated_at=$updated`).run({
3941
+ $id: loop.id,
3942
+ $name: loop.name,
3943
+ $description: loop.description ?? null,
3944
+ $status: loop.status,
3945
+ $archivedAt: loop.archivedAt ?? null,
3946
+ $archivedFromStatus: loop.archivedFromStatus ?? null,
3947
+ $schedule: JSON.stringify(loop.schedule),
3948
+ $target: JSON.stringify(loop.target),
3949
+ $goal: loop.goal ? JSON.stringify(loop.goal) : null,
3950
+ $machine: loop.machine ? JSON.stringify(loop.machine) : null,
3951
+ $nextRun: loop.nextRunAt ?? null,
3952
+ $retrySlot: loop.retryScheduledFor ?? null,
3953
+ $catchUp: loop.catchUp,
3954
+ $catchUpLimit: loop.catchUpLimit,
3955
+ $overlap: loop.overlap,
3956
+ $maxAttempts: loop.maxAttempts,
3957
+ $retryDelay: loop.retryDelayMs,
3958
+ $leaseMs: loop.leaseMs,
3959
+ $expiresAt: loop.expiresAt ?? null,
3960
+ $created: loop.createdAt,
3961
+ $updated: loop.updatedAt
3962
+ });
3963
+ const imported = this.getLoop(loop.id);
3964
+ if (!imported)
3965
+ throw new Error(`loop not found after migration import: ${loop.id}`);
3966
+ return imported;
3967
+ }
3968
+ upsertMigrationRun(run, opts = {}) {
3969
+ if (run.status === "running")
3970
+ throw new ValidationError(`cannot import running run ${run.id}`);
3971
+ const existing = this.getRun(run.id);
3972
+ if (existing && !opts.replace)
3973
+ return existing;
3974
+ this.db.query(`INSERT INTO loop_runs (id, loop_id, loop_name, scheduled_for, attempt, status, started_at, finished_at,
3975
+ claimed_by, claim_token, lease_expires_at, pid, pgid, process_started_at, exit_code, duration_ms,
3976
+ stdout, stderr, error, goal_run_id, created_at, updated_at)
3977
+ VALUES ($id, $loopId, $loopName, $scheduledFor, $attempt, $status, $startedAt, $finishedAt,
3978
+ $claimedBy, NULL, $leaseExpiresAt, $pid, $pgid, $processStartedAt, $exitCode, $durationMs,
3979
+ $stdout, $stderr, $error, $goalRunId, $created, $updated)
3980
+ ON CONFLICT(id) DO UPDATE SET
3981
+ loop_id=$loopId,
3982
+ loop_name=$loopName,
3983
+ scheduled_for=$scheduledFor,
3984
+ attempt=$attempt,
3985
+ status=$status,
3986
+ started_at=$startedAt,
3987
+ finished_at=$finishedAt,
3988
+ claimed_by=$claimedBy,
3989
+ claim_token=NULL,
3990
+ lease_expires_at=$leaseExpiresAt,
3991
+ pid=$pid,
3992
+ pgid=$pgid,
3993
+ process_started_at=$processStartedAt,
3994
+ exit_code=$exitCode,
3995
+ duration_ms=$durationMs,
3996
+ stdout=$stdout,
3997
+ stderr=$stderr,
3998
+ error=$error,
3999
+ goal_run_id=$goalRunId,
4000
+ created_at=$created,
4001
+ updated_at=$updated`).run({
4002
+ $id: run.id,
4003
+ $loopId: run.loopId,
4004
+ $loopName: run.loopName,
4005
+ $scheduledFor: run.scheduledFor,
4006
+ $attempt: run.attempt,
4007
+ $status: run.status,
4008
+ $startedAt: run.startedAt ?? null,
4009
+ $finishedAt: run.finishedAt ?? null,
4010
+ $claimedBy: run.claimedBy ?? null,
4011
+ $leaseExpiresAt: run.leaseExpiresAt ?? null,
4012
+ $pid: run.pid ?? null,
4013
+ $pgid: run.pgid ?? null,
4014
+ $processStartedAt: run.processStartedAt ?? null,
4015
+ $exitCode: run.exitCode ?? null,
4016
+ $durationMs: run.durationMs ?? null,
4017
+ $stdout: scrubbedOrNull(run.stdout),
4018
+ $stderr: scrubbedOrNull(run.stderr),
4019
+ $error: scrubbedOrNull(run.error),
4020
+ $goalRunId: run.goalRunId ?? null,
4021
+ $created: run.createdAt,
4022
+ $updated: run.updatedAt
4023
+ });
4024
+ const imported = this.getRun(run.id);
4025
+ if (!imported)
4026
+ throw new Error(`run not found after migration import: ${run.id}`);
4027
+ return imported;
4028
+ }
3846
4029
  pruneHistory(opts) {
3847
4030
  const { maxAgeDays, keepPerLoop } = opts;
3848
4031
  if (maxAgeDays === undefined && keepPerLoop === undefined) {
@@ -7623,7 +7806,7 @@ function enableStartup(result) {
7623
7806
  // package.json
7624
7807
  var package_default = {
7625
7808
  name: "@hasna/loops",
7626
- version: "0.4.3",
7809
+ version: "0.4.5",
7627
7810
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
7628
7811
  type: "module",
7629
7812
  main: "dist/index.js",
@@ -7697,7 +7880,7 @@ var package_default = {
7697
7880
  prepare: "test -d dist || bun run build",
7698
7881
  "dev:cli": "bun run src/cli/index.ts",
7699
7882
  "dev:daemon": "bun run src/daemon/index.ts",
7700
- prepublishOnly: "bun run build && bun run test:boundary"
7883
+ prepublishOnly: "bun run build && bun run typecheck && bun test && bun run test:boundary"
7701
7884
  },
7702
7885
  keywords: [
7703
7886
  "loops",
package/dist/index.d.ts CHANGED
@@ -30,7 +30,9 @@ export { workflowBodyFromJson, workflowExecutionOrder } from "./lib/workflow-spe
30
30
  export { listOpenMachines, refreshLoopMachine, resolveLoopMachine } from "./lib/machines.js";
31
31
  export { BOUNDED_AGENT_WORKER_VERIFIER_TEMPLATE_ID, EVENT_WORKER_VERIFIER_TEMPLATE_ID, TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID, getLoopTemplate, listLoopTemplates, renderBoundedAgentWorkerVerifierWorkflow, renderEventWorkerVerifierWorkflow, renderLoopTemplate, renderTodosTaskWorkerVerifierWorkflow, } from "./lib/templates.js";
32
32
  export { buildDuplicateOverlapReport, buildNameHygieneReport, buildScriptInventoryReport } from "./lib/hygiene.js";
33
+ export { LOOPS_MIGRATION_SCHEMA, applyImportMigrationBundle, buildImportMigrationPlan, buildSelfHostedMigrationPlan, exportLoopsMigrationBundle, migrationHash, publicMigrationBundle, registerSelfHostedRunner, selfHostedControlPlaneSummary, validateLoopsMigrationBundle, } from "./lib/migration.js";
33
34
  export { runGoal } from "./lib/goal/runner.js";
34
35
  export { resolveGoalModel } from "./lib/goal/model-factory.js";
35
36
  export { isTerminal as isGoalTerminal, readyNodeKeys, rollupSummary } from "./lib/goal/status.js";
36
37
  export type { CreateWorkflowInvocationInput, Goal, GoalAutoExecute, GoalExecutorResult, GoalPlan, GoalPlanNode, GoalPlanNodeStatus, GoalPlanStatus, GoalRollup, GoalRun, GoalSpec, GoalStatus, LoopMachineConfidence, LoopMachineRef, LoopMachineRoute, LoopTemplateKind, LoopTemplateSource, LoopTemplateSummary, LoopTemplateVariable, LoopTemplateVariableType, OpenAutomationsRuntimeBinding, PersistGuardOptions, UpsertWorkflowWorkItemInput, WorkflowInvocation, WorkflowInvocationIntent, WorkflowInvocationOutputPolicy, WorkflowInvocationRef, WorkflowInvocationScope, WorkflowInvocationSourceKind, WorkflowInvocationSubjectKind, WorkflowWorkItem, WorkflowWorkItemStatus, } from "./types.js";
38
+ export type { ApplyLoopsMigrationResult, ExportLoopsMigrationOptions, ImportLoopsMigrationOptions, LoopsMigrationAction, LoopsMigrationBundle, LoopsMigrationPlan, LoopsMigrationPlanRow, LoopsMigrationPlanSummary, LoopsMigrationResource, RunnerRegistrationOptions, RunnerRegistrationResult, SelfHostedPlanOptions, } from "./lib/migration.js";