specrails-hub 1.25.5 → 1.26.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specrails-hub",
3
- "version": "1.25.5",
3
+ "version": "1.26.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -90,9 +90,21 @@ class ProjectRegistry {
90
90
  }
91
91
  }
92
92
  };
93
+ // Per-project zombie timeout (stored in queue_state)
94
+ let projectZombieTimeout;
95
+ try {
96
+ const row = db.prepare(`SELECT value FROM queue_state WHERE key = 'config.zombie_timeout_ms'`).get();
97
+ if (row) {
98
+ const parsed = parseInt(row.value, 10);
99
+ if (!isNaN(parsed) && parsed > 0)
100
+ projectZombieTimeout = parsed;
101
+ }
102
+ }
103
+ catch { /* queue_state table may not exist yet */ }
93
104
  const webhookManager = this._webhookManager;
94
105
  const railJobs = new Map();
95
106
  const queueManager = new queue_manager_1.QueueManager(boundBroadcast, db, undefined, project.path, {
107
+ zombieTimeoutMs: projectZombieTimeout,
96
108
  provider: project.provider ?? 'claude',
97
109
  getCostAlertThreshold: () => {
98
110
  const val = (0, hub_db_1.getHubSetting)(this._hubDb, 'cost_alert_threshold_usd');
@@ -454,7 +454,9 @@ function createProjectRouter(registry) {
454
454
  const config = (0, config_1.getConfig)(project.path, db, project.name);
455
455
  const dailyBudgetRaw = db.prepare(`SELECT value FROM queue_state WHERE key = 'config.daily_budget_usd'`).get()?.value;
456
456
  const dailyBudgetUsd = dailyBudgetRaw != null ? parseFloat(dailyBudgetRaw) : null;
457
- res.json({ ...config, dailyBudgetUsd });
457
+ const zombieTimeoutRaw = db.prepare(`SELECT value FROM queue_state WHERE key = 'config.zombie_timeout_ms'`).get()?.value;
458
+ const zombieTimeoutMs = zombieTimeoutRaw != null ? parseInt(zombieTimeoutRaw, 10) : null;
459
+ res.json({ ...config, dailyBudgetUsd, zombieTimeoutMs });
458
460
  }
459
461
  catch (err) {
460
462
  console.error('[project-router] config error:', err);
@@ -462,8 +464,8 @@ function createProjectRouter(registry) {
462
464
  }
463
465
  });
464
466
  router.post('/:projectId/config', (req, res) => {
465
- const { active, labelFilter, dailyBudgetUsd } = req.body ?? {};
466
- const { db } = ctx(req);
467
+ const { active, labelFilter, dailyBudgetUsd, zombieTimeoutMs } = req.body ?? {};
468
+ const { db, queueManager } = ctx(req);
467
469
  try {
468
470
  if (active !== undefined) {
469
471
  db.prepare(`INSERT OR REPLACE INTO queue_state (key, value) VALUES ('config.active_tracker', ?)`).run(active ?? '');
@@ -479,6 +481,15 @@ function createProjectRouter(registry) {
479
481
  db.prepare(`INSERT OR REPLACE INTO queue_state (key, value) VALUES ('config.daily_budget_usd', ?)`).run(String(dailyBudgetUsd));
480
482
  }
481
483
  }
484
+ if (zombieTimeoutMs !== undefined) {
485
+ if (zombieTimeoutMs === null) {
486
+ db.prepare(`DELETE FROM queue_state WHERE key = 'config.zombie_timeout_ms'`).run();
487
+ }
488
+ else if (typeof zombieTimeoutMs === 'number' && zombieTimeoutMs > 0) {
489
+ db.prepare(`INSERT OR REPLACE INTO queue_state (key, value) VALUES ('config.zombie_timeout_ms', ?)`).run(String(zombieTimeoutMs));
490
+ }
491
+ queueManager.setZombieTimeout(typeof zombieTimeoutMs === 'number' && zombieTimeoutMs > 0 ? zombieTimeoutMs : queue_manager_1.DEFAULT_ZOMBIE_TIMEOUT_MS);
492
+ }
482
493
  res.json({ ok: true });
483
494
  }
484
495
  catch (err) {
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.QueueManager = exports.JobAlreadyTerminalError = exports.JobNotFoundError = exports.CodexNotFoundError = exports.ClaudeNotFoundError = void 0;
6
+ exports.QueueManager = exports.JobAlreadyTerminalError = exports.JobNotFoundError = exports.CodexNotFoundError = exports.ClaudeNotFoundError = exports.DEFAULT_ZOMBIE_TIMEOUT_MS = void 0;
7
7
  const child_process_1 = require("child_process");
8
8
  const readline_1 = require("readline");
9
9
  const uuid_1 = require("uuid");
@@ -14,7 +14,7 @@ const hooks_1 = require("./hooks");
14
14
  const db_1 = require("./db");
15
15
  const LOG_BUFFER_MAX = 5000;
16
16
  const LOG_BUFFER_DROP = 1000;
17
- const DEFAULT_ZOMBIE_TIMEOUT_MS = 300_000; // 5 minutes
17
+ exports.DEFAULT_ZOMBIE_TIMEOUT_MS = 1_800_000; // 30 minutes
18
18
  // ─── Error classes ────────────────────────────────────────────────────────────
19
19
  class ClaudeNotFoundError extends Error {
20
20
  constructor() {
@@ -127,7 +127,7 @@ class QueueManager {
127
127
  ? parseInt(process.env.WM_ZOMBIE_TIMEOUT_MS, 10)
128
128
  : null;
129
129
  this._zombieTimeoutMs = options?.zombieTimeoutMs
130
- ?? (envTimeout !== null && !isNaN(envTimeout) ? envTimeout : DEFAULT_ZOMBIE_TIMEOUT_MS);
130
+ ?? (envTimeout !== null && !isNaN(envTimeout) ? envTimeout : exports.DEFAULT_ZOMBIE_TIMEOUT_MS);
131
131
  if (this._db) {
132
132
  this._restoreFromDb();
133
133
  }
@@ -135,6 +135,13 @@ class QueueManager {
135
135
  setCommands(commands) {
136
136
  this._commands = commands;
137
137
  }
138
+ setZombieTimeout(ms) {
139
+ this._zombieTimeoutMs = ms;
140
+ // If a job is currently running, reset the timer with the new value
141
+ if (this._activeJobId) {
142
+ this._resetZombieTimer();
143
+ }
144
+ }
138
145
  // ─── Public API ─────────────────────────────────────────────────────────────
139
146
  enqueue(command, priorityOrOpts, opts) {
140
147
  // Support both: enqueue(cmd, priority, opts) and enqueue(cmd, opts)