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>
This commit is contained in:
@@ -150,6 +150,7 @@ describeEmbeddedPostgres("issueService.list participantAgentId", () => {
|
||||
await db.delete(projectWorkspaces);
|
||||
await db.delete(projects);
|
||||
await db.delete(goals);
|
||||
await db.delete(heartbeatRuns);
|
||||
await db.delete(agents);
|
||||
await db.delete(instanceSettings);
|
||||
await db.delete(companies);
|
||||
@@ -1159,6 +1160,68 @@ describeEmbeddedPostgres("issueService.list participantAgentId", () => {
|
||||
expect(comments.map((comment) => comment.id)).toEqual([firstCommentId]);
|
||||
});
|
||||
|
||||
it("lists user comments when derived run attribution scans a timestamp window", async () => {
|
||||
const companyId = randomUUID();
|
||||
const agentId = randomUUID();
|
||||
const issueId = randomUUID();
|
||||
const commentId = randomUUID();
|
||||
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
|
||||
await db.insert(agents).values({
|
||||
id: agentId,
|
||||
companyId,
|
||||
name: "CodexCoder",
|
||||
role: "engineer",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
});
|
||||
|
||||
await db.insert(issues).values({
|
||||
id: issueId,
|
||||
companyId,
|
||||
title: "Comments issue",
|
||||
status: "todo",
|
||||
priority: "medium",
|
||||
});
|
||||
|
||||
await db.insert(heartbeatRuns).values({
|
||||
id: randomUUID(),
|
||||
companyId,
|
||||
agentId,
|
||||
contextSnapshot: { issueId },
|
||||
createdAt: new Date("2026-05-12T22:58:00.000Z"),
|
||||
startedAt: new Date("2026-05-12T22:58:00.000Z"),
|
||||
finishedAt: new Date("2026-05-12T23:14:00.000Z"),
|
||||
});
|
||||
|
||||
await db.insert(issueComments).values({
|
||||
id: commentId,
|
||||
companyId,
|
||||
issueId,
|
||||
authorUserId: "user-1",
|
||||
body: "Comment should be visible",
|
||||
createdAt: new Date("2026-05-12T23:00:00.000Z"),
|
||||
updatedAt: new Date("2026-05-12T23:00:00.000Z"),
|
||||
});
|
||||
|
||||
const comments = await svc.listComments(issueId, {
|
||||
order: "desc",
|
||||
limit: 50,
|
||||
});
|
||||
|
||||
expect(comments.map((comment) => comment.id)).toEqual([commentId]);
|
||||
expect(comments[0]?.body).toBe("Comment should be visible");
|
||||
});
|
||||
|
||||
it("includes blockedBy summaries on list rows in one batched pass", async () => {
|
||||
const companyId = randomUUID();
|
||||
const blockerId = randomUUID();
|
||||
|
||||
@@ -1943,6 +1943,11 @@ export function issueService(db: Db) {
|
||||
}, null);
|
||||
if (minCommentCreatedAtMs === null || maxCommentCreatedAtMs === null) return comments;
|
||||
|
||||
const minCommentCreatedAt = new Date(minCommentCreatedAtMs).toISOString();
|
||||
const maxCommentCreatedAt = new Date(
|
||||
maxCommentCreatedAtMs + ISSUE_COMMENT_RUN_LOG_DERIVATION_END_SLACK_MS,
|
||||
).toISOString();
|
||||
|
||||
const runs = await db
|
||||
.select({
|
||||
runId: heartbeatRuns.id,
|
||||
@@ -1969,8 +1974,8 @@ export function issueService(db: Db) {
|
||||
and ${activityLog.runId} = ${heartbeatRuns.id}
|
||||
)`,
|
||||
),
|
||||
sql`coalesce(${heartbeatRuns.finishedAt}, ${heartbeatRuns.createdAt}) >= ${new Date(minCommentCreatedAtMs)}`,
|
||||
sql`coalesce(${heartbeatRuns.startedAt}, ${heartbeatRuns.createdAt}) <= ${new Date(maxCommentCreatedAtMs + ISSUE_COMMENT_RUN_LOG_DERIVATION_END_SLACK_MS)}`,
|
||||
sql`coalesce(${heartbeatRuns.finishedAt}, ${heartbeatRuns.createdAt}) >= ${minCommentCreatedAt}::timestamptz`,
|
||||
sql`coalesce(${heartbeatRuns.startedAt}, ${heartbeatRuns.createdAt}) <= ${maxCommentCreatedAt}::timestamptz`,
|
||||
),
|
||||
)
|
||||
.orderBy(desc(heartbeatRuns.createdAt));
|
||||
|
||||
Reference in New Issue
Block a user