steroids-cli 0.9.42 → 0.10.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (232) hide show
  1. package/dist/cli/base-command.d.ts +16 -0
  2. package/dist/cli/base-command.d.ts.map +1 -0
  3. package/dist/cli/base-command.js +42 -0
  4. package/dist/cli/base-command.js.map +1 -0
  5. package/dist/cli/version-check.d.ts.map +1 -1
  6. package/dist/cli/version-check.js +28 -2
  7. package/dist/cli/version-check.js.map +1 -1
  8. package/dist/commands/disputes.js +15 -30
  9. package/dist/commands/disputes.js.map +1 -1
  10. package/dist/commands/gc.d.ts +1 -1
  11. package/dist/commands/gc.d.ts.map +1 -1
  12. package/dist/commands/gc.js +49 -64
  13. package/dist/commands/gc.js.map +1 -1
  14. package/dist/commands/git.d.ts.map +1 -1
  15. package/dist/commands/git.js +5 -8
  16. package/dist/commands/git.js.map +1 -1
  17. package/dist/commands/health-stuck.js +2 -6
  18. package/dist/commands/health-stuck.js.map +1 -1
  19. package/dist/commands/health.d.ts.map +1 -1
  20. package/dist/commands/health.js +2 -6
  21. package/dist/commands/health.js.map +1 -1
  22. package/dist/commands/llm.d.ts.map +1 -1
  23. package/dist/commands/llm.js +4 -12
  24. package/dist/commands/llm.js.map +1 -1
  25. package/dist/commands/locks.js +12 -24
  26. package/dist/commands/locks.js.map +1 -1
  27. package/dist/commands/loop-phases.d.ts +12 -4
  28. package/dist/commands/loop-phases.d.ts.map +1 -1
  29. package/dist/commands/loop-phases.js +163 -111
  30. package/dist/commands/loop-phases.js.map +1 -1
  31. package/dist/commands/purge.d.ts.map +1 -1
  32. package/dist/commands/purge.js +11 -22
  33. package/dist/commands/purge.js.map +1 -1
  34. package/dist/commands/runners-list.d.ts.map +1 -1
  35. package/dist/commands/runners-list.js +11 -30
  36. package/dist/commands/runners-list.js.map +1 -1
  37. package/dist/commands/runners-parallel.d.ts.map +1 -1
  38. package/dist/commands/runners-parallel.js +3 -7
  39. package/dist/commands/runners-parallel.js.map +1 -1
  40. package/dist/commands/runners.js +2 -9
  41. package/dist/commands/runners.js.map +1 -1
  42. package/dist/commands/scan.d.ts.map +1 -1
  43. package/dist/commands/scan.js +5 -8
  44. package/dist/commands/scan.js.map +1 -1
  45. package/dist/commands/sections-commands.d.ts.map +1 -1
  46. package/dist/commands/sections-commands.js +12 -24
  47. package/dist/commands/sections-commands.js.map +1 -1
  48. package/dist/commands/sections.js +6 -12
  49. package/dist/commands/sections.js.map +1 -1
  50. package/dist/commands/skills.d.ts +11 -0
  51. package/dist/commands/skills.d.ts.map +1 -0
  52. package/dist/commands/skills.js +181 -0
  53. package/dist/commands/skills.js.map +1 -0
  54. package/dist/commands/tasks-reset.d.ts +3 -0
  55. package/dist/commands/tasks-reset.d.ts.map +1 -0
  56. package/dist/commands/tasks-reset.js +200 -0
  57. package/dist/commands/tasks-reset.js.map +1 -0
  58. package/dist/commands/tasks.d.ts.map +1 -1
  59. package/dist/commands/tasks.js +57 -67
  60. package/dist/commands/tasks.js.map +1 -1
  61. package/dist/commands/web.d.ts.map +1 -1
  62. package/dist/commands/web.js +3 -1
  63. package/dist/commands/web.js.map +1 -1
  64. package/dist/config/loader.d.ts +1 -0
  65. package/dist/config/loader.d.ts.map +1 -1
  66. package/dist/config/loader.js.map +1 -1
  67. package/dist/database/connection.d.ts +10 -1
  68. package/dist/database/connection.d.ts.map +1 -1
  69. package/dist/database/connection.js +32 -6
  70. package/dist/database/connection.js.map +1 -1
  71. package/dist/database/queries.d.ts +18 -0
  72. package/dist/database/queries.d.ts.map +1 -1
  73. package/dist/database/queries.js +91 -20
  74. package/dist/database/queries.js.map +1 -1
  75. package/dist/database/schema.d.ts +2 -2
  76. package/dist/database/schema.d.ts.map +1 -1
  77. package/dist/database/schema.js +8 -0
  78. package/dist/database/schema.js.map +1 -1
  79. package/dist/git/status.d.ts.map +1 -1
  80. package/dist/git/status.js +13 -2
  81. package/dist/git/status.js.map +1 -1
  82. package/dist/git/submission-resolution.d.ts.map +1 -1
  83. package/dist/git/submission-resolution.js +3 -7
  84. package/dist/git/submission-resolution.js.map +1 -1
  85. package/dist/index.js +5 -0
  86. package/dist/index.js.map +1 -1
  87. package/dist/migrations/backfill.d.ts +3 -0
  88. package/dist/migrations/backfill.d.ts.map +1 -0
  89. package/dist/migrations/backfill.js +53 -0
  90. package/dist/migrations/backfill.js.map +1 -0
  91. package/dist/migrations/runner.d.ts.map +1 -1
  92. package/dist/migrations/runner.js +9 -0
  93. package/dist/migrations/runner.js.map +1 -1
  94. package/dist/orchestrator/base-runner.d.ts +14 -0
  95. package/dist/orchestrator/base-runner.d.ts.map +1 -0
  96. package/dist/orchestrator/base-runner.js +57 -0
  97. package/dist/orchestrator/base-runner.js.map +1 -0
  98. package/dist/orchestrator/coder.d.ts +4 -22
  99. package/dist/orchestrator/coder.d.ts.map +1 -1
  100. package/dist/orchestrator/coder.js +136 -183
  101. package/dist/orchestrator/coder.js.map +1 -1
  102. package/dist/orchestrator/coordinator.d.ts.map +1 -1
  103. package/dist/orchestrator/coordinator.js +1 -0
  104. package/dist/orchestrator/coordinator.js.map +1 -1
  105. package/dist/orchestrator/fallback-handler.d.ts +3 -23
  106. package/dist/orchestrator/fallback-handler.d.ts.map +1 -1
  107. package/dist/orchestrator/fallback-handler.js +40 -357
  108. package/dist/orchestrator/fallback-handler.js.map +1 -1
  109. package/dist/orchestrator/history-manager.d.ts +15 -0
  110. package/dist/orchestrator/history-manager.d.ts.map +1 -0
  111. package/dist/orchestrator/history-manager.js +93 -0
  112. package/dist/orchestrator/history-manager.js.map +1 -0
  113. package/dist/orchestrator/post-coder.d.ts.map +1 -1
  114. package/dist/orchestrator/post-coder.js +16 -24
  115. package/dist/orchestrator/post-coder.js.map +1 -1
  116. package/dist/orchestrator/post-reviewer.d.ts.map +1 -1
  117. package/dist/orchestrator/post-reviewer.js +25 -37
  118. package/dist/orchestrator/post-reviewer.js.map +1 -1
  119. package/dist/orchestrator/reviewer.d.ts +5 -29
  120. package/dist/orchestrator/reviewer.d.ts.map +1 -1
  121. package/dist/orchestrator/reviewer.js +193 -255
  122. package/dist/orchestrator/reviewer.js.map +1 -1
  123. package/dist/orchestrator/signal-parser.d.ts +32 -0
  124. package/dist/orchestrator/signal-parser.d.ts.map +1 -0
  125. package/dist/orchestrator/signal-parser.js +73 -0
  126. package/dist/orchestrator/signal-parser.js.map +1 -0
  127. package/dist/orchestrator/types.d.ts +9 -13
  128. package/dist/orchestrator/types.d.ts.map +1 -1
  129. package/dist/parallel/clone.d.ts.map +1 -1
  130. package/dist/parallel/clone.js +37 -2
  131. package/dist/parallel/clone.js.map +1 -1
  132. package/dist/parallel/merge-conflict-attempts.d.ts.map +1 -1
  133. package/dist/parallel/merge-conflict-attempts.js +4 -12
  134. package/dist/parallel/merge-conflict-attempts.js.map +1 -1
  135. package/dist/parallel/merge-conflict.d.ts.map +1 -1
  136. package/dist/parallel/merge-conflict.js +3 -10
  137. package/dist/parallel/merge-conflict.js.map +1 -1
  138. package/dist/parallel/merge-sealing.d.ts +0 -3
  139. package/dist/parallel/merge-sealing.d.ts.map +1 -1
  140. package/dist/parallel/merge-sealing.js +5 -9
  141. package/dist/parallel/merge-sealing.js.map +1 -1
  142. package/dist/prompts/coder.d.ts +1 -1
  143. package/dist/prompts/coder.d.ts.map +1 -1
  144. package/dist/prompts/coder.js +66 -119
  145. package/dist/prompts/coder.js.map +1 -1
  146. package/dist/prompts/prompt-helpers.d.ts +4 -0
  147. package/dist/prompts/prompt-helpers.d.ts.map +1 -1
  148. package/dist/prompts/prompt-helpers.js +35 -0
  149. package/dist/prompts/prompt-helpers.js.map +1 -1
  150. package/dist/prompts/reviewer.d.ts.map +1 -1
  151. package/dist/prompts/reviewer.js +1 -3
  152. package/dist/prompts/reviewer.js.map +1 -1
  153. package/dist/providers/claude.d.ts.map +1 -1
  154. package/dist/providers/claude.js +9 -1
  155. package/dist/providers/claude.js.map +1 -1
  156. package/dist/providers/codex.d.ts.map +1 -1
  157. package/dist/providers/codex.js +10 -1
  158. package/dist/providers/codex.js.map +1 -1
  159. package/dist/providers/gemini.d.ts +9 -48
  160. package/dist/providers/gemini.d.ts.map +1 -1
  161. package/dist/providers/gemini.js +133 -101
  162. package/dist/providers/gemini.js.map +1 -1
  163. package/dist/providers/interface.d.ts +9 -1
  164. package/dist/providers/interface.d.ts.map +1 -1
  165. package/dist/providers/interface.js +11 -1
  166. package/dist/providers/interface.js.map +1 -1
  167. package/dist/providers/invocation-logger.d.ts +2 -0
  168. package/dist/providers/invocation-logger.d.ts.map +1 -1
  169. package/dist/providers/invocation-logger.js +3 -3
  170. package/dist/providers/invocation-logger.js.map +1 -1
  171. package/dist/providers/mistral.d.ts.map +1 -1
  172. package/dist/providers/mistral.js +12 -1
  173. package/dist/providers/mistral.js.map +1 -1
  174. package/dist/providers/ping.d.ts +5 -0
  175. package/dist/providers/ping.d.ts.map +1 -0
  176. package/dist/providers/ping.js +35 -0
  177. package/dist/providers/ping.js.map +1 -0
  178. package/dist/providers/registry.d.ts +4 -0
  179. package/dist/providers/registry.d.ts.map +1 -1
  180. package/dist/providers/registry.js +18 -0
  181. package/dist/providers/registry.js.map +1 -1
  182. package/dist/runners/activity-log.d.ts.map +1 -1
  183. package/dist/runners/activity-log.js +12 -36
  184. package/dist/runners/activity-log.js.map +1 -1
  185. package/dist/runners/credit-pause.d.ts +2 -3
  186. package/dist/runners/credit-pause.d.ts.map +1 -1
  187. package/dist/runners/credit-pause.js +25 -40
  188. package/dist/runners/credit-pause.js.map +1 -1
  189. package/dist/runners/daemon.d.ts +0 -8
  190. package/dist/runners/daemon.d.ts.map +1 -1
  191. package/dist/runners/daemon.js +23 -76
  192. package/dist/runners/daemon.js.map +1 -1
  193. package/dist/runners/global-db.d.ts +14 -1
  194. package/dist/runners/global-db.d.ts.map +1 -1
  195. package/dist/runners/global-db.js +123 -59
  196. package/dist/runners/global-db.js.map +1 -1
  197. package/dist/runners/orchestrator-loop.d.ts.map +1 -1
  198. package/dist/runners/orchestrator-loop.js +2 -6
  199. package/dist/runners/orchestrator-loop.js.map +1 -1
  200. package/dist/runners/projects.d.ts +2 -32
  201. package/dist/runners/projects.d.ts.map +1 -1
  202. package/dist/runners/projects.js +23 -103
  203. package/dist/runners/projects.js.map +1 -1
  204. package/dist/runners/wakeup-checks.d.ts +0 -3
  205. package/dist/runners/wakeup-checks.d.ts.map +1 -1
  206. package/dist/runners/wakeup-checks.js +13 -26
  207. package/dist/runners/wakeup-checks.js.map +1 -1
  208. package/dist/runners/wakeup-timing.d.ts.map +1 -1
  209. package/dist/runners/wakeup-timing.js +4 -12
  210. package/dist/runners/wakeup-timing.js.map +1 -1
  211. package/dist/runners/wakeup.d.ts +1 -1
  212. package/dist/runners/wakeup.d.ts.map +1 -1
  213. package/dist/runners/wakeup.js +394 -359
  214. package/dist/runners/wakeup.js.map +1 -1
  215. package/dist/utils/tokens.d.ts +14 -0
  216. package/dist/utils/tokens.d.ts.map +1 -0
  217. package/dist/utils/tokens.js +62 -0
  218. package/dist/utils/tokens.js.map +1 -0
  219. package/migrations/004_add_section_dependencies.sql +1 -1
  220. package/migrations/017_add_invocation_runner_id.sql +8 -0
  221. package/migrations/018_expand_audit_columns.sql +13 -0
  222. package/migrations/019_add_task_start_sha.sql +9 -0
  223. package/migrations/manifest.json +25 -1
  224. package/package.json +4 -3
  225. package/dist/orchestrator/schemas.d.ts +0 -26
  226. package/dist/orchestrator/schemas.d.ts.map +0 -1
  227. package/dist/orchestrator/schemas.js +0 -154
  228. package/dist/orchestrator/schemas.js.map +0 -1
  229. package/dist/parallel/merge.test.d.ts +0 -5
  230. package/dist/parallel/merge.test.d.ts.map +0 -1
  231. package/dist/parallel/merge.test.js +0 -322
  232. package/dist/parallel/merge.test.js.map +0 -1
@@ -24,6 +24,8 @@ Object.defineProperty(exports, "hasActiveParallelSessionForProject", { enumerabl
24
24
  const wakeup_runner_js_1 = require("./wakeup-runner.js");
25
25
  const wakeup_timing_js_1 = require("./wakeup-timing.js");
26
26
  Object.defineProperty(exports, "getLastWakeupTime", { enumerable: true, get: function () { return wakeup_timing_js_1.getLastWakeupTime; } });
27
+ // In-memory mutex to prevent concurrent wakeup cycles in the same process
28
+ let isWakeupRunning = false;
27
29
  /**
28
30
  * Main wake-up function
29
31
  * Called by cron every minute to ensure runners are healthy
@@ -37,216 +39,252 @@ async function wakeup(options = {}) {
37
39
  if (!quiet)
38
40
  console.log(msg);
39
41
  };
40
- // Record wakeup invocation time (even for dry runs)
41
- if (!dryRun) {
42
- (0, wakeup_timing_js_1.recordWakeupTime)();
42
+ if (isWakeupRunning) {
43
+ log('Wakeup cycle already running (in-memory lock), skipping.');
44
+ return [{ action: 'skipped', reason: 'Wakeup cycle already running' }];
43
45
  }
44
- // Step 1: Clean up stale runners first
45
- const global = (0, global_db_js_1.openGlobalDatabase)();
46
+ isWakeupRunning = true;
46
47
  try {
47
- try {
48
- const staleRunners = (0, heartbeat_js_1.findStaleRunners)(global.db);
49
- if (staleRunners.length > 0) {
50
- log(`Found ${staleRunners.length} stale runner(s), cleaning up...`);
51
- if (!dryRun) {
52
- for (const runner of staleRunners) {
53
- if (runner.pid) {
54
- (0, wakeup_runner_js_1.killProcess)(runner.pid);
55
- }
56
- global.db.prepare(`UPDATE workstreams
48
+ if (!(0, global_db_js_1.getDaemonActiveStatus)()) {
49
+ log('Daemon paused (is_active=false), skipping wakeup logic.');
50
+ return [{ action: 'skipped', reason: 'Daemon is paused' }];
51
+ }
52
+ // Record wakeup invocation time (even for dry runs)
53
+ if (!dryRun) {
54
+ (0, wakeup_timing_js_1.recordWakeupTime)();
55
+ }
56
+ // Step 1: Clean up stale runners first
57
+ return (0, global_db_js_1.withGlobalDatabase)(async (globalDb) => {
58
+ const global = { db: globalDb };
59
+ try {
60
+ const staleRunners = (0, heartbeat_js_1.findStaleRunners)(global.db);
61
+ if (staleRunners.length > 0) {
62
+ log(`Found ${staleRunners.length} stale runner(s), cleaning up...`);
63
+ if (!dryRun) {
64
+ for (const runner of staleRunners) {
65
+ if (runner.pid) {
66
+ (0, wakeup_runner_js_1.killProcess)(runner.pid);
67
+ }
68
+ global.db.prepare(`UPDATE workstreams
57
69
  SET runner_id = NULL,
58
70
  lease_expires_at = datetime('now')
59
71
  WHERE runner_id = ?`).run(runner.id);
60
- global.db.prepare('DELETE FROM runners WHERE id = ?').run(runner.id);
72
+ global.db.prepare('DELETE FROM runners WHERE id = ?').run(runner.id);
73
+ }
61
74
  }
75
+ results.push({
76
+ action: 'cleaned',
77
+ reason: `Cleaned ${staleRunners.length} stale runner(s)`,
78
+ staleRunners: staleRunners.length,
79
+ });
62
80
  }
63
- results.push({
64
- action: 'cleaned',
65
- reason: `Cleaned ${staleRunners.length} stale runner(s)`,
66
- staleRunners: staleRunners.length,
67
- });
68
81
  }
69
- }
70
- catch {
71
- // ignore global DB issues; wakeup will still attempt per-project checks
72
- }
73
- try {
74
- const releasedLeases = global.db.prepare(`UPDATE workstreams
82
+ catch {
83
+ // ignore global DB issues; wakeup will still attempt per-project checks
84
+ }
85
+ try {
86
+ const releasedLeases = global.db.prepare(`UPDATE workstreams
75
87
  SET runner_id = NULL,
76
88
  lease_expires_at = NULL
77
89
  WHERE status = 'running'
78
90
  AND lease_expires_at IS NOT NULL
79
91
  AND lease_expires_at <= datetime('now')`).run().changes;
80
- if (releasedLeases > 0) {
81
- log(`Released ${releasedLeases} expired workstream lease(s)`);
92
+ if (releasedLeases > 0) {
93
+ log(`Released ${releasedLeases} expired workstream lease(s)`);
94
+ }
82
95
  }
83
- }
84
- catch {
85
- // ignore lease cleanup issues in wakeup
86
- }
87
- // Step 2: Clean zombie lock if present
88
- const lockStatus = (0, lock_js_1.checkLockStatus)();
89
- if (lockStatus.isZombie && lockStatus.pid) {
90
- log(`Found zombie lock (PID: ${lockStatus.pid}), cleaning...`);
91
- if (!dryRun) {
92
- (0, lock_js_1.removeLock)();
96
+ catch {
97
+ // ignore lease cleanup issues in wakeup
93
98
  }
94
- }
95
- // Step 3: Get all registered projects from global registry
96
- const registeredProjects = (0, projects_js_1.getRegisteredProjects)(false); // enabled only
97
- if (registeredProjects.length === 0) {
98
- log('No registered projects found');
99
- log('Run "steroids projects add <path>" to register a project');
100
- results.push({
101
- action: 'none',
102
- reason: 'No registered projects',
103
- pendingTasks: 0,
104
- });
105
- return results;
106
- }
107
- log(`Checking ${registeredProjects.length} registered project(s)...`);
108
- // Step 4: Check each project and start runners as needed
109
- for (const project of registeredProjects) {
110
- // Skip if project directory doesn't exist
111
- if (!(0, node_fs_1.existsSync)(project.path)) {
112
- log(`Skipping ${project.path}: directory not found`);
99
+ // Step 2: Clean zombie lock if present
100
+ const lockStatus = (0, lock_js_1.checkLockStatus)();
101
+ if (lockStatus.isZombie && lockStatus.pid) {
102
+ log(`Found zombie lock (PID: ${lockStatus.pid}), cleaning...`);
103
+ if (!dryRun) {
104
+ (0, lock_js_1.removeLock)();
105
+ }
106
+ }
107
+ // Step 3: Get all registered projects from global registry
108
+ const registeredProjects = (0, projects_js_1.getRegisteredProjects)(false); // enabled only
109
+ if (registeredProjects.length === 0) {
110
+ log('No registered projects found');
111
+ log('Run "steroids projects add <path>" to register a project');
113
112
  results.push({
114
113
  action: 'none',
115
- reason: 'Directory not found',
116
- projectPath: project.path,
114
+ reason: 'No registered projects',
115
+ pendingTasks: 0,
117
116
  });
118
- continue;
117
+ return results;
119
118
  }
120
- // Phase 6 (live monitoring): best-effort retention cleanup of invocation activity logs.
121
- // This is safe to run even if the project has no pending tasks.
122
- let deletedInvocationLogs = 0;
123
- try {
124
- const cleanup = (0, invocation_logs_js_1.cleanupInvocationLogs)(project.path, { retentionDays: 7, dryRun });
125
- deletedInvocationLogs = cleanup.deletedFiles;
126
- if (cleanup.deletedFiles > 0 && !quiet) {
127
- log(`Cleaned ${cleanup.deletedFiles} old invocation log(s) in ${project.path}`);
119
+ log(`Checking ${registeredProjects.length} registered project(s)...`);
120
+ // Step 4: Check each project and start runners as needed
121
+ for (const project of registeredProjects) {
122
+ // Skip if project directory doesn't exist
123
+ if (!(0, node_fs_1.existsSync)(project.path)) {
124
+ log(`Skipping ${project.path}: directory not found`);
125
+ results.push({
126
+ action: 'none',
127
+ reason: 'Directory not found',
128
+ projectPath: project.path,
129
+ });
130
+ continue;
128
131
  }
129
- }
130
- catch {
131
- // Ignore cleanup errors; wakeup must remain robust.
132
- }
133
- let recoveredActions = 0;
134
- let skippedRecoveryDueToSafetyLimit = false;
135
- let sanitisedActions = 0;
136
- try {
137
- const { db: projectDb, close: closeProjectDb } = (0, connection_js_1.openDatabase)(project.path);
132
+ // Phase 6 (live monitoring): best-effort retention cleanup of invocation activity logs.
133
+ // This is safe to run even if the project has no pending tasks.
134
+ let deletedInvocationLogs = 0;
135
+ try {
136
+ const cleanup = (0, invocation_logs_js_1.cleanupInvocationLogs)(project.path, { retentionDays: 7, dryRun });
137
+ deletedInvocationLogs = cleanup.deletedFiles;
138
+ if (cleanup.deletedFiles > 0 && !quiet) {
139
+ log(`Cleaned ${cleanup.deletedFiles} old invocation log(s) in ${project.path}`);
140
+ }
141
+ }
142
+ catch {
143
+ // Ignore cleanup errors; wakeup must remain robust.
144
+ }
145
+ let recoveredActions = 0;
146
+ let skippedRecoveryDueToSafetyLimit = false;
147
+ let sanitisedActions = 0;
138
148
  try {
139
- const sanitiseSummary = (0, wakeup_sanitise_js_1.runPeriodicSanitiseForProject)(global.db, projectDb, project.path, dryRun);
140
- sanitisedActions = (0, wakeup_sanitise_js_1.sanitisedActionCount)(sanitiseSummary);
141
- if (sanitisedActions > 0 && !quiet) {
142
- log(`Sanitised ${sanitisedActions} stale item(s) in ${project.path}`);
149
+ const { db: projectDb, close: closeProjectDb } = (0, connection_js_1.openDatabase)(project.path);
150
+ try {
151
+ const sanitiseSummary = (0, wakeup_sanitise_js_1.runPeriodicSanitiseForProject)(global.db, projectDb, project.path, dryRun);
152
+ sanitisedActions = (0, wakeup_sanitise_js_1.sanitisedActionCount)(sanitiseSummary);
153
+ if (sanitisedActions > 0 && !quiet) {
154
+ log(`Sanitised ${sanitisedActions} stale item(s) in ${project.path}`);
155
+ }
156
+ // Step 4a: Recover stuck tasks (best-effort) before deciding whether to (re)start a runner.
157
+ // This is what unblocks orphaned/infinite-hang scenarios without manual intervention.
158
+ const config = (0, loader_js_1.loadConfig)(project.path);
159
+ const recovery = await (0, stuck_task_recovery_js_1.recoverStuckTasks)({
160
+ projectPath: project.path,
161
+ projectDb,
162
+ globalDb: global.db,
163
+ config,
164
+ dryRun,
165
+ });
166
+ recoveredActions = recovery.actions.length;
167
+ skippedRecoveryDueToSafetyLimit = recovery.skippedDueToSafetyLimit;
168
+ if (recoveredActions > 0 && !quiet) {
169
+ log(`Recovered ${recoveredActions} stuck item(s) in ${project.path}`);
170
+ }
171
+ if (skippedRecoveryDueToSafetyLimit && !quiet) {
172
+ log(`Skipping auto-recovery in ${project.path}: safety limit hit (maxIncidentsPerHour)`);
173
+ }
174
+ }
175
+ finally {
176
+ closeProjectDb();
143
177
  }
144
- // Step 4a: Recover stuck tasks (best-effort) before deciding whether to (re)start a runner.
145
- // This is what unblocks orphaned/infinite-hang scenarios without manual intervention.
146
- const config = (0, loader_js_1.loadConfig)(project.path);
147
- const recovery = await (0, stuck_task_recovery_js_1.recoverStuckTasks)({
178
+ }
179
+ catch {
180
+ // If sanitise/recovery can't run (DB missing/corrupt), we still proceed with runner checks.
181
+ }
182
+ // Check for pending work after sanitise/recovery
183
+ const hasWork = await (0, wakeup_checks_js_1.projectHasPendingWork)(project.path);
184
+ if (!hasWork) {
185
+ const noWorkReason = sanitisedActions > 0
186
+ ? `No pending tasks after sanitise (${sanitisedActions} action(s))`
187
+ : 'No pending tasks';
188
+ log(`Skipping ${project.path}: ${noWorkReason.toLowerCase()}`);
189
+ results.push({
190
+ action: 'none',
191
+ reason: noWorkReason,
148
192
  projectPath: project.path,
149
- projectDb,
150
- globalDb: global.db,
151
- config,
152
- dryRun,
193
+ recoveredActions,
194
+ skippedRecoveryDueToSafetyLimit,
195
+ deletedInvocationLogs,
196
+ sanitisedActions,
153
197
  });
154
- recoveredActions = recovery.actions.length;
155
- skippedRecoveryDueToSafetyLimit = recovery.skippedDueToSafetyLimit;
156
- if (recoveredActions > 0 && !quiet) {
157
- log(`Recovered ${recoveredActions} stuck item(s) in ${project.path}`);
158
- }
159
- if (skippedRecoveryDueToSafetyLimit && !quiet) {
160
- log(`Skipping auto-recovery in ${project.path}: safety limit hit (maxIncidentsPerHour)`);
198
+ continue;
199
+ }
200
+ const projectConfig = (0, loader_js_1.loadConfig)(project.path);
201
+ // Check global provider backoffs
202
+ const coderProvider = projectConfig.ai?.coder?.provider;
203
+ const reviewerProvider = projectConfig.ai?.reviewer?.provider;
204
+ const providersToCheck = [coderProvider, reviewerProvider].filter(Boolean);
205
+ let isBackedOff = false;
206
+ let backedOffProvider = '';
207
+ let remainingMs = 0;
208
+ for (const provider of providersToCheck) {
209
+ const ms = (0, global_db_js_1.getProviderBackoffRemainingMs)(provider);
210
+ if (ms > 0) {
211
+ isBackedOff = true;
212
+ backedOffProvider = provider;
213
+ remainingMs = ms;
214
+ break;
161
215
  }
162
216
  }
163
- finally {
164
- closeProjectDb();
217
+ if (isBackedOff) {
218
+ const remainingMinutes = Math.ceil(remainingMs / 60000);
219
+ log(`Skipping ${project.path}: Provider '${backedOffProvider}' is in backoff for ${remainingMinutes}m`);
220
+ results.push({
221
+ action: 'skipped',
222
+ reason: `Provider '${backedOffProvider}' backed off for ${remainingMinutes}m`,
223
+ projectPath: project.path,
224
+ });
225
+ continue;
165
226
  }
166
- }
167
- catch {
168
- // If sanitise/recovery can't run (DB missing/corrupt), we still proceed with runner checks.
169
- }
170
- // Check for pending work after sanitise/recovery
171
- const hasWork = await (0, wakeup_checks_js_1.projectHasPendingWork)(project.path);
172
- if (!hasWork) {
173
- const noWorkReason = sanitisedActions > 0
174
- ? `No pending tasks after sanitise (${sanitisedActions} action(s))`
175
- : 'No pending tasks';
176
- log(`Skipping ${project.path}: ${noWorkReason.toLowerCase()}`);
177
- results.push({
178
- action: 'none',
179
- reason: noWorkReason,
180
- projectPath: project.path,
181
- recoveredActions,
182
- skippedRecoveryDueToSafetyLimit,
183
- deletedInvocationLogs,
184
- sanitisedActions,
185
- });
186
- continue;
187
- }
188
- const projectConfig = (0, loader_js_1.loadConfig)(project.path);
189
- const parallelEnabled = projectConfig.runners?.parallel?.enabled === true;
190
- const configuredMaxClonesRaw = Number(projectConfig.runners?.parallel?.maxClones);
191
- const configuredMaxClones = Number.isFinite(configuredMaxClonesRaw) && configuredMaxClonesRaw > 0
192
- ? configuredMaxClonesRaw
193
- : 3;
194
- // Skip projects currently executing a parallel session before attempting recovery/startup.
195
- // This prevents parallel runners from being interfered with by a cron-managed runner.
196
- if ((0, wakeup_checks_js_1.hasActiveParallelSessionForProject)(project.path)) {
197
- let retrySummary = '';
198
- let skipForParallelSession = true;
199
- let scaledDown = 0;
200
- let resumed = 0;
201
- let wouldScaleDown = 0;
202
- let wouldResume = 0;
203
- const activeSessions = global.db
204
- .prepare(`SELECT id
227
+ const parallelEnabled = projectConfig.runners?.parallel?.enabled === true;
228
+ const configuredMaxClonesRaw = Number(projectConfig.runners?.parallel?.maxClones);
229
+ const configuredMaxClones = Number.isFinite(configuredMaxClonesRaw) && configuredMaxClonesRaw > 0
230
+ ? configuredMaxClonesRaw
231
+ : 3;
232
+ // Skip projects currently executing a parallel session before attempting recovery/startup.
233
+ // This prevents parallel runners from being interfered with by a cron-managed runner.
234
+ if ((0, wakeup_checks_js_1.hasActiveParallelSessionForProject)(project.path)) {
235
+ let retrySummary = '';
236
+ let skipForParallelSession = true;
237
+ let scaledDown = 0;
238
+ let resumed = 0;
239
+ let wouldScaleDown = 0;
240
+ let wouldResume = 0;
241
+ const activeSessions = global.db
242
+ .prepare(`SELECT id
205
243
  FROM parallel_sessions
206
244
  WHERE project_path = ?
207
245
  AND status NOT IN ('completed', 'failed', 'aborted', 'blocked_validation', 'blocked_recovery')`)
208
- .all(project.path);
209
- // Config-aware mode reconciliation (parallel -> single):
210
- // if parallel is disabled, convert only when the active parallel runners
211
- // are idle to avoid interrupting in-flight tasks.
212
- if (!parallelEnabled && activeSessions.length > 0) {
213
- const sessionRunners = activeSessions.flatMap((session) => global.db
214
- .prepare(`SELECT id, pid, status, current_task_id
246
+ .all(project.path);
247
+ // Config-aware mode reconciliation (parallel -> single):
248
+ // if parallel is disabled, convert only when the active parallel runners
249
+ // are idle to avoid interrupting in-flight tasks.
250
+ if (!parallelEnabled && activeSessions.length > 0) {
251
+ const sessionRunners = activeSessions.flatMap((session) => global.db
252
+ .prepare(`SELECT id, pid, status, current_task_id
215
253
  FROM runners
216
254
  WHERE parallel_session_id = ?
217
255
  AND status != 'stopped'
218
256
  AND heartbeat_at > datetime('now', '-5 minutes')`)
219
- .all(session.id));
220
- const hasBusyRunner = sessionRunners.some((runner) => (runner.status ?? '').toLowerCase() !== 'idle' || !!runner.current_task_id);
221
- if (hasBusyRunner) {
222
- const reason = 'Parallel->single mode switch pending (active workstream runner busy)';
223
- log(`Skipping ${project.path}: ${reason.toLowerCase()}`);
224
- results.push({
225
- action: dryRun ? 'would_start' : 'none',
226
- reason,
227
- projectPath: project.path,
228
- deletedInvocationLogs,
229
- });
230
- continue;
231
- }
232
- if (dryRun) {
233
- const reason = 'Would recycle idle parallel session to apply single-runner mode';
234
- log(`Would reconcile ${project.path}: ${reason.toLowerCase()}`);
235
- results.push({
236
- action: 'would_start',
237
- reason,
238
- projectPath: project.path,
239
- deletedInvocationLogs,
240
- });
241
- continue;
242
- }
243
- for (const runner of sessionRunners) {
244
- if (runner.pid)
245
- (0, wakeup_runner_js_1.killProcess)(runner.pid);
246
- global.db.prepare('DELETE FROM runners WHERE id = ?').run(runner.id);
247
- }
248
- for (const session of activeSessions) {
249
- global.db.prepare(`UPDATE workstreams
257
+ .all(session.id));
258
+ const hasBusyRunner = sessionRunners.some((runner) => (runner.status ?? '').toLowerCase() !== 'idle' || !!runner.current_task_id);
259
+ if (hasBusyRunner) {
260
+ const reason = 'Parallel->single mode switch pending (active workstream runner busy)';
261
+ log(`Skipping ${project.path}: ${reason.toLowerCase()}`);
262
+ results.push({
263
+ action: dryRun ? 'would_start' : 'none',
264
+ reason,
265
+ projectPath: project.path,
266
+ deletedInvocationLogs,
267
+ });
268
+ continue;
269
+ }
270
+ if (dryRun) {
271
+ const reason = 'Would recycle idle parallel session to apply single-runner mode';
272
+ log(`Would reconcile ${project.path}: ${reason.toLowerCase()}`);
273
+ results.push({
274
+ action: 'would_start',
275
+ reason,
276
+ projectPath: project.path,
277
+ deletedInvocationLogs,
278
+ });
279
+ continue;
280
+ }
281
+ for (const runner of sessionRunners) {
282
+ if (runner.pid)
283
+ (0, wakeup_runner_js_1.killProcess)(runner.pid);
284
+ global.db.prepare('DELETE FROM runners WHERE id = ?').run(runner.id);
285
+ }
286
+ for (const session of activeSessions) {
287
+ global.db.prepare(`UPDATE workstreams
250
288
  SET status = 'aborted',
251
289
  runner_id = NULL,
252
290
  lease_expires_at = NULL,
@@ -256,34 +294,34 @@ async function wakeup(options = {}) {
256
294
  completed_at = COALESCE(completed_at, datetime('now'))
257
295
  WHERE session_id = ?
258
296
  AND status NOT IN ('completed', 'failed', 'aborted')`).run(session.id);
259
- global.db.prepare(`UPDATE parallel_sessions
297
+ global.db.prepare(`UPDATE parallel_sessions
260
298
  SET status = 'aborted',
261
299
  completed_at = COALESCE(completed_at, datetime('now'))
262
300
  WHERE id = ?`).run(session.id);
301
+ }
302
+ skipForParallelSession = false;
303
+ retrySummary = ', recycled idle parallel session to apply single-runner mode';
263
304
  }
264
- skipForParallelSession = false;
265
- retrySummary = ', recycled idle parallel session to apply single-runner mode';
266
- }
267
- for (const session of activeSessions) {
268
- const sessionRunners = global.db
269
- .prepare(`SELECT id, pid, status, current_task_id
305
+ for (const session of activeSessions) {
306
+ const sessionRunners = global.db
307
+ .prepare(`SELECT id, pid, status, current_task_id
270
308
  FROM runners
271
309
  WHERE parallel_session_id = ?
272
310
  AND status != 'stopped'
273
311
  AND heartbeat_at > datetime('now', '-5 minutes')
274
312
  ORDER BY started_at DESC, heartbeat_at DESC`)
275
- .all(session.id);
276
- if (sessionRunners.length > configuredMaxClones) {
277
- const idleCandidate = sessionRunners.find((r) => (r.status ?? '').toLowerCase() === 'idle' && !r.current_task_id);
278
- if (idleCandidate) {
279
- if (dryRun) {
280
- wouldScaleDown += 1;
281
- }
282
- else {
283
- if (idleCandidate.pid) {
284
- (0, wakeup_runner_js_1.killProcess)(idleCandidate.pid);
313
+ .all(session.id);
314
+ if (sessionRunners.length > configuredMaxClones) {
315
+ const idleCandidate = sessionRunners.find((r) => (r.status ?? '').toLowerCase() === 'idle' && !r.current_task_id);
316
+ if (idleCandidate) {
317
+ if (dryRun) {
318
+ wouldScaleDown += 1;
285
319
  }
286
- global.db.prepare(`UPDATE workstreams
320
+ else {
321
+ if (idleCandidate.pid) {
322
+ (0, wakeup_runner_js_1.killProcess)(idleCandidate.pid);
323
+ }
324
+ global.db.prepare(`UPDATE workstreams
287
325
  SET runner_id = NULL,
288
326
  lease_expires_at = datetime('now', '+5 minutes'),
289
327
  next_retry_at = datetime('now', '+5 minutes'),
@@ -291,14 +329,14 @@ async function wakeup(options = {}) {
291
329
  last_reconciled_at = datetime('now')
292
330
  WHERE session_id = ?
293
331
  AND runner_id = ?`).run(session.id, idleCandidate.id);
294
- global.db.prepare('DELETE FROM runners WHERE id = ?').run(idleCandidate.id);
295
- scaledDown += 1;
332
+ global.db.prepare('DELETE FROM runners WHERE id = ?').run(idleCandidate.id);
333
+ scaledDown += 1;
334
+ }
296
335
  }
297
336
  }
298
- }
299
- else if (sessionRunners.length < configuredMaxClones) {
300
- const throttled = global.db
301
- .prepare(`SELECT id
337
+ else if (sessionRunners.length < configuredMaxClones) {
338
+ const throttled = global.db
339
+ .prepare(`SELECT id
302
340
  FROM workstreams
303
341
  WHERE session_id = ?
304
342
  AND status = 'running'
@@ -307,79 +345,79 @@ async function wakeup(options = {}) {
307
345
  AND last_reconcile_action = 'concurrency_throttle'
308
346
  ORDER BY last_reconciled_at ASC
309
347
  LIMIT 1`)
310
- .get(session.id);
311
- if (throttled) {
312
- if (dryRun) {
313
- wouldResume += 1;
314
- }
315
- else {
316
- global.db.prepare(`UPDATE workstreams
348
+ .get(session.id);
349
+ if (throttled) {
350
+ if (dryRun) {
351
+ wouldResume += 1;
352
+ }
353
+ else {
354
+ global.db.prepare(`UPDATE workstreams
317
355
  SET lease_expires_at = datetime('now'),
318
356
  next_retry_at = datetime('now'),
319
357
  last_reconcile_action = 'concurrency_resume',
320
358
  last_reconciled_at = datetime('now')
321
359
  WHERE id = ?`).run(throttled.id);
322
- resumed += 1;
360
+ resumed += 1;
361
+ }
323
362
  }
324
363
  }
325
364
  }
326
- }
327
- if (!dryRun) {
328
- const recovery = (0, wakeup_reconcile_js_1.reconcileParallelSessionRecovery)(global.db, project.path);
329
- if (recovery.workstreamsToRestart.length > 0) {
330
- for (const ws of recovery.workstreamsToRestart) {
331
- (0, wakeup_runner_js_1.restartWorkstreamRunner)(ws);
365
+ if (!dryRun) {
366
+ const recovery = (0, wakeup_reconcile_js_1.reconcileParallelSessionRecovery)(global.db, project.path);
367
+ if (recovery.workstreamsToRestart.length > 0) {
368
+ for (const ws of recovery.workstreamsToRestart) {
369
+ (0, wakeup_runner_js_1.restartWorkstreamRunner)(ws);
370
+ }
371
+ retrySummary += `, restarted ${recovery.workstreamsToRestart.length} workstream runner(s)`;
372
+ }
373
+ if (recovery.blockedWorkstreams > 0) {
374
+ retrySummary += `, blocked ${recovery.blockedWorkstreams} workstream(s)`;
375
+ }
376
+ if (scaledDown > 0) {
377
+ retrySummary += `, scaled down ${scaledDown} idle runner(s) to maxClones=${configuredMaxClones}`;
378
+ }
379
+ if (resumed > 0) {
380
+ retrySummary += `, resumed ${resumed} throttled workstream(s)`;
381
+ }
382
+ // Re-check activity after recovery. If reconciliation cleared stale
383
+ // session state for this project, continue to normal startup logic.
384
+ if (!(0, wakeup_checks_js_1.hasActiveParallelSessionForProject)(project.path)) {
385
+ skipForParallelSession = false;
386
+ if (retrySummary.length > 0) {
387
+ retrySummary += ', session state reconciled';
388
+ }
389
+ else {
390
+ retrySummary = ', session state reconciled';
391
+ }
332
392
  }
333
- retrySummary += `, restarted ${recovery.workstreamsToRestart.length} workstream runner(s)`;
334
- }
335
- if (recovery.blockedWorkstreams > 0) {
336
- retrySummary += `, blocked ${recovery.blockedWorkstreams} workstream(s)`;
337
- }
338
- if (scaledDown > 0) {
339
- retrySummary += `, scaled down ${scaledDown} idle runner(s) to maxClones=${configuredMaxClones}`;
340
- }
341
- if (resumed > 0) {
342
- retrySummary += `, resumed ${resumed} throttled workstream(s)`;
343
393
  }
344
- // Re-check activity after recovery. If reconciliation cleared stale
345
- // session state for this project, continue to normal startup logic.
346
- if (!(0, wakeup_checks_js_1.hasActiveParallelSessionForProject)(project.path)) {
347
- skipForParallelSession = false;
348
- if (retrySummary.length > 0) {
349
- retrySummary += ', session state reconciled';
394
+ else {
395
+ if (wouldScaleDown > 0) {
396
+ retrySummary += `, would scale down ${wouldScaleDown} idle runner(s) to maxClones=${configuredMaxClones}`;
350
397
  }
351
- else {
352
- retrySummary = ', session state reconciled';
398
+ if (wouldResume > 0) {
399
+ retrySummary += `, would resume ${wouldResume} throttled workstream(s)`;
353
400
  }
354
401
  }
355
- }
356
- else {
357
- if (wouldScaleDown > 0) {
358
- retrySummary += `, would scale down ${wouldScaleDown} idle runner(s) to maxClones=${configuredMaxClones}`;
402
+ if (!skipForParallelSession) {
403
+ log(`Reconciled stale parallel session for ${project.path}; proceeding with startup`);
359
404
  }
360
- if (wouldResume > 0) {
361
- retrySummary += `, would resume ${wouldResume} throttled workstream(s)`;
405
+ else {
406
+ log(`Skipping ${project.path}: active parallel session in progress${retrySummary}`);
407
+ results.push({
408
+ action: 'none',
409
+ reason: `Parallel session already running${retrySummary}`,
410
+ projectPath: project.path,
411
+ deletedInvocationLogs,
412
+ });
413
+ continue;
362
414
  }
363
415
  }
364
- if (!skipForParallelSession) {
365
- log(`Reconciled stale parallel session for ${project.path}; proceeding with startup`);
366
- }
367
- else {
368
- log(`Skipping ${project.path}: active parallel session in progress${retrySummary}`);
369
- results.push({
370
- action: 'none',
371
- reason: `Parallel session already running${retrySummary}`,
372
- projectPath: project.path,
373
- deletedInvocationLogs,
374
- });
375
- continue;
376
- }
377
- }
378
- // Config-aware mode reconciliation:
379
- // if parallel is enabled but an idle standalone runner is active, recycle it
380
- // so wakeup applies current parallel settings without manual restart.
381
- const activeStandaloneRunner = global.db
382
- .prepare(`SELECT id, pid, status, current_task_id
416
+ // Config-aware mode reconciliation:
417
+ // if parallel is enabled but an idle standalone runner is active, recycle it
418
+ // so wakeup applies current parallel settings without manual restart.
419
+ const activeStandaloneRunner = global.db
420
+ .prepare(`SELECT id, pid, status, current_task_id
383
421
  FROM runners
384
422
  WHERE project_path = ?
385
423
  AND parallel_session_id IS NULL
@@ -387,113 +425,114 @@ async function wakeup(options = {}) {
387
425
  AND heartbeat_at > datetime('now', '-5 minutes')
388
426
  ORDER BY heartbeat_at DESC
389
427
  LIMIT 1`)
390
- .get(project.path);
391
- if (activeStandaloneRunner && parallelEnabled) {
392
- const isIdle = (activeStandaloneRunner.status ?? '').toLowerCase() === 'idle' && !activeStandaloneRunner.current_task_id;
393
- if (isIdle) {
394
- if (dryRun) {
395
- log(`Would recycle idle standalone runner for ${project.path} to apply parallel mode`);
396
- results.push({
397
- action: 'would_start',
398
- reason: 'Would restart idle runner to apply parallel mode',
399
- projectPath: project.path,
400
- deletedInvocationLogs,
401
- });
428
+ .get(project.path);
429
+ if (activeStandaloneRunner && parallelEnabled) {
430
+ const isIdle = (activeStandaloneRunner.status ?? '').toLowerCase() === 'idle' && !activeStandaloneRunner.current_task_id;
431
+ if (isIdle) {
432
+ if (dryRun) {
433
+ log(`Would recycle idle standalone runner for ${project.path} to apply parallel mode`);
434
+ results.push({
435
+ action: 'would_start',
436
+ reason: 'Would restart idle runner to apply parallel mode',
437
+ projectPath: project.path,
438
+ deletedInvocationLogs,
439
+ });
440
+ continue;
441
+ }
442
+ if (activeStandaloneRunner.pid) {
443
+ (0, wakeup_runner_js_1.killProcess)(activeStandaloneRunner.pid);
444
+ }
445
+ global.db.prepare('DELETE FROM runners WHERE id = ?').run(activeStandaloneRunner.id);
446
+ const restartResult = (0, wakeup_runner_js_1.startRunner)(project.path);
447
+ if (restartResult) {
448
+ results.push({
449
+ action: 'restarted',
450
+ reason: 'Restarted idle runner to apply parallel mode',
451
+ pid: restartResult.pid,
452
+ projectPath: project.path,
453
+ deletedInvocationLogs,
454
+ });
455
+ }
456
+ else {
457
+ results.push({
458
+ action: 'none',
459
+ reason: 'Failed to restart idle runner for parallel mode',
460
+ projectPath: project.path,
461
+ deletedInvocationLogs,
462
+ });
463
+ }
402
464
  continue;
403
465
  }
404
- if (activeStandaloneRunner.pid) {
405
- (0, wakeup_runner_js_1.killProcess)(activeStandaloneRunner.pid);
406
- }
407
- global.db.prepare('DELETE FROM runners WHERE id = ?').run(activeStandaloneRunner.id);
408
- const restartResult = (0, wakeup_runner_js_1.startRunner)(project.path);
409
- if (restartResult) {
410
- results.push({
411
- action: 'restarted',
412
- reason: 'Restarted idle runner to apply parallel mode',
413
- pid: restartResult.pid,
414
- projectPath: project.path,
415
- deletedInvocationLogs,
416
- });
417
- }
418
- else {
419
- results.push({
420
- action: 'none',
421
- reason: 'Failed to restart idle runner for parallel mode',
422
- projectPath: project.path,
423
- deletedInvocationLogs,
424
- });
425
- }
466
+ }
467
+ // Skip if project already has an active runner (after recovery, which may have killed/removed it).
468
+ if ((0, wakeup_checks_js_1.hasActiveRunnerForProject)(project.path)) {
469
+ log(`Skipping ${project.path}: runner already active`);
470
+ results.push({
471
+ action: 'none',
472
+ reason: recoveredActions > 0
473
+ ? `Runner already active (recovered ${recoveredActions} stuck item(s))`
474
+ : 'Runner already active',
475
+ projectPath: project.path,
476
+ recoveredActions,
477
+ skippedRecoveryDueToSafetyLimit,
478
+ deletedInvocationLogs,
479
+ sanitisedActions,
480
+ });
426
481
  continue;
427
482
  }
483
+ // Start runner for this project
484
+ const willParallel = parallelEnabled;
485
+ log(`Starting ${willParallel ? 'parallel session' : 'runner'} for: ${project.path}`);
486
+ if (dryRun) {
487
+ results.push({
488
+ action: 'would_start',
489
+ reason: recoveredActions > 0 ? `Recovered ${recoveredActions} stuck item(s); would start runner (dry-run)` : `Would start runner (dry-run)`,
490
+ projectPath: project.path,
491
+ recoveredActions,
492
+ skippedRecoveryDueToSafetyLimit,
493
+ deletedInvocationLogs,
494
+ sanitisedActions,
495
+ });
496
+ continue;
497
+ }
498
+ const startResult = (0, wakeup_runner_js_1.startRunner)(project.path);
499
+ if (startResult) {
500
+ const mode = startResult.parallel ? 'parallel session' : 'runner';
501
+ results.push({
502
+ action: 'started',
503
+ reason: recoveredActions > 0 ? `Recovered ${recoveredActions} stuck item(s); started ${mode}` : `Started ${mode}`,
504
+ pid: startResult.pid,
505
+ projectPath: project.path,
506
+ recoveredActions,
507
+ skippedRecoveryDueToSafetyLimit,
508
+ deletedInvocationLogs,
509
+ sanitisedActions,
510
+ });
511
+ }
512
+ else {
513
+ results.push({
514
+ action: 'none',
515
+ reason: recoveredActions > 0 ? `Recovered ${recoveredActions} stuck item(s); failed to start runner` : 'Failed to start runner',
516
+ projectPath: project.path,
517
+ recoveredActions,
518
+ skippedRecoveryDueToSafetyLimit,
519
+ deletedInvocationLogs,
520
+ sanitisedActions,
521
+ });
522
+ }
428
523
  }
429
- // Skip if project already has an active runner (after recovery, which may have killed/removed it).
430
- if ((0, wakeup_checks_js_1.hasActiveRunnerForProject)(project.path)) {
431
- log(`Skipping ${project.path}: runner already active`);
432
- results.push({
433
- action: 'none',
434
- reason: recoveredActions > 0
435
- ? `Runner already active (recovered ${recoveredActions} stuck item(s))`
436
- : 'Runner already active',
437
- projectPath: project.path,
438
- recoveredActions,
439
- skippedRecoveryDueToSafetyLimit,
440
- deletedInvocationLogs,
441
- sanitisedActions,
442
- });
443
- continue;
444
- }
445
- // Start runner for this project
446
- const willParallel = parallelEnabled;
447
- log(`Starting ${willParallel ? 'parallel session' : 'runner'} for: ${project.path}`);
448
- if (dryRun) {
449
- results.push({
450
- action: 'would_start',
451
- reason: recoveredActions > 0 ? `Recovered ${recoveredActions} stuck item(s); would start runner (dry-run)` : `Would start runner (dry-run)`,
452
- projectPath: project.path,
453
- recoveredActions,
454
- skippedRecoveryDueToSafetyLimit,
455
- deletedInvocationLogs,
456
- sanitisedActions,
457
- });
458
- continue;
459
- }
460
- const startResult = (0, wakeup_runner_js_1.startRunner)(project.path);
461
- if (startResult) {
462
- const mode = startResult.parallel ? 'parallel session' : 'runner';
463
- results.push({
464
- action: 'started',
465
- reason: recoveredActions > 0 ? `Recovered ${recoveredActions} stuck item(s); started ${mode}` : `Started ${mode}`,
466
- pid: startResult.pid,
467
- projectPath: project.path,
468
- recoveredActions,
469
- skippedRecoveryDueToSafetyLimit,
470
- deletedInvocationLogs,
471
- sanitisedActions,
472
- });
473
- }
474
- else {
524
+ // If no specific results, add a summary
525
+ if (results.length === 0) {
475
526
  results.push({
476
527
  action: 'none',
477
- reason: recoveredActions > 0 ? `Recovered ${recoveredActions} stuck item(s); failed to start runner` : 'Failed to start runner',
478
- projectPath: project.path,
479
- recoveredActions,
480
- skippedRecoveryDueToSafetyLimit,
481
- deletedInvocationLogs,
482
- sanitisedActions,
528
+ reason: 'No action needed',
483
529
  });
484
530
  }
485
- }
486
- // If no specific results, add a summary
487
- if (results.length === 0) {
488
- results.push({
489
- action: 'none',
490
- reason: 'No action needed',
491
- });
492
- }
493
- return results;
531
+ return results;
532
+ });
494
533
  }
495
534
  finally {
496
- global.close();
535
+ isWakeupRunning = false;
497
536
  }
498
537
  }
499
538
  /**
@@ -502,8 +541,7 @@ async function wakeup(options = {}) {
502
541
  async function checkWakeupNeeded() {
503
542
  const lockStatus = (0, lock_js_1.checkLockStatus)();
504
543
  if (lockStatus.locked && lockStatus.pid) {
505
- const { db, close } = (0, global_db_js_1.openGlobalDatabase)();
506
- try {
544
+ return (0, global_db_js_1.withGlobalDatabase)(async (db) => {
507
545
  const staleRunners = (0, heartbeat_js_1.findStaleRunners)(db);
508
546
  if (staleRunners.length > 0) {
509
547
  return {
@@ -512,10 +550,7 @@ async function checkWakeupNeeded() {
512
550
  };
513
551
  }
514
552
  return { needed: false, reason: 'Runner is healthy' };
515
- }
516
- finally {
517
- close();
518
- }
553
+ });
519
554
  }
520
555
  if (lockStatus.isZombie) {
521
556
  return { needed: true, reason: 'Zombie lock needs cleanup' };