diff --git a/src/worker.ts b/src/worker.ts index fc546ce..5bf7e12 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -697,7 +697,16 @@ const plugin = definePlugin({ const matched = commentId ? comments.find((c) => c.id === commentId) : undefined; - const latest = comments[comments.length - 1]; + // Newest by createdAt — do NOT rely on listComments array order. + const latest = comments.reduce<(typeof comments)[number] | null>( + (acc, c) => + !acc || + new Date(c.createdAt).getTime() > + new Date(acc.createdAt).getTime() + ? c + : acc, + null, + ); commentBody = matched?.body || latest?.body || @@ -997,14 +1006,28 @@ const plugin = definePlugin({ ); if (comments.length === 0) continue; - // "Pending" = the conversation's last word is the chair's, with - // no agent reply after it. listComments is chronological (newest - // last). If an agent already responded, the comment was handled — - // skip. This both targets the exact failure (chair commented, no - // agent response) and avoids re-routing historically-answered - // comments on first run after deploy. - const last = comments[comments.length - 1]; - if (last.authorType !== "user") continue; + // "Pending" = the chair spoke last among real participants: the + // newest USER comment is newer than the newest AGENT comment (or + // no agent has commented yet), so an agent never responded to it. + // Order-independent (sort by createdAt, do NOT rely on the array + // order of listComments) and system comments are ignored — an + // automated "draft ready" line is not an agent response. This + // targets the exact failure (chair commented, no agent reply) and + // skips historically-answered comments. + const ts = (c: { createdAt: string | Date }) => + new Date(c.createdAt).getTime(); + let newestUser: (typeof comments)[number] | null = null; + let newestAgent: (typeof comments)[number] | null = null; + for (const c of comments) { + if (c.authorType === "user") { + if (!newestUser || ts(c) > ts(newestUser)) newestUser = c; + } else if (c.authorType === "agent") { + if (!newestAgent || ts(c) > ts(newestAgent)) newestAgent = c; + } + } + if (!newestUser) continue; // no chair comment + // An agent has already responded after the chair — handled. + if (newestAgent && ts(newestAgent) >= ts(newestUser)) continue; const marker = await ctx.state.get({ scopeKind: "issue", @@ -1012,18 +1035,18 @@ const plugin = definePlugin({ stateKey: LAST_ROUTED_COMMENT_KEY, }); // Already routed (CEO may still be working) — don't re-fire. - if (marker === last.id) continue; + if (marker === newestUser.id) continue; ctx.logger.info( "route-pending-comments: re-routing unrouted user comment", - { issueId: issue.id, commentId: last.id }, + { issueId: issue.id, commentId: newestUser.id }, ); await routeCommentToCeo({ issue, companyId: company.id, ceoAgentId, - commentBody: last.body, - commentId: last.id, + commentBody: newestUser.body, + commentId: newestUser.id, source: "sweep", }); } catch (err) {