Skip to content

Commit eb452fb

Browse files
Fix comment date binding regression (#5919)
## Thinking Path > - Paperclip is the control plane for autonomous AI companies, and issue comments are the primary durable communication surface between operators and agents. > - Commit `c445e592` (`fix(ui): fix message attribution for agent-posted comments with user author IDs (#5780)`) added server-side derived attribution for historical comments by scanning heartbeat runs near comment timestamps. > - That scan accidentally bound JavaScript `Date` objects directly into postgres-js SQL fragments for the run timestamp window. > - On real Postgres, that can fail while listing issue comments with `ERR_INVALID_ARG_TYPE`, which makes comments disappear from issue pages such as `PAP-9284`. > - This pull request keeps the attribution behavior intact while changing only the broken timestamp binding path. > - The benefit is that comments load again without weakening the conservative attribution recovery introduced by `c445e592`. ## What Changed - Convert the derived-attribution heartbeat-run window bounds to ISO timestamp strings before binding them into SQL, with explicit `::timestamptz` casts. - Add an embedded Postgres regression that inserts a heartbeat run and user-authored comment, then verifies `issueService.listComments()` returns the comment while the attribution scan runs. - Delete `heartbeat_runs` during the issue service test cleanup before deleting agents so the new test data does not leak across cases. ## Verification - `pnpm exec vitest run server/src/__tests__/issues-service.test.ts -t "lists user comments when derived run attribution scans a timestamp window"` - `pnpm --filter @paperclipai/server typecheck` - `git diff --check` ## Risks - Low risk. The change is limited to how timestamp parameters are bound for an existing query. - The derived attribution logic remains conservative and still requires exact run-log proof before relabeling a comment. - The regression uses embedded Postgres so it covers the postgres-js binding path that failed in production-like local runs. > 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 via the Paperclip `codex_local` adapter; GPT-5 coding-agent family with local terminal, file-editing, and git/GitHub CLI tool use. Exact hosted model deployment ID is not exposed by this local adapter 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 - [x] If this change affects the UI, I have included before/after screenshots (not applicable: server-side comment API bugfix) - [x] I have updated relevant documentation to reflect my changes (not applicable: no documented behavior or command changed) - [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>
1 parent b947a7d commit eb452fb

2 files changed

Lines changed: 70 additions & 2 deletions

File tree

server/src/__tests__/issues-service.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ describeEmbeddedPostgres("issueService.list participantAgentId", () => {
150150
await db.delete(projectWorkspaces);
151151
await db.delete(projects);
152152
await db.delete(goals);
153+
await db.delete(heartbeatRuns);
153154
await db.delete(agents);
154155
await db.delete(instanceSettings);
155156
await db.delete(companies);
@@ -1159,6 +1160,68 @@ describeEmbeddedPostgres("issueService.list participantAgentId", () => {
11591160
expect(comments.map((comment) => comment.id)).toEqual([firstCommentId]);
11601161
});
11611162

1163+
it("lists user comments when derived run attribution scans a timestamp window", async () => {
1164+
const companyId = randomUUID();
1165+
const agentId = randomUUID();
1166+
const issueId = randomUUID();
1167+
const commentId = randomUUID();
1168+
1169+
await db.insert(companies).values({
1170+
id: companyId,
1171+
name: "Paperclip",
1172+
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
1173+
requireBoardApprovalForNewAgents: false,
1174+
});
1175+
1176+
await db.insert(agents).values({
1177+
id: agentId,
1178+
companyId,
1179+
name: "CodexCoder",
1180+
role: "engineer",
1181+
status: "active",
1182+
adapterType: "codex_local",
1183+
adapterConfig: {},
1184+
runtimeConfig: {},
1185+
permissions: {},
1186+
});
1187+
1188+
await db.insert(issues).values({
1189+
id: issueId,
1190+
companyId,
1191+
title: "Comments issue",
1192+
status: "todo",
1193+
priority: "medium",
1194+
});
1195+
1196+
await db.insert(heartbeatRuns).values({
1197+
id: randomUUID(),
1198+
companyId,
1199+
agentId,
1200+
contextSnapshot: { issueId },
1201+
createdAt: new Date("2026-05-12T22:58:00.000Z"),
1202+
startedAt: new Date("2026-05-12T22:58:00.000Z"),
1203+
finishedAt: new Date("2026-05-12T23:14:00.000Z"),
1204+
});
1205+
1206+
await db.insert(issueComments).values({
1207+
id: commentId,
1208+
companyId,
1209+
issueId,
1210+
authorUserId: "user-1",
1211+
body: "Comment should be visible",
1212+
createdAt: new Date("2026-05-12T23:00:00.000Z"),
1213+
updatedAt: new Date("2026-05-12T23:00:00.000Z"),
1214+
});
1215+
1216+
const comments = await svc.listComments(issueId, {
1217+
order: "desc",
1218+
limit: 50,
1219+
});
1220+
1221+
expect(comments.map((comment) => comment.id)).toEqual([commentId]);
1222+
expect(comments[0]?.body).toBe("Comment should be visible");
1223+
});
1224+
11621225
it("includes blockedBy summaries on list rows in one batched pass", async () => {
11631226
const companyId = randomUUID();
11641227
const blockerId = randomUUID();

server/src/services/issues.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1943,6 +1943,11 @@ export function issueService(db: Db) {
19431943
}, null);
19441944
if (minCommentCreatedAtMs === null || maxCommentCreatedAtMs === null) return comments;
19451945

1946+
const minCommentCreatedAt = new Date(minCommentCreatedAtMs).toISOString();
1947+
const maxCommentCreatedAt = new Date(
1948+
maxCommentCreatedAtMs + ISSUE_COMMENT_RUN_LOG_DERIVATION_END_SLACK_MS,
1949+
).toISOString();
1950+
19461951
const runs = await db
19471952
.select({
19481953
runId: heartbeatRuns.id,
@@ -1969,8 +1974,8 @@ export function issueService(db: Db) {
19691974
and ${activityLog.runId} = ${heartbeatRuns.id}
19701975
)`,
19711976
),
1972-
sql`coalesce(${heartbeatRuns.finishedAt}, ${heartbeatRuns.createdAt}) >= ${new Date(minCommentCreatedAtMs)}`,
1973-
sql`coalesce(${heartbeatRuns.startedAt}, ${heartbeatRuns.createdAt}) <= ${new Date(maxCommentCreatedAtMs + ISSUE_COMMENT_RUN_LOG_DERIVATION_END_SLACK_MS)}`,
1977+
sql`coalesce(${heartbeatRuns.finishedAt}, ${heartbeatRuns.createdAt}) >= ${minCommentCreatedAt}::timestamptz`,
1978+
sql`coalesce(${heartbeatRuns.startedAt}, ${heartbeatRuns.createdAt}) <= ${maxCommentCreatedAt}::timestamptz`,
19741979
),
19751980
)
19761981
.orderBy(desc(heartbeatRuns.createdAt));

0 commit comments

Comments
 (0)