steroids-cli 0.6.5 → 0.8.0

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 (84) hide show
  1. package/dist/cleanup/invocation-logs.d.ts +30 -0
  2. package/dist/cleanup/invocation-logs.d.ts.map +1 -0
  3. package/dist/cleanup/invocation-logs.js +69 -0
  4. package/dist/cleanup/invocation-logs.js.map +1 -0
  5. package/dist/commands/cleanup.d.ts +3 -0
  6. package/dist/commands/cleanup.d.ts.map +1 -0
  7. package/dist/commands/cleanup.js +116 -0
  8. package/dist/commands/cleanup.js.map +1 -0
  9. package/dist/commands/completion.d.ts.map +1 -1
  10. package/dist/commands/completion.js +6 -1
  11. package/dist/commands/completion.js.map +1 -1
  12. package/dist/commands/health-stuck.d.ts +5 -0
  13. package/dist/commands/health-stuck.d.ts.map +1 -0
  14. package/dist/commands/health-stuck.js +312 -0
  15. package/dist/commands/health-stuck.js.map +1 -0
  16. package/dist/commands/health.d.ts.map +1 -1
  17. package/dist/commands/health.js +14 -1
  18. package/dist/commands/health.js.map +1 -1
  19. package/dist/config/loader.d.ts +8 -0
  20. package/dist/config/loader.d.ts.map +1 -1
  21. package/dist/config/loader.js +14 -0
  22. package/dist/config/loader.js.map +1 -1
  23. package/dist/config/schema.d.ts.map +1 -1
  24. package/dist/config/schema.js +40 -0
  25. package/dist/config/schema.js.map +1 -1
  26. package/dist/database/queries.d.ts +2 -0
  27. package/dist/database/queries.d.ts.map +1 -1
  28. package/dist/database/queries.js +15 -1
  29. package/dist/database/queries.js.map +1 -1
  30. package/dist/database/schema.d.ts +2 -2
  31. package/dist/database/schema.d.ts.map +1 -1
  32. package/dist/database/schema.js +26 -0
  33. package/dist/database/schema.js.map +1 -1
  34. package/dist/health/stuck-task-detector.d.ts +131 -0
  35. package/dist/health/stuck-task-detector.d.ts.map +1 -0
  36. package/dist/health/stuck-task-detector.js +238 -0
  37. package/dist/health/stuck-task-detector.js.map +1 -0
  38. package/dist/health/stuck-task-recovery.d.ts +45 -0
  39. package/dist/health/stuck-task-recovery.d.ts.map +1 -0
  40. package/dist/health/stuck-task-recovery.js +312 -0
  41. package/dist/health/stuck-task-recovery.js.map +1 -0
  42. package/dist/index.js +5 -0
  43. package/dist/index.js.map +1 -1
  44. package/dist/migrations/manifest.d.ts.map +1 -1
  45. package/dist/migrations/manifest.js +2 -1
  46. package/dist/migrations/manifest.js.map +1 -1
  47. package/dist/orchestrator/coder.d.ts.map +1 -1
  48. package/dist/orchestrator/coder.js +11 -11
  49. package/dist/orchestrator/coder.js.map +1 -1
  50. package/dist/orchestrator/coordinator.d.ts.map +1 -1
  51. package/dist/orchestrator/coordinator.js +3 -4
  52. package/dist/orchestrator/coordinator.js.map +1 -1
  53. package/dist/orchestrator/invoke.js +11 -11
  54. package/dist/orchestrator/invoke.js.map +1 -1
  55. package/dist/orchestrator/reviewer.d.ts.map +1 -1
  56. package/dist/orchestrator/reviewer.js +11 -11
  57. package/dist/orchestrator/reviewer.js.map +1 -1
  58. package/dist/providers/claude.d.ts.map +1 -1
  59. package/dist/providers/claude.js +5 -2
  60. package/dist/providers/claude.js.map +1 -1
  61. package/dist/providers/codex.d.ts.map +1 -1
  62. package/dist/providers/codex.js +44 -2
  63. package/dist/providers/codex.js.map +1 -1
  64. package/dist/providers/gemini.d.ts.map +1 -1
  65. package/dist/providers/gemini.js +5 -2
  66. package/dist/providers/gemini.js.map +1 -1
  67. package/dist/providers/interface.d.ts +12 -0
  68. package/dist/providers/interface.d.ts.map +1 -1
  69. package/dist/providers/interface.js.map +1 -1
  70. package/dist/providers/invocation-logger.d.ts +7 -24
  71. package/dist/providers/invocation-logger.d.ts.map +1 -1
  72. package/dist/providers/invocation-logger.js +132 -44
  73. package/dist/providers/invocation-logger.js.map +1 -1
  74. package/dist/providers/openai.d.ts.map +1 -1
  75. package/dist/providers/openai.js +5 -2
  76. package/dist/providers/openai.js.map +1 -1
  77. package/dist/runners/wakeup.d.ts +3 -0
  78. package/dist/runners/wakeup.d.ts.map +1 -1
  79. package/dist/runners/wakeup.js +163 -93
  80. package/dist/runners/wakeup.js.map +1 -1
  81. package/migrations/009_add_incidents_and_failure_tracking.sql +35 -0
  82. package/migrations/010_add_lifecycle_timestamps.sql +27 -0
  83. package/migrations/manifest.json +17 -1
  84. package/package.json +1 -1
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Core stuck-task detection logic (detection only, no recovery).
3
+ *
4
+ * This implementation intentionally uses the project's current schemas:
5
+ * - Project DB: tasks, audit, task_invocations
6
+ * - Global DB: runners
7
+ *
8
+ * The design doc (docs/stuck-task-detection.md) describes additional fields/tables
9
+ * (incidents, invocation started/completed timestamps, last_tool_execution, etc.).
10
+ * Those are not present in the current repo schema, so we derive signals from
11
+ * existing timestamps (tasks.updated_at, task_invocations.created_at, runners.heartbeat_at).
12
+ */
13
+ import type Database from 'better-sqlite3';
14
+ export type FailureMode = 'orphaned_task' | 'hanging_invocation' | 'zombie_runner' | 'dead_runner' | 'db_inconsistency';
15
+ export interface StuckTaskDetectionConfig {
16
+ /**
17
+ * How long a task may remain `in_progress` without a recent invocation and without
18
+ * an active runner assigned before being considered orphaned.
19
+ */
20
+ orphanedTaskTimeoutSec?: number;
21
+ /**
22
+ * Maximum allowed time a task may remain `in_progress` while an active runner is
23
+ * actively working on it (approximates "hanging coder invocation" without
24
+ * started/completed timestamps).
25
+ */
26
+ maxCoderDurationSec?: number;
27
+ /**
28
+ * Maximum allowed time a task may remain `review` while an active runner is
29
+ * actively working on it (approximates "hanging reviewer invocation").
30
+ */
31
+ maxReviewerDurationSec?: number;
32
+ /** Runner heartbeat staleness threshold. */
33
+ runnerHeartbeatTimeoutSec?: number;
34
+ /**
35
+ * How long since last task invocation to consider a task "inactive" for orphan checks.
36
+ * This is compared against task_invocations.created_at.
37
+ */
38
+ invocationStalenessSec?: number;
39
+ /** Recent-update window used for "DB inconsistency" transient detection. */
40
+ dbInconsistencyRecentUpdateSec?: number;
41
+ }
42
+ export interface OrphanedTaskSignal {
43
+ failureMode: 'orphaned_task';
44
+ taskId: string;
45
+ title: string;
46
+ status: 'in_progress';
47
+ updatedAt: Date;
48
+ secondsSinceUpdate: number;
49
+ invocationCount: number;
50
+ lastInvocationAt: Date | null;
51
+ hasActiveRunner: boolean;
52
+ }
53
+ export interface HangingTaskSignal {
54
+ failureMode: 'hanging_invocation';
55
+ phase: 'coder' | 'reviewer';
56
+ taskId: string;
57
+ title: string;
58
+ status: 'in_progress' | 'review';
59
+ updatedAt: Date;
60
+ secondsSinceUpdate: number;
61
+ runnerId: string;
62
+ runnerPid: number | null;
63
+ runnerHeartbeatAt: Date;
64
+ }
65
+ export interface ZombieRunnerSignal {
66
+ failureMode: 'zombie_runner';
67
+ runnerId: string;
68
+ pid: number | null;
69
+ status: string;
70
+ projectPath: string | null;
71
+ currentTaskId: string | null;
72
+ heartbeatAt: Date;
73
+ secondsSinceHeartbeat: number;
74
+ }
75
+ export interface DeadRunnerSignal {
76
+ failureMode: 'dead_runner';
77
+ runnerId: string;
78
+ pid: number | null;
79
+ status: string;
80
+ projectPath: string | null;
81
+ currentTaskId: string | null;
82
+ heartbeatAt: Date;
83
+ secondsSinceHeartbeat: number;
84
+ }
85
+ export interface DbInconsistencySignal {
86
+ failureMode: 'db_inconsistency';
87
+ taskId: string;
88
+ title: string;
89
+ status: 'in_progress';
90
+ updatedAt: Date;
91
+ secondsSinceUpdate: number;
92
+ invocationCount: 0;
93
+ }
94
+ export interface StuckTaskDetectionReport {
95
+ timestamp: Date;
96
+ orphanedTasks: OrphanedTaskSignal[];
97
+ hangingInvocations: HangingTaskSignal[];
98
+ zombieRunners: ZombieRunnerSignal[];
99
+ deadRunners: DeadRunnerSignal[];
100
+ dbInconsistencies: DbInconsistencySignal[];
101
+ }
102
+ export interface DetectStuckTasksOptions {
103
+ /** Absolute project path as stored in global runners DB. */
104
+ projectPath: string;
105
+ /** Project-local database connection (tasks, task_invocations). */
106
+ projectDb: Database.Database;
107
+ /** Global database connection (runners). */
108
+ globalDb: Database.Database;
109
+ /** Optional config overrides. */
110
+ config?: StuckTaskDetectionConfig;
111
+ /**
112
+ * PID liveness check override for testing.
113
+ * If omitted, a best-effort `process.kill(pid, 0)` check is used.
114
+ */
115
+ isPidAlive?: (pid: number) => boolean;
116
+ /** Override current time for deterministic tests. */
117
+ now?: Date;
118
+ }
119
+ /**
120
+ * Parse SQLite datetime('now') strings (YYYY-MM-DD HH:MM:SS) as UTC.
121
+ * Node's Date parser can treat "YYYY-MM-DD HH:MM:SS" as local time depending on runtime,
122
+ * so we normalize to ISO 8601 Zulu.
123
+ */
124
+ export declare function parseSqliteDateTimeUtc(value: string): Date;
125
+ /**
126
+ * Format a Date as SQLite datetime('now')-compatible UTC string: YYYY-MM-DD HH:MM:SS
127
+ * This keeps comparisons lexicographically safe against stored SQLite timestamps.
128
+ */
129
+ export declare function formatSqliteDateTimeUtc(date: Date): string;
130
+ export declare function detectStuckTasks(options: DetectStuckTasksOptions): StuckTaskDetectionReport;
131
+ //# sourceMappingURL=stuck-task-detector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stuck-task-detector.d.ts","sourceRoot":"","sources":["../../src/health/stuck-task-detector.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,238 @@
1
+ "use strict";
2
+ /**
3
+ * Core stuck-task detection logic (detection only, no recovery).
4
+ *
5
+ * This implementation intentionally uses the project's current schemas:
6
+ * - Project DB: tasks, audit, task_invocations
7
+ * - Global DB: runners
8
+ *
9
+ * The design doc (docs/stuck-task-detection.md) describes additional fields/tables
10
+ * (incidents, invocation started/completed timestamps, last_tool_execution, etc.).
11
+ * Those are not present in the current repo schema, so we derive signals from
12
+ * existing timestamps (tasks.updated_at, task_invocations.created_at, runners.heartbeat_at).
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.parseSqliteDateTimeUtc = parseSqliteDateTimeUtc;
16
+ exports.formatSqliteDateTimeUtc = formatSqliteDateTimeUtc;
17
+ exports.detectStuckTasks = detectStuckTasks;
18
+ const DEFAULTS = {
19
+ orphanedTaskTimeoutSec: 600,
20
+ maxCoderDurationSec: 1800,
21
+ maxReviewerDurationSec: 900,
22
+ runnerHeartbeatTimeoutSec: 300,
23
+ invocationStalenessSec: 600,
24
+ dbInconsistencyRecentUpdateSec: 60,
25
+ };
26
+ function isProcessAliveBestEffort(pid) {
27
+ try {
28
+ // Signal 0: existence check only.
29
+ process.kill(pid, 0);
30
+ return true;
31
+ }
32
+ catch {
33
+ return false;
34
+ }
35
+ }
36
+ /**
37
+ * Parse SQLite datetime('now') strings (YYYY-MM-DD HH:MM:SS) as UTC.
38
+ * Node's Date parser can treat "YYYY-MM-DD HH:MM:SS" as local time depending on runtime,
39
+ * so we normalize to ISO 8601 Zulu.
40
+ */
41
+ function parseSqliteDateTimeUtc(value) {
42
+ // Already ISO? (e.g., 2026-02-10T13:45:00.000Z)
43
+ if (value.includes('T'))
44
+ return new Date(value);
45
+ // SQLite "YYYY-MM-DD HH:MM:SS" (UTC) => "YYYY-MM-DDTHH:MM:SSZ"
46
+ return new Date(value.replace(' ', 'T') + 'Z');
47
+ }
48
+ /**
49
+ * Format a Date as SQLite datetime('now')-compatible UTC string: YYYY-MM-DD HH:MM:SS
50
+ * This keeps comparisons lexicographically safe against stored SQLite timestamps.
51
+ */
52
+ function formatSqliteDateTimeUtc(date) {
53
+ // Date#toISOString is always UTC; trim milliseconds and replace T with space.
54
+ return date.toISOString().slice(0, 19).replace('T', ' ');
55
+ }
56
+ function secondsBetween(a, b) {
57
+ return Math.max(0, Math.floor((a.getTime() - b.getTime()) / 1000));
58
+ }
59
+ function mergeConfig(config) {
60
+ return { ...DEFAULTS, ...(config ?? {}) };
61
+ }
62
+ function detectStuckTasks(options) {
63
+ const now = options.now ?? new Date();
64
+ const cfg = mergeConfig(options.config);
65
+ const isPidAlive = options.isPidAlive ?? isProcessAliveBestEffort;
66
+ const zombieRunners = detectZombieRunnersInternal(options.globalDb, options.projectPath, cfg, now, isPidAlive);
67
+ const deadRunners = detectDeadRunnersInternal(options.globalDb, options.projectPath, cfg, now, isPidAlive);
68
+ const { orphanedTasks, hangingInvocations, dbInconsistencies } = detectTaskSignalsInternal(options.projectDb, options.globalDb, options.projectPath, cfg, now, isPidAlive);
69
+ return {
70
+ timestamp: now,
71
+ orphanedTasks,
72
+ hangingInvocations,
73
+ zombieRunners,
74
+ deadRunners,
75
+ dbInconsistencies,
76
+ };
77
+ }
78
+ function detectZombieRunnersInternal(globalDb, projectPath, cfg, now, isPidAlive) {
79
+ const cutoff = formatSqliteDateTimeUtc(new Date(now.getTime() - cfg.runnerHeartbeatTimeoutSec * 1000));
80
+ const rows = globalDb.prepare(`SELECT id, status, pid, project_path, current_task_id, heartbeat_at
81
+ FROM runners
82
+ WHERE status = 'running'
83
+ AND project_path = ?
84
+ AND heartbeat_at < ?`).all(projectPath, cutoff);
85
+ const result = [];
86
+ for (const row of rows) {
87
+ if (row.pid !== null && isPidAlive(row.pid)) {
88
+ const hb = parseSqliteDateTimeUtc(row.heartbeat_at);
89
+ result.push({
90
+ failureMode: 'zombie_runner',
91
+ runnerId: row.id,
92
+ pid: row.pid,
93
+ status: row.status,
94
+ projectPath: row.project_path,
95
+ currentTaskId: row.current_task_id,
96
+ heartbeatAt: hb,
97
+ secondsSinceHeartbeat: secondsBetween(now, hb),
98
+ });
99
+ }
100
+ }
101
+ return result;
102
+ }
103
+ function detectDeadRunnersInternal(globalDb, projectPath, cfg, now, isPidAlive) {
104
+ const rows = globalDb.prepare(`SELECT id, status, pid, project_path, current_task_id, heartbeat_at
105
+ FROM runners
106
+ WHERE status = 'running'
107
+ AND project_path = ?`).all(projectPath);
108
+ const result = [];
109
+ for (const row of rows) {
110
+ const alive = row.pid !== null && isPidAlive(row.pid);
111
+ if (!alive) {
112
+ const hb = parseSqliteDateTimeUtc(row.heartbeat_at);
113
+ result.push({
114
+ failureMode: 'dead_runner',
115
+ runnerId: row.id,
116
+ pid: row.pid,
117
+ status: row.status,
118
+ projectPath: row.project_path,
119
+ currentTaskId: row.current_task_id,
120
+ heartbeatAt: hb,
121
+ secondsSinceHeartbeat: secondsBetween(now, hb),
122
+ });
123
+ }
124
+ }
125
+ return result;
126
+ }
127
+ function detectTaskSignalsInternal(projectDb, globalDb, projectPath, cfg, now, isPidAlive) {
128
+ const orphanedTasks = [];
129
+ const hangingInvocations = [];
130
+ const dbInconsistencies = [];
131
+ // DB inconsistency (transient): in_progress, no invocations, very recently updated.
132
+ {
133
+ const cutoff = formatSqliteDateTimeUtc(new Date(now.getTime() - cfg.dbInconsistencyRecentUpdateSec * 1000));
134
+ const rows = projectDb.prepare(`SELECT t.id, t.title, t.status, t.updated_at
135
+ FROM tasks t
136
+ LEFT JOIN task_invocations i ON i.task_id = t.id AND i.role = 'coder'
137
+ WHERE t.status = 'in_progress'
138
+ GROUP BY t.id
139
+ HAVING COUNT(i.id) = 0
140
+ AND t.updated_at >= ?`).all(cutoff);
141
+ for (const row of rows) {
142
+ const updatedAt = parseSqliteDateTimeUtc(row.updated_at);
143
+ dbInconsistencies.push({
144
+ failureMode: 'db_inconsistency',
145
+ taskId: row.id,
146
+ title: row.title,
147
+ status: 'in_progress',
148
+ updatedAt,
149
+ secondsSinceUpdate: secondsBetween(now, updatedAt),
150
+ invocationCount: 0,
151
+ });
152
+ }
153
+ }
154
+ // Orphaned tasks: in_progress, stale updated_at, and no recent invocations, and no active runner assigned.
155
+ {
156
+ const taskCutoff = formatSqliteDateTimeUtc(new Date(now.getTime() - cfg.orphanedTaskTimeoutSec * 1000));
157
+ const invocationCutoff = formatSqliteDateTimeUtc(new Date(now.getTime() - cfg.invocationStalenessSec * 1000));
158
+ const rows = projectDb.prepare(`SELECT
159
+ t.id,
160
+ t.title,
161
+ t.status,
162
+ t.updated_at,
163
+ COUNT(i.id) as invocation_count,
164
+ MAX(i.created_at) as last_invocation_at
165
+ FROM tasks t
166
+ LEFT JOIN task_invocations i ON i.task_id = t.id AND i.role = 'coder'
167
+ WHERE t.status = 'in_progress'
168
+ AND t.updated_at < ?
169
+ GROUP BY t.id
170
+ HAVING COUNT(i.id) = 0
171
+ OR MAX(i.created_at) < ?`).all(taskCutoff, invocationCutoff);
172
+ for (const row of rows) {
173
+ const updatedAt = parseSqliteDateTimeUtc(row.updated_at);
174
+ const lastInvocationAt = row.last_invocation_at ? parseSqliteDateTimeUtc(row.last_invocation_at) : null;
175
+ const activeRunner = getActiveRunnerForTask(globalDb, projectPath, row.id, cfg, now, isPidAlive);
176
+ if (!activeRunner) {
177
+ orphanedTasks.push({
178
+ failureMode: 'orphaned_task',
179
+ taskId: row.id,
180
+ title: row.title,
181
+ status: 'in_progress',
182
+ updatedAt,
183
+ secondsSinceUpdate: secondsBetween(now, updatedAt),
184
+ invocationCount: row.invocation_count,
185
+ lastInvocationAt,
186
+ hasActiveRunner: false,
187
+ });
188
+ }
189
+ }
190
+ }
191
+ // Hanging invocations: in_progress/review for too long with an active runner currently executing the task.
192
+ // Approximates "invocation started but not completed" using task age + runner current_task_id.
193
+ {
194
+ const coderCutoff = formatSqliteDateTimeUtc(new Date(now.getTime() - cfg.maxCoderDurationSec * 1000));
195
+ const reviewerCutoff = formatSqliteDateTimeUtc(new Date(now.getTime() - cfg.maxReviewerDurationSec * 1000));
196
+ const rows = projectDb.prepare(`SELECT t.id, t.title, t.status, t.updated_at
197
+ FROM tasks t
198
+ WHERE (t.status = 'in_progress' AND t.updated_at < ?)
199
+ OR (t.status = 'review' AND t.updated_at < ?)`).all(coderCutoff, reviewerCutoff);
200
+ for (const row of rows) {
201
+ const activeRunner = getActiveRunnerForTask(globalDb, projectPath, row.id, cfg, now, isPidAlive);
202
+ if (!activeRunner)
203
+ continue;
204
+ const updatedAt = parseSqliteDateTimeUtc(row.updated_at);
205
+ const hbAt = parseSqliteDateTimeUtc(activeRunner.heartbeat_at);
206
+ hangingInvocations.push({
207
+ failureMode: 'hanging_invocation',
208
+ phase: row.status === 'review' ? 'reviewer' : 'coder',
209
+ taskId: row.id,
210
+ title: row.title,
211
+ status: row.status,
212
+ updatedAt,
213
+ secondsSinceUpdate: secondsBetween(now, updatedAt),
214
+ runnerId: activeRunner.id,
215
+ runnerPid: activeRunner.pid,
216
+ runnerHeartbeatAt: hbAt,
217
+ });
218
+ }
219
+ }
220
+ return { orphanedTasks, hangingInvocations, dbInconsistencies };
221
+ }
222
+ function getActiveRunnerForTask(globalDb, projectPath, taskId, cfg, now, isPidAlive) {
223
+ const cutoff = formatSqliteDateTimeUtc(new Date(now.getTime() - cfg.runnerHeartbeatTimeoutSec * 1000));
224
+ const row = globalDb.prepare(`SELECT id, pid, heartbeat_at
225
+ FROM runners
226
+ WHERE project_path = ?
227
+ AND current_task_id = ?
228
+ AND status = 'running'
229
+ AND heartbeat_at >= ?
230
+ ORDER BY heartbeat_at DESC
231
+ LIMIT 1`).get(projectPath, taskId, cutoff);
232
+ if (!row)
233
+ return null;
234
+ if (row.pid !== null && !isPidAlive(row.pid))
235
+ return null;
236
+ return row;
237
+ }
238
+ //# sourceMappingURL=stuck-task-detector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stuck-task-detector.js","sourceRoot":"","sources":["../../src/health/stuck-task-detector.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Automatic recovery actions for stuck tasks.
3
+ *
4
+ * This builds on detectStuckTasks() and applies a conservative set of actions:
5
+ * - orphaned_task: reset task to pending, release any task lock, increment failure_count, log incident
6
+ * - hanging_invocation: kill runner process, remove runner row, reset task (or skip after repeated failures)
7
+ * - zombie_runner/dead_runner: stop runner (best-effort) and reset its current_task_id (if any)
8
+ *
9
+ * Note: The repo intentionally approximates "invocation started/completed" using tasks.updated_at,
10
+ * task_invocations.created_at, and runners.heartbeat_at. Recovery follows those same signals.
11
+ */
12
+ import type Database from 'better-sqlite3';
13
+ import type { SteroidsConfig } from '../config/loader.js';
14
+ import { type StuckTaskDetectionConfig, type StuckTaskDetectionReport } from './stuck-task-detector.js';
15
+ export interface StuckTaskRecoveryConfig extends StuckTaskDetectionConfig {
16
+ autoRecover?: boolean;
17
+ maxRecoveryAttempts?: number;
18
+ maxIncidentsPerHour?: number;
19
+ killGraceMs?: number;
20
+ }
21
+ export type RecoveryResolution = 'auto_restart' | 'skipped' | 'escalated' | 'none';
22
+ export interface RecoveryAction {
23
+ kind: 'task' | 'runner';
24
+ targetId: string;
25
+ failureMode: string;
26
+ resolution: RecoveryResolution;
27
+ reason: string;
28
+ }
29
+ export interface RecoverStuckTasksOptions {
30
+ projectPath: string;
31
+ projectDb: Database.Database;
32
+ globalDb: Database.Database;
33
+ config?: SteroidsConfig;
34
+ now?: Date;
35
+ dryRun?: boolean;
36
+ isPidAlive?: (pid: number) => boolean;
37
+ killPid?: (pid: number, graceMs: number) => Promise<boolean>;
38
+ }
39
+ export interface RecoverStuckTasksResult {
40
+ report: StuckTaskDetectionReport;
41
+ actions: RecoveryAction[];
42
+ skippedDueToSafetyLimit: boolean;
43
+ }
44
+ export declare function recoverStuckTasks(options: RecoverStuckTasksOptions): Promise<RecoverStuckTasksResult>;
45
+ //# sourceMappingURL=stuck-task-recovery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stuck-task-recovery.d.ts","sourceRoot":"","sources":["../../src/health/stuck-task-recovery.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAE3C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAG1D,OAAO,EAEL,KAAK,wBAAwB,EAC7B,KAAK,wBAAwB,EAK9B,MAAM,0BAA0B,CAAC;AAElC,MAAM,WAAW,uBAAwB,SAAQ,wBAAwB;IACvE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,MAAM,kBAAkB,GAAG,cAAc,GAAG,SAAS,GAAG,WAAW,GAAG,MAAM,CAAC;AAEnF,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,GAAG,QAAQ,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,kBAAkB,CAAC;IAC/B,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,wBAAwB;IACvC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,QAAQ,CAAC,QAAQ,CAAC;IAC7B,QAAQ,EAAE,QAAQ,CAAC,QAAQ,CAAC;IAC5B,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,GAAG,CAAC,EAAE,IAAI,CAAC;IACX,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IACtC,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CAC9D;AAED,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,wBAAwB,CAAC;IACjC,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,uBAAuB,EAAE,OAAO,CAAC;CAClC;AAuTD,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAqD3G"}