ework-daemon 0.4.57 → 0.4.58

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": "ework-daemon",
3
- "version": "0.4.57",
3
+ "version": "0.4.58",
4
4
  "description": "Issue-driven AI development daemon. Spawns opencode subprocesses to resolve Gitea issues.",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
package/src/config.ts CHANGED
@@ -72,6 +72,7 @@ export const configSchema = z.object({
72
72
  stuck: z.object({
73
73
  thresholdMs: z.coerce.number().positive(),
74
74
  maxNudges: z.coerce.number().int().nonnegative(),
75
+ maxRuntimeMs: z.coerce.number().positive(),
75
76
  }).optional(),
76
77
  file: z.object({
77
78
  roots: z.array(z.string()).default([]),
@@ -170,9 +171,10 @@ export function loadConfig(): Config {
170
171
  baseURL: process.env.COMPLETION_CHECK_BASE_URL ?? "",
171
172
  model: process.env.COMPLETION_CHECK_MODEL ?? "",
172
173
  } : undefined,
173
- stuck: process.env.DAEMON_STUCK_THRESHOLD_MS || process.env.DAEMON_MAX_STUCK_NUDGES ? {
174
+ stuck: process.env.DAEMON_STUCK_THRESHOLD_MS || process.env.DAEMON_MAX_STUCK_NUDGES || process.env.DAEMON_STUCK_MAX_RUNTIME_MS ? {
174
175
  thresholdMs: Number(process.env.DAEMON_STUCK_THRESHOLD_MS) || 30 * 60 * 1000,
175
176
  maxNudges: Number(process.env.DAEMON_MAX_STUCK_NUDGES) || 1,
177
+ maxRuntimeMs: Number(process.env.DAEMON_STUCK_MAX_RUNTIME_MS) || 3 * 60 * 60 * 1000,
176
178
  } : undefined,
177
179
  childEnvDeny: (process.env.WORK_CHILD_ENV_DENY ?? "").split(",").map((s) => s.trim()).filter(Boolean),
178
180
  });
@@ -217,9 +219,10 @@ export function loadConfig(): Config {
217
219
  baseURL: process.env.COMPLETION_CHECK_BASE_URL ?? "",
218
220
  model: process.env.COMPLETION_CHECK_MODEL ?? "",
219
221
  } : undefined,
220
- stuck: process.env.DAEMON_STUCK_THRESHOLD_MS || process.env.DAEMON_MAX_STUCK_NUDGES ? {
222
+ stuck: process.env.DAEMON_STUCK_THRESHOLD_MS || process.env.DAEMON_MAX_STUCK_NUDGES || process.env.DAEMON_STUCK_MAX_RUNTIME_MS ? {
221
223
  thresholdMs: Number(process.env.DAEMON_STUCK_THRESHOLD_MS) || 30 * 60 * 1000,
222
224
  maxNudges: Number(process.env.DAEMON_MAX_STUCK_NUDGES) || 1,
225
+ maxRuntimeMs: Number(process.env.DAEMON_STUCK_MAX_RUNTIME_MS) || 3 * 60 * 60 * 1000,
223
226
  } : undefined,
224
227
  file: {
225
228
  roots: (process.env.WORK_FILE_ROOTS ?? "").split(":").filter(Boolean).length > 0
package/src/opencode.ts CHANGED
@@ -359,6 +359,7 @@ export class Engine {
359
359
  private static MAX_NUDGE_ROUNDS = 1;
360
360
  private static MAX_EMPTY_RESPONSE_ROUNDS = 1;
361
361
  private static MAX_STUCK_NUDGE_ROUNDS = 1;
362
+ private static MAX_RUNTIME_MS = 3 * 60 * 60 * 1000;
362
363
  private static OBSERVER_INTERVAL_MS = 5 * 60 * 1000;
363
364
  private static STUCK_THRESHOLD_MS = 30 * 60 * 1000;
364
365
  private static MAX_PROCESS_EXIT_NUDGE_ROUNDS = 1;
@@ -557,6 +558,10 @@ export class Engine {
557
558
  return this.cfg.stuck?.maxNudges ?? Engine.MAX_STUCK_NUDGE_ROUNDS;
558
559
  }
559
560
 
561
+ private get maxRuntimeMs(): number {
562
+ return this.cfg.stuck?.maxRuntimeMs ?? Engine.MAX_RUNTIME_MS;
563
+ }
564
+
560
565
  private getTracker(type: string): IssueTracker {
561
566
  const tracker = this.trackers.get(type);
562
567
  if (!tracker) throw new Error(`Unknown tracker type: ${type}`);
@@ -1749,6 +1754,19 @@ export class Engine {
1749
1754
  continue;
1750
1755
  }
1751
1756
 
1757
+ // Process alive — cap total run time. Output-silence detection cannot
1758
+ // catch loops that keep emitting (observed: 6h of failing compress calls
1759
+ // every ~5s), so any single run is hard-stopped after maxRuntimeMs.
1760
+ const started = this.startedAt.get(k);
1761
+ if (started && Date.now() - started >= this.maxRuntimeMs) {
1762
+ const hrs = (this.maxRuntimeMs / 3600000).toFixed(1);
1763
+ log.warn(`engine: run exceeded max runtime (${hrs}h) on ${k}, stopping`);
1764
+ await tracker.createComment(this.sessionToRef(session, issue), `[system] ⏹ **${session.name}** run exceeded ${hrs}h — stopped. Reply again on the issue to continue.`).catch(() => { /* best-effort */ });
1765
+ this.nudgeRounds.set(k, Engine.MAX_NUDGE_ROUNDS);
1766
+ this.forceStop(k);
1767
+ continue;
1768
+ }
1769
+
1752
1770
  // Process alive — check stuck
1753
1771
  if (lastTs && Date.now() - lastTs >= this.stuckThresholdMs) {
1754
1772
  const minutes = Math.round((Date.now() - lastTs) / 60000);