ework-daemon 0.4.70 → 0.4.72

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.70",
3
+ "version": "0.4.72",
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/op.ts CHANGED
@@ -379,6 +379,22 @@ export class Store {
379
379
  const db = getDB();
380
380
  const cutoff = new Date(Date.now() - leaseTtlMs).toISOString();
381
381
 
382
+ // Identity reuse first: a fast restart (previous heartbeat still fresh)
383
+ // must keep the SAME row/id — daemon_id is embedded in web session links,
384
+ // so a fresh insert per boot orphans every historical link.
385
+ const mine = await db.get<{ id: number }>(
386
+ "SELECT id FROM {{daemons}} WHERE display_name = ? AND internal_endpoint = ? LIMIT 1",
387
+ [displayName, endpoint]
388
+ );
389
+ if (mine) {
390
+ const now = new Date().toISOString();
391
+ const res = await db.run(
392
+ "UPDATE {{daemons}} SET last_heartbeat = ?, status = 'active', capacity = ? WHERE id = ?",
393
+ [now, capacity, mine.id]
394
+ );
395
+ if (res.changes === 1) return mine.id;
396
+ }
397
+
382
398
  const orphan = await db.get<{ id: number }>(
383
399
  "SELECT id FROM {{daemons}} WHERE last_heartbeat < ? ORDER BY last_heartbeat LIMIT 1",
384
400
  [cutoff]
package/src/opencode.ts CHANGED
@@ -422,6 +422,13 @@ export class Engine {
422
422
  private static MAX_INLINE_SIZE = 4000;
423
423
  private static MAX_NUDGE_ROUNDS = 1;
424
424
  private static MAX_EMPTY_RESPONSE_ROUNDS = 1;
425
+ // A queued (pending) message older than this is dead context — e.g. forwards
426
+ // left over from a webhook echo storm, or comments queued behind a session
427
+ // that ran for hours. Replaying them re-runs stale prompts and ping-pongs
428
+ // nudges ("reply → done → drain next stale → nudge → reply …"), observed on
429
+ // dog/tasks#3: 17:24 storm forwards replayed at 20:09–20:11. Expire instead
430
+ // of replay; explicit retryMessage bypasses this via { force: true }.
431
+ private static MAX_PENDING_AGE_MS = 30 * 60_000;
425
432
  private static MAX_STUCK_NUDGE_ROUNDS = 1;
426
433
  private static MAX_RUNTIME_MS = 3 * 60 * 60 * 1000;
427
434
  private static OBSERVER_INTERVAL_MS = 5 * 60 * 1000;
@@ -1717,7 +1724,24 @@ export class Engine {
1717
1724
  }
1718
1725
  }
1719
1726
 
1720
- private async dequeueOrIdle(k: string, session: OpSession, issue: Issue, msg: Message) {
1727
+ private async dequeueOrIdle(k: string, session: OpSession, issue: Issue, msg: Message, opts: { force?: boolean } = {}) {
1728
+ if (!opts.force) {
1729
+ let next: Message | undefined = msg;
1730
+ while (next) {
1731
+ const age = Date.now() - next.createdAt.getTime();
1732
+ if (age <= Engine.MAX_PENDING_AGE_MS) break;
1733
+ log.warn(`engine: expiring stale pending msg ${next.id.slice(0, 8)} for ${k} (age ${Math.round(age / 60_000)}min > ${Math.round(Engine.MAX_PENDING_AGE_MS / 60_000)}min) — skipping replay`);
1734
+ await this.store.updateMessageStatus(next.id, "failed", "expired: stale pending message not replayed");
1735
+ next = await this.store.getNextPendingMessage(session.id);
1736
+ }
1737
+ if (!next) {
1738
+ this.clearRuntimeState(k);
1739
+ await this.store.updateSession(session.id, { state: "idle" });
1740
+ void this.drainGlobalPending();
1741
+ return;
1742
+ }
1743
+ msg = next;
1744
+ }
1721
1745
  log.info(`engine: running dequeued msg ${msg.id.slice(0, 8)} for ${k}`);
1722
1746
  await this.store.updateMessageStatus(msg.id, "running");
1723
1747
  this.running.add(k);
@@ -2238,7 +2262,7 @@ export class Engine {
2238
2262
  await this.store.updateMessageStatus(messageId, "pending");
2239
2263
  const k = this.sessionKey(session, issue);
2240
2264
  if (!this.running.has(k)) {
2241
- await this.dequeueOrIdle(k, session, issue, msg);
2265
+ await this.dequeueOrIdle(k, session, issue, msg, { force: true });
2242
2266
  }
2243
2267
  return true;
2244
2268
  }