ework-daemon 0.4.56 → 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.56",
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
@@ -332,6 +332,7 @@ export class Engine {
332
332
  private startedAt = new Map<string, number>();
333
333
  private progressCommentId = new Map<string, string>();
334
334
  private pickupCommentId = new Map<string, string>();
335
+ private forwardCommentId = new Map<string, string>();
335
336
 
336
337
  private nudgeRounds = new Map<string, number>();
337
338
  private emptyResponseRounds = new Map<string, number>();
@@ -358,6 +359,7 @@ export class Engine {
358
359
  private static MAX_NUDGE_ROUNDS = 1;
359
360
  private static MAX_EMPTY_RESPONSE_ROUNDS = 1;
360
361
  private static MAX_STUCK_NUDGE_ROUNDS = 1;
362
+ private static MAX_RUNTIME_MS = 3 * 60 * 60 * 1000;
361
363
  private static OBSERVER_INTERVAL_MS = 5 * 60 * 1000;
362
364
  private static STUCK_THRESHOLD_MS = 30 * 60 * 1000;
363
365
  private static MAX_PROCESS_EXIT_NUDGE_ROUNDS = 1;
@@ -556,6 +558,10 @@ export class Engine {
556
558
  return this.cfg.stuck?.maxNudges ?? Engine.MAX_STUCK_NUDGE_ROUNDS;
557
559
  }
558
560
 
561
+ private get maxRuntimeMs(): number {
562
+ return this.cfg.stuck?.maxRuntimeMs ?? Engine.MAX_RUNTIME_MS;
563
+ }
564
+
559
565
  private getTracker(type: string): IssueTracker {
560
566
  const tracker = this.trackers.get(type);
561
567
  if (!tracker) throw new Error(`Unknown tracker type: ${type}`);
@@ -894,7 +900,10 @@ export class Engine {
894
900
  );
895
901
 
896
902
  // Immediate ack
897
- await tracker.createComment(ref, `[system] ✓ Message forwarded to **${session.name}**${this.running.has(this.sessionKey(session, issue)) ? " (running)" : ""}.\n> session: ${this.sessionRef(session)} | workdir: ${this.workdirLink(workdir)}`);
903
+ {
904
+ const c = await tracker.createComment(ref, `[system] ✓ Message forwarded to **${session.name}**${this.running.has(this.sessionKey(session, issue)) ? " (running)" : ""}.\n> session: ${this.sessionRef(session)} | workdir: ${this.workdirLink(workdir)}`);
905
+ if (!session.opencodeSessionId) this.forwardCommentId.set(this.sessionKey(session, issue), c.id);
906
+ }
898
907
 
899
908
  await this.enqueueOrRun(session, issue, prompt, comment.id, model);
900
909
  } else {
@@ -1230,10 +1239,17 @@ export class Engine {
1230
1239
  const pickupId = this.pickupCommentId.get(k);
1231
1240
  if (pickupId) {
1232
1241
  this.pickupCommentId.delete(k);
1242
+ this.forwardCommentId.delete(k);
1233
1243
  await tracker
1234
1244
  .editComment(ref, pickupId, `[system] 🔄 **${session.name}** picked up this issue.\n> session: ${this.sessionRef(session)} | workdir: ${this.workdirLink(workdir)}`)
1235
1245
  .catch((err) => log.error(`engine: failed to rewrite pickup comment for ${k}:`, (err as Error).message));
1236
1246
  }
1247
+ const forwardId = this.forwardCommentId.get(k);
1248
+ if (forwardId) {
1249
+ this.forwardCommentId.delete(k);
1250
+ const body = `[system] ✓ Message forwarded to **${session.name}**${this.running.has(k) ? " (running)" : ""}.\n> session: ${this.sessionRef(session)} | workdir: ${this.workdirLink(workdir)}`;
1251
+ await tracker.editComment(ref, forwardId, body).catch(() => { /* cosmetic rewrite */ });
1252
+ }
1237
1253
  }
1238
1254
  },
1239
1255
  },
@@ -1738,6 +1754,19 @@ export class Engine {
1738
1754
  continue;
1739
1755
  }
1740
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
+
1741
1770
  // Process alive — check stuck
1742
1771
  if (lastTs && Date.now() - lastTs >= this.stuckThresholdMs) {
1743
1772
  const minutes = Math.round((Date.now() - lastTs) / 60000);