forked from farhoodlabs/paperclip
Fix company export with missing run logs (#5960)
## Thinking Path > - Paperclip is the control plane for autonomous AI companies. > - Company export/import lets operators move company state, including issue threads and agent execution context, between Paperclip instances. > - Issue comments can be enriched by nearby heartbeat run logs so exported threads preserve useful agent/run attribution metadata. > - Some local instances can have heartbeat run database rows whose local log files were deleted or never copied into the current workspace. > - The export path should still include the original user comments instead of failing because optional run-log metadata is unavailable. > - This pull request makes comment run-log metadata derivation tolerate missing local log files, logs the missing-file condition for operators, and adds a regression test. > - The benefit is safer company exports for real instances with incomplete local run-log storage. ## What Changed - Treat missing local heartbeat run logs as absent optional metadata while listing issue comments. - Emit a structured warning with `runId` and `logRef` when optional comment-attribution log content is missing. - Preserve the existing error behavior for non-404 run-log read failures. - Added a regression test proving user comments still list when a candidate attribution run has a missing local log reference. ## Verification - `pnpm exec vitest run server/src/__tests__/issues-service.test.ts -t "candidate attribution run log is missing"` passed: 1 selected test passed, 47 skipped. - `pnpm --filter @paperclipai/server typecheck` passed. - Greptile Review passed with Confidence Score 5/5 and zero unresolved threads on commit `f68cac02bf98d7d31e7831e5bdfa95cffa85e254`. - GitHub PR workflow run succeeded: `policy`, `verify`, four serialized server suites, `e2e`, and `Canary Dry Run` all passed. - `security/snyk (cryppadotta)` passed. - Confirmed this branch is on top of `public-gh/master` and `pnpm-lock.yaml` is not in the PR diff. ## Risks - Low risk. The change only softens optional comment metadata derivation for 404/missing local log files; other log read errors still throw. - Exported comments in this edge case may lack derived run metadata, but they remain visible/exportable instead of failing the request. - Operators may see new warnings when historical run-log references point to missing local files; those warnings indicate degraded optional metadata, not data loss. ## Model Used - OpenAI Codex, GPT-5 coding agent in this Paperclip heartbeat, with shell/git/GitHub CLI tool use. ## 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 - [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:
@@ -1222,6 +1222,72 @@ describeEmbeddedPostgres("issueService.list participantAgentId", () => {
|
||||
expect(comments[0]?.body).toBe("Comment should be visible");
|
||||
});
|
||||
|
||||
it("lists user comments when a candidate attribution run log is missing", 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 with missing run log",
|
||||
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"),
|
||||
logStore: "local_file",
|
||||
logRef: "missing/run-log.ndjson",
|
||||
logBytes: 128,
|
||||
});
|
||||
|
||||
await db.insert(issueComments).values({
|
||||
id: commentId,
|
||||
companyId,
|
||||
issueId,
|
||||
authorUserId: "user-1",
|
||||
body: "Comment should still 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 still be visible");
|
||||
expect(comments[0]?.metadata).toBeNull();
|
||||
});
|
||||
|
||||
it("includes blockedBy summaries on list rows in one batched pass", async () => {
|
||||
const companyId = randomUUID();
|
||||
const blockerId = randomUUID();
|
||||
|
||||
@@ -50,7 +50,8 @@ import {
|
||||
isUuidLike,
|
||||
normalizeIssueIdentifier as normalizeIssueReferenceIdentifier,
|
||||
} from "@paperclipai/shared";
|
||||
import { conflict, notFound, unprocessable } from "../errors.js";
|
||||
import { conflict, HttpError, notFound, unprocessable } from "../errors.js";
|
||||
import { logger } from "../middleware/logger.js";
|
||||
import { parseObject } from "../adapters/utils.js";
|
||||
import {
|
||||
defaultIssueExecutionWorkspaceSettingsForProject,
|
||||
@@ -2804,6 +2805,7 @@ export function issueService(db: Db) {
|
||||
}
|
||||
|
||||
async function readRunLogText(run: {
|
||||
runId?: string | null;
|
||||
logStore: string | null;
|
||||
logRef: string | null;
|
||||
logBytes: number | null;
|
||||
@@ -2817,19 +2819,30 @@ export function issueService(db: Db) {
|
||||
let content = "";
|
||||
let nextOffset: number | undefined = 0;
|
||||
|
||||
while (nextOffset !== undefined) {
|
||||
const remainingBytes = ISSUE_COMMENT_RUN_LOG_DERIVATION_MAX_LOG_BYTES - Buffer.byteLength(content, "utf8");
|
||||
if (remainingBytes <= 0) break;
|
||||
const chunk = await store.read(
|
||||
{ store: "local_file", logRef: run.logRef },
|
||||
{
|
||||
offset,
|
||||
limitBytes: Math.min(ISSUE_COMMENT_RUN_LOG_DERIVATION_CHUNK_BYTES, remainingBytes),
|
||||
},
|
||||
);
|
||||
content += chunk.content;
|
||||
nextOffset = chunk.nextOffset;
|
||||
offset = chunk.nextOffset ?? 0;
|
||||
try {
|
||||
while (nextOffset !== undefined) {
|
||||
const remainingBytes = ISSUE_COMMENT_RUN_LOG_DERIVATION_MAX_LOG_BYTES - Buffer.byteLength(content, "utf8");
|
||||
if (remainingBytes <= 0) break;
|
||||
const chunk = await store.read(
|
||||
{ store: "local_file", logRef: run.logRef },
|
||||
{
|
||||
offset,
|
||||
limitBytes: Math.min(ISSUE_COMMENT_RUN_LOG_DERIVATION_CHUNK_BYTES, remainingBytes),
|
||||
},
|
||||
);
|
||||
content += chunk.content;
|
||||
nextOffset = chunk.nextOffset;
|
||||
offset = chunk.nextOffset ?? 0;
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof HttpError && err.status === 404) {
|
||||
logger.warn(
|
||||
{ err, runId: run.runId ?? undefined, logRef: run.logRef },
|
||||
"missing heartbeat run log while deriving issue comment metadata",
|
||||
);
|
||||
return content;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
return content;
|
||||
|
||||
Reference in New Issue
Block a user