[codex] Surface live run comment context (#4957)

## Thinking Path

> - Paperclip orchestrates AI agents through issue comments and
heartbeat runs
> - The board UI needs to distinguish a comment that triggered a live
run from comments queued after that run started
> - The run payload already stores comment context, but active-run API
responses did not expose the ids the UI needs
> - Without those ids, the triggering comment can flash as queued while
the agent is already responding to it
> - This pull request exposes live-run comment context and teaches the
optimistic comment helper to ignore the trigger comment
> - The benefit is clearer issue-chat state during comment-triggered
agent interruptions

## What Changed

- Added `contextCommentId` and `contextWakeCommentId` to active/live run
payloads.
- Threaded those ids through server routes, heartbeat summaries, UI API
types, and issue detail rendering.
- Updated optimistic comment classification to avoid marking the
triggering comment as queued.
- Added server and UI regression coverage.

## Verification

- `pnpm exec vitest run
server/src/__tests__/agent-live-run-routes.test.ts
ui/src/lib/optimistic-issue-comments.test.ts`

## Risks

- Low-to-medium risk: adds optional fields to existing run payloads.
Existing consumers should ignore unknown fields, and UI handling is
null-safe.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- OpenAI Codex, GPT-5 coding agent, tool use and local command
execution. Exact context window was not exposed in the runtime.

## 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
- [x] 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:
Dotta
2026-05-01 10:44:11 -05:00
committed by GitHub
parent e8275318ba
commit 3cd26a78fc
7 changed files with 72 additions and 0 deletions
@@ -187,6 +187,8 @@ describe("agent live run routes", () => {
status: "running",
invocationSource: "on_demand",
triggerDetail: "manual",
contextCommentId: "comment-1",
contextWakeCommentId: "comment-1",
startedAt: new Date("2026-04-10T09:30:00.000Z"),
finishedAt: null,
createdAt: new Date("2026-04-10T09:29:59.000Z"),
@@ -224,6 +226,8 @@ describe("agent live run routes", () => {
status: "running",
invocationSource: "on_demand",
triggerDetail: "manual",
contextCommentId: "comment-1",
contextWakeCommentId: "comment-1",
startedAt: "2026-04-10T09:30:00.000Z",
finishedAt: null,
createdAt: "2026-04-10T09:29:59.000Z",
+4
View File
@@ -2858,6 +2858,8 @@ export function agentRoutes(
status: heartbeatRuns.status,
invocationSource: heartbeatRuns.invocationSource,
triggerDetail: heartbeatRuns.triggerDetail,
contextCommentId: sql<string | null>`${heartbeatRuns.contextSnapshot} ->> 'commentId'`.as("contextCommentId"),
contextWakeCommentId: sql<string | null>`${heartbeatRuns.contextSnapshot} ->> 'wakeCommentId'`.as("contextWakeCommentId"),
startedAt: heartbeatRuns.startedAt,
finishedAt: heartbeatRuns.finishedAt,
createdAt: heartbeatRuns.createdAt,
@@ -3094,6 +3096,8 @@ export function agentRoutes(
status: heartbeatRuns.status,
invocationSource: heartbeatRuns.invocationSource,
triggerDetail: heartbeatRuns.triggerDetail,
contextCommentId: sql<string | null>`${heartbeatRuns.contextSnapshot} ->> 'commentId'`.as("contextCommentId"),
contextWakeCommentId: sql<string | null>`${heartbeatRuns.contextSnapshot} ->> 'wakeCommentId'`.as("contextWakeCommentId"),
startedAt: heartbeatRuns.startedAt,
finishedAt: heartbeatRuns.finishedAt,
createdAt: heartbeatRuns.createdAt,
+2
View File
@@ -744,6 +744,8 @@ const heartbeatRunIssueSummaryColumns = {
status: heartbeatRuns.status,
invocationSource: heartbeatRuns.invocationSource,
triggerDetail: heartbeatRuns.triggerDetail,
contextCommentId: sql<string | null>`${heartbeatRuns.contextSnapshot} ->> 'commentId'`.as("contextCommentId"),
contextWakeCommentId: sql<string | null>`${heartbeatRuns.contextSnapshot} ->> 'wakeCommentId'`.as("contextWakeCommentId"),
startedAt: heartbeatRuns.startedAt,
finishedAt: heartbeatRuns.finishedAt,
createdAt: heartbeatRuns.createdAt,
+4
View File
@@ -19,6 +19,8 @@ export interface ActiveRunForIssue {
status: string;
invocationSource: string;
triggerDetail: string | null;
contextCommentId?: string | null;
contextWakeCommentId?: string | null;
startedAt: string | Date | null;
finishedAt: string | Date | null;
createdAt: string | Date;
@@ -41,6 +43,8 @@ export interface LiveRunForIssue {
status: string;
invocationSource: string;
triggerDetail: string | null;
contextCommentId?: string | null;
contextWakeCommentId?: string | null;
startedAt: string | null;
finishedAt: string | null;
createdAt: string;
@@ -726,14 +726,61 @@ describe("optimistic issue comments", () => {
expect(
isQueuedIssueComment({
comment: {
id: "comment-2",
createdAt: new Date("2026-03-28T16:20:05.000Z"),
},
activeRunStartedAt: new Date("2026-03-28T16:20:00.000Z"),
activeRunWakeCommentId: "comment-1",
runId: null,
}),
).toBe(true);
});
it("does not mark the comment that triggered the active run as queued", () => {
expect(
isQueuedIssueComment({
comment: {
id: "comment-1",
createdAt: new Date("2026-03-28T16:20:05.000Z"),
},
activeRunStartedAt: new Date("2026-03-28T16:20:00.000Z"),
activeRunCommentId: "comment-1",
activeRunWakeCommentId: "comment-1",
runId: null,
}),
).toBe(false);
});
it("does not mark the active run context comment as queued", () => {
expect(
isQueuedIssueComment({
comment: {
id: "context-comment",
createdAt: new Date("2026-03-28T16:20:05.000Z"),
},
activeRunStartedAt: new Date("2026-03-28T16:20:00.000Z"),
activeRunCommentId: "context-comment",
activeRunWakeCommentId: "wake-comment",
runId: null,
}),
).toBe(false);
});
it("does not mark the active run wake comment as queued", () => {
expect(
isQueuedIssueComment({
comment: {
id: "wake-comment",
createdAt: new Date("2026-03-28T16:20:05.000Z"),
},
activeRunStartedAt: new Date("2026-03-28T16:20:00.000Z"),
activeRunCommentId: "context-comment",
activeRunWakeCommentId: "wake-comment",
runId: null,
}),
).toBe(false);
});
it("does not mark comments with an associated run as queued", () => {
expect(
isQueuedIssueComment({
+9
View File
@@ -70,15 +70,24 @@ export function createOptimisticIssueComment(params: {
export function isQueuedIssueComment(params: {
comment: Pick<IssueTimelineComment, "createdAt"> &
Partial<Pick<OptimisticIssueComment, "clientStatus">> & {
id?: string;
authorAgentId?: string | null;
};
activeRunStartedAt?: Date | string | null;
activeRunAgentId?: string | null;
activeRunCommentId?: string | null;
activeRunWakeCommentId?: string | null;
runId?: string | null;
interruptedRunId?: string | null;
}) {
if (params.runId) return false;
if (params.interruptedRunId) return false;
if (
params.comment.id &&
(params.comment.id === params.activeRunWakeCommentId || params.comment.id === params.activeRunCommentId)
) {
return false;
}
if (params.comment.authorAgentId && params.activeRunAgentId && params.comment.authorAgentId === params.activeRunAgentId) {
return false;
}
+2
View File
@@ -778,6 +778,8 @@ const IssueDetailChatTab = memo(function IssueDetailChatTab({
comment: nextComment,
activeRunStartedAt,
activeRunAgentId: runningIssueRun?.agentId ?? null,
activeRunCommentId: runningIssueRun?.contextCommentId ?? null,
activeRunWakeCommentId: runningIssueRun?.contextWakeCommentId ?? null,
runId: meta?.runId ?? nextComment.runId ?? null,
interruptedRunId: meta?.interruptedRunId ?? nextComment.interruptedRunId ?? null,
})