fix(sweep): בחירת הערת-יו"ר אחרונה לפי זמן, לא לפי מיקום-מערך #4

Merged
chaim merged 1 commits from fix/sweep-comment-ordering into main 2026-06-30 11:41:32 +00:00

View File

@@ -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) {