@hasna/loops 0.3.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -1
- package/dist/cli/index.js +510 -159
- package/dist/daemon/index.js +447 -146
- package/dist/index.js +446 -146
- package/dist/lib/executor.d.ts +2 -2
- package/dist/lib/format.d.ts +2 -1
- package/dist/lib/scheduler.d.ts +10 -1
- package/dist/lib/store.d.ts +21 -12
- package/dist/lib/store.js +304 -107
- package/dist/sdk/index.js +446 -146
- package/dist/types.d.ts +5 -0
- package/docs/USAGE.md +21 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -260,6 +260,11 @@ function validateTarget(value, label) {
|
|
|
260
260
|
const providers = ["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"];
|
|
261
261
|
if (!providers.includes(value.provider))
|
|
262
262
|
throw new Error(`${label}.provider must be one of ${providers.join(", ")}`);
|
|
263
|
+
if (value.authProfile !== undefined) {
|
|
264
|
+
assertString(value.authProfile, `${label}.authProfile`);
|
|
265
|
+
if (value.provider !== "codewith")
|
|
266
|
+
throw new Error(`${label}.authProfile is currently supported only for provider codewith`);
|
|
267
|
+
}
|
|
263
268
|
return value;
|
|
264
269
|
}
|
|
265
270
|
throw new Error(`${label}.type must be command or agent`);
|
|
@@ -610,6 +615,13 @@ class Store {
|
|
|
610
615
|
} catch {}
|
|
611
616
|
this.db.query("INSERT OR IGNORE INTO schema_migrations (id, applied_at) VALUES (?, ?)").run("0001_initial_and_workflows", nowIso());
|
|
612
617
|
}
|
|
618
|
+
assertDaemonLeaseFence(opts = {}, now = nowIso()) {
|
|
619
|
+
if (!opts.daemonLeaseId)
|
|
620
|
+
return;
|
|
621
|
+
const row = this.db.query("SELECT id FROM daemon_lease WHERE id = ? AND expires_at > ?").get(opts.daemonLeaseId, now);
|
|
622
|
+
if (!row)
|
|
623
|
+
throw new Error("daemon lease lost");
|
|
624
|
+
}
|
|
613
625
|
createLoop(input, from = new Date) {
|
|
614
626
|
const now = nowIso();
|
|
615
627
|
const loop = {
|
|
@@ -679,21 +691,31 @@ class Store {
|
|
|
679
691
|
ORDER BY next_run_at ASC`).all(now.toISOString());
|
|
680
692
|
return rows.map(rowToLoop);
|
|
681
693
|
}
|
|
682
|
-
updateLoop(id, patch) {
|
|
694
|
+
updateLoop(id, patch, opts = {}) {
|
|
683
695
|
const current = this.getLoop(id);
|
|
684
696
|
if (!current)
|
|
685
697
|
throw new Error(`loop not found: ${id}`);
|
|
686
|
-
const
|
|
698
|
+
const updated = (opts.now ?? new Date).toISOString();
|
|
699
|
+
const merged = { ...current, ...patch, updatedAt: updated };
|
|
687
700
|
this.db.query(`UPDATE loops SET status=$status, next_run_at=$nextRun, retry_scheduled_for=$retrySlot,
|
|
688
|
-
expires_at=$expiresAt, updated_at=$updated
|
|
701
|
+
expires_at=$expiresAt, updated_at=$updated
|
|
702
|
+
WHERE id=$id
|
|
703
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
704
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
705
|
+
))`).run({
|
|
689
706
|
$id: id,
|
|
690
707
|
$status: merged.status,
|
|
691
708
|
$nextRun: merged.nextRunAt ?? null,
|
|
692
709
|
$retrySlot: merged.retryScheduledFor ?? null,
|
|
693
710
|
$expiresAt: merged.expiresAt ?? null,
|
|
694
|
-
$updated: merged.updatedAt
|
|
711
|
+
$updated: merged.updatedAt,
|
|
712
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
713
|
+
$now: updated
|
|
695
714
|
});
|
|
696
|
-
|
|
715
|
+
const after = this.getLoop(id);
|
|
716
|
+
if (!after)
|
|
717
|
+
throw new Error(`loop not found after update: ${id}`);
|
|
718
|
+
return after;
|
|
697
719
|
}
|
|
698
720
|
deleteLoop(idOrName) {
|
|
699
721
|
const loop = this.requireLoop(idOrName);
|
|
@@ -757,11 +779,14 @@ class Store {
|
|
|
757
779
|
const now = nowIso();
|
|
758
780
|
if (input.idempotencyKey) {
|
|
759
781
|
const existing = this.db.query("SELECT * FROM workflow_runs WHERE workflow_id = ? AND idempotency_key = ? LIMIT 1").get(input.workflow.id, input.idempotencyKey);
|
|
760
|
-
if (existing)
|
|
782
|
+
if (existing) {
|
|
783
|
+
this.assertDaemonLeaseFence(input);
|
|
761
784
|
return rowToWorkflowRun(existing);
|
|
785
|
+
}
|
|
762
786
|
}
|
|
763
787
|
this.db.exec("BEGIN IMMEDIATE");
|
|
764
788
|
try {
|
|
789
|
+
this.assertDaemonLeaseFence(input, now);
|
|
765
790
|
if (input.idempotencyKey) {
|
|
766
791
|
const existing = this.db.query("SELECT * FROM workflow_runs WHERE workflow_id = ? AND idempotency_key = ? LIMIT 1").get(input.workflow.id, input.idempotencyKey);
|
|
767
792
|
if (existing) {
|
|
@@ -860,31 +885,60 @@ class Store {
|
|
|
860
885
|
const run = this.getWorkflowRun(workflowRunId);
|
|
861
886
|
return Boolean(run && ["succeeded", "failed", "timed_out", "cancelled"].includes(run.status));
|
|
862
887
|
}
|
|
863
|
-
startWorkflowStepRun(workflowRunId, stepId) {
|
|
864
|
-
const now =
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
888
|
+
startWorkflowStepRun(workflowRunId, stepId, opts = {}) {
|
|
889
|
+
const now = (opts.now ?? new Date).toISOString();
|
|
890
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
891
|
+
try {
|
|
892
|
+
const res = this.db.query(`UPDATE workflow_step_runs
|
|
893
|
+
SET status='running', started_at=$started, finished_at=NULL, exit_code=NULL, duration_ms=NULL,
|
|
894
|
+
pid=NULL, stdout=NULL, stderr=NULL, error=NULL, updated_at=$updated
|
|
895
|
+
WHERE workflow_run_id=$workflowRunId
|
|
896
|
+
AND step_id=$stepId
|
|
897
|
+
AND status IN ('pending', 'failed', 'timed_out')
|
|
898
|
+
AND EXISTS (
|
|
899
|
+
SELECT 1 FROM workflow_runs
|
|
900
|
+
WHERE id=$workflowRunId AND status='running'
|
|
901
|
+
)
|
|
902
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
903
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
904
|
+
))`).run({
|
|
905
|
+
$workflowRunId: workflowRunId,
|
|
906
|
+
$stepId: stepId,
|
|
907
|
+
$started: now,
|
|
908
|
+
$updated: now,
|
|
909
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
910
|
+
$now: now
|
|
911
|
+
});
|
|
912
|
+
const run = this.getWorkflowStepRun(workflowRunId, stepId);
|
|
913
|
+
if (!run)
|
|
914
|
+
throw new Error(`workflow step run not found: ${workflowRunId}/${stepId}`);
|
|
915
|
+
if (res.changes !== 1) {
|
|
916
|
+
throw new Error(`workflow step is not claimable: ${workflowRunId}/${stepId} status=${run.status}`);
|
|
917
|
+
}
|
|
918
|
+
this.appendWorkflowEvent(workflowRunId, "step_started", stepId);
|
|
919
|
+
this.db.exec("COMMIT");
|
|
920
|
+
return run;
|
|
921
|
+
} catch (error) {
|
|
922
|
+
try {
|
|
923
|
+
this.db.exec("ROLLBACK");
|
|
924
|
+
} catch {}
|
|
925
|
+
throw error;
|
|
880
926
|
}
|
|
881
|
-
this.appendWorkflowEvent(workflowRunId, "step_started", stepId);
|
|
882
|
-
return run;
|
|
883
927
|
}
|
|
884
|
-
markWorkflowStepPid(workflowRunId, stepId, pid) {
|
|
885
|
-
const now =
|
|
928
|
+
markWorkflowStepPid(workflowRunId, stepId, pid, opts = {}) {
|
|
929
|
+
const now = (opts.now ?? new Date).toISOString();
|
|
886
930
|
this.db.query(`UPDATE workflow_step_runs SET pid=$pid, updated_at=$updated
|
|
887
|
-
WHERE workflow_run_id=$workflowRunId AND step_id=$stepId AND status='running'
|
|
931
|
+
WHERE workflow_run_id=$workflowRunId AND step_id=$stepId AND status='running'
|
|
932
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
933
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
934
|
+
))`).run({
|
|
935
|
+
$workflowRunId: workflowRunId,
|
|
936
|
+
$stepId: stepId,
|
|
937
|
+
$pid: pid,
|
|
938
|
+
$updated: now,
|
|
939
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
940
|
+
$now: now
|
|
941
|
+
});
|
|
888
942
|
const run = this.getWorkflowStepRun(workflowRunId, stepId);
|
|
889
943
|
if (!run)
|
|
890
944
|
throw new Error(`workflow step run not found after pid update: ${workflowRunId}/${stepId}`);
|
|
@@ -912,60 +966,110 @@ class Store {
|
|
|
912
966
|
recoveredSteps: before.map((step) => this.getWorkflowStepRun(workflowRunId, step.stepId)).filter(Boolean)
|
|
913
967
|
};
|
|
914
968
|
}
|
|
915
|
-
finalizeWorkflowStepRun(workflowRunId, stepId, patch) {
|
|
969
|
+
finalizeWorkflowStepRun(workflowRunId, stepId, patch, opts = {}) {
|
|
916
970
|
const finishedAt = patch.finishedAt ?? nowIso();
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
971
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
972
|
+
try {
|
|
973
|
+
const res = this.db.query(`UPDATE workflow_step_runs SET status=$status, finished_at=$finished, exit_code=$exitCode, duration_ms=$durationMs,
|
|
974
|
+
pid=NULL, stdout=$stdout, stderr=$stderr, error=$error, updated_at=$updated
|
|
975
|
+
WHERE workflow_run_id=$workflowRunId AND step_id=$stepId AND status='running'
|
|
976
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
977
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
978
|
+
))`).run({
|
|
979
|
+
$workflowRunId: workflowRunId,
|
|
980
|
+
$stepId: stepId,
|
|
981
|
+
$status: patch.status,
|
|
982
|
+
$finished: finishedAt,
|
|
983
|
+
$exitCode: patch.exitCode ?? null,
|
|
984
|
+
$durationMs: patch.durationMs ?? null,
|
|
985
|
+
$stdout: patch.stdout ?? null,
|
|
986
|
+
$stderr: patch.stderr ?? null,
|
|
987
|
+
$error: patch.error ?? null,
|
|
988
|
+
$updated: finishedAt,
|
|
989
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
990
|
+
$now: (opts.now ?? new Date(finishedAt)).toISOString()
|
|
935
991
|
});
|
|
992
|
+
if (res.changes === 1) {
|
|
993
|
+
this.appendWorkflowEvent(workflowRunId, `step_${patch.status}`, stepId, {
|
|
994
|
+
exitCode: patch.exitCode,
|
|
995
|
+
error: patch.error
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
this.db.exec("COMMIT");
|
|
999
|
+
} catch (error) {
|
|
1000
|
+
try {
|
|
1001
|
+
this.db.exec("ROLLBACK");
|
|
1002
|
+
} catch {}
|
|
1003
|
+
throw error;
|
|
936
1004
|
}
|
|
937
1005
|
const run = this.getWorkflowStepRun(workflowRunId, stepId);
|
|
938
1006
|
if (!run)
|
|
939
1007
|
throw new Error(`workflow step run not found after finalize: ${workflowRunId}/${stepId}`);
|
|
940
1008
|
return run;
|
|
941
1009
|
}
|
|
942
|
-
skipWorkflowStepRun(workflowRunId, stepId, reason) {
|
|
943
|
-
const now =
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
1010
|
+
skipWorkflowStepRun(workflowRunId, stepId, reason, opts = {}) {
|
|
1011
|
+
const now = (opts.now ?? new Date).toISOString();
|
|
1012
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
1013
|
+
try {
|
|
1014
|
+
const res = this.db.query(`UPDATE workflow_step_runs SET status='skipped', finished_at=$finished, pid=NULL, error=$error, updated_at=$updated
|
|
1015
|
+
WHERE workflow_run_id=$workflowRunId AND step_id=$stepId AND status IN ('pending', 'running')
|
|
1016
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
1017
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
1018
|
+
))`).run({
|
|
1019
|
+
$workflowRunId: workflowRunId,
|
|
1020
|
+
$stepId: stepId,
|
|
1021
|
+
$finished: now,
|
|
1022
|
+
$error: reason,
|
|
1023
|
+
$updated: now,
|
|
1024
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
1025
|
+
$now: now
|
|
1026
|
+
});
|
|
1027
|
+
if (res.changes === 1)
|
|
1028
|
+
this.appendWorkflowEvent(workflowRunId, "step_skipped", stepId, { reason });
|
|
1029
|
+
this.db.exec("COMMIT");
|
|
1030
|
+
} catch (error) {
|
|
1031
|
+
try {
|
|
1032
|
+
this.db.exec("ROLLBACK");
|
|
1033
|
+
} catch {}
|
|
1034
|
+
throw error;
|
|
1035
|
+
}
|
|
948
1036
|
const run = this.getWorkflowStepRun(workflowRunId, stepId);
|
|
949
1037
|
if (!run)
|
|
950
1038
|
throw new Error(`workflow step run not found after skip: ${workflowRunId}/${stepId}`);
|
|
951
1039
|
return run;
|
|
952
1040
|
}
|
|
953
|
-
finalizeWorkflowRun(workflowRunId, status, patch = {}) {
|
|
1041
|
+
finalizeWorkflowRun(workflowRunId, status, patch = {}, opts = {}) {
|
|
954
1042
|
const finishedAt = patch.finishedAt ?? nowIso();
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
1043
|
+
let changed = false;
|
|
1044
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
1045
|
+
try {
|
|
1046
|
+
const res = this.db.query(`UPDATE workflow_runs SET status=$status, finished_at=$finished, duration_ms=$durationMs, error=$error, updated_at=$updated
|
|
1047
|
+
WHERE id=$id AND status NOT IN ('succeeded', 'failed', 'timed_out', 'cancelled')
|
|
1048
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
1049
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
1050
|
+
))`).run({
|
|
1051
|
+
$id: workflowRunId,
|
|
1052
|
+
$status: status,
|
|
1053
|
+
$finished: finishedAt,
|
|
1054
|
+
$durationMs: patch.durationMs ?? null,
|
|
1055
|
+
$error: patch.error ?? null,
|
|
1056
|
+
$updated: finishedAt,
|
|
1057
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
1058
|
+
$now: (opts.now ?? new Date(finishedAt)).toISOString()
|
|
1059
|
+
});
|
|
1060
|
+
changed = res.changes === 1;
|
|
1061
|
+
if (changed)
|
|
1062
|
+
this.appendWorkflowEvent(workflowRunId, status, undefined, { error: patch.error });
|
|
1063
|
+
this.db.exec("COMMIT");
|
|
1064
|
+
} catch (error) {
|
|
1065
|
+
try {
|
|
1066
|
+
this.db.exec("ROLLBACK");
|
|
1067
|
+
} catch {}
|
|
1068
|
+
throw error;
|
|
1069
|
+
}
|
|
964
1070
|
const run = this.getWorkflowRun(workflowRunId);
|
|
965
1071
|
if (!run)
|
|
966
1072
|
throw new Error(`workflow run not found after finalize: ${workflowRunId}`);
|
|
967
|
-
if (res.changes === 1)
|
|
968
|
-
this.appendWorkflowEvent(workflowRunId, status, undefined, { error: patch.error });
|
|
969
1073
|
return run;
|
|
970
1074
|
}
|
|
971
1075
|
cancelWorkflowRun(workflowRunId, reason = "cancelled by user") {
|
|
@@ -1019,10 +1123,24 @@ class Store {
|
|
|
1019
1123
|
const row = this.db.query("SELECT COUNT(*) AS count FROM loop_runs WHERE loop_id = ? AND status = 'running'").get(loopId);
|
|
1020
1124
|
return (row?.count ?? 0) > 0;
|
|
1021
1125
|
}
|
|
1022
|
-
markRunPid(id, pid, claimedBy) {
|
|
1023
|
-
const now =
|
|
1126
|
+
markRunPid(id, pid, claimedBy, opts = {}) {
|
|
1127
|
+
const now = (opts.now ?? new Date).toISOString();
|
|
1024
1128
|
const res = claimedBy ? this.db.query(`UPDATE loop_runs SET pid=$pid, updated_at=$updated
|
|
1025
|
-
WHERE id=$id AND status='running' AND claimed_by=$claimedBy
|
|
1129
|
+
WHERE id=$id AND status='running' AND claimed_by=$claimedBy
|
|
1130
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
1131
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
1132
|
+
))`).run({
|
|
1133
|
+
$id: id,
|
|
1134
|
+
$pid: pid,
|
|
1135
|
+
$updated: now,
|
|
1136
|
+
$claimedBy: claimedBy,
|
|
1137
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
1138
|
+
$now: now
|
|
1139
|
+
}) : this.db.query(`UPDATE loop_runs SET pid=$pid, updated_at=$updated
|
|
1140
|
+
WHERE id=$id AND status='running'
|
|
1141
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
1142
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
1143
|
+
))`).run({ $id: id, $pid: pid, $updated: now, $daemonLeaseId: opts.daemonLeaseId ?? null, $now: now });
|
|
1026
1144
|
if (res.changes !== 1)
|
|
1027
1145
|
return;
|
|
1028
1146
|
return this.getRun(id);
|
|
@@ -1037,7 +1155,7 @@ class Store {
|
|
|
1037
1155
|
AND wsr.pid IS NOT NULL`).all(loopRunId);
|
|
1038
1156
|
return liveWorkflowSteps.some((step) => isProcessAlive(step.pid));
|
|
1039
1157
|
}
|
|
1040
|
-
createSkippedRun(loop, scheduledFor, reason) {
|
|
1158
|
+
createSkippedRun(loop, scheduledFor, reason, opts = {}) {
|
|
1041
1159
|
const now = nowIso();
|
|
1042
1160
|
const run = {
|
|
1043
1161
|
id: genId(),
|
|
@@ -1051,21 +1169,31 @@ class Store {
|
|
|
1051
1169
|
createdAt: now,
|
|
1052
1170
|
updatedAt: now
|
|
1053
1171
|
};
|
|
1054
|
-
this.db.
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1172
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
1173
|
+
try {
|
|
1174
|
+
this.assertDaemonLeaseFence(opts, now);
|
|
1175
|
+
this.db.query(`INSERT OR IGNORE INTO loop_runs (id, loop_id, loop_name, scheduled_for, attempt, status, started_at, finished_at,
|
|
1176
|
+
claimed_by, lease_expires_at, pid, exit_code, duration_ms, stdout, stderr, error, created_at, updated_at)
|
|
1177
|
+
VALUES ($id, $loopId, $loopName, $scheduledFor, $attempt, $status, NULL, $finished, NULL, NULL, NULL, NULL, NULL,
|
|
1178
|
+
NULL, NULL, $error, $created, $updated)`).run({
|
|
1179
|
+
$id: run.id,
|
|
1180
|
+
$loopId: run.loopId,
|
|
1181
|
+
$loopName: run.loopName,
|
|
1182
|
+
$scheduledFor: run.scheduledFor,
|
|
1183
|
+
$attempt: run.attempt,
|
|
1184
|
+
$status: run.status,
|
|
1185
|
+
$finished: run.finishedAt ?? null,
|
|
1186
|
+
$error: run.error ?? null,
|
|
1187
|
+
$created: run.createdAt,
|
|
1188
|
+
$updated: run.updatedAt
|
|
1189
|
+
});
|
|
1190
|
+
this.db.exec("COMMIT");
|
|
1191
|
+
} catch (error) {
|
|
1192
|
+
try {
|
|
1193
|
+
this.db.exec("ROLLBACK");
|
|
1194
|
+
} catch {}
|
|
1195
|
+
throw error;
|
|
1196
|
+
}
|
|
1069
1197
|
return this.getRunBySlot(loop.id, scheduledFor) ?? run;
|
|
1070
1198
|
}
|
|
1071
1199
|
getRun(id) {
|
|
@@ -1076,11 +1204,20 @@ class Store {
|
|
|
1076
1204
|
const row = this.db.query("SELECT * FROM loop_runs WHERE loop_id = ? AND scheduled_for = ?").get(loopId, scheduledFor);
|
|
1077
1205
|
return row ? rowToRun(row) : undefined;
|
|
1078
1206
|
}
|
|
1079
|
-
|
|
1207
|
+
nextRetryableRun(loopId, maxAttempts, afterScheduledFor) {
|
|
1208
|
+
const row = afterScheduledFor ? this.db.query(`SELECT * FROM loop_runs
|
|
1209
|
+
WHERE loop_id = ? AND scheduled_for > ? AND status IN ('failed', 'timed_out', 'abandoned') AND attempt < ?
|
|
1210
|
+
ORDER BY scheduled_for ASC LIMIT 1`).get(loopId, afterScheduledFor, maxAttempts) : this.db.query(`SELECT * FROM loop_runs
|
|
1211
|
+
WHERE loop_id = ? AND status IN ('failed', 'timed_out', 'abandoned') AND attempt < ?
|
|
1212
|
+
ORDER BY scheduled_for ASC LIMIT 1`).get(loopId, maxAttempts);
|
|
1213
|
+
return row ? rowToRun(row) : undefined;
|
|
1214
|
+
}
|
|
1215
|
+
claimRun(loop, scheduledFor, runnerId, now = new Date, opts = {}) {
|
|
1080
1216
|
const startedAt = now.toISOString();
|
|
1081
1217
|
const leaseExpiresAt = new Date(now.getTime() + loop.leaseMs).toISOString();
|
|
1082
1218
|
this.db.exec("BEGIN IMMEDIATE");
|
|
1083
1219
|
try {
|
|
1220
|
+
this.assertDaemonLeaseFence(opts, startedAt);
|
|
1084
1221
|
const existing = this.getRunBySlot(loop.id, scheduledFor);
|
|
1085
1222
|
if (existing) {
|
|
1086
1223
|
if (existing.status === "running") {
|
|
@@ -1175,11 +1312,15 @@ class Store {
|
|
|
1175
1312
|
$error: patch.error ?? null,
|
|
1176
1313
|
$updated: finishedAt,
|
|
1177
1314
|
$claimedBy: opts.claimedBy ?? null,
|
|
1178
|
-
$now: (opts.now ?? new Date).toISOString()
|
|
1315
|
+
$now: (opts.now ?? new Date).toISOString(),
|
|
1316
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null
|
|
1179
1317
|
};
|
|
1180
1318
|
const res = opts.claimedBy ? this.db.query(`UPDATE loop_runs SET status=$status, finished_at=$finished, lease_expires_at=NULL, pid=$pid, exit_code=$exitCode,
|
|
1181
1319
|
duration_ms=$durationMs, stdout=$stdout, stderr=$stderr, error=$error, updated_at=$updated
|
|
1182
|
-
WHERE id=$id AND status='running' AND claimed_by=$claimedBy AND lease_expires_at > $now
|
|
1320
|
+
WHERE id=$id AND status='running' AND claimed_by=$claimedBy AND lease_expires_at > $now
|
|
1321
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
1322
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
1323
|
+
))`).run(params) : this.db.query(`UPDATE loop_runs SET status=$status, finished_at=$finished, lease_expires_at=NULL, pid=$pid, exit_code=$exitCode,
|
|
1183
1324
|
duration_ms=$durationMs, stdout=$stdout, stderr=$stderr, error=$error, updated_at=$updated WHERE id=$id`).run(params);
|
|
1184
1325
|
const run = this.getRun(id);
|
|
1185
1326
|
if (!run)
|
|
@@ -1188,10 +1329,20 @@ class Store {
|
|
|
1188
1329
|
return run;
|
|
1189
1330
|
return run;
|
|
1190
1331
|
}
|
|
1191
|
-
heartbeatRunLease(id, claimedBy, leaseMs, now = new Date) {
|
|
1332
|
+
heartbeatRunLease(id, claimedBy, leaseMs, now = new Date, opts = {}) {
|
|
1192
1333
|
const expiresAt = new Date(now.getTime() + leaseMs).toISOString();
|
|
1193
1334
|
const res = this.db.query(`UPDATE loop_runs SET lease_expires_at=$expires, updated_at=$updated
|
|
1194
|
-
WHERE id=$id AND status='running' AND claimed_by=$claimedBy
|
|
1335
|
+
WHERE id=$id AND status='running' AND claimed_by=$claimedBy AND lease_expires_at > $now
|
|
1336
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
1337
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
1338
|
+
))`).run({
|
|
1339
|
+
$id: id,
|
|
1340
|
+
$claimedBy: claimedBy,
|
|
1341
|
+
$expires: expiresAt,
|
|
1342
|
+
$updated: now.toISOString(),
|
|
1343
|
+
$now: now.toISOString(),
|
|
1344
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null
|
|
1345
|
+
});
|
|
1195
1346
|
if (res.changes !== 1)
|
|
1196
1347
|
return;
|
|
1197
1348
|
return this.getRun(id);
|
|
@@ -1210,7 +1361,7 @@ class Store {
|
|
|
1210
1361
|
}
|
|
1211
1362
|
return rows.map(rowToRun);
|
|
1212
1363
|
}
|
|
1213
|
-
recoverExpiredRunLeases(now = new Date) {
|
|
1364
|
+
recoverExpiredRunLeases(now = new Date, opts = {}) {
|
|
1214
1365
|
const rows = this.db.query("SELECT * FROM loop_runs WHERE status = 'running' AND lease_expires_at <= ?").all(now.toISOString());
|
|
1215
1366
|
const recovered = [];
|
|
1216
1367
|
for (const row of rows) {
|
|
@@ -1219,20 +1370,63 @@ class Store {
|
|
|
1219
1370
|
if (this.hasLiveWorkflowStepProcesses(row.id))
|
|
1220
1371
|
continue;
|
|
1221
1372
|
const finished = now.toISOString();
|
|
1222
|
-
this.db.
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1373
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
1374
|
+
try {
|
|
1375
|
+
const res = this.db.query(`UPDATE loop_runs SET status='abandoned', finished_at=$finished, lease_expires_at=NULL,
|
|
1376
|
+
error='run lease expired before completion', updated_at=$updated
|
|
1377
|
+
WHERE id=$id AND status='running' AND lease_expires_at <= $now
|
|
1378
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
1379
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
1380
|
+
))`).run({
|
|
1381
|
+
$id: row.id,
|
|
1382
|
+
$finished: finished,
|
|
1383
|
+
$updated: finished,
|
|
1384
|
+
$now: finished,
|
|
1385
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null
|
|
1235
1386
|
});
|
|
1387
|
+
if (res.changes !== 1) {
|
|
1388
|
+
this.db.exec("COMMIT");
|
|
1389
|
+
continue;
|
|
1390
|
+
}
|
|
1391
|
+
const workflowRows = this.db.query("SELECT * FROM workflow_runs WHERE loop_run_id = ? AND status NOT IN ('succeeded', 'failed', 'timed_out', 'cancelled')").all(row.id);
|
|
1392
|
+
for (const workflowRow of workflowRows) {
|
|
1393
|
+
const workflowRes = this.db.query(`UPDATE workflow_runs
|
|
1394
|
+
SET status='failed', finished_at=$finished, error='parent loop run lease expired before completion', updated_at=$updated
|
|
1395
|
+
WHERE id=$id AND status NOT IN ('succeeded', 'failed', 'timed_out', 'cancelled')
|
|
1396
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
1397
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
1398
|
+
))`).run({
|
|
1399
|
+
$id: workflowRow.id,
|
|
1400
|
+
$finished: finished,
|
|
1401
|
+
$updated: finished,
|
|
1402
|
+
$now: finished,
|
|
1403
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null
|
|
1404
|
+
});
|
|
1405
|
+
if (workflowRes.changes !== 1)
|
|
1406
|
+
continue;
|
|
1407
|
+
this.db.query(`UPDATE workflow_step_runs
|
|
1408
|
+
SET status='skipped', finished_at=$finished, pid=NULL, error='parent loop run lease expired before completion', updated_at=$updated
|
|
1409
|
+
WHERE workflow_run_id=$workflowRunId AND status IN ('pending', 'running')
|
|
1410
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
1411
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
1412
|
+
))`).run({
|
|
1413
|
+
$workflowRunId: workflowRow.id,
|
|
1414
|
+
$finished: finished,
|
|
1415
|
+
$updated: finished,
|
|
1416
|
+
$now: finished,
|
|
1417
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null
|
|
1418
|
+
});
|
|
1419
|
+
this.appendWorkflowEvent(workflowRow.id, "failed", undefined, {
|
|
1420
|
+
error: "parent loop run lease expired before completion",
|
|
1421
|
+
loopRunId: row.id
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
this.db.exec("COMMIT");
|
|
1425
|
+
} catch (error) {
|
|
1426
|
+
try {
|
|
1427
|
+
this.db.exec("ROLLBACK");
|
|
1428
|
+
} catch {}
|
|
1429
|
+
throw error;
|
|
1236
1430
|
}
|
|
1237
1431
|
const run = this.getRun(row.id);
|
|
1238
1432
|
if (run)
|
|
@@ -1240,11 +1434,14 @@ class Store {
|
|
|
1240
1434
|
}
|
|
1241
1435
|
return recovered;
|
|
1242
1436
|
}
|
|
1243
|
-
expireLoops(now = new Date) {
|
|
1437
|
+
expireLoops(now = new Date, opts = {}) {
|
|
1244
1438
|
const rows = this.db.query("SELECT * FROM loops WHERE status = 'active' AND expires_at IS NOT NULL AND expires_at <= ?").all(now.toISOString());
|
|
1245
1439
|
const expired = [];
|
|
1246
|
-
for (const row of rows)
|
|
1247
|
-
|
|
1440
|
+
for (const row of rows) {
|
|
1441
|
+
const updated = this.updateLoop(row.id, { status: "expired", nextRunAt: undefined }, opts);
|
|
1442
|
+
if (updated.status === "expired")
|
|
1443
|
+
expired.push(updated);
|
|
1444
|
+
}
|
|
1248
1445
|
return expired;
|
|
1249
1446
|
}
|
|
1250
1447
|
countLoops(status) {
|
|
@@ -1287,7 +1484,7 @@ class Store {
|
|
|
1287
1484
|
}
|
|
1288
1485
|
heartbeatDaemonLease(id, ttlMs, now = new Date) {
|
|
1289
1486
|
const expiresAt = new Date(now.getTime() + ttlMs).toISOString();
|
|
1290
|
-
const res = this.db.query(`UPDATE daemon_lease SET heartbeat_at=$heartbeat, expires_at=$expires, updated_at=$updated WHERE id=$id`).run({ $id: id, $heartbeat: now.toISOString(), $expires: expiresAt, $updated: now.toISOString() });
|
|
1487
|
+
const res = this.db.query(`UPDATE daemon_lease SET heartbeat_at=$heartbeat, expires_at=$expires, updated_at=$updated WHERE id=$id AND expires_at > $now`).run({ $id: id, $heartbeat: now.toISOString(), $expires: expiresAt, $updated: now.toISOString(), $now: now.toISOString() });
|
|
1291
1488
|
if (res.changes !== 1)
|
|
1292
1489
|
return;
|
|
1293
1490
|
return this.getDaemonLease();
|
|
@@ -1310,11 +1507,14 @@ import { Command } from "commander";
|
|
|
1310
1507
|
|
|
1311
1508
|
// src/lib/format.ts
|
|
1312
1509
|
var TEXT_OUTPUT_LIMIT = 32 * 1024;
|
|
1313
|
-
|
|
1510
|
+
var SENSITIVE_PAYLOAD_KEYS = new Set(["env", "error", "prompt", "reason", "stderr", "stdout"]);
|
|
1511
|
+
function redact(value, visible = 0) {
|
|
1314
1512
|
if (!value)
|
|
1315
1513
|
return value;
|
|
1316
1514
|
if (value.length <= visible)
|
|
1317
1515
|
return value;
|
|
1516
|
+
if (visible <= 0)
|
|
1517
|
+
return `[redacted ${value.length} chars]`;
|
|
1318
1518
|
return `${value.slice(0, visible)}... [redacted ${value.length - visible} chars]`;
|
|
1319
1519
|
}
|
|
1320
1520
|
function truncateTextOutput(value) {
|
|
@@ -1323,6 +1523,21 @@ function truncateTextOutput(value) {
|
|
|
1323
1523
|
return `${value.slice(0, TEXT_OUTPUT_LIMIT)}
|
|
1324
1524
|
[truncated ${value.length - TEXT_OUTPUT_LIMIT} chars]`;
|
|
1325
1525
|
}
|
|
1526
|
+
function redactSensitivePayload(value, key) {
|
|
1527
|
+
if (key && SENSITIVE_PAYLOAD_KEYS.has(key)) {
|
|
1528
|
+
if (typeof value === "string")
|
|
1529
|
+
return redact(value);
|
|
1530
|
+
if (value === undefined || value === null)
|
|
1531
|
+
return value;
|
|
1532
|
+
return "[redacted]";
|
|
1533
|
+
}
|
|
1534
|
+
if (Array.isArray(value))
|
|
1535
|
+
return value.map((item) => redactSensitivePayload(item));
|
|
1536
|
+
if (value && typeof value === "object") {
|
|
1537
|
+
return Object.fromEntries(Object.entries(value).map(([entryKey, entryValue]) => [entryKey, redactSensitivePayload(entryValue, entryKey)]));
|
|
1538
|
+
}
|
|
1539
|
+
return value;
|
|
1540
|
+
}
|
|
1326
1541
|
function textOutputBlocks(value, opts = {}) {
|
|
1327
1542
|
const indent = opts.indent ?? "";
|
|
1328
1543
|
const nested = `${indent} `;
|
|
@@ -1354,6 +1569,14 @@ function publicRun(run, showOutput = false) {
|
|
|
1354
1569
|
stderr: showOutput ? run.stderr : run.stderr ? `[redacted ${run.stderr.length} chars]` : undefined
|
|
1355
1570
|
};
|
|
1356
1571
|
}
|
|
1572
|
+
function publicExecutorResult(result, showOutput = false) {
|
|
1573
|
+
return {
|
|
1574
|
+
...result,
|
|
1575
|
+
stdout: showOutput ? result.stdout : result.stdout ? `[redacted ${result.stdout.length} chars]` : undefined,
|
|
1576
|
+
stderr: showOutput ? result.stderr : result.stderr ? `[redacted ${result.stderr.length} chars]` : undefined,
|
|
1577
|
+
error: redact(result.error)
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1357
1580
|
function publicWorkflow(workflow) {
|
|
1358
1581
|
return {
|
|
1359
1582
|
...workflow,
|
|
@@ -1364,17 +1587,18 @@ function publicWorkflow(workflow) {
|
|
|
1364
1587
|
};
|
|
1365
1588
|
}
|
|
1366
1589
|
function publicWorkflowRun(run) {
|
|
1367
|
-
return { ...run };
|
|
1590
|
+
return { ...run, error: redact(run.error) };
|
|
1368
1591
|
}
|
|
1369
1592
|
function publicWorkflowStepRun(run, showOutput = false) {
|
|
1370
1593
|
return {
|
|
1371
1594
|
...run,
|
|
1372
1595
|
stdout: showOutput ? run.stdout : run.stdout ? `[redacted ${run.stdout.length} chars]` : undefined,
|
|
1373
|
-
stderr: showOutput ? run.stderr : run.stderr ? `[redacted ${run.stderr.length} chars]` : undefined
|
|
1596
|
+
stderr: showOutput ? run.stderr : run.stderr ? `[redacted ${run.stderr.length} chars]` : undefined,
|
|
1597
|
+
error: redact(run.error)
|
|
1374
1598
|
};
|
|
1375
1599
|
}
|
|
1376
1600
|
function publicWorkflowEvent(event) {
|
|
1377
|
-
return { ...event };
|
|
1601
|
+
return { ...event, payload: redactSensitivePayload(event.payload) };
|
|
1378
1602
|
}
|
|
1379
1603
|
|
|
1380
1604
|
// src/lib/executor.ts
|
|
@@ -1610,7 +1834,7 @@ function agentArgs(target) {
|
|
|
1610
1834
|
args.push("--model", target.model);
|
|
1611
1835
|
if (target.agent)
|
|
1612
1836
|
args.push("--agent", target.agent);
|
|
1613
|
-
args.push(...target.extraArgs ?? []
|
|
1837
|
+
args.push(...target.extraArgs ?? []);
|
|
1614
1838
|
return args;
|
|
1615
1839
|
case "cursor":
|
|
1616
1840
|
args.push("-p");
|
|
@@ -1618,10 +1842,10 @@ function agentArgs(target) {
|
|
|
1618
1842
|
args.push("--model", target.model);
|
|
1619
1843
|
if (target.agent)
|
|
1620
1844
|
args.push("--agent", target.agent);
|
|
1621
|
-
args.push(...target.extraArgs ?? []
|
|
1845
|
+
args.push(...target.extraArgs ?? []);
|
|
1622
1846
|
return args;
|
|
1623
1847
|
case "codewith":
|
|
1624
|
-
args.push("--ask-for-approval", "never", "exec", "--json", "--ephemeral", "--sandbox", "workspace-write", "--skip-git-repo-check");
|
|
1848
|
+
args.push(...target.authProfile ? ["--auth-profile", target.authProfile] : [], "--ask-for-approval", "never", "exec", "--json", "--ephemeral", "--sandbox", "workspace-write", "--skip-git-repo-check");
|
|
1625
1849
|
if (isolation === "safe")
|
|
1626
1850
|
args.push("--ignore-rules");
|
|
1627
1851
|
if (target.cwd)
|
|
@@ -1630,7 +1854,7 @@ function agentArgs(target) {
|
|
|
1630
1854
|
args.push("--model", target.model);
|
|
1631
1855
|
if (target.agent)
|
|
1632
1856
|
args.push("--agent", target.agent);
|
|
1633
|
-
args.push(...target.extraArgs ?? []
|
|
1857
|
+
args.push(...target.extraArgs ?? []);
|
|
1634
1858
|
return args;
|
|
1635
1859
|
case "codex":
|
|
1636
1860
|
args.push("exec", "--json", "--ephemeral", "--ask-for-approval", "never", "--sandbox", "workspace-write");
|
|
@@ -1640,7 +1864,7 @@ function agentArgs(target) {
|
|
|
1640
1864
|
args.push("--cd", target.cwd);
|
|
1641
1865
|
if (target.model)
|
|
1642
1866
|
args.push("--model", target.model);
|
|
1643
|
-
args.push(...target.extraArgs ?? []
|
|
1867
|
+
args.push(...target.extraArgs ?? []);
|
|
1644
1868
|
return args;
|
|
1645
1869
|
case "aicopilot":
|
|
1646
1870
|
args.push("run", "--format", "json");
|
|
@@ -1652,7 +1876,7 @@ function agentArgs(target) {
|
|
|
1652
1876
|
args.push("--model", target.model);
|
|
1653
1877
|
if (target.agent)
|
|
1654
1878
|
args.push("--agent", target.agent);
|
|
1655
|
-
args.push(...target.extraArgs ?? []
|
|
1879
|
+
args.push(...target.extraArgs ?? []);
|
|
1656
1880
|
return args;
|
|
1657
1881
|
case "opencode":
|
|
1658
1882
|
args.push("run", "--format", "json");
|
|
@@ -1664,7 +1888,7 @@ function agentArgs(target) {
|
|
|
1664
1888
|
args.push("--model", target.model);
|
|
1665
1889
|
if (target.agent)
|
|
1666
1890
|
args.push("--agent", target.agent);
|
|
1667
|
-
args.push(...target.extraArgs ?? []
|
|
1891
|
+
args.push(...target.extraArgs ?? []);
|
|
1668
1892
|
return args;
|
|
1669
1893
|
}
|
|
1670
1894
|
}
|
|
@@ -1689,7 +1913,8 @@ function commandSpec(target) {
|
|
|
1689
1913
|
cwd: agentTarget.cwd,
|
|
1690
1914
|
timeoutMs: agentTarget.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
1691
1915
|
account: agentTarget.account,
|
|
1692
|
-
accountTool: agentTarget.account?.tool ?? accountToolForProvider(agentTarget.provider)
|
|
1916
|
+
accountTool: agentTarget.account?.tool ?? accountToolForProvider(agentTarget.provider),
|
|
1917
|
+
stdin: agentTarget.prompt
|
|
1693
1918
|
};
|
|
1694
1919
|
}
|
|
1695
1920
|
function executionEnv(spec, metadata, opts) {
|
|
@@ -1758,10 +1983,17 @@ async function executeTarget(target, metadata = {}, opts = {}) {
|
|
|
1758
1983
|
env,
|
|
1759
1984
|
shell: spec.shell ?? false,
|
|
1760
1985
|
detached: true,
|
|
1761
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
1986
|
+
stdio: spec.stdin === undefined ? ["ignore", "pipe", "pipe"] : ["pipe", "pipe", "pipe"]
|
|
1762
1987
|
});
|
|
1763
1988
|
if (child.pid)
|
|
1764
1989
|
opts.onSpawn?.(child.pid);
|
|
1990
|
+
if (spec.stdin !== undefined && child.stdin) {
|
|
1991
|
+
child.stdin.on("error", (err) => {
|
|
1992
|
+
if (err.code !== "EPIPE")
|
|
1993
|
+
error = err.message;
|
|
1994
|
+
});
|
|
1995
|
+
child.stdin.end(spec.stdin);
|
|
1996
|
+
}
|
|
1765
1997
|
const abortHandler = () => {
|
|
1766
1998
|
error = "cancelled";
|
|
1767
1999
|
if (child.pid)
|
|
@@ -1770,10 +2002,10 @@ async function executeTarget(target, metadata = {}, opts = {}) {
|
|
|
1770
2002
|
if (opts.signal?.aborted)
|
|
1771
2003
|
abortHandler();
|
|
1772
2004
|
opts.signal?.addEventListener("abort", abortHandler, { once: true });
|
|
1773
|
-
child.stdout
|
|
2005
|
+
child.stdout?.on("data", (chunk) => {
|
|
1774
2006
|
stdout = appendBounded(stdout, chunk, maxOutputBytes);
|
|
1775
2007
|
});
|
|
1776
|
-
child.stderr
|
|
2008
|
+
child.stderr?.on("data", (chunk) => {
|
|
1777
2009
|
stderr = appendBounded(stderr, chunk, maxOutputBytes);
|
|
1778
2010
|
});
|
|
1779
2011
|
const timer = setTimeout(() => {
|
|
@@ -1872,7 +2104,8 @@ async function executeWorkflow(store, workflow, opts = {}) {
|
|
|
1872
2104
|
loop: opts.loop,
|
|
1873
2105
|
loopRun: opts.loopRun,
|
|
1874
2106
|
scheduledFor: opts.scheduledFor,
|
|
1875
|
-
idempotencyKey: opts.idempotencyKey
|
|
2107
|
+
idempotencyKey: opts.idempotencyKey,
|
|
2108
|
+
daemonLeaseId: opts.daemonLeaseId
|
|
1876
2109
|
});
|
|
1877
2110
|
const startedAt = run.startedAt ?? nowIso();
|
|
1878
2111
|
if (run.status === "succeeded" || run.status === "failed" || run.status === "timed_out" || run.status === "cancelled") {
|
|
@@ -1906,12 +2139,14 @@ async function executeWorkflow(store, workflow, opts = {}) {
|
|
|
1906
2139
|
return !dependencyStep?.continueOnFailure;
|
|
1907
2140
|
});
|
|
1908
2141
|
if (blockedBy) {
|
|
1909
|
-
|
|
2142
|
+
opts.beforePersist?.();
|
|
2143
|
+
store.skipWorkflowStepRun(run.id, step.id, `dependency did not succeed: ${blockedBy}`, { daemonLeaseId: opts.daemonLeaseId });
|
|
1910
2144
|
blockingError ??= `step ${step.id} blocked by dependency ${blockedBy}`;
|
|
1911
2145
|
terminalStatus = "failed";
|
|
1912
2146
|
continue;
|
|
1913
2147
|
}
|
|
1914
|
-
|
|
2148
|
+
opts.beforePersist?.();
|
|
2149
|
+
const startedStep = store.startWorkflowStepRun(run.id, step.id, { daemonLeaseId: opts.daemonLeaseId });
|
|
1915
2150
|
if (startedStep.status !== "running") {
|
|
1916
2151
|
terminalStatus = "failed";
|
|
1917
2152
|
blockingError = `step ${step.id} could not start because workflow is no longer running`;
|
|
@@ -1943,7 +2178,8 @@ async function executeWorkflow(store, workflow, opts = {}) {
|
|
|
1943
2178
|
...opts,
|
|
1944
2179
|
signal: controller.signal,
|
|
1945
2180
|
onSpawn: (pid) => {
|
|
1946
|
-
|
|
2181
|
+
opts.beforePersist?.();
|
|
2182
|
+
store.markWorkflowStepPid(run.id, step.id, pid, { daemonLeaseId: opts.daemonLeaseId });
|
|
1947
2183
|
opts.onSpawn?.(pid);
|
|
1948
2184
|
}
|
|
1949
2185
|
});
|
|
@@ -1971,6 +2207,7 @@ async function executeWorkflow(store, workflow, opts = {}) {
|
|
|
1971
2207
|
blockingError = "workflow run was cancelled";
|
|
1972
2208
|
break;
|
|
1973
2209
|
}
|
|
2210
|
+
opts.beforePersist?.();
|
|
1974
2211
|
store.finalizeWorkflowStepRun(run.id, step.id, {
|
|
1975
2212
|
status: result.status,
|
|
1976
2213
|
finishedAt: result.finishedAt,
|
|
@@ -1979,6 +2216,8 @@ async function executeWorkflow(store, workflow, opts = {}) {
|
|
|
1979
2216
|
stderr: result.stderr,
|
|
1980
2217
|
exitCode: result.exitCode,
|
|
1981
2218
|
error: result.error
|
|
2219
|
+
}, {
|
|
2220
|
+
daemonLeaseId: opts.daemonLeaseId
|
|
1982
2221
|
});
|
|
1983
2222
|
if (result.status !== "succeeded" && !step.continueOnFailure) {
|
|
1984
2223
|
terminalStatus = result.status;
|
|
@@ -1990,7 +2229,9 @@ async function executeWorkflow(store, workflow, opts = {}) {
|
|
|
1990
2229
|
for (const step of ordered) {
|
|
1991
2230
|
const existing = store.getWorkflowStepRun(run.id, step.id);
|
|
1992
2231
|
if (existing?.status === "pending" || existing?.status === "running") {
|
|
1993
|
-
store.skipWorkflowStepRun(run.id, step.id, blockingError ?? "workflow stopped before step could run"
|
|
2232
|
+
store.skipWorkflowStepRun(run.id, step.id, blockingError ?? "workflow stopped before step could run", {
|
|
2233
|
+
daemonLeaseId: opts.daemonLeaseId
|
|
2234
|
+
});
|
|
1994
2235
|
}
|
|
1995
2236
|
}
|
|
1996
2237
|
}
|
|
@@ -2000,10 +2241,13 @@ async function executeWorkflow(store, workflow, opts = {}) {
|
|
|
2000
2241
|
const steps2 = store.listWorkflowStepRuns(run.id);
|
|
2001
2242
|
return workflowResult(terminalRun, terminalRun.status, startedAt, terminalRun.finishedAt ?? finishedAt, JSON.stringify({ workflowRun: terminalRun, steps: steps2 }, null, 2), terminalRun.error ?? blockingError);
|
|
2002
2243
|
}
|
|
2244
|
+
opts.beforePersist?.();
|
|
2003
2245
|
const finalRun = store.finalizeWorkflowRun(run.id, terminalStatus, {
|
|
2004
2246
|
finishedAt,
|
|
2005
2247
|
durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime(),
|
|
2006
2248
|
error: blockingError
|
|
2249
|
+
}, {
|
|
2250
|
+
daemonLeaseId: opts.daemonLeaseId
|
|
2007
2251
|
});
|
|
2008
2252
|
const steps = store.listWorkflowStepRuns(run.id);
|
|
2009
2253
|
return workflowResult(finalRun, terminalStatus, startedAt, finishedAt, JSON.stringify({ workflowRun: finalRun, steps }, null, 2), blockingError);
|
|
@@ -2051,52 +2295,81 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
|
2051
2295
|
|
|
2052
2296
|
// src/lib/scheduler.ts
|
|
2053
2297
|
function manualRunScheduledFor(loop, now = new Date) {
|
|
2054
|
-
if (loop.nextRunAt && new Date(loop.nextRunAt).getTime() <= now.getTime()) {
|
|
2298
|
+
if (loop.status === "active" && loop.nextRunAt && new Date(loop.nextRunAt).getTime() <= now.getTime()) {
|
|
2055
2299
|
return loop.retryScheduledFor ?? loop.nextRunAt;
|
|
2056
2300
|
}
|
|
2057
2301
|
return now.toISOString();
|
|
2058
2302
|
}
|
|
2059
2303
|
function shouldAdvanceManualRun(loop, scheduledFor, now = new Date) {
|
|
2304
|
+
if (loop.status !== "active")
|
|
2305
|
+
return false;
|
|
2060
2306
|
if (!loop.nextRunAt || new Date(loop.nextRunAt).getTime() > now.getTime())
|
|
2061
2307
|
return false;
|
|
2062
2308
|
return scheduledFor === (loop.retryScheduledFor ?? loop.nextRunAt);
|
|
2063
2309
|
}
|
|
2310
|
+
function manualRunSource(loop, scheduledFor, now = new Date) {
|
|
2311
|
+
if (loop.status !== "active")
|
|
2312
|
+
return "ad_hoc";
|
|
2313
|
+
if (!loop.nextRunAt || new Date(loop.nextRunAt).getTime() > now.getTime())
|
|
2314
|
+
return "ad_hoc";
|
|
2315
|
+
if (loop.retryScheduledFor && scheduledFor === loop.retryScheduledFor)
|
|
2316
|
+
return "retry_slot";
|
|
2317
|
+
return "due_slot";
|
|
2318
|
+
}
|
|
2064
2319
|
function nextAfterRetry(loop, now) {
|
|
2065
2320
|
return new Date(now.getTime() + loop.retryDelayMs).toISOString();
|
|
2066
2321
|
}
|
|
2067
|
-
function
|
|
2322
|
+
function isDaemonLeaseLost(error) {
|
|
2323
|
+
return error instanceof Error && error.message === "daemon lease lost";
|
|
2324
|
+
}
|
|
2325
|
+
function advanceLoop(store, loop, run, finishedAt, succeeded, opts = {}) {
|
|
2068
2326
|
if (run.status === "running")
|
|
2069
2327
|
return;
|
|
2070
2328
|
const current = store.getLoop(loop.id);
|
|
2071
2329
|
if (!current || current.status !== "active")
|
|
2072
2330
|
return;
|
|
2073
|
-
|
|
2331
|
+
if (current.retryScheduledFor && current.retryScheduledFor !== run.scheduledFor)
|
|
2332
|
+
return;
|
|
2333
|
+
const shouldRetry = !succeeded && run.attempt < current.maxAttempts;
|
|
2074
2334
|
if (shouldRetry) {
|
|
2075
|
-
store.updateLoop(
|
|
2335
|
+
store.updateLoop(current.id, {
|
|
2076
2336
|
status: "active",
|
|
2077
|
-
nextRunAt: nextAfterRetry(
|
|
2337
|
+
nextRunAt: nextAfterRetry(current, finishedAt),
|
|
2078
2338
|
retryScheduledFor: run.scheduledFor
|
|
2079
|
-
});
|
|
2339
|
+
}, { daemonLeaseId: opts.daemonLeaseId });
|
|
2340
|
+
return;
|
|
2341
|
+
}
|
|
2342
|
+
const deferredRetry = store.nextRetryableRun(current.id, current.maxAttempts, run.scheduledFor);
|
|
2343
|
+
if (deferredRetry) {
|
|
2344
|
+
store.updateLoop(current.id, {
|
|
2345
|
+
status: "active",
|
|
2346
|
+
nextRunAt: nextAfterRetry(current, finishedAt),
|
|
2347
|
+
retryScheduledFor: deferredRetry.scheduledFor
|
|
2348
|
+
}, { daemonLeaseId: opts.daemonLeaseId });
|
|
2080
2349
|
return;
|
|
2081
2350
|
}
|
|
2082
|
-
const nextRunAt = computeNextAfter(
|
|
2083
|
-
store.updateLoop(
|
|
2351
|
+
const nextRunAt = computeNextAfter(current.schedule, new Date(run.scheduledFor), finishedAt);
|
|
2352
|
+
store.updateLoop(current.id, {
|
|
2084
2353
|
status: nextRunAt ? "active" : "stopped",
|
|
2085
2354
|
nextRunAt,
|
|
2086
2355
|
retryScheduledFor: undefined
|
|
2087
|
-
});
|
|
2356
|
+
}, { daemonLeaseId: opts.daemonLeaseId });
|
|
2088
2357
|
}
|
|
2089
2358
|
async function executeClaimedRun(deps) {
|
|
2090
2359
|
let heartbeat;
|
|
2091
2360
|
const heartbeatEveryMs = Math.max(10, Math.min(60000, Math.floor(deps.loop.leaseMs / 3)));
|
|
2092
2361
|
heartbeat = setInterval(() => {
|
|
2093
|
-
deps.store.heartbeatRunLease(deps.run.id, deps.runnerId, deps.loop.leaseMs
|
|
2362
|
+
deps.store.heartbeatRunLease(deps.run.id, deps.runnerId, deps.loop.leaseMs, new Date, {
|
|
2363
|
+
daemonLeaseId: deps.daemonLeaseId
|
|
2364
|
+
});
|
|
2094
2365
|
}, heartbeatEveryMs);
|
|
2095
2366
|
heartbeat.unref();
|
|
2096
2367
|
try {
|
|
2097
2368
|
const result = await (deps.execute ?? ((loop, run) => executeLoopTarget(deps.store, loop, run, {
|
|
2098
|
-
|
|
2369
|
+
daemonLeaseId: deps.daemonLeaseId,
|
|
2370
|
+
onSpawn: (pid) => deps.store.markRunPid(run.id, pid, deps.runnerId, { daemonLeaseId: deps.daemonLeaseId })
|
|
2099
2371
|
})))(deps.loop, deps.run);
|
|
2372
|
+
deps.beforeFinalize?.(deps.loop, deps.run);
|
|
2100
2373
|
return deps.store.finalizeRun(deps.run.id, {
|
|
2101
2374
|
status: result.status,
|
|
2102
2375
|
finishedAt: result.finishedAt,
|
|
@@ -2108,10 +2381,16 @@ async function executeClaimedRun(deps) {
|
|
|
2108
2381
|
pid: result.pid
|
|
2109
2382
|
}, {
|
|
2110
2383
|
claimedBy: deps.runnerId,
|
|
2384
|
+
daemonLeaseId: deps.daemonLeaseId,
|
|
2111
2385
|
now: deps.now?.() ?? new Date(result.finishedAt)
|
|
2112
2386
|
});
|
|
2113
2387
|
} catch (err) {
|
|
2114
2388
|
deps.onError?.(deps.loop, err);
|
|
2389
|
+
try {
|
|
2390
|
+
deps.beforeFinalize?.(deps.loop, deps.run);
|
|
2391
|
+
} catch {
|
|
2392
|
+
return deps.store.getRun(deps.run.id) ?? deps.run;
|
|
2393
|
+
}
|
|
2115
2394
|
const finishedAt = new Date;
|
|
2116
2395
|
return deps.store.finalizeRun(deps.run.id, {
|
|
2117
2396
|
status: "failed",
|
|
@@ -2122,6 +2401,7 @@ async function executeClaimedRun(deps) {
|
|
|
2122
2401
|
error: err instanceof Error ? err.message : String(err)
|
|
2123
2402
|
}, {
|
|
2124
2403
|
claimedBy: deps.runnerId,
|
|
2404
|
+
daemonLeaseId: deps.daemonLeaseId,
|
|
2125
2405
|
now: deps.now?.() ?? finishedAt
|
|
2126
2406
|
});
|
|
2127
2407
|
} finally {
|
|
@@ -2131,15 +2411,33 @@ async function executeClaimedRun(deps) {
|
|
|
2131
2411
|
}
|
|
2132
2412
|
async function runSlot(deps, loop, scheduledFor) {
|
|
2133
2413
|
const now = deps.now?.() ?? new Date;
|
|
2414
|
+
deps.beforeRun?.(loop, scheduledFor);
|
|
2134
2415
|
if (loop.overlap === "skip" && deps.store.hasRunningRun(loop.id)) {
|
|
2135
|
-
|
|
2136
|
-
|
|
2416
|
+
let skipped;
|
|
2417
|
+
try {
|
|
2418
|
+
skipped = deps.store.createSkippedRun(loop, scheduledFor, "previous run still active", {
|
|
2419
|
+
daemonLeaseId: deps.daemonLeaseId
|
|
2420
|
+
});
|
|
2421
|
+
} catch (error) {
|
|
2422
|
+
if (deps.daemonLeaseId && isDaemonLeaseLost(error))
|
|
2423
|
+
return;
|
|
2424
|
+
throw error;
|
|
2425
|
+
}
|
|
2426
|
+
advanceLoop(deps.store, loop, skipped, now, true, { daemonLeaseId: deps.daemonLeaseId });
|
|
2137
2427
|
deps.onRun?.(skipped);
|
|
2138
2428
|
return skipped;
|
|
2139
2429
|
}
|
|
2140
|
-
|
|
2430
|
+
let claim;
|
|
2431
|
+
try {
|
|
2432
|
+
claim = deps.store.claimRun(loop, scheduledFor, deps.runnerId, now, { daemonLeaseId: deps.daemonLeaseId });
|
|
2433
|
+
} catch (error) {
|
|
2434
|
+
if (deps.daemonLeaseId && isDaemonLeaseLost(error))
|
|
2435
|
+
return;
|
|
2436
|
+
throw error;
|
|
2437
|
+
}
|
|
2141
2438
|
if (!claim)
|
|
2142
2439
|
return;
|
|
2440
|
+
deps.beforeRun?.(claim.loop, claim.run.scheduledFor);
|
|
2143
2441
|
deps.onRun?.(claim.run);
|
|
2144
2442
|
const finalRun = await executeClaimedRun({
|
|
2145
2443
|
store: deps.store,
|
|
@@ -2148,21 +2446,42 @@ async function runSlot(deps, loop, scheduledFor) {
|
|
|
2148
2446
|
run: claim.run,
|
|
2149
2447
|
now: deps.now,
|
|
2150
2448
|
execute: deps.execute,
|
|
2449
|
+
beforeFinalize: deps.beforeFinalize,
|
|
2450
|
+
daemonLeaseId: deps.daemonLeaseId,
|
|
2151
2451
|
onError: deps.onError
|
|
2152
2452
|
});
|
|
2153
|
-
advanceLoop(deps.store, claim.loop, finalRun, new Date(finalRun.finishedAt ?? new Date), finalRun.status === "succeeded");
|
|
2453
|
+
advanceLoop(deps.store, claim.loop, finalRun, new Date(finalRun.finishedAt ?? new Date), finalRun.status === "succeeded", { daemonLeaseId: deps.daemonLeaseId });
|
|
2154
2454
|
deps.onRun?.(finalRun);
|
|
2155
2455
|
return finalRun;
|
|
2156
2456
|
}
|
|
2157
2457
|
async function tick(deps) {
|
|
2158
2458
|
const now = deps.now?.() ?? new Date;
|
|
2159
|
-
const recovered = deps.store.recoverExpiredRunLeases(now);
|
|
2459
|
+
const recovered = deps.store.recoverExpiredRunLeases(now, { daemonLeaseId: deps.daemonLeaseId });
|
|
2460
|
+
const recoveredByLoop = new Map;
|
|
2160
2461
|
for (const run of recovered) {
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2462
|
+
recoveredByLoop.set(run.loopId, [...recoveredByLoop.get(run.loopId) ?? [], run]);
|
|
2463
|
+
}
|
|
2464
|
+
for (const runs of recoveredByLoop.values()) {
|
|
2465
|
+
const loop = deps.store.getLoop(runs[0].loopId);
|
|
2466
|
+
if (!loop)
|
|
2467
|
+
continue;
|
|
2468
|
+
const retryable = runs.filter((run) => run.attempt < loop.maxAttempts).sort((a, b) => new Date(a.scheduledFor).getTime() - new Date(b.scheduledFor).getTime())[0];
|
|
2469
|
+
if (retryable) {
|
|
2470
|
+
advanceLoop(deps.store, loop, retryable, new Date(retryable.finishedAt ?? now), false, {
|
|
2471
|
+
daemonLeaseId: deps.daemonLeaseId
|
|
2472
|
+
});
|
|
2473
|
+
continue;
|
|
2474
|
+
}
|
|
2475
|
+
for (const run of runs) {
|
|
2476
|
+
const current = deps.store.getLoop(run.loopId);
|
|
2477
|
+
if (current) {
|
|
2478
|
+
advanceLoop(deps.store, current, run, new Date(run.finishedAt ?? now), false, {
|
|
2479
|
+
daemonLeaseId: deps.daemonLeaseId
|
|
2480
|
+
});
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2164
2483
|
}
|
|
2165
|
-
const expired = deps.store.expireLoops(now);
|
|
2484
|
+
const expired = deps.store.expireLoops(now, { daemonLeaseId: deps.daemonLeaseId });
|
|
2166
2485
|
const claimed = [];
|
|
2167
2486
|
const completed = [];
|
|
2168
2487
|
const skipped = [];
|
|
@@ -2383,8 +2702,10 @@ async function runDaemon(opts = {}) {
|
|
|
2383
2702
|
const result = await tick({
|
|
2384
2703
|
store,
|
|
2385
2704
|
runnerId,
|
|
2705
|
+
daemonLeaseId: leaseId,
|
|
2706
|
+
beforeRun: () => ensureLease(),
|
|
2386
2707
|
execute: async (loop, run) => {
|
|
2387
|
-
const heartbeatMs = Math.max(1000, Math.floor(leaseTtlMs /
|
|
2708
|
+
const heartbeatMs = Math.max(25, Math.min(1000, intervalMs, Math.floor(leaseTtlMs / 10)));
|
|
2388
2709
|
const timer = setInterval(() => {
|
|
2389
2710
|
try {
|
|
2390
2711
|
ensureLease();
|
|
@@ -2396,8 +2717,14 @@ async function runDaemon(opts = {}) {
|
|
|
2396
2717
|
try {
|
|
2397
2718
|
const result2 = await executeLoopTarget(store, loop, run, {
|
|
2398
2719
|
signal: runAbort.signal,
|
|
2399
|
-
|
|
2720
|
+
beforePersist: () => ensureLease(),
|
|
2721
|
+
daemonLeaseId: leaseId,
|
|
2722
|
+
onSpawn: (pid) => {
|
|
2723
|
+
ensureLease();
|
|
2724
|
+
store.markRunPid(run.id, pid, runnerId, { daemonLeaseId: leaseId });
|
|
2725
|
+
}
|
|
2400
2726
|
});
|
|
2727
|
+
ensureLease();
|
|
2401
2728
|
if (leaseLost)
|
|
2402
2729
|
throw new Error("daemon lease lost during run");
|
|
2403
2730
|
return result2;
|
|
@@ -2405,6 +2732,7 @@ async function runDaemon(opts = {}) {
|
|
|
2405
2732
|
clearInterval(timer);
|
|
2406
2733
|
}
|
|
2407
2734
|
},
|
|
2735
|
+
beforeFinalize: () => ensureLease(),
|
|
2408
2736
|
onError: (loop, err) => log(`loop ${loop.id} failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
2409
2737
|
});
|
|
2410
2738
|
const changed = result.completed.length + result.skipped.length + result.recovered.length + result.expired.length;
|
|
@@ -2610,7 +2938,7 @@ function runDoctor(store) {
|
|
|
2610
2938
|
|
|
2611
2939
|
// src/cli/index.ts
|
|
2612
2940
|
var program = new Command;
|
|
2613
|
-
program.name("loops").description("Persistent local loops for commands and headless coding agents").version("0.3.
|
|
2941
|
+
program.name("loops").description("Persistent local loops for commands and headless coding agents").version("0.3.3");
|
|
2614
2942
|
program.option("-j, --json", "print JSON");
|
|
2615
2943
|
function isJson() {
|
|
2616
2944
|
return Boolean(program.opts().json);
|
|
@@ -2704,6 +3032,13 @@ function accountFromOpts(opts) {
|
|
|
2704
3032
|
throw new Error("--account-tool requires --account");
|
|
2705
3033
|
return opts.account ? { profile: opts.account, tool: opts.accountTool } : undefined;
|
|
2706
3034
|
}
|
|
3035
|
+
function providerAuthProfileFromOpts(opts, provider) {
|
|
3036
|
+
if (!opts.authProfile)
|
|
3037
|
+
return;
|
|
3038
|
+
if (provider !== "codewith")
|
|
3039
|
+
throw new Error("--auth-profile is currently supported only for --provider codewith");
|
|
3040
|
+
return opts.authProfile;
|
|
3041
|
+
}
|
|
2707
3042
|
var create = program.command("create").description("create loops");
|
|
2708
3043
|
addAccountOptions(addScheduleOptions(create.command("command <name>").description("create a deterministic shell command loop").requiredOption("--cmd <command>", "command string to execute").option("--cwd <dir>", "working directory").option("--timeout <duration>", "run timeout").option("--no-shell", "execute without a shell"))).action((name, opts) => {
|
|
2709
3044
|
const store = new Store;
|
|
@@ -2722,7 +3057,7 @@ addAccountOptions(addScheduleOptions(create.command("command <name>").descriptio
|
|
|
2722
3057
|
store.close();
|
|
2723
3058
|
}
|
|
2724
3059
|
});
|
|
2725
|
-
addAccountOptions(addScheduleOptions(create.command("agent <name>").description("create a headless coding-agent loop").requiredOption("--provider <provider>", "claude, cursor, codewith, aicopilot, opencode, or codex").requiredOption("--prompt <prompt>", "agent prompt").option("--cwd <dir>", "working directory").option("--model <model>", "model").option("--agent <agent>", "provider-specific agent").option("--timeout <duration>", "run timeout").option("--config-isolation <mode>", "safe or none", "safe"))).action((name, opts) => {
|
|
3060
|
+
addAccountOptions(addScheduleOptions(create.command("agent <name>").description("create a headless coding-agent loop").requiredOption("--provider <provider>", "claude, cursor, codewith, aicopilot, opencode, or codex").requiredOption("--prompt <prompt>", "agent prompt").option("--cwd <dir>", "working directory").option("--model <model>", "model").option("--agent <agent>", "provider-specific agent").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--timeout <duration>", "run timeout").option("--config-isolation <mode>", "safe or none", "safe"))).action((name, opts) => {
|
|
2726
3061
|
const provider = opts.provider;
|
|
2727
3062
|
if (!["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"].includes(provider)) {
|
|
2728
3063
|
throw new Error("unsupported provider");
|
|
@@ -2739,6 +3074,7 @@ addAccountOptions(addScheduleOptions(create.command("agent <name>").description(
|
|
|
2739
3074
|
cwd: opts.cwd,
|
|
2740
3075
|
model: opts.model,
|
|
2741
3076
|
agent: opts.agent,
|
|
3077
|
+
authProfile: providerAuthProfileFromOpts(opts, provider),
|
|
2742
3078
|
timeoutMs: opts.timeout ? parseDuration(opts.timeout) : undefined,
|
|
2743
3079
|
configIsolation: opts.configIsolation,
|
|
2744
3080
|
account: accountFromOpts(opts)
|
|
@@ -2821,7 +3157,7 @@ workflows.command("inspect <runId>").description("show a workflow run with steps
|
|
|
2821
3157
|
const events = store.listWorkflowEvents(run.id);
|
|
2822
3158
|
const value = {
|
|
2823
3159
|
workflowRun: publicWorkflowRun(run),
|
|
2824
|
-
steps: steps.map((step) => publicWorkflowStepRun(step
|
|
3160
|
+
steps: steps.map((step) => publicWorkflowStepRun(step)),
|
|
2825
3161
|
events: events.map(publicWorkflowEvent)
|
|
2826
3162
|
};
|
|
2827
3163
|
if (isJson())
|
|
@@ -2829,7 +3165,8 @@ workflows.command("inspect <runId>").description("show a workflow run with steps
|
|
|
2829
3165
|
else {
|
|
2830
3166
|
console.log(`${run.id} ${run.status} ${run.workflowName}`);
|
|
2831
3167
|
for (const step of steps) {
|
|
2832
|
-
|
|
3168
|
+
const publicStep = publicWorkflowStepRun(step);
|
|
3169
|
+
console.log(` ${String(step.sequence).padStart(2, "0")} ${step.status.padEnd(10)} ${step.stepId} ${publicStep.error ?? ""}`);
|
|
2833
3170
|
}
|
|
2834
3171
|
console.log(` events=${events.length}`);
|
|
2835
3172
|
}
|
|
@@ -2845,7 +3182,7 @@ workflows.command("run <idOrName>").option("--show-output", "show step stdout/st
|
|
|
2845
3182
|
const run = store.listWorkflowRuns({ workflowId: workflow.id, limit: 1 })[0];
|
|
2846
3183
|
const steps = run ? store.listWorkflowStepRuns(run.id) : [];
|
|
2847
3184
|
const value = {
|
|
2848
|
-
result,
|
|
3185
|
+
result: publicExecutorResult(result),
|
|
2849
3186
|
workflowRun: run ? publicWorkflowRun(run) : undefined,
|
|
2850
3187
|
steps: steps.map((step) => publicWorkflowStepRun(step, opts.showOutput))
|
|
2851
3188
|
};
|
|
@@ -2854,7 +3191,8 @@ workflows.command("run <idOrName>").option("--show-output", "show step stdout/st
|
|
|
2854
3191
|
else {
|
|
2855
3192
|
console.log(`${run?.id ?? workflow.id} ${result.status}`);
|
|
2856
3193
|
for (const step of steps) {
|
|
2857
|
-
|
|
3194
|
+
const publicStep = publicWorkflowStepRun(step, opts.showOutput);
|
|
3195
|
+
console.log(` ${String(step.sequence).padStart(2, "0")} ${step.status.padEnd(10)} ${step.stepId} ${publicStep.error ?? ""}`);
|
|
2858
3196
|
if (opts.showOutput)
|
|
2859
3197
|
printTextOutput(step);
|
|
2860
3198
|
}
|
|
@@ -2993,18 +3331,31 @@ program.command("run-now <idOrName>").option("--show-output", "show stdout/stder
|
|
|
2993
3331
|
const loop = store.requireLoop(idOrName);
|
|
2994
3332
|
const runnerId = `manual:${process.pid}`;
|
|
2995
3333
|
const now = new Date;
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
3334
|
+
let scheduledFor = manualRunScheduledFor(loop, now);
|
|
3335
|
+
let source = manualRunSource(loop, scheduledFor, now);
|
|
3336
|
+
let shouldAdvance = shouldAdvanceManualRun(loop, scheduledFor, now);
|
|
3337
|
+
let claim = store.claimRun(loop, scheduledFor, runnerId, now);
|
|
3338
|
+
if (!claim && shouldAdvance) {
|
|
3339
|
+
const existing = store.getRunBySlot(loop.id, scheduledFor);
|
|
3340
|
+
if (existing && existing.status !== "running") {
|
|
3341
|
+
scheduledFor = now.toISOString();
|
|
3342
|
+
source = "ad_hoc";
|
|
3343
|
+
shouldAdvance = false;
|
|
3344
|
+
claim = store.claimRun(loop, scheduledFor, runnerId, now);
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
2999
3347
|
if (!claim)
|
|
3000
3348
|
throw new Error("could not claim manual run");
|
|
3001
3349
|
const run = await executeClaimedRun({ store, runnerId, loop: claim.loop, run: claim.run });
|
|
3002
3350
|
if (shouldAdvance) {
|
|
3003
3351
|
advanceLoop(store, claim.loop, run, new Date(run.finishedAt ?? new Date), run.status === "succeeded");
|
|
3004
3352
|
}
|
|
3005
|
-
|
|
3353
|
+
const value = { ...publicRun(run, opts.showOutput), runNow: { source, advancesLoop: shouldAdvance } };
|
|
3354
|
+
print(value, `${run.id} ${run.status} source=${source} slot=${run.scheduledFor}`);
|
|
3006
3355
|
if (!isJson() && opts.showOutput)
|
|
3007
3356
|
printTextOutput(run);
|
|
3357
|
+
if (run.status !== "succeeded")
|
|
3358
|
+
process.exitCode = 1;
|
|
3008
3359
|
} finally {
|
|
3009
3360
|
store.close();
|
|
3010
3361
|
}
|