steroids-api 0.2.7
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/dist/API/src/index.d.ts +10 -0
- package/dist/API/src/index.d.ts.map +1 -0
- package/dist/API/src/index.js +130 -0
- package/dist/API/src/index.js.map +1 -0
- package/dist/API/src/routes/activity.d.ts +7 -0
- package/dist/API/src/routes/activity.d.ts.map +1 -0
- package/dist/API/src/routes/activity.js +252 -0
- package/dist/API/src/routes/activity.js.map +1 -0
- package/dist/API/src/routes/config.d.ts +7 -0
- package/dist/API/src/routes/config.d.ts.map +1 -0
- package/dist/API/src/routes/config.js +521 -0
- package/dist/API/src/routes/config.js.map +1 -0
- package/dist/API/src/routes/health.d.ts +7 -0
- package/dist/API/src/routes/health.d.ts.map +1 -0
- package/dist/API/src/routes/health.js +172 -0
- package/dist/API/src/routes/health.js.map +1 -0
- package/dist/API/src/routes/incidents.d.ts +7 -0
- package/dist/API/src/routes/incidents.d.ts.map +1 -0
- package/dist/API/src/routes/incidents.js +117 -0
- package/dist/API/src/routes/incidents.js.map +1 -0
- package/dist/API/src/routes/projects.d.ts +7 -0
- package/dist/API/src/routes/projects.d.ts.map +1 -0
- package/dist/API/src/routes/projects.js +398 -0
- package/dist/API/src/routes/projects.js.map +1 -0
- package/dist/API/src/routes/runners.d.ts +7 -0
- package/dist/API/src/routes/runners.d.ts.map +1 -0
- package/dist/API/src/routes/runners.js +242 -0
- package/dist/API/src/routes/runners.js.map +1 -0
- package/dist/API/src/routes/tasks.d.ts +7 -0
- package/dist/API/src/routes/tasks.d.ts.map +1 -0
- package/dist/API/src/routes/tasks.js +1007 -0
- package/dist/API/src/routes/tasks.js.map +1 -0
- package/dist/API/src/utils/validation.d.ts +22 -0
- package/dist/API/src/utils/validation.d.ts.map +1 -0
- package/dist/API/src/utils/validation.js +50 -0
- package/dist/API/src/utils/validation.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +184 -0
- package/dist/index.js.map +1 -0
- package/dist/routes/activity.d.ts +7 -0
- package/dist/routes/activity.d.ts.map +1 -0
- package/dist/routes/activity.js +252 -0
- package/dist/routes/activity.js.map +1 -0
- package/dist/routes/config.d.ts +7 -0
- package/dist/routes/config.d.ts.map +1 -0
- package/dist/routes/config.js +647 -0
- package/dist/routes/config.js.map +1 -0
- package/dist/routes/credit-alerts.d.ts +2 -0
- package/dist/routes/credit-alerts.d.ts.map +1 -0
- package/dist/routes/credit-alerts.js +97 -0
- package/dist/routes/credit-alerts.js.map +1 -0
- package/dist/routes/health.d.ts +7 -0
- package/dist/routes/health.d.ts.map +1 -0
- package/dist/routes/health.js +200 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/incidents.d.ts +7 -0
- package/dist/routes/incidents.d.ts.map +1 -0
- package/dist/routes/incidents.js +117 -0
- package/dist/routes/incidents.js.map +1 -0
- package/dist/routes/projects.d.ts +7 -0
- package/dist/routes/projects.d.ts.map +1 -0
- package/dist/routes/projects.js +643 -0
- package/dist/routes/projects.js.map +1 -0
- package/dist/routes/runners.d.ts +7 -0
- package/dist/routes/runners.d.ts.map +1 -0
- package/dist/routes/runners.js +299 -0
- package/dist/routes/runners.js.map +1 -0
- package/dist/routes/skills.d.ts +3 -0
- package/dist/routes/skills.d.ts.map +1 -0
- package/dist/routes/skills.js +109 -0
- package/dist/routes/skills.js.map +1 -0
- package/dist/routes/storage.d.ts +7 -0
- package/dist/routes/storage.d.ts.map +1 -0
- package/dist/routes/storage.js +93 -0
- package/dist/routes/storage.js.map +1 -0
- package/dist/routes/tasks.d.ts +7 -0
- package/dist/routes/tasks.d.ts.map +1 -0
- package/dist/routes/tasks.js +1145 -0
- package/dist/routes/tasks.js.map +1 -0
- package/dist/src/cleanup/invocation-logs.d.ts +30 -0
- package/dist/src/cleanup/invocation-logs.d.ts.map +1 -0
- package/dist/src/cleanup/invocation-logs.js +66 -0
- package/dist/src/cleanup/invocation-logs.js.map +1 -0
- package/dist/src/commands/loop-phases.d.ts +11 -0
- package/dist/src/commands/loop-phases.d.ts.map +1 -0
- package/dist/src/commands/loop-phases.js +304 -0
- package/dist/src/commands/loop-phases.js.map +1 -0
- package/dist/src/config/loader.d.ts +160 -0
- package/dist/src/config/loader.d.ts.map +1 -0
- package/dist/src/config/loader.js +276 -0
- package/dist/src/config/loader.js.map +1 -0
- package/dist/src/database/connection.d.ts +35 -0
- package/dist/src/database/connection.d.ts.map +1 -0
- package/dist/src/database/connection.js +197 -0
- package/dist/src/database/connection.js.map +1 -0
- package/dist/src/database/queries.d.ts +220 -0
- package/dist/src/database/queries.d.ts.map +1 -0
- package/dist/src/database/queries.js +589 -0
- package/dist/src/database/queries.js.map +1 -0
- package/dist/src/database/schema.d.ts +8 -0
- package/dist/src/database/schema.d.ts.map +1 -0
- package/dist/src/database/schema.js +184 -0
- package/dist/src/database/schema.js.map +1 -0
- package/dist/src/git/push.d.ts +26 -0
- package/dist/src/git/push.d.ts.map +1 -0
- package/dist/src/git/push.js +91 -0
- package/dist/src/git/push.js.map +1 -0
- package/dist/src/git/status.d.ts +83 -0
- package/dist/src/git/status.d.ts.map +1 -0
- package/dist/src/git/status.js +315 -0
- package/dist/src/git/status.js.map +1 -0
- package/dist/src/health/stuck-task-detector.d.ts +131 -0
- package/dist/src/health/stuck-task-detector.d.ts.map +1 -0
- package/dist/src/health/stuck-task-detector.js +233 -0
- package/dist/src/health/stuck-task-detector.js.map +1 -0
- package/dist/src/health/stuck-task-recovery.d.ts +45 -0
- package/dist/src/health/stuck-task-recovery.d.ts.map +1 -0
- package/dist/src/health/stuck-task-recovery.js +309 -0
- package/dist/src/health/stuck-task-recovery.js.map +1 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +130 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/locking/queries.d.ts +116 -0
- package/dist/src/locking/queries.d.ts.map +1 -0
- package/dist/src/locking/queries.js +232 -0
- package/dist/src/locking/queries.js.map +1 -0
- package/dist/src/locking/section-lock.d.ts +74 -0
- package/dist/src/locking/section-lock.d.ts.map +1 -0
- package/dist/src/locking/section-lock.js +196 -0
- package/dist/src/locking/section-lock.js.map +1 -0
- package/dist/src/locking/task-lock.d.ts +92 -0
- package/dist/src/locking/task-lock.d.ts.map +1 -0
- package/dist/src/locking/task-lock.js +233 -0
- package/dist/src/locking/task-lock.js.map +1 -0
- package/dist/src/migrations/index.d.ts +7 -0
- package/dist/src/migrations/index.d.ts.map +1 -0
- package/dist/src/migrations/index.js +9 -0
- package/dist/src/migrations/index.js.map +1 -0
- package/dist/src/migrations/manifest.d.ts +92 -0
- package/dist/src/migrations/manifest.d.ts.map +1 -0
- package/dist/src/migrations/manifest.js +255 -0
- package/dist/src/migrations/manifest.js.map +1 -0
- package/dist/src/migrations/runner.d.ts +84 -0
- package/dist/src/migrations/runner.d.ts.map +1 -0
- package/dist/src/migrations/runner.js +338 -0
- package/dist/src/migrations/runner.js.map +1 -0
- package/dist/src/orchestrator/coder.d.ts +32 -0
- package/dist/src/orchestrator/coder.d.ts.map +1 -0
- package/dist/src/orchestrator/coder.js +170 -0
- package/dist/src/orchestrator/coder.js.map +1 -0
- package/dist/src/orchestrator/coordinator.d.ts +28 -0
- package/dist/src/orchestrator/coordinator.d.ts.map +1 -0
- package/dist/src/orchestrator/coordinator.js +252 -0
- package/dist/src/orchestrator/coordinator.js.map +1 -0
- package/dist/src/orchestrator/fallback-handler.d.ts +24 -0
- package/dist/src/orchestrator/fallback-handler.d.ts.map +1 -0
- package/dist/src/orchestrator/fallback-handler.js +280 -0
- package/dist/src/orchestrator/fallback-handler.js.map +1 -0
- package/dist/src/orchestrator/invoke.d.ts +14 -0
- package/dist/src/orchestrator/invoke.d.ts.map +1 -0
- package/dist/src/orchestrator/invoke.js +76 -0
- package/dist/src/orchestrator/invoke.js.map +1 -0
- package/dist/src/orchestrator/post-coder.d.ts +10 -0
- package/dist/src/orchestrator/post-coder.d.ts.map +1 -0
- package/dist/src/orchestrator/post-coder.js +198 -0
- package/dist/src/orchestrator/post-coder.js.map +1 -0
- package/dist/src/orchestrator/post-reviewer.d.ts +10 -0
- package/dist/src/orchestrator/post-reviewer.d.ts.map +1 -0
- package/dist/src/orchestrator/post-reviewer.js +199 -0
- package/dist/src/orchestrator/post-reviewer.js.map +1 -0
- package/dist/src/orchestrator/reviewer.d.ts +35 -0
- package/dist/src/orchestrator/reviewer.d.ts.map +1 -0
- package/dist/src/orchestrator/reviewer.js +237 -0
- package/dist/src/orchestrator/reviewer.js.map +1 -0
- package/dist/src/orchestrator/schemas.d.ts +10 -0
- package/dist/src/orchestrator/schemas.d.ts.map +1 -0
- package/dist/src/orchestrator/schemas.js +81 -0
- package/dist/src/orchestrator/schemas.js.map +1 -0
- package/dist/src/orchestrator/task-selector.d.ts +102 -0
- package/dist/src/orchestrator/task-selector.d.ts.map +1 -0
- package/dist/src/orchestrator/task-selector.js +326 -0
- package/dist/src/orchestrator/task-selector.js.map +1 -0
- package/dist/src/orchestrator/types.d.ts +74 -0
- package/dist/src/orchestrator/types.d.ts.map +1 -0
- package/dist/src/orchestrator/types.js +5 -0
- package/dist/src/orchestrator/types.js.map +1 -0
- package/dist/src/prompts/coder.d.ts +36 -0
- package/dist/src/prompts/coder.d.ts.map +1 -0
- package/dist/src/prompts/coder.js +303 -0
- package/dist/src/prompts/coder.js.map +1 -0
- package/dist/src/prompts/prompt-helpers.d.ts +51 -0
- package/dist/src/prompts/prompt-helpers.d.ts.map +1 -0
- package/dist/src/prompts/prompt-helpers.js +299 -0
- package/dist/src/prompts/prompt-helpers.js.map +1 -0
- package/dist/src/prompts/reviewer.d.ts +40 -0
- package/dist/src/prompts/reviewer.d.ts.map +1 -0
- package/dist/src/prompts/reviewer.js +416 -0
- package/dist/src/prompts/reviewer.js.map +1 -0
- package/dist/src/providers/claude.d.ts +53 -0
- package/dist/src/providers/claude.d.ts.map +1 -0
- package/dist/src/providers/claude.js +227 -0
- package/dist/src/providers/claude.js.map +1 -0
- package/dist/src/providers/codex.d.ts +53 -0
- package/dist/src/providers/codex.d.ts.map +1 -0
- package/dist/src/providers/codex.js +253 -0
- package/dist/src/providers/codex.js.map +1 -0
- package/dist/src/providers/gemini.d.ts +58 -0
- package/dist/src/providers/gemini.d.ts.map +1 -0
- package/dist/src/providers/gemini.js +240 -0
- package/dist/src/providers/gemini.js.map +1 -0
- package/dist/src/providers/interface.d.ts +185 -0
- package/dist/src/providers/interface.d.ts.map +1 -0
- package/dist/src/providers/interface.js +92 -0
- package/dist/src/providers/interface.js.map +1 -0
- package/dist/src/providers/invocation-logger.d.ts +97 -0
- package/dist/src/providers/invocation-logger.d.ts.map +1 -0
- package/dist/src/providers/invocation-logger.js +378 -0
- package/dist/src/providers/invocation-logger.js.map +1 -0
- package/dist/src/providers/openai.d.ts +53 -0
- package/dist/src/providers/openai.d.ts.map +1 -0
- package/dist/src/providers/openai.js +230 -0
- package/dist/src/providers/openai.js.map +1 -0
- package/dist/src/providers/registry.d.ts +100 -0
- package/dist/src/providers/registry.d.ts.map +1 -0
- package/dist/src/providers/registry.js +170 -0
- package/dist/src/providers/registry.js.map +1 -0
- package/dist/src/routes/activity.d.ts +7 -0
- package/dist/src/routes/activity.d.ts.map +1 -0
- package/dist/src/routes/activity.js +252 -0
- package/dist/src/routes/activity.js.map +1 -0
- package/dist/src/routes/config.d.ts +7 -0
- package/dist/src/routes/config.d.ts.map +1 -0
- package/dist/src/routes/config.js +521 -0
- package/dist/src/routes/config.js.map +1 -0
- package/dist/src/routes/health.d.ts +7 -0
- package/dist/src/routes/health.d.ts.map +1 -0
- package/dist/src/routes/health.js +172 -0
- package/dist/src/routes/health.js.map +1 -0
- package/dist/src/routes/incidents.d.ts +7 -0
- package/dist/src/routes/incidents.d.ts.map +1 -0
- package/dist/src/routes/incidents.js +117 -0
- package/dist/src/routes/incidents.js.map +1 -0
- package/dist/src/routes/projects.d.ts +7 -0
- package/dist/src/routes/projects.d.ts.map +1 -0
- package/dist/src/routes/projects.js +398 -0
- package/dist/src/routes/projects.js.map +1 -0
- package/dist/src/routes/runners.d.ts +7 -0
- package/dist/src/routes/runners.d.ts.map +1 -0
- package/dist/src/routes/runners.js +242 -0
- package/dist/src/routes/runners.js.map +1 -0
- package/dist/src/routes/tasks.d.ts +7 -0
- package/dist/src/routes/tasks.d.ts.map +1 -0
- package/dist/src/routes/tasks.js +1007 -0
- package/dist/src/routes/tasks.js.map +1 -0
- package/dist/src/runners/activity-log.d.ts +65 -0
- package/dist/src/runners/activity-log.d.ts.map +1 -0
- package/dist/src/runners/activity-log.js +140 -0
- package/dist/src/runners/activity-log.js.map +1 -0
- package/dist/src/runners/cron.d.ts +30 -0
- package/dist/src/runners/cron.d.ts.map +1 -0
- package/dist/src/runners/cron.js +333 -0
- package/dist/src/runners/cron.js.map +1 -0
- package/dist/src/runners/daemon.d.ts +71 -0
- package/dist/src/runners/daemon.d.ts.map +1 -0
- package/dist/src/runners/daemon.js +233 -0
- package/dist/src/runners/daemon.js.map +1 -0
- package/dist/src/runners/global-db.d.ts +31 -0
- package/dist/src/runners/global-db.d.ts.map +1 -0
- package/dist/src/runners/global-db.js +220 -0
- package/dist/src/runners/global-db.js.map +1 -0
- package/dist/src/runners/hang-detector.d.ts +38 -0
- package/dist/src/runners/hang-detector.d.ts.map +1 -0
- package/dist/src/runners/hang-detector.js +130 -0
- package/dist/src/runners/hang-detector.js.map +1 -0
- package/dist/src/runners/heartbeat.d.ts +39 -0
- package/dist/src/runners/heartbeat.d.ts.map +1 -0
- package/dist/src/runners/heartbeat.js +71 -0
- package/dist/src/runners/heartbeat.js.map +1 -0
- package/dist/src/runners/lock.d.ts +47 -0
- package/dist/src/runners/lock.d.ts.map +1 -0
- package/dist/src/runners/lock.js +140 -0
- package/dist/src/runners/lock.js.map +1 -0
- package/dist/src/runners/orchestrator-loop.d.ts +20 -0
- package/dist/src/runners/orchestrator-loop.d.ts.map +1 -0
- package/dist/src/runners/orchestrator-loop.js +208 -0
- package/dist/src/runners/orchestrator-loop.js.map +1 -0
- package/dist/src/runners/projects.d.ts +96 -0
- package/dist/src/runners/projects.d.ts.map +1 -0
- package/dist/src/runners/projects.js +243 -0
- package/dist/src/runners/projects.js.map +1 -0
- package/dist/src/runners/wakeup.d.ts +37 -0
- package/dist/src/runners/wakeup.d.ts.map +1 -0
- package/dist/src/runners/wakeup.js +355 -0
- package/dist/src/runners/wakeup.js.map +1 -0
- package/dist/src/utils/validation.d.ts +22 -0
- package/dist/src/utils/validation.d.ts.map +1 -0
- package/dist/src/utils/validation.js +50 -0
- package/dist/src/utils/validation.js.map +1 -0
- package/dist/utils/sqlite.d.ts +17 -0
- package/dist/utils/sqlite.d.ts.map +1 -0
- package/dist/utils/sqlite.js +27 -0
- package/dist/utils/sqlite.js.map +1 -0
- package/dist/utils/storage-cache.d.ts +33 -0
- package/dist/utils/storage-cache.d.ts.map +1 -0
- package/dist/utils/storage-cache.js +81 -0
- package/dist/utils/storage-cache.js.map +1 -0
- package/dist/utils/validation.d.ts +22 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +51 -0
- package/dist/utils/validation.js.map +1 -0
- package/package.json +39 -0
- package/src/index.ts +199 -0
- package/src/routes/activity.ts +302 -0
- package/src/routes/config.ts +723 -0
- package/src/routes/credit-alerts.ts +73 -0
- package/src/routes/health.ts +219 -0
- package/src/routes/incidents.ts +131 -0
- package/src/routes/projects.ts +854 -0
- package/src/routes/runners.ts +357 -0
- package/src/routes/skills.ts +127 -0
- package/src/routes/storage.ts +108 -0
- package/src/routes/tasks.ts +1372 -0
- package/src/utils/sqlite.ts +36 -0
- package/src/utils/storage-cache.ts +107 -0
- package/src/utils/validation.ts +61 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credit Alerts API routes — credit exhaustion incidents for dashboard notifications.
|
|
3
|
+
*/
|
|
4
|
+
import { Router, Request, Response } from 'express';
|
|
5
|
+
import Database from 'better-sqlite3';
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { openSqliteForRead } from '../utils/sqlite.js';
|
|
9
|
+
import { getRegisteredProjects } from '../../../dist/runners/projects.js';
|
|
10
|
+
|
|
11
|
+
export const creditAlertRoutes = Router();
|
|
12
|
+
|
|
13
|
+
interface IncidentRow { id: string; runner_id: string | null; details: string | null; created_at: string }
|
|
14
|
+
|
|
15
|
+
function openProjectDb(path: string, readonly: boolean): Database.Database | null {
|
|
16
|
+
const dbPath = join(path, '.steroids', 'steroids.db');
|
|
17
|
+
if (!existsSync(dbPath)) return null;
|
|
18
|
+
try { return readonly ? openSqliteForRead(dbPath) : new Database(dbPath); } catch { return null; }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseRow(row: IncidentRow) {
|
|
22
|
+
let provider = '', model = '', role = '', message = '';
|
|
23
|
+
if (row.details) {
|
|
24
|
+
try { const d = JSON.parse(row.details); provider = d.provider ?? ''; model = d.model ?? ''; role = d.role ?? ''; message = d.message ?? ''; } catch { /* malformed */ }
|
|
25
|
+
}
|
|
26
|
+
return { id: row.id, provider, model, role, message, runnerId: row.runner_id, createdAt: row.created_at };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const ALERT_SQL = `SELECT id, runner_id, details, created_at FROM incidents
|
|
30
|
+
WHERE failure_mode = 'credit_exhaustion' AND resolved_at IS NULL ORDER BY created_at DESC`;
|
|
31
|
+
|
|
32
|
+
const DISMISS_SQL = `UPDATE incidents SET resolved_at = datetime('now'), resolution = ?
|
|
33
|
+
WHERE id = ? AND resolved_at IS NULL AND failure_mode = 'credit_exhaustion'`;
|
|
34
|
+
|
|
35
|
+
const RETRY_SQL = `UPDATE incidents SET resolved_at = datetime('now'), resolution = 'retry'
|
|
36
|
+
WHERE id = ? AND resolved_at IS NULL AND failure_mode = 'credit_exhaustion'`;
|
|
37
|
+
|
|
38
|
+
function resolveIncident(req: Request, res: Response, sql: string, params: unknown[]) {
|
|
39
|
+
const projectPath = req.body?.project as string | undefined;
|
|
40
|
+
if (!projectPath) { res.status(400).json({ error: 'Missing body param: project' }); return; }
|
|
41
|
+
const db = openProjectDb(projectPath, false);
|
|
42
|
+
if (!db) { res.status(404).json({ error: 'Project database not found' }); return; }
|
|
43
|
+
try {
|
|
44
|
+
const r = db.prepare(sql).run(...params);
|
|
45
|
+
if (r.changes === 0) { res.status(404).json({ error: 'Incident not found or already resolved' }); return; }
|
|
46
|
+
res.json({ ok: true });
|
|
47
|
+
} catch { res.status(500).json({ error: 'Failed to update alert' }); } finally { db.close(); }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** GET / — list active credit exhaustion incidents. project query param is optional. */
|
|
51
|
+
creditAlertRoutes.get('/', (req: Request, res: Response) => {
|
|
52
|
+
const project = req.query.project as string | undefined;
|
|
53
|
+
const paths = project ? [project] : getRegisteredProjects(false).map((p: { path: string }) => p.path);
|
|
54
|
+
const alerts: ReturnType<typeof parseRow>[] = [];
|
|
55
|
+
for (const p of paths) {
|
|
56
|
+
const db = openProjectDb(p, true);
|
|
57
|
+
if (!db) continue;
|
|
58
|
+
try { (db.prepare(ALERT_SQL).all() as IncidentRow[]).forEach((r) => alerts.push(parseRow(r))); } catch { /* best-effort */ } finally { db.close(); }
|
|
59
|
+
}
|
|
60
|
+
alerts.sort((a, b) => (b.createdAt > a.createdAt ? 1 : b.createdAt < a.createdAt ? -1 : 0));
|
|
61
|
+
res.json({ alerts });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
/** POST /:id/dismiss — resolve a credit alert. Body: optional {resolution}, defaults "dismissed". */
|
|
65
|
+
creditAlertRoutes.post('/:id/dismiss', (req: Request, res: Response) => {
|
|
66
|
+
const resolution = (req.body?.resolution as string) || 'dismissed';
|
|
67
|
+
resolveIncident(req, res, DISMISS_SQL, [resolution, req.params.id]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
/** POST /:id/retry — resolve with retry signal. Body: {project} only. */
|
|
71
|
+
creditAlertRoutes.post('/:id/retry', (req: Request, res: Response) => {
|
|
72
|
+
resolveIncident(req, res, RETRY_SQL, [req.params.id]);
|
|
73
|
+
});
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health API routes
|
|
3
|
+
* Exposes stuck-task detection health summary for the dashboard/monitor.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Router, Request, Response } from 'express';
|
|
7
|
+
import Database from 'better-sqlite3';
|
|
8
|
+
import { existsSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { loadConfig } from '../../../dist/config/loader.js';
|
|
11
|
+
import {
|
|
12
|
+
detectStuckTasks,
|
|
13
|
+
type StuckTaskDetectionConfig,
|
|
14
|
+
type StuckTaskDetectionReport,
|
|
15
|
+
} from '../../../dist/health/stuck-task-detector.js';
|
|
16
|
+
import { getGlobalDbPath } from '../../../dist/runners/global-db.js';
|
|
17
|
+
import { openSqliteForRead } from '../utils/sqlite.js';
|
|
18
|
+
|
|
19
|
+
const router = Router();
|
|
20
|
+
|
|
21
|
+
function openProjectDatabaseReadonly(projectPath: string): Database.Database | null {
|
|
22
|
+
const dbPath = join(projectPath, '.steroids', 'steroids.db');
|
|
23
|
+
if (!existsSync(dbPath)) return null;
|
|
24
|
+
try {
|
|
25
|
+
return openSqliteForRead(dbPath);
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function openGlobalDatabaseReadonlyOrMemory(): { db: Database.Database; close: () => void } {
|
|
32
|
+
const dbPath = getGlobalDbPath();
|
|
33
|
+
if (existsSync(dbPath)) {
|
|
34
|
+
const db = openSqliteForRead(dbPath);
|
|
35
|
+
return { db, close: () => db.close() };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// No global DB yet; use an in-memory DB with an empty runners table.
|
|
39
|
+
const db = new Database(':memory:');
|
|
40
|
+
db.exec(`
|
|
41
|
+
CREATE TABLE IF NOT EXISTS runners (
|
|
42
|
+
id TEXT PRIMARY KEY,
|
|
43
|
+
status TEXT NOT NULL DEFAULT 'idle',
|
|
44
|
+
pid INTEGER,
|
|
45
|
+
project_path TEXT,
|
|
46
|
+
current_task_id TEXT,
|
|
47
|
+
started_at TEXT,
|
|
48
|
+
heartbeat_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
49
|
+
section_id TEXT
|
|
50
|
+
);
|
|
51
|
+
`);
|
|
52
|
+
return { db, close: () => db.close() };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parseBoolean(value: unknown): boolean | undefined {
|
|
56
|
+
if (value === undefined) return undefined;
|
|
57
|
+
if (value === 'true') return true;
|
|
58
|
+
if (value === 'false') return false;
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isLikelySchemaError(error: unknown): boolean {
|
|
63
|
+
if (!(error instanceof Error)) return false;
|
|
64
|
+
return /no such table|no such column|has no column|syntax error/i.test(error.message);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function fallbackStuckTaskReport(): StuckTaskDetectionReport {
|
|
68
|
+
return {
|
|
69
|
+
timestamp: new Date(),
|
|
70
|
+
orphanedTasks: [],
|
|
71
|
+
hangingInvocations: [],
|
|
72
|
+
zombieRunners: [],
|
|
73
|
+
deadRunners: [],
|
|
74
|
+
dbInconsistencies: [],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
type HealthStatus = 'healthy' | 'degraded' | 'unhealthy';
|
|
79
|
+
|
|
80
|
+
function computeStatus(args: {
|
|
81
|
+
orphanedTasks: number;
|
|
82
|
+
hangingInvocations: number;
|
|
83
|
+
zombieRunners: number;
|
|
84
|
+
deadRunners: number;
|
|
85
|
+
activeIncidents: number;
|
|
86
|
+
}): HealthStatus {
|
|
87
|
+
if (args.deadRunners > 0 || args.zombieRunners > 0) return 'unhealthy';
|
|
88
|
+
if (args.hangingInvocations > 0 || args.orphanedTasks > 0 || args.activeIncidents > 0) return 'degraded';
|
|
89
|
+
return 'healthy';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* GET /api/health
|
|
94
|
+
* Query params:
|
|
95
|
+
* - project: string (required) - project path
|
|
96
|
+
* - includeSignals: boolean (optional) - include raw signal arrays (default: false)
|
|
97
|
+
*/
|
|
98
|
+
router.get('/health', (req: Request, res: Response) => {
|
|
99
|
+
const projectPath = req.query.project as string | undefined;
|
|
100
|
+
if (!projectPath) {
|
|
101
|
+
res.status(400).json({
|
|
102
|
+
success: false,
|
|
103
|
+
error: 'Project path required (query param: project)',
|
|
104
|
+
});
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const includeSignals = parseBoolean(req.query.includeSignals) ?? false;
|
|
109
|
+
|
|
110
|
+
const projectDb = openProjectDatabaseReadonly(projectPath);
|
|
111
|
+
if (!projectDb) {
|
|
112
|
+
res.status(404).json({
|
|
113
|
+
success: false,
|
|
114
|
+
error: 'Project database not found at .steroids/steroids.db',
|
|
115
|
+
project: projectPath,
|
|
116
|
+
});
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const { db: globalDb, close: closeGlobal } = openGlobalDatabaseReadonlyOrMemory();
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const cfg = loadConfig(projectPath);
|
|
124
|
+
const detectionConfig: StuckTaskDetectionConfig = {
|
|
125
|
+
orphanedTaskTimeoutSec: cfg.health?.orphanedTaskTimeout,
|
|
126
|
+
maxCoderDurationSec: cfg.health?.maxCoderDuration,
|
|
127
|
+
maxReviewerDurationSec: cfg.health?.maxReviewerDuration,
|
|
128
|
+
runnerHeartbeatTimeoutSec: cfg.health?.runnerHeartbeatTimeout,
|
|
129
|
+
invocationStalenessSec: cfg.health?.invocationStaleness,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
let report: StuckTaskDetectionReport;
|
|
133
|
+
try {
|
|
134
|
+
report = detectStuckTasks({
|
|
135
|
+
projectPath,
|
|
136
|
+
projectDb,
|
|
137
|
+
globalDb,
|
|
138
|
+
config: detectionConfig,
|
|
139
|
+
});
|
|
140
|
+
} catch (error) {
|
|
141
|
+
if (!isLikelySchemaError(error)) {
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
145
|
+
console.warn('Falling back to minimal health signals due to missing schema:', error);
|
|
146
|
+
}
|
|
147
|
+
report = fallbackStuckTaskReport();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Incident counts (best-effort: table might not exist)
|
|
151
|
+
let activeIncidents = 0;
|
|
152
|
+
let recentIncidents = 0;
|
|
153
|
+
try {
|
|
154
|
+
const rowActive = projectDb
|
|
155
|
+
.prepare(`SELECT COUNT(*) as count FROM incidents WHERE resolved_at IS NULL`)
|
|
156
|
+
.get() as { count: number } | undefined;
|
|
157
|
+
const rowRecent = projectDb
|
|
158
|
+
.prepare(`SELECT COUNT(*) as count FROM incidents WHERE detected_at >= datetime('now', '-24 hours')`)
|
|
159
|
+
.get() as { count: number } | undefined;
|
|
160
|
+
activeIncidents = rowActive?.count ?? 0;
|
|
161
|
+
recentIncidents = rowRecent?.count ?? 0;
|
|
162
|
+
} catch {
|
|
163
|
+
// ignore
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const orphanedTasks = report.orphanedTasks.length;
|
|
167
|
+
const hangingInvocations = report.hangingInvocations.length;
|
|
168
|
+
const zombieRunners = report.zombieRunners.length;
|
|
169
|
+
const deadRunners = report.deadRunners.length;
|
|
170
|
+
|
|
171
|
+
const health = {
|
|
172
|
+
status: computeStatus({
|
|
173
|
+
orphanedTasks,
|
|
174
|
+
hangingInvocations,
|
|
175
|
+
zombieRunners,
|
|
176
|
+
deadRunners,
|
|
177
|
+
activeIncidents,
|
|
178
|
+
}),
|
|
179
|
+
lastCheck: new Date().toISOString(),
|
|
180
|
+
checks: [
|
|
181
|
+
{ type: 'orphaned_tasks', healthy: orphanedTasks === 0, found: orphanedTasks },
|
|
182
|
+
{ type: 'hanging_invocations', healthy: hangingInvocations === 0, found: hangingInvocations },
|
|
183
|
+
{ type: 'zombie_runners', healthy: zombieRunners === 0, found: zombieRunners },
|
|
184
|
+
{ type: 'dead_runners', healthy: deadRunners === 0, found: deadRunners },
|
|
185
|
+
],
|
|
186
|
+
activeIncidents,
|
|
187
|
+
recentIncidents,
|
|
188
|
+
...(includeSignals
|
|
189
|
+
? {
|
|
190
|
+
signals: {
|
|
191
|
+
orphanedTasks: report.orphanedTasks,
|
|
192
|
+
hangingInvocations: report.hangingInvocations,
|
|
193
|
+
zombieRunners: report.zombieRunners,
|
|
194
|
+
deadRunners: report.deadRunners,
|
|
195
|
+
dbInconsistencies: report.dbInconsistencies,
|
|
196
|
+
},
|
|
197
|
+
}
|
|
198
|
+
: {}),
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
res.json({
|
|
202
|
+
success: true,
|
|
203
|
+
project: projectPath,
|
|
204
|
+
health,
|
|
205
|
+
});
|
|
206
|
+
} catch (error) {
|
|
207
|
+
console.error('Error computing health status:', error);
|
|
208
|
+
res.status(500).json({
|
|
209
|
+
success: false,
|
|
210
|
+
error: 'Failed to compute health status',
|
|
211
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
212
|
+
});
|
|
213
|
+
} finally {
|
|
214
|
+
projectDb.close();
|
|
215
|
+
closeGlobal();
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
export default router;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Incidents API routes
|
|
3
|
+
* Exposes stuck-task incident history for dashboard/monitor.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Router, Request, Response } from 'express';
|
|
7
|
+
import Database from 'better-sqlite3';
|
|
8
|
+
import { existsSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { openSqliteForRead } from '../utils/sqlite.js';
|
|
11
|
+
|
|
12
|
+
const router = Router();
|
|
13
|
+
|
|
14
|
+
interface IncidentRow {
|
|
15
|
+
id: string;
|
|
16
|
+
task_id: string | null;
|
|
17
|
+
runner_id: string | null;
|
|
18
|
+
failure_mode: string;
|
|
19
|
+
detected_at: string;
|
|
20
|
+
resolved_at: string | null;
|
|
21
|
+
resolution: string | null;
|
|
22
|
+
details: string | null;
|
|
23
|
+
created_at: string;
|
|
24
|
+
task_title?: string | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function openProjectDatabaseReadonly(projectPath: string): Database.Database | null {
|
|
28
|
+
const dbPath = join(projectPath, '.steroids', 'steroids.db');
|
|
29
|
+
if (!existsSync(dbPath)) return null;
|
|
30
|
+
try {
|
|
31
|
+
return openSqliteForRead(dbPath);
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parsePositiveInt(value: unknown, fallback: number): number {
|
|
38
|
+
if (value === undefined) return fallback;
|
|
39
|
+
const n = parseInt(String(value), 10);
|
|
40
|
+
if (!Number.isFinite(n) || n <= 0) return fallback;
|
|
41
|
+
return n;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function parseBoolean(value: unknown): boolean | undefined {
|
|
45
|
+
if (value === undefined) return undefined;
|
|
46
|
+
if (value === 'true') return true;
|
|
47
|
+
if (value === 'false') return false;
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* GET /api/incidents
|
|
53
|
+
* Query params:
|
|
54
|
+
* - project: string (required) - project path
|
|
55
|
+
* - limit: number (optional, default 50, max 200)
|
|
56
|
+
* - task: string (optional) - filter by task ID prefix
|
|
57
|
+
* - unresolved: boolean (optional) - true => resolved_at IS NULL, false => resolved_at IS NOT NULL
|
|
58
|
+
*/
|
|
59
|
+
router.get('/incidents', (req: Request, res: Response) => {
|
|
60
|
+
const projectPath = req.query.project as string | undefined;
|
|
61
|
+
if (!projectPath) {
|
|
62
|
+
res.status(400).json({
|
|
63
|
+
success: false,
|
|
64
|
+
error: 'Project path required (query param: project)',
|
|
65
|
+
});
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const limit = Math.min(parsePositiveInt(req.query.limit, 50), 200);
|
|
70
|
+
const taskPrefix = (req.query.task as string | undefined)?.trim();
|
|
71
|
+
const unresolved = parseBoolean(req.query.unresolved);
|
|
72
|
+
|
|
73
|
+
const db = openProjectDatabaseReadonly(projectPath);
|
|
74
|
+
if (!db) {
|
|
75
|
+
res.status(404).json({
|
|
76
|
+
success: false,
|
|
77
|
+
error: 'Project database not found at .steroids/steroids.db',
|
|
78
|
+
project: projectPath,
|
|
79
|
+
});
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
// Best-effort: incidents table may not exist (migrations disabled).
|
|
85
|
+
let incidents: IncidentRow[] = [];
|
|
86
|
+
try {
|
|
87
|
+
const where: string[] = [];
|
|
88
|
+
const params: Array<string | number> = [];
|
|
89
|
+
|
|
90
|
+
if (taskPrefix) {
|
|
91
|
+
where.push('i.task_id LIKE ?');
|
|
92
|
+
params.push(`${taskPrefix}%`);
|
|
93
|
+
}
|
|
94
|
+
if (unresolved === true) where.push('i.resolved_at IS NULL');
|
|
95
|
+
if (unresolved === false) where.push('i.resolved_at IS NOT NULL');
|
|
96
|
+
|
|
97
|
+
const whereSql = where.length ? `WHERE ${where.join(' AND ')}` : '';
|
|
98
|
+
const sql = `
|
|
99
|
+
SELECT
|
|
100
|
+
i.id, i.task_id, i.runner_id, i.failure_mode, i.detected_at, i.resolved_at, i.resolution, i.details, i.created_at,
|
|
101
|
+
t.title as task_title
|
|
102
|
+
FROM incidents i
|
|
103
|
+
LEFT JOIN tasks t ON t.id = i.task_id
|
|
104
|
+
${whereSql}
|
|
105
|
+
ORDER BY i.detected_at DESC
|
|
106
|
+
LIMIT ?
|
|
107
|
+
`;
|
|
108
|
+
incidents = db.prepare(sql).all(...params, limit) as IncidentRow[];
|
|
109
|
+
} catch {
|
|
110
|
+
incidents = [];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
res.json({
|
|
114
|
+
success: true,
|
|
115
|
+
project: projectPath,
|
|
116
|
+
total: incidents.length,
|
|
117
|
+
incidents,
|
|
118
|
+
});
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.error('Error listing incidents:', error);
|
|
121
|
+
res.status(500).json({
|
|
122
|
+
success: false,
|
|
123
|
+
error: 'Failed to list incidents',
|
|
124
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
125
|
+
});
|
|
126
|
+
} finally {
|
|
127
|
+
db.close();
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
export default router;
|