ework-daemon 0.4.55 → 0.4.57

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/opencode.ts +28 -17
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ework-daemon",
3
- "version": "0.4.55",
3
+ "version": "0.4.57",
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/opencode.ts CHANGED
@@ -98,28 +98,24 @@ const RECENT_BOT_REPLY_THRESHOLD_MS = 5 * 60_000; // 5 minutes
98
98
 
99
99
  /**
100
100
  * Determine whether any non-system bot reply exists that is causally after
101
- * `promptTime` (when provided) or within a 5-minute absolute window (fallback).
102
- * Exported for unit testing the causal vs absolute distinction is the P0
103
- * correctness fix for preempt/nudge false-done detection.
101
+ * `promptTime` (when provided) AND within a 5-minute absolute window.
102
+ * The causal bound prevents preempt/nudge false-done detection (a reply from
103
+ * the previous run must not satisfy this run); the recency bound prevents an
104
+ * early ack from satisfying a long run that ended silently (awork policy).
105
+ * Exported for unit testing.
104
106
  */
105
107
  export function hasRecentBotReply(
106
108
  comments: TrackerComment[],
107
109
  isBotUser: (author: string) => boolean,
108
110
  promptTime?: number,
109
111
  ): boolean {
110
- if (promptTime) {
111
- return comments.some(c => {
112
- if (!isBotUser(c.author) || c.body.startsWith(SYSTEM_PREFIX)) return false;
113
- if (!c.createdAt) return false;
114
- return new Date(c.createdAt).getTime() > promptTime;
115
- });
116
- }
117
112
  const now = Date.now();
118
113
  return comments.some(c => {
119
114
  if (!isBotUser(c.author) || c.body.startsWith(SYSTEM_PREFIX)) return false;
120
- if (!c.createdAt) return true;
121
- const age = now - new Date(c.createdAt).getTime();
122
- return age < RECENT_BOT_REPLY_THRESHOLD_MS;
115
+ if (!c.createdAt) return !promptTime;
116
+ const created = new Date(c.createdAt).getTime();
117
+ if (promptTime && created <= promptTime) return false;
118
+ return now - created < RECENT_BOT_REPLY_THRESHOLD_MS;
123
119
  });
124
120
  }
125
121
 
@@ -336,6 +332,7 @@ export class Engine {
336
332
  private startedAt = new Map<string, number>();
337
333
  private progressCommentId = new Map<string, string>();
338
334
  private pickupCommentId = new Map<string, string>();
335
+ private forwardCommentId = new Map<string, string>();
339
336
 
340
337
  private nudgeRounds = new Map<string, number>();
341
338
  private emptyResponseRounds = new Map<string, number>();
@@ -898,7 +895,10 @@ export class Engine {
898
895
  );
899
896
 
900
897
  // Immediate ack
901
- 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)}`);
898
+ {
899
+ 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)}`);
900
+ if (!session.opencodeSessionId) this.forwardCommentId.set(this.sessionKey(session, issue), c.id);
901
+ }
902
902
 
903
903
  await this.enqueueOrRun(session, issue, prompt, comment.id, model);
904
904
  } else {
@@ -1234,10 +1234,17 @@ export class Engine {
1234
1234
  const pickupId = this.pickupCommentId.get(k);
1235
1235
  if (pickupId) {
1236
1236
  this.pickupCommentId.delete(k);
1237
+ this.forwardCommentId.delete(k);
1237
1238
  await tracker
1238
1239
  .editComment(ref, pickupId, `[system] 🔄 **${session.name}** picked up this issue.\n> session: ${this.sessionRef(session)} | workdir: ${this.workdirLink(workdir)}`)
1239
1240
  .catch((err) => log.error(`engine: failed to rewrite pickup comment for ${k}:`, (err as Error).message));
1240
1241
  }
1242
+ const forwardId = this.forwardCommentId.get(k);
1243
+ if (forwardId) {
1244
+ this.forwardCommentId.delete(k);
1245
+ const body = `[system] ✓ Message forwarded to **${session.name}**${this.running.has(k) ? " (running)" : ""}.\n> session: ${this.sessionRef(session)} | workdir: ${this.workdirLink(workdir)}`;
1246
+ await tracker.editComment(ref, forwardId, body).catch(() => { /* cosmetic rewrite */ });
1247
+ }
1241
1248
  }
1242
1249
  },
1243
1250
  },
@@ -1361,9 +1368,13 @@ export class Engine {
1361
1368
  const hasRecent = this.hasRecentBotReply(commentsNow, tracker, started ?? undefined);
1362
1369
 
1363
1370
  if (hasRecent) {
1364
- // oldest-first comment lists would match a pre-run bot reply when started
1365
- // is undefined; pick the LAST qualifying comment instead.
1366
- const matched = [...commentsNow].reverse().find(c => tracker.isBotUser(c.author) && !this.isSystemComment(c) && c.createdAt && new Date(c.createdAt).getTime() > (started ?? 0));
1371
+ const nowMs = Date.now();
1372
+ const matched = [...commentsNow].reverse().find(c => {
1373
+ if (!tracker.isBotUser(c.author) || this.isSystemComment(c) || !c.createdAt) return false;
1374
+ const created = new Date(c.createdAt).getTime();
1375
+ if (started && created <= started) return false;
1376
+ return nowMs - created < RECENT_BOT_REPLY_THRESHOLD_MS;
1377
+ });
1367
1378
  log.info(`engine: [bot] reply found for ${k} after prompt (comment ${matched?.id ?? "?"} createdAt ${matched?.createdAt ?? "?"}), marking done`);
1368
1379
  if (matched && !usedModel) {
1369
1380
  const fromSession = await this.backend.lastSessionModel(session.opencodeSessionId).catch(() => ({ model: "" }));