mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-15 18:30:39 +09:00
fix(ui): fix message attribution for agent-posted comments with user author IDs (#5780)
## Thinking Path > - Paperclip’s issue chat is an audit surface: reviewers need to trust who actually authored a message. > - Some historical agent comments were persisted with `authorUserId` and no surviving `createdByRunId`, so the UI rendered real agent output as if it came from the board user. > - A pure timestamp-window fallback is too risky because human reviewers can comment while agents are running. > - The safe recovery path is to derive attribution only when the server can prove it from same-issue run logs that include the exact posted comment id, then let the chat renderer prefer that recovered agent attribution. > - This keeps historical threads trustworthy without mutating old database rows or guessing in ambiguous cases. ## What Changed - Added shared `IssueComment` fields for derived attribution so server and UI can carry recovered `derivedAuthorAgentId`, `derivedCreatedByRunId`, and `derivedAuthorSource` consistently. - Added server-side attribution recovery in `server/src/services/issues.ts` that reads same-issue run logs and only derives agent authorship when a run log contains the exact `comment id: ...` emitted during posting. - Updated issue chat rendering in `ui/src/lib/issue-chat-messages.ts` to prefer direct agent authorship, then activity-log `runAgentId`, then the server-derived attribution. - Removed the unsafe UI-only run-window fallback from `ui/src/pages/IssueDetail.tsx` so human comments posted during an active run are not silently relabeled as agent output. - Added regression coverage for both the run-log derivation path and the chat-rendering fallback behavior. - Bounded server-side run-log enrichment to 8 concurrent reads per request and removed the unused `issueCommentSchema` declaration during PR cleanup. ## Verification - `pnpm exec vitest run ui/src/lib/issue-chat-messages.test.ts server/src/__tests__/issues-service.test.ts` - `pnpm test:run:general` - Live validation on May 12, 2026 in `PAPA-322`: confirmed the previously misattributed historical comments on `PAPA-316` now render as Claude-authored on `http://goldie.gerbil-company.ts.net:3100`. - Reviewer check: open `PAPA-316` in the running instance and confirm historical comments such as `## Investigation: exe.dev 422 + codex re-test` render under Claude instead of the board user. ## Risks - Low risk. The change is scoped to comment attribution recovery and rendering. - Derived attribution is intentionally conservative: if there is no exact run-log proof, the comment remains user-authored instead of guessing. - Run-log recovery depends on retained same-issue logs, so older comments without that evidence remain unchanged. ## Model Used - OpenAI Codex via the Paperclip `codex_local` adapter (GPT-5-class coding agent with tool use in the local Paperclip runtime; the exact deployment/model ID is not surfaced by this workspace). ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [ ] If this change affects the UI, I have included before/after screenshots - [ ] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
9746dab4e8
commit
c445e59256
5 changed files with 379 additions and 17 deletions
|
|
@ -324,6 +324,72 @@ describe("buildIssueChatMessages", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("prefers derived agent attribution when a board-authored comment is proven to come from a run", () => {
|
||||
const agentMap = new Map<string, Agent>([["agent-1", createAgent("agent-1", "Claude")]]);
|
||||
const messages = buildIssueChatMessages({
|
||||
comments: [
|
||||
createComment({
|
||||
authorUserId: "user-1",
|
||||
derivedAuthorAgentId: "agent-1",
|
||||
derivedCreatedByRunId: "run-1",
|
||||
}),
|
||||
],
|
||||
timelineEvents: [],
|
||||
linkedRuns: [],
|
||||
liveRuns: [],
|
||||
agentMap,
|
||||
currentUserId: "user-1",
|
||||
userLabelMap: new Map([["user-1", "Dotta"]]),
|
||||
});
|
||||
|
||||
expect(messages[0]).toMatchObject({
|
||||
role: "assistant",
|
||||
metadata: {
|
||||
custom: {
|
||||
authorName: "Claude",
|
||||
authorType: "agent",
|
||||
authorAgentId: "agent-1",
|
||||
authorUserId: "user-1",
|
||||
runId: "run-1",
|
||||
runAgentId: "agent-1",
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("renders a comment as agent-authored when runAgentId is set from activity log", () => {
|
||||
const agentMap = new Map<string, Agent>([["agent-1", createAgent("agent-1", "Claude")]]);
|
||||
const messages = buildIssueChatMessages({
|
||||
comments: [
|
||||
createComment({
|
||||
authorUserId: "user-1",
|
||||
runId: "run-1",
|
||||
runAgentId: "agent-1",
|
||||
}),
|
||||
],
|
||||
timelineEvents: [],
|
||||
linkedRuns: [],
|
||||
liveRuns: [],
|
||||
agentMap,
|
||||
currentUserId: "user-1",
|
||||
userLabelMap: new Map([["user-1", "Dotta"]]),
|
||||
});
|
||||
|
||||
expect(messages[0]).toMatchObject({
|
||||
role: "assistant",
|
||||
metadata: {
|
||||
custom: {
|
||||
authorName: "Claude",
|
||||
authorType: "agent",
|
||||
authorAgentId: "agent-1",
|
||||
authorUserId: "user-1",
|
||||
runId: "run-1",
|
||||
runAgentId: "agent-1",
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("orders events before comments and appends active live runs as running assistant messages", () => {
|
||||
const agentMap = new Map<string, Agent>([["agent-1", createAgent("agent-1", "CodexCoder")]]);
|
||||
const comments = [
|
||||
|
|
|
|||
|
|
@ -337,14 +337,32 @@ function createAssistantMetadata(custom: Record<string, unknown>) {
|
|||
} as const;
|
||||
}
|
||||
|
||||
function effectiveCommentAuthorAgentId(comment: IssueChatComment) {
|
||||
return comment.authorAgentId ?? comment.runAgentId ?? comment.derivedAuthorAgentId ?? null;
|
||||
}
|
||||
|
||||
function effectiveCommentRunId(comment: IssueChatComment) {
|
||||
return comment.runId ?? comment.derivedCreatedByRunId ?? null;
|
||||
}
|
||||
|
||||
function effectiveCommentRunAgentId(comment: IssueChatComment) {
|
||||
return comment.runAgentId ?? effectiveCommentAuthorAgentId(comment);
|
||||
}
|
||||
|
||||
function effectiveCommentAuthorType(comment: IssueChatComment) {
|
||||
return effectiveCommentAuthorAgentId(comment) ? "agent" : comment.authorType;
|
||||
}
|
||||
|
||||
function authorNameForComment(
|
||||
comment: IssueChatComment,
|
||||
agentMap?: Map<string, Agent>,
|
||||
currentUserId?: string | null,
|
||||
userLabelMap?: ReadonlyMap<string, string> | null,
|
||||
options?: { isSystemNotice?: boolean },
|
||||
) {
|
||||
if (comment.authorAgentId) {
|
||||
return agentMap?.get(comment.authorAgentId)?.name ?? comment.authorAgentId.slice(0, 8);
|
||||
const authorAgentId = effectiveCommentAuthorAgentId(comment);
|
||||
if (authorAgentId) {
|
||||
return agentMap?.get(authorAgentId)?.name ?? (options?.isSystemNotice ? "Paperclip" : authorAgentId.slice(0, 8));
|
||||
}
|
||||
const authorUserId = comment.authorUserId ?? null;
|
||||
if (!authorUserId) return "You";
|
||||
|
|
@ -367,20 +385,21 @@ function createCommentMessage(args: {
|
|||
}): ThreadMessage {
|
||||
const { comment, agentMap, currentUserId, userLabelMap, companyId, projectId } = args;
|
||||
const createdAt = toDate(comment.createdAt);
|
||||
const authorName = authorNameForComment(comment, agentMap, currentUserId, userLabelMap);
|
||||
const isSystemNotice = comment.authorType === "system";
|
||||
const authorAgentId = effectiveCommentAuthorAgentId(comment);
|
||||
const authorName = authorNameForComment(comment, agentMap, currentUserId, userLabelMap, { isSystemNotice });
|
||||
const custom = {
|
||||
kind: isSystemNotice ? "system_notice" : "comment",
|
||||
commentId: comment.id,
|
||||
anchorId: `comment-${comment.id}`,
|
||||
authorName,
|
||||
authorType: comment.authorType,
|
||||
authorAgentId: comment.authorAgentId,
|
||||
authorType: effectiveCommentAuthorType(comment),
|
||||
authorAgentId,
|
||||
authorUserId: comment.authorUserId,
|
||||
companyId: companyId ?? comment.companyId,
|
||||
projectId: projectId ?? null,
|
||||
runId: comment.runId ?? null,
|
||||
runAgentId: comment.runAgentId ?? null,
|
||||
runId: effectiveCommentRunId(comment),
|
||||
runAgentId: effectiveCommentRunAgentId(comment),
|
||||
clientStatus: comment.clientStatus ?? null,
|
||||
queueState: comment.queueState ?? null,
|
||||
queueTargetRunId: comment.queueTargetRunId ?? null,
|
||||
|
|
@ -402,7 +421,7 @@ function createCommentMessage(args: {
|
|||
return message;
|
||||
}
|
||||
|
||||
if (comment.authorAgentId) {
|
||||
if (authorAgentId) {
|
||||
const message: ThreadAssistantMessage = {
|
||||
id: comment.id,
|
||||
role: "assistant",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue